flexdesk 0.17.2 → 0.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -63
- package/README.md +27 -4517
- package/css/base.css +18 -1
- package/css/flexdesk.css +18 -1
- package/dist/{chunk-4A7SJC3T.js → chunk-RKNFBX5K.js} +15 -4
- package/dist/{chunk-4A7SJC3T.js.map → chunk-RKNFBX5K.js.map} +2 -2
- package/dist/flexdesk.css +18 -1
- package/dist/tiles.js +1 -1
- package/dist/widgets.js +59 -26
- package/dist/widgets.js.map +2 -2
- package/docs/README.md +97 -0
- package/docs/best-practices.md +45 -0
- package/docs/components/charts.md +211 -0
- package/docs/components/command-palette.md +102 -0
- package/docs/components/conversation.md +72 -0
- package/docs/components/data-table-grid.md +328 -0
- package/docs/components/data-table.md +596 -0
- package/docs/components/editor.md +22 -0
- package/docs/components/flow-canvas-editor.md +482 -0
- package/docs/components/flow-editors.md +176 -0
- package/docs/components/flow-lane-editor.md +165 -0
- package/docs/components/flow-outline-editor.md +176 -0
- package/docs/components/forms-and-dialogs.md +294 -0
- package/docs/components/input-fields.md +72 -0
- package/docs/components/keymap.md +143 -0
- package/docs/components/layout-widgets.md +163 -0
- package/docs/components/markdown-and-outline.md +106 -0
- package/docs/components/menus.md +86 -0
- package/docs/components/notifications.md +194 -0
- package/docs/components/panels-and-desktops.md +127 -0
- package/docs/components/record-list-and-fact-table.md +128 -0
- package/docs/components/settings-view.md +112 -0
- package/docs/components/shell.md +189 -0
- package/docs/components/slide-out-panel.md +68 -0
- package/docs/components/tiles.md +210 -0
- package/docs/components/wizard.md +54 -0
- package/docs/concepts/building-and-packaging.md +33 -0
- package/docs/concepts/domain-neutrality.md +110 -0
- package/docs/concepts/host-port.md +137 -0
- package/docs/concepts/packages-and-entry-points.md +32 -0
- package/docs/concepts/peer-dependencies.md +42 -0
- package/docs/concepts/state-guard.md +58 -0
- package/docs/concepts/styling.md +47 -0
- package/docs/quickstart.md +179 -0
- package/package.json +2 -1
- package/src/ui/components/notification_history.js +17 -24
- package/src/ui/components/slide_out_panel.js +14 -3
- package/src/ui/notification_center.js +41 -3
- package/src/ui/utils/auto_scrollbars.js +16 -2
package/css/base.css
CHANGED
|
@@ -4754,7 +4754,11 @@ button.twm-about-dialog__btn--close:focus {
|
|
|
4754
4754
|
border-left: 1px solid #333;
|
|
4755
4755
|
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.35);
|
|
4756
4756
|
|
|
4757
|
-
|
|
4757
|
+
/* Never wider than the host (BUG-0035). `#trackGeometry` sets
|
|
4758
|
+
* `--twm-slide-out-host-w` to the host's width while open; the 400px
|
|
4759
|
+
* floor gives way to it too, since `min-width` beats `max-width`. */
|
|
4760
|
+
min-width: min(400px, var(--twm-slide-out-host-w, 400px));
|
|
4761
|
+
max-width: var(--twm-slide-out-host-w, none);
|
|
4758
4762
|
|
|
4759
4763
|
transform: translateX(100%);
|
|
4760
4764
|
transition: transform 200ms ease-out, visibility 0s linear 200ms;
|
|
@@ -4788,6 +4792,19 @@ button.twm-about-dialog__btn--close:focus {
|
|
|
4788
4792
|
flex-shrink: 0;
|
|
4789
4793
|
margin-left: auto;
|
|
4790
4794
|
}
|
|
4795
|
+
/* Both icons at 18px (BUG-0035). Two classes each, for the Material Symbols
|
|
4796
|
+
* tie — see `.twm-leaf__icon`: a single-class rule loses to Google's
|
|
4797
|
+
* `.material-symbols-outlined { font-size: 24px }`, which loads later. */
|
|
4798
|
+
.twm-slide-out-panel__icon.material-symbols-outlined {
|
|
4799
|
+
flex-shrink: 0;
|
|
4800
|
+
font-size: 18px;
|
|
4801
|
+
line-height: 1;
|
|
4802
|
+
color: var(--text-secondary);
|
|
4803
|
+
}
|
|
4804
|
+
.twm-slide-out-panel__close .material-symbols-outlined {
|
|
4805
|
+
font-size: 18px;
|
|
4806
|
+
line-height: 1;
|
|
4807
|
+
}
|
|
4791
4808
|
/* Scrollable body */
|
|
4792
4809
|
.twm-slide-out-panel__body {
|
|
4793
4810
|
flex: 1;
|
package/css/flexdesk.css
CHANGED
|
@@ -5678,7 +5678,11 @@ button.twm-about-dialog__btn--close:focus {
|
|
|
5678
5678
|
border-left: 1px solid #333;
|
|
5679
5679
|
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.35);
|
|
5680
5680
|
|
|
5681
|
-
|
|
5681
|
+
/* Never wider than the host (BUG-0035). `#trackGeometry` sets
|
|
5682
|
+
* `--twm-slide-out-host-w` to the host's width while open; the 400px
|
|
5683
|
+
* floor gives way to it too, since `min-width` beats `max-width`. */
|
|
5684
|
+
min-width: min(400px, var(--twm-slide-out-host-w, 400px));
|
|
5685
|
+
max-width: var(--twm-slide-out-host-w, none);
|
|
5682
5686
|
|
|
5683
5687
|
transform: translateX(100%);
|
|
5684
5688
|
transition: transform 200ms ease-out, visibility 0s linear 200ms;
|
|
@@ -5712,6 +5716,19 @@ button.twm-about-dialog__btn--close:focus {
|
|
|
5712
5716
|
flex-shrink: 0;
|
|
5713
5717
|
margin-left: auto;
|
|
5714
5718
|
}
|
|
5719
|
+
/* Both icons at 18px (BUG-0035). Two classes each, for the Material Symbols
|
|
5720
|
+
* tie — see `.twm-leaf__icon`: a single-class rule loses to Google's
|
|
5721
|
+
* `.material-symbols-outlined { font-size: 24px }`, which loads later. */
|
|
5722
|
+
.twm-slide-out-panel__icon.material-symbols-outlined {
|
|
5723
|
+
flex-shrink: 0;
|
|
5724
|
+
font-size: 18px;
|
|
5725
|
+
line-height: 1;
|
|
5726
|
+
color: var(--text-secondary);
|
|
5727
|
+
}
|
|
5728
|
+
.twm-slide-out-panel__close .material-symbols-outlined {
|
|
5729
|
+
font-size: 18px;
|
|
5730
|
+
line-height: 1;
|
|
5731
|
+
}
|
|
5715
5732
|
/* Scrollable body */
|
|
5716
5733
|
.twm-slide-out-panel__body {
|
|
5717
5734
|
flex: 1;
|
|
@@ -361,7 +361,7 @@ var SlideOutPanel = class extends ComponentBase {
|
|
|
361
361
|
const header = document.createElement("div");
|
|
362
362
|
header.className = "twm-slide-out-panel__header";
|
|
363
363
|
this.#headerIcon = document.createElement("span");
|
|
364
|
-
this.#headerIcon.className = "material-symbols-outlined";
|
|
364
|
+
this.#headerIcon.className = "twm-slide-out-panel__icon material-symbols-outlined";
|
|
365
365
|
this.#headerTitle = document.createElement("span");
|
|
366
366
|
this.#headerTitle.className = "twm-slide-out-panel__title";
|
|
367
367
|
const closeBtn = document.createElement("button");
|
|
@@ -480,13 +480,24 @@ var SlideOutPanel = class extends ComponentBase {
|
|
|
480
480
|
* engine that predates it), and the width subtracted is the panel's
|
|
481
481
|
* laid-out one, which `min-width` may hold above the requested
|
|
482
482
|
* width. Rounding happens in screen pixels, so at 100% the output is
|
|
483
|
-
* exactly what it always was.
|
|
483
|
+
* exactly what it always was.
|
|
484
|
+
*
|
|
485
|
+
* NEVER WIDER THAN THE HOST (BUG-0035). The right edge is pinned, so a
|
|
486
|
+
* panel wider than its host (700px by default, `min-width: 400px`)
|
|
487
|
+
* reached past the host's left edge across the neighbouring pane. The
|
|
488
|
+
* host's width, in the panel's CSS pixels, goes on the panel as
|
|
489
|
+
* `--twm-slide-out-host-w`, which the stylesheet uses as `max-width`
|
|
490
|
+
* and as the ceiling of the 400px `min-width`; it is set BEFORE the
|
|
491
|
+
* width is read so the read sees the capped layout, and the width is
|
|
492
|
+
* capped here too for an engine that lays nothing out. */
|
|
484
493
|
#trackGeometry = () => {
|
|
485
494
|
if (!this.#open || !this.#panel || !this.#container) return;
|
|
486
495
|
const r = this.#container.getBoundingClientRect();
|
|
487
496
|
const zoom = this.#panel.currentCSSZoom || 1;
|
|
488
|
-
const
|
|
497
|
+
const hostWidth = r.width / zoom;
|
|
489
498
|
const s = this.#panel.style;
|
|
499
|
+
s.setProperty("--twm-slide-out-host-w", `${hostWidth}px`);
|
|
500
|
+
const width = Math.min(this.#panel.offsetWidth || this.#width, hostWidth);
|
|
490
501
|
s.position = "fixed";
|
|
491
502
|
s.top = `${Math.round(r.top) / zoom}px`;
|
|
492
503
|
s.left = `${Math.round(r.right - width * zoom) / zoom}px`;
|
|
@@ -500,4 +511,4 @@ export {
|
|
|
500
511
|
ActionDropdown,
|
|
501
512
|
SlideOutPanel
|
|
502
513
|
};
|
|
503
|
-
//# sourceMappingURL=chunk-
|
|
514
|
+
//# sourceMappingURL=chunk-RKNFBX5K.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/ui/base/component_base.js", "../src/ui/components/action_dropdown.js", "../src/ui/components/slide_out_panel.js"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * ComponentBase (js_new)\r\n *\r\n * Ultra-lightweight base for leaf UI widgets rendered inside controllers/panels.\r\n * Handles props/state updates and deterministic teardown without storing data on\r\n * DOM elements.\r\n */\r\nexport class ComponentBase {\r\n constructor({ eventBus, logger } = {}) {\r\n if (new.target === ComponentBase) {\r\n throw new Error('ComponentBase is abstract \u2013 extend it instead');\r\n }\r\n\r\n this.eventBus = eventBus;\r\n this.logger = logger;\r\n this._mounted = false;\r\n }\r\n\r\n mount(/* container, props */) {\r\n throw new Error('ComponentBase.mount must be implemented by subclasses');\r\n }\r\n\r\n update(/* props */) {\r\n // default no-op for stateless components\r\n }\r\n\r\n dispose() {\r\n this._mounted = false;\r\n }\r\n}\r\n", "/**\n * action_dropdown.js\n *\n * Reusable dropdown component for action buttons (Add Scenario, Add Widget, etc).\n * Handles opening, closing, auto-positioning, keyboard navigation, and outside clicks.\n *\n * Auto-positioning: measures available viewport space around the trigger element\n * and picks the best direction (above/below) and alignment (left/right).\n * Adds scrolling when the menu would exceed available vertical space.\n */\n\n/** Viewport margin in px \u2014 dropdown stays this far from edges. */\nconst EDGE_MARGIN = 8;\n/** Gap between trigger and dropdown in px. */\nconst GAP = 8;\n\n/**\n * ActionDropdown - A reusable dropdown component with smart auto-positioning.\n *\n * Usage:\n * ```javascript\n * const dropdown = new ActionDropdown({\n * trigger: buttonElement,\n * options: [\n * { type: 'option1', label: 'Option 1', icon: 'icon_name', description: 'Description' },\n * { separator: true },\n * { type: 'option2', label: 'Option 2', icon: 'icon_name', description: 'Description' }\n * ],\n * onSelect: (option) => console.log('Selected:', option.type),\n * });\n *\n * // Control programmatically\n * dropdown.open();\n * dropdown.close();\n * dropdown.toggle();\n * dropdown.destroy();\n * ```\n *\n * \u2500\u2500 Separators \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * An entry `{ separator: true }` draws a divider instead of a button: a\n * `<div role=\"separator\" class=\"twm-action-dropdown-separator\">`, which takes\n * no focus, is never reached by the arrow keys (they move between the option\n * buttons only) and never reaches `onSelect` (it has no click handler). A\n * separator only separates: one at the start or the end of the list, or one\n * right after another, is dropped, so a list assembled from groups that may\n * come out empty never draws a stray or doubled rule.\n */\nexport class ActionDropdown {\n /**\n * @param {Object} config\n * @param {HTMLElement} config.trigger - Button element that triggers the dropdown\n * @param {Array} config.options - Array of option objects with { type, label, icon, description? },\n * or `{ separator: true }` for a divider between them\n * @param {Function} config.onSelect - Callback when an option is selected\n * @param {string} [config.className=''] - Additional CSS class for the dropdown\n * @param {string} [config.menuId] - Optional ID for the menu element\n */\n constructor(config) {\n this.trigger = config.trigger;\n this.options = config.options || [];\n this.onSelect = config.onSelect;\n this.className = config.className || '';\n this.menuId = config.menuId;\n\n this.isOpen = false;\n this.menuEl = null;\n\n this._boundHandleDocumentClick = this._handleDocumentClick.bind(this);\n this._boundHandleKeydown = this._handleKeydown.bind(this);\n this._boundHandleTriggerClick = this._handleTriggerClick.bind(this);\n\n this._init();\n }\n\n /**\n * Initialize the dropdown.\n * @private\n */\n _init() {\n if (!this.trigger) {\n console.warn('[ActionDropdown] No trigger element provided');\n return;\n }\n\n this._createMenu();\n this.trigger.addEventListener('click', this._boundHandleTriggerClick);\n }\n\n /**\n * Create the dropdown menu element.\n * @private\n */\n _createMenu() {\n const menu = document.createElement('div');\n menu.className = `twm-action-dropdown-menu ${this.className}`.trim();\n menu.setAttribute('role', 'menu');\n menu.hidden = true;\n\n if (this.menuId) {\n menu.id = this.menuId;\n }\n\n this._renderOptions(menu);\n\n // Append to body so position:fixed works without clipping.\n document.body.appendChild(menu);\n\n this.menuEl = menu;\n }\n\n /**\n * Render option buttons, and dividers for separator entries, into a\n * container element. `data-index` stays the entry's index in `options`.\n * @param {HTMLElement} container\n * @private\n */\n _renderOptions(container) {\n container.innerHTML = '';\n let pendingSeparator = false;\n this.options.forEach((opt, index) => {\n if (opt?.separator) {\n // Drawn only once an option follows it, and only if an option\n // came before it: leading, trailing and doubled separators\n // collapse.\n pendingSeparator = container.childElementCount > 0;\n return;\n }\n if (pendingSeparator) {\n const sep = document.createElement('div');\n sep.className = 'twm-action-dropdown-separator';\n sep.setAttribute('role', 'separator');\n container.appendChild(sep);\n pendingSeparator = false;\n }\n\n const optionBtn = document.createElement('button');\n optionBtn.type = 'button';\n optionBtn.className = 'twm-action-dropdown-option';\n optionBtn.dataset.index = index;\n optionBtn.dataset.type = opt.type;\n optionBtn.setAttribute('role', 'menuitem');\n\n const iconHtml = opt.icon\n ? `<span class=\"material-symbols-outlined twm-option-icon\">${opt.icon}</span>`\n : '';\n\n const descHtml = opt.description\n ? `<span class=\"twm-option-desc\">${opt.description}</span>`\n : '';\n\n optionBtn.innerHTML = `\n ${iconHtml}\n <span class=\"option-text\">\n <span class=\"twm-option-label\">${opt.label}</span>\n ${descHtml}\n </span>\n `;\n\n optionBtn.addEventListener('click', (e) => {\n e.preventDefault();\n e.stopPropagation();\n this._selectOption(opt);\n });\n\n container.appendChild(optionBtn);\n });\n }\n\n /**\n * Handle trigger button click.\n * @param {MouseEvent} e\n * @private\n */\n _handleTriggerClick(e) {\n e.preventDefault();\n e.stopPropagation();\n this.toggle();\n }\n\n /**\n * Toggle the dropdown open/closed.\n * @param {boolean} [forceState] - Optional forced state\n */\n toggle(forceState) {\n const shouldOpen = typeof forceState === 'boolean' ? forceState : !this.isOpen;\n if (shouldOpen) {\n this.open();\n } else {\n this.close();\n }\n }\n\n /**\n * Open the dropdown with auto-positioning.\n */\n open() {\n if (this.isOpen || !this.menuEl) return;\n\n this.isOpen = true;\n this.menuEl.hidden = false;\n\n // Position against trigger\n ActionDropdown.position(this.trigger, this.menuEl);\n\n // Animate in.\n //\n // OPTIONAL CHAINING, AND IT IS LOAD-BEARING. This callback runs a frame\n // after `open()` returned, and `destroy()` sets `this.menuEl = null`\n // (see below) \u2014 so a dropdown that is opened and then destroyed inside\n // one frame threw an uncaught `TypeError: Cannot read properties of\n // null` out of an animation-frame callback, where no caller has a stack\n // to catch it. That is not a hypothetical: it is what a user does every\n // time they open a picker and then click something that unmounts the\n // pane around it, and it was reproduced eighteen times in one run of a\n // consumer's settings suite. Nothing is lost by skipping the class \u2014 the\n // element it would have been added to no longer exists.\n requestAnimationFrame(() => {\n this.menuEl?.classList.add('visible');\n });\n\n // Update trigger state\n this.trigger?.classList.add('twm-is-open');\n if (this.trigger?.hasAttribute('aria-expanded')) {\n this.trigger.setAttribute('aria-expanded', 'true');\n }\n\n // Add document listeners\n document.addEventListener('click', this._boundHandleDocumentClick, true);\n document.addEventListener('keydown', this._boundHandleKeydown);\n\n // Focus first option for keyboard accessibility\n const firstOption = this.menuEl.querySelector('.twm-action-dropdown-option');\n firstOption?.focus();\n }\n\n /**\n * Close the dropdown.\n */\n close() {\n if (!this.isOpen) return;\n\n this.isOpen = false;\n\n if (this.menuEl) {\n this.menuEl.classList.remove('visible');\n this.menuEl.hidden = true;\n }\n\n // Update trigger state. `aria-expanded` belongs on the TRIGGER and has\n // to be written on every close, not only on the ones a click caused \u2014\n // an embedder that synced it from its own click handler was announcing\n // an expanded menu to a screen reader every time Escape or an outside\n // click dismissed one. The component knows when it closed; nothing else\n // reliably does.\n this.trigger?.classList.remove('twm-is-open');\n if (this.trigger?.hasAttribute('aria-expanded')) {\n this.trigger.setAttribute('aria-expanded', 'false');\n }\n\n // Remove document listeners\n document.removeEventListener('click', this._boundHandleDocumentClick, true);\n document.removeEventListener('keydown', this._boundHandleKeydown);\n }\n\n /**\n * Handle document click (close on outside click).\n * @param {MouseEvent} e\n * @private\n */\n _handleDocumentClick(e) {\n if (!this.isOpen) return;\n\n // Don't close if clicking inside dropdown or trigger\n if (this.menuEl?.contains(e.target) || this.trigger?.contains(e.target)) {\n return;\n }\n\n this.close();\n }\n\n /**\n * Handle keyboard events.\n * @param {KeyboardEvent} e\n * @private\n */\n _handleKeydown(e) {\n if (e.key === 'Escape') {\n // AND NOBODY ELSE GETS IT. An open dropdown is the innermost thing\n // on screen, so Escape means \"close this\" and nothing further \u2014\n // but the event was left to bubble, and inside a `ManagedWindow`\n // (which binds its own Escape to dismiss) that meant one keystroke\n // closed the dropdown AND the dialog around it. The user loses a\n // form they were filling in because they changed their mind about\n // one field.\n e.preventDefault();\n e.stopPropagation();\n this.close();\n this.trigger?.focus();\n return;\n }\n\n // Arrow key navigation\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n e.preventDefault();\n this._navigateOptions(e.key === 'ArrowDown' ? 1 : -1);\n }\n }\n\n /**\n * Navigate options with arrow keys.\n * @param {number} direction - 1 for down, -1 for up\n * @private\n */\n _navigateOptions(direction) {\n const options = this.menuEl?.querySelectorAll('.twm-action-dropdown-option');\n if (!options || options.length === 0) return;\n\n const currentIndex = Array.from(options).findIndex(opt => opt === document.activeElement);\n let nextIndex = currentIndex + direction;\n\n if (nextIndex < 0) nextIndex = options.length - 1;\n if (nextIndex >= options.length) nextIndex = 0;\n\n options[nextIndex]?.focus();\n }\n\n /**\n * Handle option selection.\n * @param {Object} option - The selected option\n * @private\n */\n _selectOption(option) {\n this.close();\n\n if (this.onSelect) {\n this.onSelect(option);\n }\n }\n\n /**\n * Update the options dynamically.\n * @param {Array} newOptions - New options array\n */\n setOptions(newOptions) {\n this.options = newOptions;\n if (this.menuEl) {\n this._renderOptions(this.menuEl);\n }\n }\n\n /**\n * Destroy the dropdown and clean up.\n */\n destroy() {\n this.close();\n\n // Remove trigger listener\n this.trigger?.removeEventListener('click', this._boundHandleTriggerClick);\n\n // Remove menu element\n this.menuEl?.remove();\n this.menuEl = null;\n\n this.trigger = null;\n this.options = [];\n this.onSelect = null;\n }\n\n // =========================================================================\n // STATIC \u2014 shared auto-positioning for any trigger + menu pair\n // =========================================================================\n\n /**\n * Position a dropdown menu relative to a trigger element.\n * Uses `position: fixed` to avoid overflow clipping.\n * Picks above/below based on available space, aligns left/right edge,\n * and applies `max-height` + `overflow-y: auto` when the menu is taller\n * than the available space.\n *\n * @param {HTMLElement} trigger - The element the dropdown opens from\n * @param {HTMLElement} menu - The dropdown menu element to position\n */\n static position(trigger, menu) {\n if (!trigger || !menu) return;\n\n // Reset any prior inline position so natural size can be measured.\n Object.assign(menu.style, {\n position: 'fixed',\n top: 'auto',\n bottom: 'auto',\n left: 'auto',\n right: 'auto',\n maxHeight: '',\n overflowY: ''\n });\n\n const triggerRect = trigger.getBoundingClientRect();\n const menuRect = menu.getBoundingClientRect();\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n\n // \u2500\u2500 Vertical: prefer below, flip above if insufficient room \u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const spaceBelow = vh - triggerRect.bottom - GAP - EDGE_MARGIN;\n const spaceAbove = triggerRect.top - GAP - EDGE_MARGIN;\n\n let top;\n let maxHeight;\n\n if (menuRect.height <= spaceBelow) {\n // Fits below \u2014 use natural height.\n top = triggerRect.bottom + GAP;\n maxHeight = spaceBelow;\n } else if (menuRect.height <= spaceAbove) {\n // Fits above \u2014 use natural height.\n top = triggerRect.top - GAP - menuRect.height;\n maxHeight = spaceAbove;\n } else if (spaceBelow >= spaceAbove) {\n // More space below \u2014 scroll.\n top = triggerRect.bottom + GAP;\n maxHeight = spaceBelow;\n } else {\n // More space above \u2014 scroll.\n maxHeight = spaceAbove;\n top = EDGE_MARGIN;\n }\n\n // \u2500\u2500 Horizontal: align left-edge with trigger, flip if off-screen \u2500\u2500\n let left = triggerRect.left;\n\n if (left + menuRect.width > vw - EDGE_MARGIN) {\n // Align right edges instead.\n left = triggerRect.right - menuRect.width;\n }\n\n // Still off-screen? Clamp to edges.\n left = Math.max(EDGE_MARGIN, Math.min(left, vw - menuRect.width - EDGE_MARGIN));\n\n Object.assign(menu.style, {\n position: 'fixed',\n top: `${Math.round(top)}px`,\n left: `${Math.round(left)}px`,\n maxHeight: `${Math.round(maxHeight)}px`,\n overflowY: 'auto'\n });\n }\n}\n\nexport default ActionDropdown;\n", "/**\n * SlideOutPanel\n *\n * Reusable inline slide-out config panel that overlays its host container\n * from the right edge. Used by pages to show element-specific config\n * (plot config, widget config, ETL node config) without the global right panel.\n *\n * Host container must have `position: relative; overflow: hidden`.\n */\nimport { ComponentBase } from '../base/component_base.js';\n\nexport class SlideOutPanel extends ComponentBase {\n\n /** @type {HTMLElement} */ #container = null;\n /** @type {HTMLElement} */ #backdrop = null;\n /** @type {HTMLElement} */ #panel = null;\n /** @type {HTMLElement} */ #headerIcon = null;\n /** @type {HTMLElement} */ #headerTitle = null;\n /** @type {HTMLElement} */ #body = null;\n /** @type {number} */ #width;\n /** @type {Function|null} */#onClose;\n /** @type {boolean} */ #open = false;\n /** @type {ResizeObserver|null} */ #hostObserver = null;\n\n /**\n * @param {object} opts\n * @param {number} [opts.width=380] Panel width in px.\n * @param {Function} [opts.onClose] Called after the panel closes.\n */\n constructor({ width = 700, onClose } = {}) {\n super();\n this.#width = width;\n this.#onClose = onClose ?? null;\n }\n\n // \u2500\u2500\u2500 Lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Mount the panel DOM into a host container.\n * @param {HTMLElement} container Must have position:relative; overflow:hidden.\n */\n mount(container) {\n if (this._mounted) return;\n this.#container = container;\n\n // Backdrop (transparent click-catcher)\n this.#backdrop = document.createElement('div');\n this.#backdrop.className = 'twm-slide-out-panel__backdrop';\n this.#backdrop.addEventListener('click', this.#handleBackdropClick);\n\n // Panel\n this.#panel = document.createElement('div');\n this.#panel.className = 'twm-slide-out-panel';\n this.#panel.style.width = `${this.#width}px`;\n\n // Header\n const header = document.createElement('div');\n header.className = 'twm-slide-out-panel__header';\n\n this.#headerIcon = document.createElement('span');\n this.#headerIcon.className = 'material-symbols-outlined';\n\n this.#headerTitle = document.createElement('span');\n this.#headerTitle.className = 'twm-slide-out-panel__title';\n\n const closeBtn = document.createElement('button');\n closeBtn.className = 'twm-slide-out-panel__close twm-btn-icon';\n closeBtn.type = 'button';\n closeBtn.title = 'Close';\n closeBtn.innerHTML = '<span class=\"material-symbols-outlined\">close</span>';\n closeBtn.addEventListener('click', this.#handleCloseClick);\n\n header.append(this.#headerIcon, this.#headerTitle, closeBtn);\n\n // Body (scrollable content area)\n this.#body = document.createElement('div');\n this.#body.className = 'twm-slide-out-panel__body';\n\n this.#panel.append(header, this.#body);\n this.#container.append(this.#backdrop, this.#panel);\n\n this._mounted = true;\n }\n\n dispose() {\n this.close();\n this.#backdrop?.remove();\n this.#panel?.remove();\n this.#backdrop = null;\n this.#panel = null;\n this.#body = null;\n this.#container = null;\n super.dispose();\n }\n\n // \u2500\u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** The inner content element \u2014 callers render config into this. */\n get contentEl() { return this.#body; }\n\n get isOpen() { return this.#open; }\n\n /**\n * Open (or re-open) the panel with a new title/icon.\n * Clears previous content. Returns contentEl for rendering.\n * @param {string} title\n * @param {string} icon Material Symbols icon name.\n * @returns {HTMLElement} contentEl\n */\n open(title, icon) {\n if (!this._mounted) return null;\n\n this.#body.innerHTML = '';\n this.#headerIcon.textContent = icon || '';\n this.#headerIcon.style.display = icon ? '' : 'none';\n this.#headerTitle.textContent = title || '';\n\n if (!this.#open) {\n this.#open = true;\n this.#backdrop.classList.add('twm-slide-out-panel__backdrop--visible');\n this.#panel.classList.add('twm-slide-out-panel--open');\n document.addEventListener('keydown', this.#handleEsc);\n\n // The host may be a scroll container \u2014 absolutely-positioned\n // descendants of one DO scroll along with the content\n // (Web spec: a position:absolute element is clipped + offset\n // by its containing scroll context). We escape that by\n // switching the panel to `position: fixed` while open and\n // tracking the host's bounding rect on scroll / resize, so\n // the panel stays pinned to the host's visible frame at\n // full height regardless of how the user scrolls inside.\n this.#trackGeometry();\n window.addEventListener('scroll', this.#trackGeometry, true);\n window.addEventListener('resize', this.#trackGeometry);\n this.#observeHost();\n }\n\n return this.#body;\n }\n\n /** Close the panel and clear content. */\n close() {\n if (!this.#open) return;\n this.#open = false;\n\n this.#panel.classList.remove('twm-slide-out-panel--open');\n this.#backdrop.classList.remove('twm-slide-out-panel__backdrop--visible');\n document.removeEventListener('keydown', this.#handleEsc);\n window.removeEventListener('scroll', this.#trackGeometry, true);\n window.removeEventListener('resize', this.#trackGeometry);\n this.#hostObserver?.disconnect();\n this.#hostObserver = null;\n // Leave the inline position:fixed geometry in place. The panel\n // slides off-screen via the transform animation (translateX\n // 100%); the next open() re-runs #trackGeometry which will\n // refresh top/left/height from the host's current rect.\n\n this.#body.innerHTML = '';\n this.#onClose?.();\n }\n\n // \u2500\u2500\u2500 Event handlers (arrow fns for stable `this`) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n #handleBackdropClick = () => { this.close(); };\n\n #handleCloseClick = () => { this.close(); };\n\n #handleEsc = (e) => {\n if (e.key === 'Escape') {\n e.stopPropagation();\n this.close();\n }\n };\n\n /** The host moves or resizes without any window event when a tile\n * splitter is dragged or a dock panel toggles, and the fixed geometry\n * would then stay at the old coordinates. Observe the host AND each\n * ancestor: a host can move without changing size of its own (a dock\n * panel toggling beside a fixed-width tile shrinks the content area,\n * an ancestor, and the tile only shifts). The callback runs\n * synchronously, after layout and before paint, so the panel follows\n * a drag without a frame of lag; it only writes the panel's own\n * `position: fixed` geometry, which is out of flow and resizes\n * nothing observed, so it cannot loop. */\n #observeHost() {\n this.#hostObserver = new ResizeObserver(this.#trackGeometry);\n for (let el = this.#container; el; el = el.parentElement) {\n this.#hostObserver.observe(el);\n }\n }\n\n /** Pin the panel to the right edge of its host's *visible* frame\n * using `position: fixed`. Re-run on every window scroll/resize and\n * every host resize (#observeHost) so the panel always overlays the\n * host correctly even when the host itself sits inside a scrolled\n * ancestor.\n *\n * UNDER A CSS `zoom` (the shell's content zoom scales a tile body),\n * `getBoundingClientRect()` answers in screen pixels, which already\n * carry the zoom, while the panel's own `top`/`left`/`height` are\n * CSS pixels that the same zoom multiplies again. Copied across\n * unchanged, every length was scaled twice: at 150% the panel landed\n * right of and below its host, at 50% it was half the host's height.\n * So the rect is divided by the panel's effective zoom\n * (`currentCSSZoom`: 1 when nothing above it zooms, `undefined` in an\n * engine that predates it), and the width subtracted is the panel's\n * laid-out one, which `min-width` may hold above the requested\n * width. Rounding happens in screen pixels, so at 100% the output is\n * exactly what it always was. */\n #trackGeometry = () => {\n if (!this.#open || !this.#panel || !this.#container) return;\n const r = this.#container.getBoundingClientRect();\n const zoom = this.#panel.currentCSSZoom || 1;\n const width = this.#panel.offsetWidth || this.#width;\n const s = this.#panel.style;\n s.position = 'fixed';\n s.top = `${Math.round(r.top) / zoom}px`;\n s.left = `${Math.round(r.right - width * zoom) / zoom}px`;\n s.right = 'auto';\n s.height = `${Math.round(r.height) / zoom}px`;\n };\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\r\n * ComponentBase (js_new)\r\n *\r\n * Ultra-lightweight base for leaf UI widgets rendered inside controllers/panels.\r\n * Handles props/state updates and deterministic teardown without storing data on\r\n * DOM elements.\r\n */\r\nexport class ComponentBase {\r\n constructor({ eventBus, logger } = {}) {\r\n if (new.target === ComponentBase) {\r\n throw new Error('ComponentBase is abstract \u2013 extend it instead');\r\n }\r\n\r\n this.eventBus = eventBus;\r\n this.logger = logger;\r\n this._mounted = false;\r\n }\r\n\r\n mount(/* container, props */) {\r\n throw new Error('ComponentBase.mount must be implemented by subclasses');\r\n }\r\n\r\n update(/* props */) {\r\n // default no-op for stateless components\r\n }\r\n\r\n dispose() {\r\n this._mounted = false;\r\n }\r\n}\r\n", "/**\n * action_dropdown.js\n *\n * Reusable dropdown component for action buttons (Add Scenario, Add Widget, etc).\n * Handles opening, closing, auto-positioning, keyboard navigation, and outside clicks.\n *\n * Auto-positioning: measures available viewport space around the trigger element\n * and picks the best direction (above/below) and alignment (left/right).\n * Adds scrolling when the menu would exceed available vertical space.\n */\n\n/** Viewport margin in px \u2014 dropdown stays this far from edges. */\nconst EDGE_MARGIN = 8;\n/** Gap between trigger and dropdown in px. */\nconst GAP = 8;\n\n/**\n * ActionDropdown - A reusable dropdown component with smart auto-positioning.\n *\n * Usage:\n * ```javascript\n * const dropdown = new ActionDropdown({\n * trigger: buttonElement,\n * options: [\n * { type: 'option1', label: 'Option 1', icon: 'icon_name', description: 'Description' },\n * { separator: true },\n * { type: 'option2', label: 'Option 2', icon: 'icon_name', description: 'Description' }\n * ],\n * onSelect: (option) => console.log('Selected:', option.type),\n * });\n *\n * // Control programmatically\n * dropdown.open();\n * dropdown.close();\n * dropdown.toggle();\n * dropdown.destroy();\n * ```\n *\n * \u2500\u2500 Separators \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * An entry `{ separator: true }` draws a divider instead of a button: a\n * `<div role=\"separator\" class=\"twm-action-dropdown-separator\">`, which takes\n * no focus, is never reached by the arrow keys (they move between the option\n * buttons only) and never reaches `onSelect` (it has no click handler). A\n * separator only separates: one at the start or the end of the list, or one\n * right after another, is dropped, so a list assembled from groups that may\n * come out empty never draws a stray or doubled rule.\n */\nexport class ActionDropdown {\n /**\n * @param {Object} config\n * @param {HTMLElement} config.trigger - Button element that triggers the dropdown\n * @param {Array} config.options - Array of option objects with { type, label, icon, description? },\n * or `{ separator: true }` for a divider between them\n * @param {Function} config.onSelect - Callback when an option is selected\n * @param {string} [config.className=''] - Additional CSS class for the dropdown\n * @param {string} [config.menuId] - Optional ID for the menu element\n */\n constructor(config) {\n this.trigger = config.trigger;\n this.options = config.options || [];\n this.onSelect = config.onSelect;\n this.className = config.className || '';\n this.menuId = config.menuId;\n\n this.isOpen = false;\n this.menuEl = null;\n\n this._boundHandleDocumentClick = this._handleDocumentClick.bind(this);\n this._boundHandleKeydown = this._handleKeydown.bind(this);\n this._boundHandleTriggerClick = this._handleTriggerClick.bind(this);\n\n this._init();\n }\n\n /**\n * Initialize the dropdown.\n * @private\n */\n _init() {\n if (!this.trigger) {\n console.warn('[ActionDropdown] No trigger element provided');\n return;\n }\n\n this._createMenu();\n this.trigger.addEventListener('click', this._boundHandleTriggerClick);\n }\n\n /**\n * Create the dropdown menu element.\n * @private\n */\n _createMenu() {\n const menu = document.createElement('div');\n menu.className = `twm-action-dropdown-menu ${this.className}`.trim();\n menu.setAttribute('role', 'menu');\n menu.hidden = true;\n\n if (this.menuId) {\n menu.id = this.menuId;\n }\n\n this._renderOptions(menu);\n\n // Append to body so position:fixed works without clipping.\n document.body.appendChild(menu);\n\n this.menuEl = menu;\n }\n\n /**\n * Render option buttons, and dividers for separator entries, into a\n * container element. `data-index` stays the entry's index in `options`.\n * @param {HTMLElement} container\n * @private\n */\n _renderOptions(container) {\n container.innerHTML = '';\n let pendingSeparator = false;\n this.options.forEach((opt, index) => {\n if (opt?.separator) {\n // Drawn only once an option follows it, and only if an option\n // came before it: leading, trailing and doubled separators\n // collapse.\n pendingSeparator = container.childElementCount > 0;\n return;\n }\n if (pendingSeparator) {\n const sep = document.createElement('div');\n sep.className = 'twm-action-dropdown-separator';\n sep.setAttribute('role', 'separator');\n container.appendChild(sep);\n pendingSeparator = false;\n }\n\n const optionBtn = document.createElement('button');\n optionBtn.type = 'button';\n optionBtn.className = 'twm-action-dropdown-option';\n optionBtn.dataset.index = index;\n optionBtn.dataset.type = opt.type;\n optionBtn.setAttribute('role', 'menuitem');\n\n const iconHtml = opt.icon\n ? `<span class=\"material-symbols-outlined twm-option-icon\">${opt.icon}</span>`\n : '';\n\n const descHtml = opt.description\n ? `<span class=\"twm-option-desc\">${opt.description}</span>`\n : '';\n\n optionBtn.innerHTML = `\n ${iconHtml}\n <span class=\"option-text\">\n <span class=\"twm-option-label\">${opt.label}</span>\n ${descHtml}\n </span>\n `;\n\n optionBtn.addEventListener('click', (e) => {\n e.preventDefault();\n e.stopPropagation();\n this._selectOption(opt);\n });\n\n container.appendChild(optionBtn);\n });\n }\n\n /**\n * Handle trigger button click.\n * @param {MouseEvent} e\n * @private\n */\n _handleTriggerClick(e) {\n e.preventDefault();\n e.stopPropagation();\n this.toggle();\n }\n\n /**\n * Toggle the dropdown open/closed.\n * @param {boolean} [forceState] - Optional forced state\n */\n toggle(forceState) {\n const shouldOpen = typeof forceState === 'boolean' ? forceState : !this.isOpen;\n if (shouldOpen) {\n this.open();\n } else {\n this.close();\n }\n }\n\n /**\n * Open the dropdown with auto-positioning.\n */\n open() {\n if (this.isOpen || !this.menuEl) return;\n\n this.isOpen = true;\n this.menuEl.hidden = false;\n\n // Position against trigger\n ActionDropdown.position(this.trigger, this.menuEl);\n\n // Animate in.\n //\n // OPTIONAL CHAINING, AND IT IS LOAD-BEARING. This callback runs a frame\n // after `open()` returned, and `destroy()` sets `this.menuEl = null`\n // (see below) \u2014 so a dropdown that is opened and then destroyed inside\n // one frame threw an uncaught `TypeError: Cannot read properties of\n // null` out of an animation-frame callback, where no caller has a stack\n // to catch it. That is not a hypothetical: it is what a user does every\n // time they open a picker and then click something that unmounts the\n // pane around it, and it was reproduced eighteen times in one run of a\n // consumer's settings suite. Nothing is lost by skipping the class \u2014 the\n // element it would have been added to no longer exists.\n requestAnimationFrame(() => {\n this.menuEl?.classList.add('visible');\n });\n\n // Update trigger state\n this.trigger?.classList.add('twm-is-open');\n if (this.trigger?.hasAttribute('aria-expanded')) {\n this.trigger.setAttribute('aria-expanded', 'true');\n }\n\n // Add document listeners\n document.addEventListener('click', this._boundHandleDocumentClick, true);\n document.addEventListener('keydown', this._boundHandleKeydown);\n\n // Focus first option for keyboard accessibility\n const firstOption = this.menuEl.querySelector('.twm-action-dropdown-option');\n firstOption?.focus();\n }\n\n /**\n * Close the dropdown.\n */\n close() {\n if (!this.isOpen) return;\n\n this.isOpen = false;\n\n if (this.menuEl) {\n this.menuEl.classList.remove('visible');\n this.menuEl.hidden = true;\n }\n\n // Update trigger state. `aria-expanded` belongs on the TRIGGER and has\n // to be written on every close, not only on the ones a click caused \u2014\n // an embedder that synced it from its own click handler was announcing\n // an expanded menu to a screen reader every time Escape or an outside\n // click dismissed one. The component knows when it closed; nothing else\n // reliably does.\n this.trigger?.classList.remove('twm-is-open');\n if (this.trigger?.hasAttribute('aria-expanded')) {\n this.trigger.setAttribute('aria-expanded', 'false');\n }\n\n // Remove document listeners\n document.removeEventListener('click', this._boundHandleDocumentClick, true);\n document.removeEventListener('keydown', this._boundHandleKeydown);\n }\n\n /**\n * Handle document click (close on outside click).\n * @param {MouseEvent} e\n * @private\n */\n _handleDocumentClick(e) {\n if (!this.isOpen) return;\n\n // Don't close if clicking inside dropdown or trigger\n if (this.menuEl?.contains(e.target) || this.trigger?.contains(e.target)) {\n return;\n }\n\n this.close();\n }\n\n /**\n * Handle keyboard events.\n * @param {KeyboardEvent} e\n * @private\n */\n _handleKeydown(e) {\n if (e.key === 'Escape') {\n // AND NOBODY ELSE GETS IT. An open dropdown is the innermost thing\n // on screen, so Escape means \"close this\" and nothing further \u2014\n // but the event was left to bubble, and inside a `ManagedWindow`\n // (which binds its own Escape to dismiss) that meant one keystroke\n // closed the dropdown AND the dialog around it. The user loses a\n // form they were filling in because they changed their mind about\n // one field.\n e.preventDefault();\n e.stopPropagation();\n this.close();\n this.trigger?.focus();\n return;\n }\n\n // Arrow key navigation\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n e.preventDefault();\n this._navigateOptions(e.key === 'ArrowDown' ? 1 : -1);\n }\n }\n\n /**\n * Navigate options with arrow keys.\n * @param {number} direction - 1 for down, -1 for up\n * @private\n */\n _navigateOptions(direction) {\n const options = this.menuEl?.querySelectorAll('.twm-action-dropdown-option');\n if (!options || options.length === 0) return;\n\n const currentIndex = Array.from(options).findIndex(opt => opt === document.activeElement);\n let nextIndex = currentIndex + direction;\n\n if (nextIndex < 0) nextIndex = options.length - 1;\n if (nextIndex >= options.length) nextIndex = 0;\n\n options[nextIndex]?.focus();\n }\n\n /**\n * Handle option selection.\n * @param {Object} option - The selected option\n * @private\n */\n _selectOption(option) {\n this.close();\n\n if (this.onSelect) {\n this.onSelect(option);\n }\n }\n\n /**\n * Update the options dynamically.\n * @param {Array} newOptions - New options array\n */\n setOptions(newOptions) {\n this.options = newOptions;\n if (this.menuEl) {\n this._renderOptions(this.menuEl);\n }\n }\n\n /**\n * Destroy the dropdown and clean up.\n */\n destroy() {\n this.close();\n\n // Remove trigger listener\n this.trigger?.removeEventListener('click', this._boundHandleTriggerClick);\n\n // Remove menu element\n this.menuEl?.remove();\n this.menuEl = null;\n\n this.trigger = null;\n this.options = [];\n this.onSelect = null;\n }\n\n // =========================================================================\n // STATIC \u2014 shared auto-positioning for any trigger + menu pair\n // =========================================================================\n\n /**\n * Position a dropdown menu relative to a trigger element.\n * Uses `position: fixed` to avoid overflow clipping.\n * Picks above/below based on available space, aligns left/right edge,\n * and applies `max-height` + `overflow-y: auto` when the menu is taller\n * than the available space.\n *\n * @param {HTMLElement} trigger - The element the dropdown opens from\n * @param {HTMLElement} menu - The dropdown menu element to position\n */\n static position(trigger, menu) {\n if (!trigger || !menu) return;\n\n // Reset any prior inline position so natural size can be measured.\n Object.assign(menu.style, {\n position: 'fixed',\n top: 'auto',\n bottom: 'auto',\n left: 'auto',\n right: 'auto',\n maxHeight: '',\n overflowY: ''\n });\n\n const triggerRect = trigger.getBoundingClientRect();\n const menuRect = menu.getBoundingClientRect();\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n\n // \u2500\u2500 Vertical: prefer below, flip above if insufficient room \u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const spaceBelow = vh - triggerRect.bottom - GAP - EDGE_MARGIN;\n const spaceAbove = triggerRect.top - GAP - EDGE_MARGIN;\n\n let top;\n let maxHeight;\n\n if (menuRect.height <= spaceBelow) {\n // Fits below \u2014 use natural height.\n top = triggerRect.bottom + GAP;\n maxHeight = spaceBelow;\n } else if (menuRect.height <= spaceAbove) {\n // Fits above \u2014 use natural height.\n top = triggerRect.top - GAP - menuRect.height;\n maxHeight = spaceAbove;\n } else if (spaceBelow >= spaceAbove) {\n // More space below \u2014 scroll.\n top = triggerRect.bottom + GAP;\n maxHeight = spaceBelow;\n } else {\n // More space above \u2014 scroll.\n maxHeight = spaceAbove;\n top = EDGE_MARGIN;\n }\n\n // \u2500\u2500 Horizontal: align left-edge with trigger, flip if off-screen \u2500\u2500\n let left = triggerRect.left;\n\n if (left + menuRect.width > vw - EDGE_MARGIN) {\n // Align right edges instead.\n left = triggerRect.right - menuRect.width;\n }\n\n // Still off-screen? Clamp to edges.\n left = Math.max(EDGE_MARGIN, Math.min(left, vw - menuRect.width - EDGE_MARGIN));\n\n Object.assign(menu.style, {\n position: 'fixed',\n top: `${Math.round(top)}px`,\n left: `${Math.round(left)}px`,\n maxHeight: `${Math.round(maxHeight)}px`,\n overflowY: 'auto'\n });\n }\n}\n\nexport default ActionDropdown;\n", "/**\n * SlideOutPanel\n *\n * Reusable inline slide-out config panel that overlays its host container\n * from the right edge. Used by pages to show element-specific config\n * (plot config, widget config, ETL node config) without the global right panel.\n *\n * Host container must have `position: relative; overflow: hidden`.\n */\nimport { ComponentBase } from '../base/component_base.js';\n\nexport class SlideOutPanel extends ComponentBase {\n\n /** @type {HTMLElement} */ #container = null;\n /** @type {HTMLElement} */ #backdrop = null;\n /** @type {HTMLElement} */ #panel = null;\n /** @type {HTMLElement} */ #headerIcon = null;\n /** @type {HTMLElement} */ #headerTitle = null;\n /** @type {HTMLElement} */ #body = null;\n /** @type {number} */ #width;\n /** @type {Function|null} */#onClose;\n /** @type {boolean} */ #open = false;\n /** @type {ResizeObserver|null} */ #hostObserver = null;\n\n /**\n * @param {object} opts\n * @param {number} [opts.width=380] Panel width in px.\n * @param {Function} [opts.onClose] Called after the panel closes.\n */\n constructor({ width = 700, onClose } = {}) {\n super();\n this.#width = width;\n this.#onClose = onClose ?? null;\n }\n\n // \u2500\u2500\u2500 Lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Mount the panel DOM into a host container.\n * @param {HTMLElement} container Must have position:relative; overflow:hidden.\n */\n mount(container) {\n if (this._mounted) return;\n this.#container = container;\n\n // Backdrop (transparent click-catcher)\n this.#backdrop = document.createElement('div');\n this.#backdrop.className = 'twm-slide-out-panel__backdrop';\n this.#backdrop.addEventListener('click', this.#handleBackdropClick);\n\n // Panel\n this.#panel = document.createElement('div');\n this.#panel.className = 'twm-slide-out-panel';\n this.#panel.style.width = `${this.#width}px`;\n\n // Header\n const header = document.createElement('div');\n header.className = 'twm-slide-out-panel__header';\n\n this.#headerIcon = document.createElement('span');\n this.#headerIcon.className = 'twm-slide-out-panel__icon material-symbols-outlined';\n\n this.#headerTitle = document.createElement('span');\n this.#headerTitle.className = 'twm-slide-out-panel__title';\n\n const closeBtn = document.createElement('button');\n closeBtn.className = 'twm-slide-out-panel__close twm-btn-icon';\n closeBtn.type = 'button';\n closeBtn.title = 'Close';\n closeBtn.innerHTML = '<span class=\"material-symbols-outlined\">close</span>';\n closeBtn.addEventListener('click', this.#handleCloseClick);\n\n header.append(this.#headerIcon, this.#headerTitle, closeBtn);\n\n // Body (scrollable content area)\n this.#body = document.createElement('div');\n this.#body.className = 'twm-slide-out-panel__body';\n\n this.#panel.append(header, this.#body);\n this.#container.append(this.#backdrop, this.#panel);\n\n this._mounted = true;\n }\n\n dispose() {\n this.close();\n this.#backdrop?.remove();\n this.#panel?.remove();\n this.#backdrop = null;\n this.#panel = null;\n this.#body = null;\n this.#container = null;\n super.dispose();\n }\n\n // \u2500\u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** The inner content element \u2014 callers render config into this. */\n get contentEl() { return this.#body; }\n\n get isOpen() { return this.#open; }\n\n /**\n * Open (or re-open) the panel with a new title/icon.\n * Clears previous content. Returns contentEl for rendering.\n * @param {string} title\n * @param {string} icon Material Symbols icon name.\n * @returns {HTMLElement} contentEl\n */\n open(title, icon) {\n if (!this._mounted) return null;\n\n this.#body.innerHTML = '';\n this.#headerIcon.textContent = icon || '';\n this.#headerIcon.style.display = icon ? '' : 'none';\n this.#headerTitle.textContent = title || '';\n\n if (!this.#open) {\n this.#open = true;\n this.#backdrop.classList.add('twm-slide-out-panel__backdrop--visible');\n this.#panel.classList.add('twm-slide-out-panel--open');\n document.addEventListener('keydown', this.#handleEsc);\n\n // The host may be a scroll container \u2014 absolutely-positioned\n // descendants of one DO scroll along with the content\n // (Web spec: a position:absolute element is clipped + offset\n // by its containing scroll context). We escape that by\n // switching the panel to `position: fixed` while open and\n // tracking the host's bounding rect on scroll / resize, so\n // the panel stays pinned to the host's visible frame at\n // full height regardless of how the user scrolls inside.\n this.#trackGeometry();\n window.addEventListener('scroll', this.#trackGeometry, true);\n window.addEventListener('resize', this.#trackGeometry);\n this.#observeHost();\n }\n\n return this.#body;\n }\n\n /** Close the panel and clear content. */\n close() {\n if (!this.#open) return;\n this.#open = false;\n\n this.#panel.classList.remove('twm-slide-out-panel--open');\n this.#backdrop.classList.remove('twm-slide-out-panel__backdrop--visible');\n document.removeEventListener('keydown', this.#handleEsc);\n window.removeEventListener('scroll', this.#trackGeometry, true);\n window.removeEventListener('resize', this.#trackGeometry);\n this.#hostObserver?.disconnect();\n this.#hostObserver = null;\n // Leave the inline position:fixed geometry in place. The panel\n // slides off-screen via the transform animation (translateX\n // 100%); the next open() re-runs #trackGeometry which will\n // refresh top/left/height from the host's current rect.\n\n this.#body.innerHTML = '';\n this.#onClose?.();\n }\n\n // \u2500\u2500\u2500 Event handlers (arrow fns for stable `this`) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n #handleBackdropClick = () => { this.close(); };\n\n #handleCloseClick = () => { this.close(); };\n\n #handleEsc = (e) => {\n if (e.key === 'Escape') {\n e.stopPropagation();\n this.close();\n }\n };\n\n /** The host moves or resizes without any window event when a tile\n * splitter is dragged or a dock panel toggles, and the fixed geometry\n * would then stay at the old coordinates. Observe the host AND each\n * ancestor: a host can move without changing size of its own (a dock\n * panel toggling beside a fixed-width tile shrinks the content area,\n * an ancestor, and the tile only shifts). The callback runs\n * synchronously, after layout and before paint, so the panel follows\n * a drag without a frame of lag; it only writes the panel's own\n * `position: fixed` geometry, which is out of flow and resizes\n * nothing observed, so it cannot loop. */\n #observeHost() {\n this.#hostObserver = new ResizeObserver(this.#trackGeometry);\n for (let el = this.#container; el; el = el.parentElement) {\n this.#hostObserver.observe(el);\n }\n }\n\n /** Pin the panel to the right edge of its host's *visible* frame\n * using `position: fixed`. Re-run on every window scroll/resize and\n * every host resize (#observeHost) so the panel always overlays the\n * host correctly even when the host itself sits inside a scrolled\n * ancestor.\n *\n * UNDER A CSS `zoom` (the shell's content zoom scales a tile body),\n * `getBoundingClientRect()` answers in screen pixels, which already\n * carry the zoom, while the panel's own `top`/`left`/`height` are\n * CSS pixels that the same zoom multiplies again. Copied across\n * unchanged, every length was scaled twice: at 150% the panel landed\n * right of and below its host, at 50% it was half the host's height.\n * So the rect is divided by the panel's effective zoom\n * (`currentCSSZoom`: 1 when nothing above it zooms, `undefined` in an\n * engine that predates it), and the width subtracted is the panel's\n * laid-out one, which `min-width` may hold above the requested\n * width. Rounding happens in screen pixels, so at 100% the output is\n * exactly what it always was.\n *\n * NEVER WIDER THAN THE HOST (BUG-0035). The right edge is pinned, so a\n * panel wider than its host (700px by default, `min-width: 400px`)\n * reached past the host's left edge across the neighbouring pane. The\n * host's width, in the panel's CSS pixels, goes on the panel as\n * `--twm-slide-out-host-w`, which the stylesheet uses as `max-width`\n * and as the ceiling of the 400px `min-width`; it is set BEFORE the\n * width is read so the read sees the capped layout, and the width is\n * capped here too for an engine that lays nothing out. */\n #trackGeometry = () => {\n if (!this.#open || !this.#panel || !this.#container) return;\n const r = this.#container.getBoundingClientRect();\n const zoom = this.#panel.currentCSSZoom || 1;\n const hostWidth = r.width / zoom;\n const s = this.#panel.style;\n s.setProperty('--twm-slide-out-host-w', `${hostWidth}px`);\n const width = Math.min(this.#panel.offsetWidth || this.#width, hostWidth);\n s.position = 'fixed';\n s.top = `${Math.round(r.top) / zoom}px`;\n s.left = `${Math.round(r.right - width * zoom) / zoom}px`;\n s.right = 'auto';\n s.height = `${Math.round(r.height) / zoom}px`;\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/flexdesk.css
CHANGED
|
@@ -5678,7 +5678,11 @@ button.twm-about-dialog__btn--close:focus {
|
|
|
5678
5678
|
border-left: 1px solid #333;
|
|
5679
5679
|
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.35);
|
|
5680
5680
|
|
|
5681
|
-
|
|
5681
|
+
/* Never wider than the host (BUG-0035). `#trackGeometry` sets
|
|
5682
|
+
* `--twm-slide-out-host-w` to the host's width while open; the 400px
|
|
5683
|
+
* floor gives way to it too, since `min-width` beats `max-width`. */
|
|
5684
|
+
min-width: min(400px, var(--twm-slide-out-host-w, 400px));
|
|
5685
|
+
max-width: var(--twm-slide-out-host-w, none);
|
|
5682
5686
|
|
|
5683
5687
|
transform: translateX(100%);
|
|
5684
5688
|
transition: transform 200ms ease-out, visibility 0s linear 200ms;
|
|
@@ -5712,6 +5716,19 @@ button.twm-about-dialog__btn--close:focus {
|
|
|
5712
5716
|
flex-shrink: 0;
|
|
5713
5717
|
margin-left: auto;
|
|
5714
5718
|
}
|
|
5719
|
+
/* Both icons at 18px (BUG-0035). Two classes each, for the Material Symbols
|
|
5720
|
+
* tie — see `.twm-leaf__icon`: a single-class rule loses to Google's
|
|
5721
|
+
* `.material-symbols-outlined { font-size: 24px }`, which loads later. */
|
|
5722
|
+
.twm-slide-out-panel__icon.material-symbols-outlined {
|
|
5723
|
+
flex-shrink: 0;
|
|
5724
|
+
font-size: 18px;
|
|
5725
|
+
line-height: 1;
|
|
5726
|
+
color: var(--text-secondary);
|
|
5727
|
+
}
|
|
5728
|
+
.twm-slide-out-panel__close .material-symbols-outlined {
|
|
5729
|
+
font-size: 18px;
|
|
5730
|
+
line-height: 1;
|
|
5731
|
+
}
|
|
5715
5732
|
/* Scrollable body */
|
|
5716
5733
|
.twm-slide-out-panel__body {
|
|
5717
5734
|
flex: 1;
|
package/dist/tiles.js
CHANGED
package/dist/widgets.js
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
ActionDropdown,
|
|
23
23
|
ComponentBase,
|
|
24
24
|
SlideOutPanel
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-RKNFBX5K.js";
|
|
26
26
|
import {
|
|
27
27
|
hideContextMenu,
|
|
28
28
|
showContextMenu
|
|
@@ -3033,7 +3033,7 @@ var NotificationHistory = class {
|
|
|
3033
3033
|
this.onOpen = typeof onOpen === "function" ? onOpen : null;
|
|
3034
3034
|
this.triggerEl = null;
|
|
3035
3035
|
this.badgeEl = null;
|
|
3036
|
-
this.
|
|
3036
|
+
this.unreadCount = 0;
|
|
3037
3037
|
this._popoverCleanup = null;
|
|
3038
3038
|
this._subscriptions = [];
|
|
3039
3039
|
}
|
|
@@ -3064,8 +3064,13 @@ var NotificationHistory = class {
|
|
|
3064
3064
|
this.#updateBadge();
|
|
3065
3065
|
return;
|
|
3066
3066
|
}
|
|
3067
|
-
this.
|
|
3068
|
-
this
|
|
3067
|
+
this.unreadCount = 0;
|
|
3068
|
+
const unsubscribe = this.notificationCenter?.subscribe?.(() => {
|
|
3069
|
+
this.unreadCount += 1;
|
|
3070
|
+
this.#updateBadge();
|
|
3071
|
+
});
|
|
3072
|
+
if (typeof unsubscribe === "function") this._subscriptions.push(unsubscribe);
|
|
3073
|
+
this.#updateBadge();
|
|
3069
3074
|
}
|
|
3070
3075
|
unmount() {
|
|
3071
3076
|
this.#closePanel();
|
|
@@ -3079,21 +3084,6 @@ var NotificationHistory = class {
|
|
|
3079
3084
|
this.triggerEl = null;
|
|
3080
3085
|
this.badgeEl = null;
|
|
3081
3086
|
}
|
|
3082
|
-
#subscribeEvents() {
|
|
3083
|
-
if (!this.eventBus) return;
|
|
3084
|
-
const badgeUpdate = () => queueMicrotask(() => this.#updateBadge());
|
|
3085
|
-
const events = [
|
|
3086
|
-
"toast:show",
|
|
3087
|
-
"notification:show",
|
|
3088
|
-
"log:entry",
|
|
3089
|
-
"workspace:save:failed",
|
|
3090
|
-
"workspace:import:failed"
|
|
3091
|
-
];
|
|
3092
|
-
events.forEach((name) => {
|
|
3093
|
-
const handle = this.eventBus.on(name, badgeUpdate);
|
|
3094
|
-
this._subscriptions.push(() => handle?.dispose?.());
|
|
3095
|
-
});
|
|
3096
|
-
}
|
|
3097
3087
|
/** The Alerts tab, asked for on the bus — or the embedder's own verb. */
|
|
3098
3088
|
#openAlerts() {
|
|
3099
3089
|
if (this.onOpen) {
|
|
@@ -3108,7 +3098,7 @@ var NotificationHistory = class {
|
|
|
3108
3098
|
}
|
|
3109
3099
|
#updateBadge() {
|
|
3110
3100
|
const historyLen = this.notificationCenter?.history?.length ?? 0;
|
|
3111
|
-
const unread = this.alertLog ? this.alertLog.unreadCount() : Math.
|
|
3101
|
+
const unread = this.alertLog ? this.alertLog.unreadCount() : Math.min(this.unreadCount, historyLen);
|
|
3112
3102
|
if (!this.badgeEl) return;
|
|
3113
3103
|
if (this.alertLog) {
|
|
3114
3104
|
const level = unread > 0 ? this.alertLog.worstUnread() : null;
|
|
@@ -3140,7 +3130,7 @@ var NotificationHistory = class {
|
|
|
3140
3130
|
}
|
|
3141
3131
|
#openPanel() {
|
|
3142
3132
|
this.#closePanel();
|
|
3143
|
-
this.
|
|
3133
|
+
this.unreadCount = 0;
|
|
3144
3134
|
this.#updateBadge();
|
|
3145
3135
|
const panel = document.createElement("div");
|
|
3146
3136
|
panel.className = "twm-notification-history__panel";
|
|
@@ -3155,7 +3145,7 @@ var NotificationHistory = class {
|
|
|
3155
3145
|
header.querySelector(".twm-notification-history__close-btn").addEventListener("click", () => this.#closePanel());
|
|
3156
3146
|
header.querySelector(".twm-notification-history__clear-btn").addEventListener("click", () => {
|
|
3157
3147
|
this.notificationCenter.history.splice(0);
|
|
3158
|
-
this.
|
|
3148
|
+
this.unreadCount = 0;
|
|
3159
3149
|
this.#updateBadge();
|
|
3160
3150
|
this.#renderList(list);
|
|
3161
3151
|
});
|
|
@@ -6699,6 +6689,7 @@ var SUPPRESSED_NAMESPACE_TITLES = /* @__PURE__ */ new Set(["bootstrap", "app"]);
|
|
|
6699
6689
|
var NotificationCenter = class _NotificationCenter extends ComponentBase {
|
|
6700
6690
|
#items;
|
|
6701
6691
|
#summaryEl;
|
|
6692
|
+
#historyListeners;
|
|
6702
6693
|
constructor({
|
|
6703
6694
|
eventBus,
|
|
6704
6695
|
logger,
|
|
@@ -6729,6 +6720,7 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
|
|
|
6729
6720
|
this.eventDisposers = [];
|
|
6730
6721
|
this.#items = /* @__PURE__ */ new Map();
|
|
6731
6722
|
this.#summaryEl = null;
|
|
6723
|
+
this.#historyListeners = /* @__PURE__ */ new Set();
|
|
6732
6724
|
this.container = null;
|
|
6733
6725
|
this.rootEl = null;
|
|
6734
6726
|
this.listEl = null;
|
|
@@ -6777,6 +6769,7 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
|
|
|
6777
6769
|
dispose() {
|
|
6778
6770
|
this.#clearTimers();
|
|
6779
6771
|
this.#unsubscribeEvents();
|
|
6772
|
+
this.#historyListeners.clear();
|
|
6780
6773
|
if (this.eventBus && getToastBus() === this.eventBus) setToastBus(null);
|
|
6781
6774
|
this.#items.forEach((entry) => entry.bar?.dispose());
|
|
6782
6775
|
this.#items.clear();
|
|
@@ -6809,7 +6802,7 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
|
|
|
6809
6802
|
if (idx !== -1) {
|
|
6810
6803
|
const [removed] = this.notifications.splice(idx, 1);
|
|
6811
6804
|
this.#stopTimer(removed.id);
|
|
6812
|
-
this.#
|
|
6805
|
+
this.#recordDismissal(removed, "overflow");
|
|
6813
6806
|
}
|
|
6814
6807
|
}
|
|
6815
6808
|
this.#recordHistory(normalized);
|
|
@@ -6824,13 +6817,13 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
|
|
|
6824
6817
|
const h = this.held.findIndex((item) => item.id === notificationId);
|
|
6825
6818
|
if (h === -1) return false;
|
|
6826
6819
|
const [gone] = this.held.splice(h, 1);
|
|
6827
|
-
this.#
|
|
6820
|
+
this.#recordDismissal(gone, reason);
|
|
6828
6821
|
this.#render();
|
|
6829
6822
|
return true;
|
|
6830
6823
|
}
|
|
6831
6824
|
const [removed] = this.notifications.splice(idx, 1);
|
|
6832
6825
|
this.#stopTimer(notificationId);
|
|
6833
|
-
this.#
|
|
6826
|
+
this.#recordDismissal(removed, reason);
|
|
6834
6827
|
this.#rebalance();
|
|
6835
6828
|
this.#render();
|
|
6836
6829
|
return true;
|
|
@@ -7057,12 +7050,40 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
|
|
|
7057
7050
|
this.activeTimers.forEach((timer) => clearTimeout(timer));
|
|
7058
7051
|
this.activeTimers.clear();
|
|
7059
7052
|
}
|
|
7053
|
+
/**
|
|
7054
|
+
* Hear of every entry added to `history`, by `show()`, `warn()`/`error()`/
|
|
7055
|
+
* `info()`/`success()` and the bus alike: `listener(entry, center)`, once
|
|
7056
|
+
* per toast, after the entry is in `history`. A dismissal changes the
|
|
7057
|
+
* toast's own entry and is not an entry of its own. Answers the unsubscribe.
|
|
7058
|
+
*/
|
|
7059
|
+
subscribe(listener) {
|
|
7060
|
+
if (typeof listener !== "function") return () => {
|
|
7061
|
+
};
|
|
7062
|
+
this.#historyListeners.add(listener);
|
|
7063
|
+
return () => {
|
|
7064
|
+
this.#historyListeners.delete(listener);
|
|
7065
|
+
};
|
|
7066
|
+
}
|
|
7060
7067
|
#recordHistory(entry) {
|
|
7061
7068
|
if (entry.skipHistory) return;
|
|
7062
7069
|
this.history.push(entry);
|
|
7063
7070
|
if (this.history.length > this.historyLimit) {
|
|
7064
7071
|
this.history.shift();
|
|
7065
7072
|
}
|
|
7073
|
+
[...this.#historyListeners].forEach((listener) => {
|
|
7074
|
+
try {
|
|
7075
|
+
listener(entry, this);
|
|
7076
|
+
} catch (err) {
|
|
7077
|
+
this.logger?.warn?.("twm-notification-center", "History listener failed", { err });
|
|
7078
|
+
}
|
|
7079
|
+
});
|
|
7080
|
+
}
|
|
7081
|
+
/** A toast that left marks its own entry: one toast, one entry. */
|
|
7082
|
+
#recordDismissal(notification, reason) {
|
|
7083
|
+
if (notification.skipHistory) return;
|
|
7084
|
+
const at = this.history.findLastIndex((entry) => entry.id === notification.id);
|
|
7085
|
+
if (at === -1) return;
|
|
7086
|
+
this.history[at] = { ...notification, dismissed: true, reason };
|
|
7066
7087
|
}
|
|
7067
7088
|
/** A progress toast that has not been given an outcome yet. */
|
|
7068
7089
|
static #isRunning(notification) {
|
|
@@ -7450,8 +7471,9 @@ function thumbGeometry(scrollSize, clientSize, scrollPos, trackLen) {
|
|
|
7450
7471
|
return { size, pos };
|
|
7451
7472
|
}
|
|
7452
7473
|
function barCovered(el3, stacks) {
|
|
7474
|
+
const passThrough = (h) => h.closest?.(".twm-autoscroll-layer") || h.closest?.(".twm-resize-handle");
|
|
7453
7475
|
return stacks.some((stack) => {
|
|
7454
|
-
const top = (stack || []).find((h) => !h
|
|
7476
|
+
const top = (stack || []).find((h) => !passThrough(h));
|
|
7455
7477
|
return Boolean(top) && top !== el3 && !el3.contains(top);
|
|
7456
7478
|
});
|
|
7457
7479
|
}
|
|
@@ -7658,6 +7680,16 @@ function installAutoScrollbars(root = document.body, { exclude = null } = {}) {
|
|
|
7658
7680
|
for (const el3 of managed.keys()) place(el3);
|
|
7659
7681
|
});
|
|
7660
7682
|
};
|
|
7683
|
+
const sweep = new MutationObserver((mutations) => {
|
|
7684
|
+
if (!managed.size || !mutations.some((m) => m.removedNodes.length)) return;
|
|
7685
|
+
for (const el3 of [...managed.keys()]) {
|
|
7686
|
+
if (!el3.isConnected) {
|
|
7687
|
+
drop(el3);
|
|
7688
|
+
hoverChain.delete(el3);
|
|
7689
|
+
}
|
|
7690
|
+
}
|
|
7691
|
+
});
|
|
7692
|
+
sweep.observe(root, { childList: true, subtree: true });
|
|
7661
7693
|
doc.addEventListener("mouseover", onOver, true);
|
|
7662
7694
|
doc.addEventListener("mouseout", onLeaveWindow, true);
|
|
7663
7695
|
doc.addEventListener("scroll", onScroll, true);
|
|
@@ -7670,6 +7702,7 @@ function installAutoScrollbars(root = document.body, { exclude = null } = {}) {
|
|
|
7670
7702
|
doc.removeEventListener("scroll", onScroll, true);
|
|
7671
7703
|
win.removeEventListener("resize", onResize);
|
|
7672
7704
|
doc.removeEventListener("focusin", onResize, true);
|
|
7705
|
+
sweep.disconnect();
|
|
7673
7706
|
for (const el3 of [...managed.keys()]) drop(el3);
|
|
7674
7707
|
root.classList.remove(ROOT_CLASS);
|
|
7675
7708
|
}
|