-
🌐 async-utils.jsWeb, 3.69 kB⬇
-
🌐 auth-storage.jsWeb, 735 B⬇
-
🌐 chat-helpers.jsWeb, 8.85 kB⬇
-
🌐 chat-renderer.jsWeb, 70.00 kB⬇
-
🌐 code-overlay.jsWeb, 21.77 kB⬇
-
🌐 confirm-dialog.jsWeb, 3.17 kB⬇
-
🌐 conversation-format-utils.jsWeb, 21.51 kB⬇
-
🌐 diagram-overlay.jsWeb, 7.93 kB⬇
-
🌐 dialog.jsWeb, 2.50 kB⬇
-
🌐 dom-utils.jsWeb, 8.33 kB⬇
-
🌐 drag-reorder.jsWeb, 4.82 kB⬇
-
🌐 elements.jsWeb, 11.06 kB⬇
-
🌐 export-utils.jsWeb, 1.53 kB⬇
-
🌐 format-utils.jsWeb, 29.60 kB⬇
-
🌐 history-navigation.jsWeb, 1.95 kB⬇
-
🌐 hljs-yaml.jsWeb, 5.07 kB⬇
-
🌐 inline-format.jsWeb, 7.35 kB⬇
-
🌐 instant-answer.jsWeb, 19.82 kB⬇
-
🌐 interactive-tools.jsWeb, 38.77 kB⬇
-
🌐 mermaid-fixes.jsWeb, 4.60 kB⬇
-
🌐 mermaid-render.jsWeb, 5.10 kB⬇
-
🌐 mermaid.jsWeb, 195 B⬇
-
🌐 migrations.jsWeb, 12.11 kB⬇
-
🌐 multi-choice-dialog.jsWeb, 1.33 kB⬇
-
🌐 notification-stack.jsWeb, 2.50 kB⬇
-
🌐 options-manager.jsWeb, 45.56 kB⬇
-
🌐 overlay-utils.jsWeb, 4.14 kB⬇
-
🌐 pdf-export.jsWeb, 23.59 kB⬇
-
🌐 pinch-zoom.jsWeb, 3.21 kB⬇
-
🌐 plugin-dialog.jsWeb, 2.17 kB⬇
-
🌐 read-aloud.jsWeb, 33.84 kB⬇
-
🌐 response-pipeline.jsWeb, 55.19 kB⬇
-
🌐 scratchpad-editor.jsWeb, 8.64 kB⬇
-
🌐 scratchpad-export.jsWeb, 3.56 kB⬇
-
🌐 scrollbars.jsWeb, 3.12 kB⬇
-
🌐 selection-manager.jsWeb, 3.76 kB⬇
-
🌐 settings-utils.jsWeb, 31.08 kB⬇
-
🌐 share.jsWeb, 10.75 kB⬇
-
🌐 side-drawer.jsWeb, 9.37 kB⬇
-
🌐 speech-text.jsWeb, 6.60 kB⬇
-
🌐 swipe-navigation.jsWeb, 6.16 kB⬇
-
🌐 table-overlay.jsWeb, 8.10 kB⬇
-
🌐 theme-utils.jsWeb, 8.34 kB⬇
-
🌐 thinking-panel-base.jsWeb, 14.88 kB⬇
-
🌐 ui-sets.jsWeb, 1.00 kB⬇
-
🌐 version-overlay.jsWeb, 4.63 kB⬇
/**
* Class SideDrawer
* Shared slide-in drawer for editing surfaces: an overlay panel that slides
* in from the left of its container with a grip toggle on its right edge,
* plus a pin that keeps it docked open beneath the surface's sticky header.
* Consumers own the content through the content slot API; an empty drawer
* cannot be opened (dimmed grip) and cannot be pinned.
*
* While visible, the drawer's parent container carries the
* side-drawer-active class so consumer CSS can reserve space for the closed
* grip track and dock the drawer when pinned. An optional top offset
* provider (bound with the root) keeps the drawer clear of a sticky header
* on the surface above it.
*/
import { content_of } from './scrollbars.js';
/**
* Returns the cumulative CSS zoom applied over an element and its ancestors,
* which scales its rendered geometry relative to its local px units.
* @param {HTMLElement} el - The element whose rendered scale is measured.
* @returns {number} The cumulative zoom factor (1 when unzoomed).
*/
function zoom_scale_of(el) {
let scale = 1;
for (let node = el; node; node = node.parentElement) {
const zoom = parseFloat(getComputedStyle(node).zoom);
if (zoom > 0) scale *= zoom;
}
return scale;
}
/**
* Class SideDrawer
* The shared slide-in panel used by the thinking panels and the scratchpad
* table of contents. One instance wraps one drawer element (see shared.css
* .div-side-drawer and the shared host rules there): grip to open and close,
* optional pin to dock it into the host's flex flow, availability dimming
* while empty, and top-offset handling under sticky headers. Hosts declare
* the drawer variables (--side-drawer-*) from shared.css. Editors drive it
* through ThinkingPanelBase rather than directly, so their panels stay
* uniform; app.js binds the scratchpad drawer itself.
*/
class SideDrawer {
#root = null;
#grip = null;
#pin = null;
#get_top_offset = null;
#source = null;
#open = false;
#pinned = false;
#labels = { open: 'Close Panel', close: 'Open Panel' };
/**
* Returns the drawer's content slot: resolved live because
* SimpleBar may restructure the content element's children.
* @returns {HTMLElement|null} The element to write content into.
*/
#content_el() {
return this.#root ? content_of(this.#root.querySelector('.div-side-drawer-content')) : null;
}
/**
* Binds the drawer chrome: grip toggle, pin button, and the
* resize-driven availability updates.
* @param {HTMLElement} root - The drawer root element (.div-side-drawer).
* @param {function(): number|null} [get_top_offset] - Returns the drawer's
* top inset in rendered (viewport) px to clear a sticky header, or null
* for the CSS default.
* @param {Object} [labels] - Grip titles { open, close }: the title shown
* while the drawer is open (the close action) and while closed (the
* open action), overriding the defaults.
*/
bind(root, get_top_offset = null, labels = null) {
this.#root = root;
this.#grip = root.querySelector('.div-side-drawer-grip');
this.#pin = root.querySelector('.div-side-drawer-pin');
this.#get_top_offset = get_top_offset;
this.#labels = { open: 'Close Panel', close: 'Open Panel', ...labels };
if (this.#grip) {
this.#grip.addEventListener('click', (e) => {
e.stopPropagation();
if (!this.is_empty()) {
this.set_open(!this.#open);
return;
}
// empty drawer: the grip only closes it, never opens it
if (this.#open) this.set_open(false);
});
}
if (this.#pin) {
this.#pin.addEventListener('click', (e) => {
e.stopPropagation();
this.set_pinned(!this.#pinned);
});
}
window.addEventListener('resize', () => this.update_availability());
// the sticky header's height follows the container's width (panel
// toggles, rewraps) without a window resize; re-measure when it does
if (root.parentElement) {
new ResizeObserver(() => this.update_availability()).observe(root.parentElement);
}
}
/**
* Records what the current content belongs to (e.g. the conversation
* guid the drawer was filled from), so consumers can detect stale
* content when the selection changes.
* @param {string|null} source - Identifier of the content's owner.
*/
set_source(source) {
this.#source = source;
}
/**
* Returns the identifier of the content's owner (see set_source).
* @returns {string|null} The source identifier, or null when untracked.
*/
get source() {
return this.#source;
}
/**
* Returns whether the drawer has no content.
* @returns {boolean} True when the content slot is empty.
*/
is_empty() {
const content = this.#content_el();
return !content || content.children.length === 0;
}
/**
* Returns whether the drawer is currently slid open.
* @returns {boolean} True when open.
*/
is_open() {
return this.#open;
}
/**
* Slides the drawer in or out and updates the grip label.
* @param {boolean} open - True to show the panel, false to collapse it to the grip track.
*/
set_open(open) {
this.#open = open;
this.#root?.classList.toggle('side-open', open);
const label = open ? this.#labels.open : this.#labels.close;
if (this.#grip) {
this.#grip.title = label;
}
}
/**
* Toggles the drawer between open and closed.
*/
toggle() {
this.set_open(!this.#open);
}
/**
* Pins the drawer: it stays docked open as an overlay beneath the
* surface's sticky header instead of sliding out over the content.
* Unpinning returns it to the sliding overlay.
* @param {boolean} pinned - Whether the drawer is pinned.
*/
set_pinned(pinned) {
this.#pinned = pinned;
this.#root?.classList.toggle('pinned', pinned);
if (this.#pin) {
this.#pin.title = pinned ? 'Unpin' : 'Pin';
}
}
/**
* Shows or hides the whole drawer for the current surface, independent
* of its open/pinned state.
* @param {boolean} visible - Whether the drawer is available on the current surface.
*/
set_visible(visible) {
if (!this.#root) return;
this.#root.style.display = visible ? '' : 'none';
this.#root.parentElement?.classList.toggle('side-drawer-active', visible);
if (visible) {
this.update_availability();
}
}
/**
* Replaces the drawer content with the given element.
* @param {HTMLElement|null} element - The content element, or null to clear.
* @param {object} [options=null] - Optional behaviour.
* @param {boolean} [options.close_if_empty=false] - Also close and unpin the
* drawer when the new content is empty, so an emptied drawer does not
* linger open on the surface.
*/
set_content(element, options = null) {
const content = this.#content_el();
if (!content) return;
content.innerHTML = '';
if (element) {
content.appendChild(element);
}
if (options?.close_if_empty && !element) {
// nothing to show: an empty drawer must not stay open or pinned
this.set_open(false);
this.set_pinned(false);
}
this.update_availability();
if (element && this.#pin) {
// a populated drawer always ends with the pin visible: the pass
// above can measure a still-settling layout right after load
this.#pin.style.display = '';
}
}
/**
* Replaces the drawer content with the given html.
* @param {string} html - The html to render into the drawer.
*/
set_content_html(html) {
const content = this.#content_el();
if (!content) return;
content.innerHTML = html;
this.update_availability();
}
/**
* Clears the drawer content.
*/
clear() {
this.set_content(null);
}
/**
* Updates the drawer's dynamic state: the sticky-header top offset, the
* empty state (an empty drawer cannot be opened — dimmed grip, open
* drawer stays closable), and the pin button, which only appears when
* the drawer's share of the container width can fit its minimum width —
* otherwise it is hidden and any pinned state is cleared. The pin stays
* put while the content refills, so switching surfaces does not make it
* flicker. The empty and pin checks are skipped while the drawer is
* hidden, since layout is then unmeasurable; set_visible(true) re-runs
* them.
*/
update_availability() {
this.#apply_top_offset();
if (!this.#root) return;
// the empty state is layout-free, so it stays correct even while the
// drawer's surface is hidden and measurements are impossible
this.#root.classList.toggle('empty', this.is_empty());
if (!this.#root.offsetParent) return;
// two- and three-column layouts always fit the pin; only the narrow
// single-column surface hides it
const pin_feasible = window.innerWidth > 780;
if (this.#pin) {
this.#pin.style.display = pin_feasible ? '' : 'none';
}
if (!pin_feasible && this.#pinned) {
this.set_pinned(false);
}
}
/**
* Applies the bound top offset provider, keeping the drawer below a
* sticky header on the surface; a null result restores the CSS default.
* The offset arrives in rendered (viewport) px, while style.top is
* interpreted in the drawer's local units, so it is divided by the
* cumulative CSS zoom. The bottom inset is mirrored on top so the drawer
* floats evenly between header and container edge.
* @private
*/
#apply_top_offset() {
if (!this.#root || !this.#get_top_offset) return;
const offset = this.#get_top_offset();
if (offset === null || offset === undefined) {
this.#root.style.top = '';
return;
}
const gap = parseFloat(getComputedStyle(this.#root).bottom) || 0;
this.#root.style.top = offset / zoom_scale_of(this.#root) + gap + 'px';
}
}
export default SideDrawer;
side-drawer.js
×
Type: Web, text/plain
9.37 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File
Type: Web, text/plain
9.37 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File