anentrypoint-design 0.0.381 → 0.0.382

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.
Files changed (49) hide show
  1. package/dist/247420.css +228 -162
  2. package/dist/247420.js +24 -24
  3. package/package.json +1 -1
  4. package/src/components/content/avatar.js +31 -0
  5. package/src/components/content/charts.js +50 -0
  6. package/src/components/content/cli.js +46 -0
  7. package/src/components/content/feedback.js +77 -0
  8. package/src/components/content/fields.js +136 -0
  9. package/src/components/content/hero.js +146 -0
  10. package/src/components/content/lists.js +85 -0
  11. package/src/components/content/panel.js +79 -0
  12. package/src/components/content/row.js +115 -0
  13. package/src/components/content/table.js +97 -0
  14. package/src/components/content/views.js +81 -0
  15. package/src/components/content.js +29 -861
  16. package/src/components/editor-primitives/batch.js +60 -0
  17. package/src/components/editor-primitives/chrome.js +146 -0
  18. package/src/components/editor-primitives/collapse.js +46 -0
  19. package/src/components/editor-primitives/context-menu.js +127 -0
  20. package/src/components/editor-primitives/diagnostics.js +44 -0
  21. package/src/components/editor-primitives/focus-trap.js +26 -0
  22. package/src/components/editor-primitives/json-viewer.js +123 -0
  23. package/src/components/editor-primitives/layout.js +89 -0
  24. package/src/components/editor-primitives/modals.js +89 -0
  25. package/src/components/editor-primitives/pager.js +74 -0
  26. package/src/components/editor-primitives/property-grid.js +57 -0
  27. package/src/components/editor-primitives/shared.js +18 -0
  28. package/src/components/editor-primitives/split-panel.js +101 -0
  29. package/src/components/editor-primitives/toast.js +59 -0
  30. package/src/components/editor-primitives/tree.js +75 -0
  31. package/src/components/editor-primitives.js +35 -1048
  32. package/src/components/overlay-primitives/approval-prompt.js +38 -0
  33. package/src/components/overlay-primitives/auth-modal.js +89 -0
  34. package/src/components/overlay-primitives/command-palette.js +101 -0
  35. package/src/components/overlay-primitives/emoji-picker.js +103 -0
  36. package/src/components/overlay-primitives/floating.js +146 -0
  37. package/src/components/overlay-primitives/full-screen.js +45 -0
  38. package/src/components/overlay-primitives/menus.js +131 -0
  39. package/src/components/overlay-primitives/popover.js +51 -0
  40. package/src/components/overlay-primitives/roving-menu.js +73 -0
  41. package/src/components/overlay-primitives/settings-popover.js +85 -0
  42. package/src/components/overlay-primitives/tooltip.js +55 -0
  43. package/src/components/overlay-primitives.js +33 -855
  44. package/src/css/app-shell/chat-polish.css +39 -32
  45. package/src/css/app-shell/data-density.css +25 -21
  46. package/src/css/app-shell/files.css +37 -29
  47. package/src/css/app-shell/kits-appended.css +73 -50
  48. package/src/css/app-shell/responsive.css +47 -29
  49. package/src/kits/os/freddie-dashboard.css +2 -2
@@ -0,0 +1,131 @@
1
+ // Trigger-anchored menus — Dropdown (role=menuitem action list),
2
+ // PermissionMenu (role=menuitemcheckbox category toggles) and MenuButton
3
+ // (role=menuitemradio single-select with a check mark). All three share the
4
+ // open/close/outside-click/roving-nav/typeahead machine in ./roving-menu.js
5
+ // and differ only in the DOM/roles they build for their own menu element.
6
+
7
+ import * as webjsx from '../../../vendor/webjsx/index.js';
8
+ import { Icon } from '../shell.js';
9
+ import { useRovingMenu } from './roving-menu.js';
10
+ const h = webjsx.createElement;
11
+
12
+ // Dropdown — button trigger + portaled menu.
13
+ export function Dropdown({ trigger, items = [], onSelect, placement = 'bottom-start', ariaLabel } = {}) {
14
+ const menu = useRovingMenu({ itemSelector: '[role="menuitem"]:not([aria-disabled="true"])', items, typeahead: true, placement });
15
+ const select = (it) => { if (it.disabled || it.separator) return; if (onSelect) onSelect(it.id, it); menu.close(); };
16
+ const buildMenuEl = () => {
17
+ const el = document.createElement('div');
18
+ el.className = 'ds-popover ds-dropdown-menu';
19
+ el.setAttribute('role', 'menu');
20
+ if (ariaLabel) el.setAttribute('aria-label', ariaLabel);
21
+ const tree = h('div', { class: 'ds-dropdown-list' },
22
+ ...items.map((it, i) => it.separator
23
+ ? h('div', { key: 'sep' + i, class: 'ds-dropdown-separator', role: 'separator' })
24
+ : h('button', {
25
+ key: it.id || i, type: 'button', role: 'menuitem',
26
+ class: 'ds-dropdown-item' + (it.danger ? ' is-danger' : ''),
27
+ 'aria-disabled': it.disabled ? 'true' : 'false',
28
+ tabindex: '-1', onclick: () => select(it),
29
+ },
30
+ it.glyph != null ? h('span', { class: 'ds-dropdown-glyph', 'aria-hidden': 'true' }, it.glyph) : null,
31
+ h('span', { class: 'ds-dropdown-label' }, it.label)
32
+ )));
33
+ webjsx.applyDiff(el, tree);
34
+ return el;
35
+ };
36
+ const onTrigClick = () => menu.onTrigClick(buildMenuEl);
37
+ const onTrigKey = (e) => menu.onTrigKey(e, buildMenuEl);
38
+ const refFn = menu.refFn('_dsDropdown');
39
+ const child = (typeof trigger === 'function') ? trigger() : trigger;
40
+ const wireRef = (el) => { refFn(el); if (el) { el.addEventListener('click', onTrigClick); el.addEventListener('keydown', onTrigKey); } };
41
+ return (child && child.type)
42
+ ? webjsx.createElement(child.type, { ...(child.props || {}), ref: wireRef }, ...(child.children || []))
43
+ : h('button', { type: 'button', class: 'ds-dropdown-trigger', ref: wireRef }, child || 'Menu');
44
+ }
45
+
46
+ // PermissionMenu — a role=menu of role=menuitemcheckbox rows, one per
47
+ // category, with roving tabindex + Arrow-up/down/Home/End navigation and
48
+ // Escape-closes-and-restores-focus, plus "Approve all"/"Revoke all" actions.
49
+ // Mirrors Dropdown's own open/close + outside-click wiring (a portaled menu
50
+ // element, a document-level mousedown listener, focus restored to the
51
+ // trigger on close) rather than reimplementing that plumbing.
52
+ export function PermissionMenu({ trigger, categories = [], approved = [], onToggle, onToggleAll, placement = 'bottom-start', ariaLabel = 'Permissions' } = {}) {
53
+ const isApproved = (id) => approved.indexOf(id) !== -1;
54
+ const menu = useRovingMenu({ itemSelector: '[role="menuitemcheckbox"]', items: categories, getLabel: (cat) => cat.label || cat.id, typeahead: true, placement });
55
+ const toggle = (cat) => { if (onToggle) onToggle(cat.id, !isApproved(cat.id)); };
56
+ const buildMenuEl = () => {
57
+ const el = document.createElement('div');
58
+ el.className = 'ds-popover ov-perm-menu';
59
+ el.setAttribute('role', 'menu');
60
+ el.setAttribute('aria-label', ariaLabel);
61
+ const rows = categories.map((cat, i) => h('button', {
62
+ key: cat.id || i, type: 'button', role: 'menuitemcheckbox',
63
+ 'aria-checked': isApproved(cat.id) ? 'true' : 'false',
64
+ class: 'ov-perm-item' + (isApproved(cat.id) ? ' is-approved' : ''),
65
+ tabindex: '-1',
66
+ onclick: () => toggle(cat),
67
+ }, h('span', { class: 'ov-perm-label' }, cat.label || cat.id)));
68
+ const actionsRow = h('div', { class: 'ov-perm-actions' },
69
+ h('button', { type: 'button', class: 'ov-perm-action', onclick: () => onToggleAll && onToggleAll(true) }, 'Approve all'),
70
+ h('button', { type: 'button', class: 'ov-perm-action', onclick: () => onToggleAll && onToggleAll(false) }, 'Revoke all'));
71
+ webjsx.applyDiff(el, h('div', { class: 'ov-perm-list' }, ...rows, actionsRow));
72
+ return el;
73
+ };
74
+ const onTrigClick = () => menu.onTrigClick(buildMenuEl);
75
+ const onTrigKey = (e) => menu.onTrigKey(e, buildMenuEl);
76
+ const refFn = menu.refFn('_dsPermMenu');
77
+ const child = (typeof trigger === 'function') ? trigger() : trigger;
78
+ const wireRef = (el) => { refFn(el); if (el) { el.addEventListener('click', onTrigClick); el.addEventListener('keydown', onTrigKey); } };
79
+ return (child && child.type)
80
+ ? webjsx.createElement(child.type, { ...(child.props || {}), ref: wireRef }, ...(child.children || []))
81
+ : h('button', { type: 'button', class: 'ov-perm-trigger', ref: wireRef }, child || 'Permissions');
82
+ }
83
+
84
+ // MenuButton — icon-trigger select menu: one option carries a checkmark
85
+ // (the active selection), roving keyboard nav mirrors Dropdown's own
86
+ // open/close/outside-click/typeahead wiring, plus a stale/unavailable
87
+ // per-item state that renders as a muted "unavailable — retry" row instead
88
+ // of a normal selectable item (ported from docstudio's model-picker menu,
89
+ // which shows a retry affordance when its option list fails to load).
90
+ // Zero-option and single-option lists degrade gracefully: an empty list
91
+ // renders a static "No options available" row (no crash, no keyboard trap,
92
+ // nothing focusable); roving nav on a single-option list simply refocuses
93
+ // the same item on every Arrow press (wrap-to-self), never throws.
94
+ export function MenuButton({ trigger, items = [], selected, onSelect, onRetry, placement = 'bottom-start', ariaLabel = 'Menu', emptyText = 'No options available' } = {}) {
95
+ const menu = useRovingMenu({ itemSelector: '[role="menuitemradio"]:not([aria-disabled="true"])', items, typeahead: true, placement });
96
+ const select = (it) => { if (it.disabled || it.unavailable) return; if (onSelect) onSelect(it.id, it); menu.close(); };
97
+ const buildMenuEl = () => {
98
+ const el = document.createElement('div');
99
+ el.className = 'ds-popover ov-menubutton-menu';
100
+ el.setAttribute('role', 'menu');
101
+ el.setAttribute('aria-label', ariaLabel);
102
+ const tree = items.length
103
+ ? h('div', { class: 'ov-menubutton-list' },
104
+ ...items.map((it, i) => it.unavailable
105
+ ? h('div', { key: it.id || i, class: 'ov-menubutton-item is-unavailable' },
106
+ h('span', { class: 'ov-menubutton-label' }, it.label || 'Unavailable'),
107
+ h('button', { type: 'button', class: 'ov-menubutton-retry', onclick: () => onRetry && onRetry(it.id, it) }, 'Retry')
108
+ )
109
+ : h('button', {
110
+ key: it.id || i, type: 'button', role: 'menuitemradio',
111
+ 'aria-checked': it.id === selected ? 'true' : 'false',
112
+ class: 'ov-menubutton-item' + (it.disabled ? '' : ''),
113
+ 'aria-disabled': it.disabled ? 'true' : 'false',
114
+ tabindex: '-1', onclick: () => select(it),
115
+ },
116
+ h('span', { class: 'ov-menubutton-check', 'aria-hidden': 'true' }, it.id === selected ? Icon('check', { size: 14 }) : ''),
117
+ h('span', { class: 'ov-menubutton-label' }, it.label)
118
+ )))
119
+ : h('div', { class: 'ov-menubutton-empty' }, emptyText);
120
+ webjsx.applyDiff(el, tree);
121
+ return el;
122
+ };
123
+ const onTrigClick = () => menu.onTrigClick(buildMenuEl);
124
+ const onTrigKey = (e) => menu.onTrigKey(e, buildMenuEl);
125
+ const refFn = menu.refFn('_ovMenuButton');
126
+ const child = (typeof trigger === 'function') ? trigger() : trigger;
127
+ const wireRef = (el) => { refFn(el); if (el) { el.addEventListener('click', onTrigClick); el.addEventListener('keydown', onTrigKey); } };
128
+ return (child && child.type)
129
+ ? webjsx.createElement(child.type, { ...(child.props || {}), ref: wireRef }, ...(child.children || []))
130
+ : h('button', { type: 'button', class: 'ov-menubutton-trigger', ref: wireRef }, child || 'Select');
131
+ }
@@ -0,0 +1,51 @@
1
+ // Popover — controlled, portaled to <body>. Unlike the other overlays in
2
+ // this group it renders no VElement of its own: it imperatively creates,
3
+ // positions and tears down a body-level element keyed off the anchor, and
4
+ // always returns null.
5
+
6
+ import * as webjsx from '../../../vendor/webjsx/index.js';
7
+ import { useFloating, FLOAT_OFFSET_POPOVER, FOCUSABLE_SEL, kids } from './floating.js';
8
+ const h = webjsx.createElement;
9
+
10
+ const _popovers = new WeakMap();
11
+
12
+ export function Popover({ open, anchorEl, onClose, placement = 'bottom-start', children, ariaLabel } = {}) {
13
+ if (typeof document === 'undefined') return null;
14
+ const existing = anchorEl ? _popovers.get(anchorEl) : null;
15
+ if (!open) {
16
+ if (existing) { existing.dispose(); _popovers.delete(anchorEl); if (anchorEl && anchorEl.focus) anchorEl.focus(); }
17
+ return null;
18
+ }
19
+ if (existing || !anchorEl) return null;
20
+ const el = document.createElement('div');
21
+ el.className = 'ds-popover';
22
+ el.setAttribute('role', 'dialog');
23
+ el.setAttribute('aria-modal', 'true');
24
+ if (ariaLabel) el.setAttribute('aria-label', ariaLabel);
25
+ el.tabIndex = -1;
26
+ document.body.appendChild(el);
27
+ webjsx.applyDiff(el, h('div', { class: 'ds-popover-inner' }, ...kids(children)));
28
+ const floating = useFloating(anchorEl, el, { placement, offset: FLOAT_OFFSET_POPOVER });
29
+ const close = () => onClose && onClose();
30
+ const onDown = (e) => { if (el.contains(e.target) || anchorEl.contains(e.target)) return; close(); };
31
+ const onKey = (e) => {
32
+ if (e.key === 'Escape') { e.preventDefault(); close(); return; }
33
+ if (e.key !== 'Tab') return;
34
+ const nodes = el.querySelectorAll(FOCUSABLE_SEL); if (!nodes.length) { e.preventDefault(); return; }
35
+ const first = nodes[0], last = nodes[nodes.length - 1], a = document.activeElement;
36
+ if (e.shiftKey && a === first) { e.preventDefault(); last.focus(); }
37
+ else if (!e.shiftKey && a === last) { e.preventDefault(); first.focus(); }
38
+ };
39
+ el.addEventListener('keydown', onKey);
40
+ document.addEventListener('mousedown', onDown, true);
41
+ // setTimeout(0), not queueMicrotask — see _anchoredOverlayLifecycle's
42
+ // comment: the opening click's own default focus-on-click can otherwise
43
+ // win the race and leave focus outside el, breaking Escape/Tab-trap.
44
+ setTimeout(() => { const f = el.querySelector(FOCUSABLE_SEL); (f || el).focus(); }, 0);
45
+ _popovers.set(anchorEl, { dispose() {
46
+ document.removeEventListener('mousedown', onDown, true);
47
+ floating.dispose();
48
+ if (el.parentNode) el.parentNode.removeChild(el);
49
+ }});
50
+ return null;
51
+ }
@@ -0,0 +1,73 @@
1
+ // useRovingMenu — the shared open/close/outside-click/roving-nav/typeahead
2
+ // state machine behind Dropdown, PermissionMenu, and MenuButton (all three in
3
+ // ./menus.js). All three previously reimplemented an identical ~70-line
4
+ // skeleton (byte-identical close() teardown, near-identical onMenuKey); this
5
+ // factors it into one place so a fix/feature (e.g. typeahead) lands for every
6
+ // consumer instead of drifting per-copy. `itemSelector` picks the live
7
+ // focusable items inside the rendered menu (each consumer uses a different
8
+ // role: menuitem / menuitemcheckbox / menuitemradio); `getLabel(item)` +
9
+ // `items` enable typeahead when `typeahead` is true (Dropdown/MenuButton have
10
+ // it, PermissionMenu's categories aren't typically typeahead-searched so it
11
+ // defaults off but can opt in). Returns { refFn, onTrigClick, onTrigKey,
12
+ // openMenu, close, focusItem, isOpen } — the caller still owns rendering the
13
+ // menu's DOM/CSS (role/class per consumer stays distinct) and wires
14
+ // `menuEl.addEventListener('keydown', onMenuKey)` itself via the returned
15
+ // `onMenuKey`, since only the caller knows when its menuEl exists.
16
+
17
+ import { useFloating, FLOAT_OFFSET_DROPDOWN } from './floating.js';
18
+
19
+ export function useRovingMenu({ itemSelector, items = [], getLabel = (it) => it.label, typeahead = false, placement = 'bottom-start', onOpenChange } = {}) {
20
+ let triggerEl = null, open = false, menuEl = null, floating = null, typeBuf = '', typeTimer = null;
21
+ const liveItems = () => menuEl ? [...menuEl.querySelectorAll(itemSelector)] : [];
22
+ const focusItem = (idx) => { const b = liveItems(); if (!b.length) return; b[((idx % b.length) + b.length) % b.length].focus(); };
23
+ const onDown = (e) => { if (menuEl && menuEl.contains(e.target)) return; if (triggerEl && triggerEl.contains(e.target)) return; close(false); };
24
+ const close = (restore = true) => {
25
+ if (!open) return; open = false;
26
+ if (floating) { floating.dispose(); floating = null; }
27
+ if (menuEl && menuEl.parentNode) menuEl.parentNode.removeChild(menuEl);
28
+ menuEl = null;
29
+ document.removeEventListener('mousedown', onDown, true);
30
+ if (triggerEl) triggerEl.setAttribute('aria-expanded', 'false');
31
+ if (restore && triggerEl) triggerEl.focus();
32
+ if (onOpenChange) onOpenChange(false);
33
+ };
34
+ const onMenuKey = (e) => {
35
+ const b = liveItems(), idx = b.indexOf(document.activeElement);
36
+ if (e.key === 'Escape') { e.preventDefault(); close(); }
37
+ else if (e.key === 'ArrowDown') { e.preventDefault(); focusItem(idx + 1); }
38
+ else if (e.key === 'ArrowUp') { e.preventDefault(); focusItem(idx - 1); }
39
+ else if (e.key === 'Home') { e.preventDefault(); focusItem(0); }
40
+ else if (e.key === 'End') { e.preventDefault(); focusItem(b.length - 1); }
41
+ else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (idx >= 0) b[idx].click(); }
42
+ else if (typeahead && e.key.length === 1 && /\S/.test(e.key)) {
43
+ typeBuf += e.key.toLowerCase();
44
+ if (typeTimer) clearTimeout(typeTimer);
45
+ typeTimer = setTimeout(() => { typeBuf = ''; }, 600);
46
+ const selectable = items.filter(it => !it.separator && !it.disabled && !it.unavailable);
47
+ const m = selectable.findIndex(it => (getLabel(it) || '').toLowerCase().startsWith(typeBuf));
48
+ if (m >= 0) focusItem(m);
49
+ }
50
+ };
51
+ const openMenu = (buildMenuEl, focusFirst = true) => {
52
+ if (open || !triggerEl) return;
53
+ open = true;
54
+ menuEl = buildMenuEl();
55
+ menuEl.tabIndex = -1;
56
+ document.body.appendChild(menuEl);
57
+ menuEl.addEventListener('keydown', onMenuKey);
58
+ floating = useFloating(triggerEl, menuEl, { placement, offset: FLOAT_OFFSET_DROPDOWN });
59
+ document.addEventListener('mousedown', onDown, true);
60
+ triggerEl.setAttribute('aria-expanded', 'true');
61
+ if (focusFirst && liveItems().length) queueMicrotask(() => focusItem(0));
62
+ if (onOpenChange) onOpenChange(true);
63
+ };
64
+ const onTrigClick = (buildMenuEl) => { if (open) close(false); else openMenu(buildMenuEl, true); };
65
+ const onTrigKey = (e, buildMenuEl) => { if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (!open) openMenu(buildMenuEl, true); else focusItem(0); } };
66
+ const refFn = (dsFlag) => (el) => {
67
+ if (!el || el[dsFlag]) return;
68
+ el[dsFlag] = true; triggerEl = el;
69
+ el.setAttribute('aria-haspopup', 'menu');
70
+ el.setAttribute('aria-expanded', 'false');
71
+ };
72
+ return { refFn, onTrigClick, onTrigKey, openMenu, close, focusItem, isOpen: () => open };
73
+ }
@@ -0,0 +1,85 @@
1
+ // SettingsPopover — fixed popover with generic section/row control rendering.
2
+ // Rows are data-driven: `kind` picks the control (select/toggle/range/button,
3
+ // or a non-interactive value row), and every interactive control gets a
4
+ // stable id so the visible row label is its accessible name.
5
+
6
+ import * as webjsx from '../../../vendor/webjsx/index.js';
7
+ import { trapTab, _anchoredOverlayLifecycle } from './floating.js';
8
+ const h = webjsx.createElement;
9
+
10
+ export function SettingsPopover({ title = 'Settings', open, anchorX = 0, anchorY = 0, sections = [], onClose } = {}) {
11
+ if (!open) return null;
12
+ let rootEl = null;
13
+ const close = () => onClose && onClose();
14
+ const secs = Array.isArray(sections) ? sections : [];
15
+
16
+ const renderRow = (row, i) => {
17
+ const label = row.label != null ? row.label : (row.title != null ? row.title : '');
18
+ const kind = row.kind;
19
+ // Give every interactive control a stable id and point the row label's
20
+ // `for` at it, so the visible label is the control's accessible name.
21
+ const ctrlId = 'ov-set-' + i + '-' + kind;
22
+ const labelNode = h('label', { class: 'ov-set-row-label', for: ctrlId }, String(label));
23
+ let control = null;
24
+ if (kind === 'select') {
25
+ const opts = Array.isArray(row.options) ? row.options : [];
26
+ // Controlled via the `value` prop only — per-option `selected` is
27
+ // dropped so the two don't fight (value wins).
28
+ control = h('select', {
29
+ id: ctrlId,
30
+ class: 'ov-set-control', value: row.value != null ? String(row.value) : undefined,
31
+ onchange: (e) => row.onChange && row.onChange(e.target.value),
32
+ }, ...opts.map(o => {
33
+ const v = (o && typeof o === 'object') ? o.value : o;
34
+ const l = (o && typeof o === 'object') ? (o.label != null ? o.label : o.value) : o;
35
+ return h('option', { value: String(v) }, String(l));
36
+ }));
37
+ } else if (kind === 'toggle') {
38
+ control = h('input', {
39
+ id: ctrlId,
40
+ type: 'checkbox', class: 'ov-set-toggle',
41
+ checked: row.value ? 'checked' : undefined,
42
+ onchange: (e) => row.onChange && row.onChange(e.target.checked),
43
+ });
44
+ } else if (kind === 'range') {
45
+ control = h('input', {
46
+ id: ctrlId,
47
+ type: 'range', class: 'ov-set-control',
48
+ min: String(row.min != null ? row.min : 0),
49
+ max: String(row.max != null ? row.max : 100),
50
+ step: String(row.step != null ? row.step : 1),
51
+ value: String(row.value != null ? row.value : 0),
52
+ oninput: (e) => row.onChange && row.onChange(Number(e.target.value)),
53
+ });
54
+ } else if (kind === 'button') {
55
+ control = h('button', { type: 'button', class: 'ov-set-btn',
56
+ onclick: () => row.onClick && row.onClick() }, String(label || 'Action'));
57
+ return h('div', { class: 'ov-set-row', key: i }, control);
58
+ } else {
59
+ control = h('span', { class: 'ov-set-row-value' }, String(row.value != null ? row.value : ''));
60
+ // Non-interactive value row: a plain span label (no `for` target).
61
+ return h('div', { class: 'ov-set-row', key: i }, h('span', { class: 'ov-set-row-label' }, String(label)), control);
62
+ }
63
+ return h('div', { class: 'ov-set-row', key: i }, labelNode, control);
64
+ };
65
+
66
+ return h('div', {
67
+ class: 'ov-set-root', role: 'dialog', 'aria-modal': 'true', 'aria-label': String(title), tabindex: '-1',
68
+ onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); return; } if (rootEl) trapTab(rootEl, e); },
69
+ ref: (el) => {
70
+ if (!el) { if (rootEl && rootEl._ovSetCleanup) rootEl._ovSetCleanup(); return; }
71
+ if (el._ovSet) return; el._ovSet = true; rootEl = el;
72
+ el._ovSetCleanup = _anchoredOverlayLifecycle(el, { anchorX, anchorY, fallbackW: 280, fallbackH: 200, close });
73
+ },
74
+ },
75
+ h('div', { class: 'ov-set-head' }, String(title)),
76
+ h('div', { class: 'ov-set-body' },
77
+ ...secs.map((sec, si) => {
78
+ const slabel = sec.label != null ? sec.label : (sec.title != null ? sec.title : '');
79
+ const rows = Array.isArray(sec.rows) ? sec.rows : (Array.isArray(sec.items) ? sec.items : []);
80
+ return h('div', { class: 'ov-set-section', key: si },
81
+ slabel ? h('div', { class: 'ov-set-section-head' }, String(slabel)) : null,
82
+ ...rows.map((r, ri) => renderRow(r, ri)));
83
+ }))
84
+ );
85
+ }
@@ -0,0 +1,55 @@
1
+ // Tooltip — single shared bubble appended to <body>. One module-scope
2
+ // element and one module-scope scroll listener serve every trigger on the
3
+ // page (per-trigger bubbles/listeners leaked one per element).
4
+
5
+ import * as webjsx from '../../../vendor/webjsx/index.js';
6
+ import { useFloating, useLongPress, FLOAT_OFFSET_TOOLTIP, kids } from './floating.js';
7
+
8
+ let _tipEl = null, _tipFloat = null, _tipTimer = null, _tipId = 0;
9
+ function _hideTip() {
10
+ if (_tipTimer) { clearTimeout(_tipTimer); _tipTimer = null; }
11
+ if (_tipFloat) { _tipFloat.dispose(); _tipFloat = null; }
12
+ if (_tipEl) { _tipEl.hidden = true; _tipEl.className = 'ds-tooltip'; }
13
+ }
14
+ // One module-scope scroll listener hides the shared bubble on any scroll —
15
+ // registered once, never per-trigger (per-trigger leaked a listener per element).
16
+ if (typeof window !== 'undefined' && !window.__dsTipScrollBound) {
17
+ window.__dsTipScrollBound = true;
18
+ window.addEventListener('scroll', _hideTip, true);
19
+ }
20
+ function _showTip(trigger, label, placement, kind) {
21
+ if (typeof document === 'undefined') return;
22
+ if (!_tipEl || !document.body.contains(_tipEl)) {
23
+ _tipEl = document.createElement('div');
24
+ _tipEl.className = 'ds-tooltip';
25
+ _tipEl.setAttribute('role', 'tooltip');
26
+ document.body.appendChild(_tipEl);
27
+ }
28
+ _tipEl.textContent = label;
29
+ _tipEl.className = 'ds-tooltip kind-' + (kind || 'default');
30
+ _tipEl.hidden = false;
31
+ _tipEl.id = 'ds-tip-' + (++_tipId);
32
+ trigger.setAttribute('aria-describedby', _tipEl.id);
33
+ if (_tipFloat) _tipFloat.dispose();
34
+ _tipFloat = useFloating(trigger, _tipEl, { placement, offset: FLOAT_OFFSET_TOOLTIP });
35
+ }
36
+
37
+ export function Tooltip({ children, label, placement = 'top', delay = 350, kind = 'default' } = {}) {
38
+ const child = kids(children)[0];
39
+ if (!child || !label) return child || null;
40
+ const refFn = (el) => {
41
+ if (!el || el._dsTip) return;
42
+ el._dsTip = true;
43
+ const schedule = () => { if (_tipTimer) clearTimeout(_tipTimer); _tipTimer = setTimeout(() => _showTip(el, label, placement, kind), delay); };
44
+ const show = () => _showTip(el, label, placement, kind);
45
+ el.addEventListener('pointerenter', schedule);
46
+ el.addEventListener('pointerleave', _hideTip);
47
+ el.addEventListener('focus', show);
48
+ el.addEventListener('blur', _hideTip);
49
+ el.addEventListener('keydown', (e) => { if (e.key === 'Escape') _hideTip(); });
50
+ useLongPress(el, show, { ms: 500 });
51
+ };
52
+ const prevRef = child.props && child.props.ref;
53
+ const wrap = (el) => { refFn(el); if (typeof prevRef === 'function') prevRef(el); };
54
+ return webjsx.createElement(child.type, { ...(child.props || {}), ref: wrap }, ...(child.children || []));
55
+ }