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.
- package/dist/247420.css +228 -162
- package/dist/247420.js +24 -24
- package/package.json +1 -1
- package/src/components/content/avatar.js +31 -0
- package/src/components/content/charts.js +50 -0
- package/src/components/content/cli.js +46 -0
- package/src/components/content/feedback.js +77 -0
- package/src/components/content/fields.js +136 -0
- package/src/components/content/hero.js +146 -0
- package/src/components/content/lists.js +85 -0
- package/src/components/content/panel.js +79 -0
- package/src/components/content/row.js +115 -0
- package/src/components/content/table.js +97 -0
- package/src/components/content/views.js +81 -0
- package/src/components/content.js +29 -861
- package/src/components/editor-primitives/batch.js +60 -0
- package/src/components/editor-primitives/chrome.js +146 -0
- package/src/components/editor-primitives/collapse.js +46 -0
- package/src/components/editor-primitives/context-menu.js +127 -0
- package/src/components/editor-primitives/diagnostics.js +44 -0
- package/src/components/editor-primitives/focus-trap.js +26 -0
- package/src/components/editor-primitives/json-viewer.js +123 -0
- package/src/components/editor-primitives/layout.js +89 -0
- package/src/components/editor-primitives/modals.js +89 -0
- package/src/components/editor-primitives/pager.js +74 -0
- package/src/components/editor-primitives/property-grid.js +57 -0
- package/src/components/editor-primitives/shared.js +18 -0
- package/src/components/editor-primitives/split-panel.js +101 -0
- package/src/components/editor-primitives/toast.js +59 -0
- package/src/components/editor-primitives/tree.js +75 -0
- package/src/components/editor-primitives.js +35 -1048
- package/src/components/overlay-primitives/approval-prompt.js +38 -0
- package/src/components/overlay-primitives/auth-modal.js +89 -0
- package/src/components/overlay-primitives/command-palette.js +101 -0
- package/src/components/overlay-primitives/emoji-picker.js +103 -0
- package/src/components/overlay-primitives/floating.js +146 -0
- package/src/components/overlay-primitives/full-screen.js +45 -0
- package/src/components/overlay-primitives/menus.js +131 -0
- package/src/components/overlay-primitives/popover.js +51 -0
- package/src/components/overlay-primitives/roving-menu.js +73 -0
- package/src/components/overlay-primitives/settings-popover.js +85 -0
- package/src/components/overlay-primitives/tooltip.js +55 -0
- package/src/components/overlay-primitives.js +33 -855
- package/src/css/app-shell/chat-polish.css +39 -32
- package/src/css/app-shell/data-density.css +25 -21
- package/src/css/app-shell/files.css +37 -29
- package/src/css/app-shell/kits-appended.css +73 -50
- package/src/css/app-shell/responsive.css +47 -29
- package/src/kits/os/freddie-dashboard.css +2 -2
|
@@ -2,858 +2,36 @@
|
|
|
2
2
|
// Shared positioning (auto-flip + viewport clamp) in useFloating; consumed by
|
|
3
3
|
// all three. No inline styles except runtime left/top. CSS classes scoped to
|
|
4
4
|
// .ds-247420 (see editor-primitives.css).
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
x = align === 'start' ? a.left : align === 'end' ? a.right - c.width : a.left + (a.width - c.width) / 2;
|
|
39
|
-
} else {
|
|
40
|
-
x = s === 'right' ? a.right + offset : a.left - offset - c.width;
|
|
41
|
-
y = align === 'start' ? a.top : align === 'end' ? a.bottom - c.height : a.top + (a.height - c.height) / 2;
|
|
42
|
-
}
|
|
43
|
-
x = Math.max(FLOAT_EDGE, Math.min(vw - c.width - FLOAT_EDGE, x));
|
|
44
|
-
y = Math.max(FLOAT_EDGE, Math.min(vh - c.height - FLOAT_EDGE, y));
|
|
45
|
-
contentEl.style.left = x + 'px';
|
|
46
|
-
contentEl.style.top = y + 'px';
|
|
47
|
-
finalPlacement = s + '-' + align;
|
|
48
|
-
};
|
|
49
|
-
compute();
|
|
50
|
-
const cb = () => compute();
|
|
51
|
-
window.addEventListener('resize', cb);
|
|
52
|
-
window.addEventListener('scroll', cb, true);
|
|
53
|
-
// Reposition when the content box itself resizes (async-loaded content
|
|
54
|
-
// grows the popover after initial positioning, pushing it off-viewport).
|
|
55
|
-
const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(cb) : null;
|
|
56
|
-
if (ro) ro.observe(contentEl);
|
|
57
|
-
return {
|
|
58
|
-
update: compute,
|
|
59
|
-
dispose() { window.removeEventListener('resize', cb); window.removeEventListener('scroll', cb, true); if (ro) ro.disconnect(); },
|
|
60
|
-
get finalPlacement() { return finalPlacement; }
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// useLongPress — fire callback after ms held without movement.
|
|
65
|
-
export function useLongPress(targetEl, callback, { ms = 500 } = {}) {
|
|
66
|
-
if (!targetEl) return () => {};
|
|
67
|
-
let timer = null, sx = 0, sy = 0;
|
|
68
|
-
const cancel = () => { if (timer) { clearTimeout(timer); timer = null; } };
|
|
69
|
-
const onDown = (e) => { sx = e.clientX || 0; sy = e.clientY || 0; cancel(); timer = setTimeout(() => { timer = null; callback(e); }, ms); };
|
|
70
|
-
const onMove = (e) => { if (!timer) return; const dx = (e.clientX || 0) - sx, dy = (e.clientY || 0) - sy; if (dx * dx + dy * dy > 64) cancel(); };
|
|
71
|
-
const evts = [['pointerdown', onDown], ['pointermove', onMove], ['pointerup', cancel], ['pointerleave', cancel], ['pointercancel', cancel]];
|
|
72
|
-
evts.forEach(([k, fn]) => targetEl.addEventListener(k, fn));
|
|
73
|
-
return () => { cancel(); evts.forEach(([k, fn]) => targetEl.removeEventListener(k, fn)); };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// withBusy — run an async action with its triggering button disabled +
|
|
77
|
-
// busy-labelled, so a double-click/double-tap can't fire it twice and the
|
|
78
|
-
// user sees progress. Restores the button (label, disabled state,
|
|
79
|
-
// aria-busy) when the action settles, including on throw. Re-entry while
|
|
80
|
-
// already busy is dropped silently rather than queued. Mirrors docstudio's
|
|
81
|
-
// dom-busy.js withButtonBusy — agentgui's app.js has no equivalent anywhere,
|
|
82
|
-
// so every async-click handler (share/delete/retry/approve-deny) is
|
|
83
|
-
// currently unguarded against rapid repeat clicks firing the same mutating
|
|
84
|
-
// request twice.
|
|
85
|
-
export async function withBusy(btn, fn, busyLabel = '...') {
|
|
86
|
-
if (!btn) return fn();
|
|
87
|
-
if (btn.disabled) return; // already in flight -> drop the repeat
|
|
88
|
-
const prevHtml = btn.innerHTML;
|
|
89
|
-
const prevDisabled = btn.disabled;
|
|
90
|
-
btn.disabled = true;
|
|
91
|
-
btn.setAttribute('aria-busy', 'true');
|
|
92
|
-
if (busyLabel != null) btn.textContent = busyLabel;
|
|
93
|
-
try {
|
|
94
|
-
return await fn();
|
|
95
|
-
} finally {
|
|
96
|
-
btn.disabled = prevDisabled;
|
|
97
|
-
btn.removeAttribute('aria-busy');
|
|
98
|
-
btn.innerHTML = prevHtml;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Tooltip — single shared bubble appended to <body>.
|
|
103
|
-
let _tipEl = null, _tipFloat = null, _tipTimer = null, _tipId = 0;
|
|
104
|
-
function _hideTip() {
|
|
105
|
-
if (_tipTimer) { clearTimeout(_tipTimer); _tipTimer = null; }
|
|
106
|
-
if (_tipFloat) { _tipFloat.dispose(); _tipFloat = null; }
|
|
107
|
-
if (_tipEl) { _tipEl.hidden = true; _tipEl.className = 'ds-tooltip'; }
|
|
108
|
-
}
|
|
109
|
-
// One module-scope scroll listener hides the shared bubble on any scroll —
|
|
110
|
-
// registered once, never per-trigger (per-trigger leaked a listener per element).
|
|
111
|
-
if (typeof window !== 'undefined' && !window.__dsTipScrollBound) {
|
|
112
|
-
window.__dsTipScrollBound = true;
|
|
113
|
-
window.addEventListener('scroll', _hideTip, true);
|
|
114
|
-
}
|
|
115
|
-
function _showTip(trigger, label, placement, kind) {
|
|
116
|
-
if (typeof document === 'undefined') return;
|
|
117
|
-
if (!_tipEl || !document.body.contains(_tipEl)) {
|
|
118
|
-
_tipEl = document.createElement('div');
|
|
119
|
-
_tipEl.className = 'ds-tooltip';
|
|
120
|
-
_tipEl.setAttribute('role', 'tooltip');
|
|
121
|
-
document.body.appendChild(_tipEl);
|
|
122
|
-
}
|
|
123
|
-
_tipEl.textContent = label;
|
|
124
|
-
_tipEl.className = 'ds-tooltip kind-' + (kind || 'default');
|
|
125
|
-
_tipEl.hidden = false;
|
|
126
|
-
_tipEl.id = 'ds-tip-' + (++_tipId);
|
|
127
|
-
trigger.setAttribute('aria-describedby', _tipEl.id);
|
|
128
|
-
if (_tipFloat) _tipFloat.dispose();
|
|
129
|
-
_tipFloat = useFloating(trigger, _tipEl, { placement, offset: FLOAT_OFFSET_TOOLTIP });
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function Tooltip({ children, label, placement = 'top', delay = 350, kind = 'default' } = {}) {
|
|
133
|
-
const child = kids(children)[0];
|
|
134
|
-
if (!child || !label) return child || null;
|
|
135
|
-
const refFn = (el) => {
|
|
136
|
-
if (!el || el._dsTip) return;
|
|
137
|
-
el._dsTip = true;
|
|
138
|
-
const schedule = () => { if (_tipTimer) clearTimeout(_tipTimer); _tipTimer = setTimeout(() => _showTip(el, label, placement, kind), delay); };
|
|
139
|
-
const show = () => _showTip(el, label, placement, kind);
|
|
140
|
-
el.addEventListener('pointerenter', schedule);
|
|
141
|
-
el.addEventListener('pointerleave', _hideTip);
|
|
142
|
-
el.addEventListener('focus', show);
|
|
143
|
-
el.addEventListener('blur', _hideTip);
|
|
144
|
-
el.addEventListener('keydown', (e) => { if (e.key === 'Escape') _hideTip(); });
|
|
145
|
-
useLongPress(el, show, { ms: 500 });
|
|
146
|
-
};
|
|
147
|
-
const prevRef = child.props && child.props.ref;
|
|
148
|
-
const wrap = (el) => { refFn(el); if (typeof prevRef === 'function') prevRef(el); };
|
|
149
|
-
return webjsx.createElement(child.type, { ...(child.props || {}), ref: wrap }, ...(child.children || []));
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Popover — controlled, portaled to <body>.
|
|
153
|
-
const _popovers = new WeakMap();
|
|
154
|
-
export function Popover({ open, anchorEl, onClose, placement = 'bottom-start', children, ariaLabel } = {}) {
|
|
155
|
-
if (typeof document === 'undefined') return null;
|
|
156
|
-
const existing = anchorEl ? _popovers.get(anchorEl) : null;
|
|
157
|
-
if (!open) {
|
|
158
|
-
if (existing) { existing.dispose(); _popovers.delete(anchorEl); if (anchorEl && anchorEl.focus) anchorEl.focus(); }
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
if (existing || !anchorEl) return null;
|
|
162
|
-
const el = document.createElement('div');
|
|
163
|
-
el.className = 'ds-popover';
|
|
164
|
-
el.setAttribute('role', 'dialog');
|
|
165
|
-
el.setAttribute('aria-modal', 'true');
|
|
166
|
-
if (ariaLabel) el.setAttribute('aria-label', ariaLabel);
|
|
167
|
-
el.tabIndex = -1;
|
|
168
|
-
document.body.appendChild(el);
|
|
169
|
-
webjsx.applyDiff(el, h('div', { class: 'ds-popover-inner' }, ...kids(children)));
|
|
170
|
-
const floating = useFloating(anchorEl, el, { placement, offset: FLOAT_OFFSET_POPOVER });
|
|
171
|
-
const close = () => onClose && onClose();
|
|
172
|
-
const onDown = (e) => { if (el.contains(e.target) || anchorEl.contains(e.target)) return; close(); };
|
|
173
|
-
const onKey = (e) => {
|
|
174
|
-
if (e.key === 'Escape') { e.preventDefault(); close(); return; }
|
|
175
|
-
if (e.key !== 'Tab') return;
|
|
176
|
-
const nodes = el.querySelectorAll(FOCUSABLE_SEL); if (!nodes.length) { e.preventDefault(); return; }
|
|
177
|
-
const first = nodes[0], last = nodes[nodes.length - 1], a = document.activeElement;
|
|
178
|
-
if (e.shiftKey && a === first) { e.preventDefault(); last.focus(); }
|
|
179
|
-
else if (!e.shiftKey && a === last) { e.preventDefault(); first.focus(); }
|
|
180
|
-
};
|
|
181
|
-
el.addEventListener('keydown', onKey);
|
|
182
|
-
document.addEventListener('mousedown', onDown, true);
|
|
183
|
-
// setTimeout(0), not queueMicrotask — see _anchoredOverlayLifecycle's
|
|
184
|
-
// comment: the opening click's own default focus-on-click can otherwise
|
|
185
|
-
// win the race and leave focus outside el, breaking Escape/Tab-trap.
|
|
186
|
-
setTimeout(() => { const f = el.querySelector(FOCUSABLE_SEL); (f || el).focus(); }, 0);
|
|
187
|
-
_popovers.set(anchorEl, { dispose() {
|
|
188
|
-
document.removeEventListener('mousedown', onDown, true);
|
|
189
|
-
floating.dispose();
|
|
190
|
-
if (el.parentNode) el.parentNode.removeChild(el);
|
|
191
|
-
}});
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// useRovingMenu — the shared open/close/outside-click/roving-nav/typeahead
|
|
196
|
-
// state machine behind Dropdown, PermissionMenu, and MenuButton. All three
|
|
197
|
-
// previously reimplemented an identical ~70-line skeleton (byte-identical
|
|
198
|
-
// close() teardown, near-identical onMenuKey); this factors it into one
|
|
199
|
-
// place so a fix/feature (e.g. typeahead) lands for every consumer instead
|
|
200
|
-
// of drifting per-copy. `itemSelector` picks the live focusable items inside
|
|
201
|
-
// the rendered menu (each consumer uses a different role: menuitem /
|
|
202
|
-
// menuitemcheckbox / menuitemradio); `getLabel(item)` + `items` enable
|
|
203
|
-
// typeahead when `typeahead` is true (Dropdown/MenuButton have it,
|
|
204
|
-
// PermissionMenu's categories aren't typically typeahead-searched so it
|
|
205
|
-
// defaults off but can opt in). Returns { refFn, onTrigClick, onTrigKey,
|
|
206
|
-
// openMenu, close, focusItem, isOpen } — the caller still owns rendering the
|
|
207
|
-
// menu's DOM/CSS (role/class per consumer stays distinct) and wires
|
|
208
|
-
// `menuEl.addEventListener('keydown', onMenuKey)` itself via the returned
|
|
209
|
-
// `onMenuKey`, since only the caller knows when its menuEl exists.
|
|
210
|
-
export function useRovingMenu({ itemSelector, items = [], getLabel = (it) => it.label, typeahead = false, placement = 'bottom-start', onOpenChange } = {}) {
|
|
211
|
-
let triggerEl = null, open = false, menuEl = null, floating = null, typeBuf = '', typeTimer = null;
|
|
212
|
-
const liveItems = () => menuEl ? [...menuEl.querySelectorAll(itemSelector)] : [];
|
|
213
|
-
const focusItem = (idx) => { const b = liveItems(); if (!b.length) return; b[((idx % b.length) + b.length) % b.length].focus(); };
|
|
214
|
-
const onDown = (e) => { if (menuEl && menuEl.contains(e.target)) return; if (triggerEl && triggerEl.contains(e.target)) return; close(false); };
|
|
215
|
-
const close = (restore = true) => {
|
|
216
|
-
if (!open) return; open = false;
|
|
217
|
-
if (floating) { floating.dispose(); floating = null; }
|
|
218
|
-
if (menuEl && menuEl.parentNode) menuEl.parentNode.removeChild(menuEl);
|
|
219
|
-
menuEl = null;
|
|
220
|
-
document.removeEventListener('mousedown', onDown, true);
|
|
221
|
-
if (triggerEl) triggerEl.setAttribute('aria-expanded', 'false');
|
|
222
|
-
if (restore && triggerEl) triggerEl.focus();
|
|
223
|
-
if (onOpenChange) onOpenChange(false);
|
|
224
|
-
};
|
|
225
|
-
const onMenuKey = (e) => {
|
|
226
|
-
const b = liveItems(), idx = b.indexOf(document.activeElement);
|
|
227
|
-
if (e.key === 'Escape') { e.preventDefault(); close(); }
|
|
228
|
-
else if (e.key === 'ArrowDown') { e.preventDefault(); focusItem(idx + 1); }
|
|
229
|
-
else if (e.key === 'ArrowUp') { e.preventDefault(); focusItem(idx - 1); }
|
|
230
|
-
else if (e.key === 'Home') { e.preventDefault(); focusItem(0); }
|
|
231
|
-
else if (e.key === 'End') { e.preventDefault(); focusItem(b.length - 1); }
|
|
232
|
-
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (idx >= 0) b[idx].click(); }
|
|
233
|
-
else if (typeahead && e.key.length === 1 && /\S/.test(e.key)) {
|
|
234
|
-
typeBuf += e.key.toLowerCase();
|
|
235
|
-
if (typeTimer) clearTimeout(typeTimer);
|
|
236
|
-
typeTimer = setTimeout(() => { typeBuf = ''; }, 600);
|
|
237
|
-
const selectable = items.filter(it => !it.separator && !it.disabled && !it.unavailable);
|
|
238
|
-
const m = selectable.findIndex(it => (getLabel(it) || '').toLowerCase().startsWith(typeBuf));
|
|
239
|
-
if (m >= 0) focusItem(m);
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
const openMenu = (buildMenuEl, focusFirst = true) => {
|
|
243
|
-
if (open || !triggerEl) return;
|
|
244
|
-
open = true;
|
|
245
|
-
menuEl = buildMenuEl();
|
|
246
|
-
menuEl.tabIndex = -1;
|
|
247
|
-
document.body.appendChild(menuEl);
|
|
248
|
-
menuEl.addEventListener('keydown', onMenuKey);
|
|
249
|
-
floating = useFloating(triggerEl, menuEl, { placement, offset: FLOAT_OFFSET_DROPDOWN });
|
|
250
|
-
document.addEventListener('mousedown', onDown, true);
|
|
251
|
-
triggerEl.setAttribute('aria-expanded', 'true');
|
|
252
|
-
if (focusFirst && liveItems().length) queueMicrotask(() => focusItem(0));
|
|
253
|
-
if (onOpenChange) onOpenChange(true);
|
|
254
|
-
};
|
|
255
|
-
const onTrigClick = (buildMenuEl) => { if (open) close(false); else openMenu(buildMenuEl, true); };
|
|
256
|
-
const onTrigKey = (e, buildMenuEl) => { if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (!open) openMenu(buildMenuEl, true); else focusItem(0); } };
|
|
257
|
-
const refFn = (dsFlag) => (el) => {
|
|
258
|
-
if (!el || el[dsFlag]) return;
|
|
259
|
-
el[dsFlag] = true; triggerEl = el;
|
|
260
|
-
el.setAttribute('aria-haspopup', 'menu');
|
|
261
|
-
el.setAttribute('aria-expanded', 'false');
|
|
262
|
-
};
|
|
263
|
-
return { refFn, onTrigClick, onTrigKey, openMenu, close, focusItem, isOpen: () => open };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Dropdown — button trigger + portaled menu.
|
|
267
|
-
export function Dropdown({ trigger, items = [], onSelect, placement = 'bottom-start', ariaLabel } = {}) {
|
|
268
|
-
const menu = useRovingMenu({ itemSelector: '[role="menuitem"]:not([aria-disabled="true"])', items, typeahead: true, placement });
|
|
269
|
-
const select = (it) => { if (it.disabled || it.separator) return; if (onSelect) onSelect(it.id, it); menu.close(); };
|
|
270
|
-
const buildMenuEl = () => {
|
|
271
|
-
const el = document.createElement('div');
|
|
272
|
-
el.className = 'ds-popover ds-dropdown-menu';
|
|
273
|
-
el.setAttribute('role', 'menu');
|
|
274
|
-
if (ariaLabel) el.setAttribute('aria-label', ariaLabel);
|
|
275
|
-
const tree = h('div', { class: 'ds-dropdown-list' },
|
|
276
|
-
...items.map((it, i) => it.separator
|
|
277
|
-
? h('div', { key: 'sep' + i, class: 'ds-dropdown-separator', role: 'separator' })
|
|
278
|
-
: h('button', {
|
|
279
|
-
key: it.id || i, type: 'button', role: 'menuitem',
|
|
280
|
-
class: 'ds-dropdown-item' + (it.danger ? ' is-danger' : ''),
|
|
281
|
-
'aria-disabled': it.disabled ? 'true' : 'false',
|
|
282
|
-
tabindex: '-1', onclick: () => select(it),
|
|
283
|
-
},
|
|
284
|
-
it.glyph != null ? h('span', { class: 'ds-dropdown-glyph', 'aria-hidden': 'true' }, it.glyph) : null,
|
|
285
|
-
h('span', { class: 'ds-dropdown-label' }, it.label)
|
|
286
|
-
)));
|
|
287
|
-
webjsx.applyDiff(el, tree);
|
|
288
|
-
return el;
|
|
289
|
-
};
|
|
290
|
-
const onTrigClick = () => menu.onTrigClick(buildMenuEl);
|
|
291
|
-
const onTrigKey = (e) => menu.onTrigKey(e, buildMenuEl);
|
|
292
|
-
const refFn = menu.refFn('_dsDropdown');
|
|
293
|
-
const child = (typeof trigger === 'function') ? trigger() : trigger;
|
|
294
|
-
const wireRef = (el) => { refFn(el); if (el) { el.addEventListener('click', onTrigClick); el.addEventListener('keydown', onTrigKey); } };
|
|
295
|
-
return (child && child.type)
|
|
296
|
-
? webjsx.createElement(child.type, { ...(child.props || {}), ref: wireRef }, ...(child.children || []))
|
|
297
|
-
: h('button', { type: 'button', class: 'ds-dropdown-trigger', ref: wireRef }, child || 'Menu');
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// PermissionMenu — a role=menu of role=menuitemcheckbox rows, one per
|
|
301
|
-
// category, with roving tabindex + Arrow-up/down/Home/End navigation and
|
|
302
|
-
// Escape-closes-and-restores-focus, plus "Approve all"/"Revoke all" actions.
|
|
303
|
-
// Mirrors Dropdown's own open/close + outside-click wiring (a portaled menu
|
|
304
|
-
// element, a document-level mousedown listener, focus restored to the
|
|
305
|
-
// trigger on close) rather than reimplementing that plumbing.
|
|
306
|
-
export function PermissionMenu({ trigger, categories = [], approved = [], onToggle, onToggleAll, placement = 'bottom-start', ariaLabel = 'Permissions' } = {}) {
|
|
307
|
-
const isApproved = (id) => approved.indexOf(id) !== -1;
|
|
308
|
-
const menu = useRovingMenu({ itemSelector: '[role="menuitemcheckbox"]', items: categories, getLabel: (cat) => cat.label || cat.id, typeahead: true, placement });
|
|
309
|
-
const toggle = (cat) => { if (onToggle) onToggle(cat.id, !isApproved(cat.id)); };
|
|
310
|
-
const buildMenuEl = () => {
|
|
311
|
-
const el = document.createElement('div');
|
|
312
|
-
el.className = 'ds-popover ov-perm-menu';
|
|
313
|
-
el.setAttribute('role', 'menu');
|
|
314
|
-
el.setAttribute('aria-label', ariaLabel);
|
|
315
|
-
const rows = categories.map((cat, i) => h('button', {
|
|
316
|
-
key: cat.id || i, type: 'button', role: 'menuitemcheckbox',
|
|
317
|
-
'aria-checked': isApproved(cat.id) ? 'true' : 'false',
|
|
318
|
-
class: 'ov-perm-item' + (isApproved(cat.id) ? ' is-approved' : ''),
|
|
319
|
-
tabindex: '-1',
|
|
320
|
-
onclick: () => toggle(cat),
|
|
321
|
-
}, h('span', { class: 'ov-perm-label' }, cat.label || cat.id)));
|
|
322
|
-
const actionsRow = h('div', { class: 'ov-perm-actions' },
|
|
323
|
-
h('button', { type: 'button', class: 'ov-perm-action', onclick: () => onToggleAll && onToggleAll(true) }, 'Approve all'),
|
|
324
|
-
h('button', { type: 'button', class: 'ov-perm-action', onclick: () => onToggleAll && onToggleAll(false) }, 'Revoke all'));
|
|
325
|
-
webjsx.applyDiff(el, h('div', { class: 'ov-perm-list' }, ...rows, actionsRow));
|
|
326
|
-
return el;
|
|
327
|
-
};
|
|
328
|
-
const onTrigClick = () => menu.onTrigClick(buildMenuEl);
|
|
329
|
-
const onTrigKey = (e) => menu.onTrigKey(e, buildMenuEl);
|
|
330
|
-
const refFn = menu.refFn('_dsPermMenu');
|
|
331
|
-
const child = (typeof trigger === 'function') ? trigger() : trigger;
|
|
332
|
-
const wireRef = (el) => { refFn(el); if (el) { el.addEventListener('click', onTrigClick); el.addEventListener('keydown', onTrigKey); } };
|
|
333
|
-
return (child && child.type)
|
|
334
|
-
? webjsx.createElement(child.type, { ...(child.props || {}), ref: wireRef }, ...(child.children || []))
|
|
335
|
-
: h('button', { type: 'button', class: 'ov-perm-trigger', ref: wireRef }, child || 'Permissions');
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// ApprovalPrompt — an inline, in-thread tool-permission card (as opposed to
|
|
339
|
-
// PermissionMenu's settings-style dropdown): shows the tool name + an
|
|
340
|
-
// optional args preview, an optional free-text note the user can attach to
|
|
341
|
-
// their decision (auto-focused, since the note is usually the primary
|
|
342
|
-
// reason to open this card at all), and up to four resolution actions
|
|
343
|
-
// (once/session/all/deny). Mirrors docstudio's chat-approval-prompts.js
|
|
344
|
-
// buildApprovalPrompt shape. The note textarea is entirely optional -
|
|
345
|
-
// omitting `onDecision`'s use of the note arg keeps existing simpler
|
|
346
|
-
// once/deny-only call sites unaffected.
|
|
347
|
-
export function ApprovalPrompt({ toolName, categoryLabel, argsPreview, onDecision, autoFocusNote = true } = {}) {
|
|
348
|
-
let noteEl = null;
|
|
349
|
-
const noteRef = (el) => {
|
|
350
|
-
if (!el || noteEl === el) return;
|
|
351
|
-
noteEl = el;
|
|
352
|
-
if (autoFocusNote) queueMicrotask(() => noteEl && noteEl.focus());
|
|
353
|
-
};
|
|
354
|
-
const decide = (kind) => { if (onDecision) onDecision(kind, (noteEl && noteEl.value || '').trim()); };
|
|
355
|
-
return h('div', { class: 'ov-approval', role: 'group', 'aria-label': toolName ? `Permission requested: ${toolName}` : 'Permission requested' },
|
|
356
|
-
h('div', { class: 'ov-approval-head' },
|
|
357
|
-
h('span', { class: 'ov-approval-icon' }, Icon('lock', { size: 16 })),
|
|
358
|
-
h('strong', { class: 'ov-approval-tool' }, toolName || ''),
|
|
359
|
-
categoryLabel ? h('span', { class: 'ov-approval-cat' }, '- ' + categoryLabel) : null),
|
|
360
|
-
argsPreview ? h('pre', { class: 'ov-approval-args' }, argsPreview) : null,
|
|
361
|
-
h('textarea', {
|
|
362
|
-
class: 'ov-approval-note', ref: noteRef,
|
|
363
|
-
placeholder: 'Add instructions for the assistant (optional)...',
|
|
364
|
-
}),
|
|
365
|
-
h('div', { class: 'ov-approval-actions' },
|
|
366
|
-
h('button', { type: 'button', class: 'ov-approval-btn ov-approval-btn-primary', onclick: () => decide('once') }, 'Allow once'),
|
|
367
|
-
h('button', { type: 'button', class: 'ov-approval-btn ov-approval-btn-soft', onclick: () => decide('session') }, 'Allow for session'),
|
|
368
|
-
h('button', { type: 'button', class: 'ov-approval-btn', onclick: () => decide('all') }, 'Allow all'),
|
|
369
|
-
h('button', { type: 'button', class: 'ov-approval-btn ov-approval-btn-deny', onclick: () => decide('deny') }, 'Deny')));
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Clamp a fixed-position box to the viewport given desired top-left coords.
|
|
373
|
-
function _clampToViewport(x, y, w, h, margin = CLAMP_MARGIN) {
|
|
374
|
-
const vw = (typeof window !== 'undefined' ? window.innerWidth : 1024);
|
|
375
|
-
const vh = (typeof window !== 'undefined' ? window.innerHeight : 768);
|
|
376
|
-
return {
|
|
377
|
-
left: Math.max(margin, Math.min(vw - w - margin, x)),
|
|
378
|
-
top: Math.max(margin, Math.min(vh - h - margin, y)),
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Tab focus trap for a dialog root — keeps Tab/Shift+Tab cycling inside `el`.
|
|
383
|
-
// Call from an onkeydown handler; returns true if it handled the event.
|
|
384
|
-
export function trapTab(el, e) {
|
|
385
|
-
if (e.key !== 'Tab') return false;
|
|
386
|
-
const nodes = el.querySelectorAll(FOCUSABLE_SEL);
|
|
387
|
-
if (!nodes.length) { e.preventDefault(); return true; }
|
|
388
|
-
const first = nodes[0], last = nodes[nodes.length - 1], a = document.activeElement;
|
|
389
|
-
if (e.shiftKey && a === first) { e.preventDefault(); last.focus(); return true; }
|
|
390
|
-
if (!e.shiftKey && a === last) { e.preventDefault(); first.focus(); return true; }
|
|
391
|
-
return false;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Shared lifecycle for fixed anchor-positioned popovers (EmojiPicker,
|
|
395
|
-
// SettingsPopover): on mount, place+clamp near (anchorX, anchorY), focus the
|
|
396
|
-
// root, and wire an outside-mousedown close. Returns a cleanup fn the ref(null)
|
|
397
|
-
// branch must call. Both consumers deduped through this so the
|
|
398
|
-
// queueMicrotask/place/clamp/outside-close dance is authored once.
|
|
399
|
-
function _anchoredOverlayLifecycle(el, { anchorX, anchorY, fallbackW, fallbackH, close }) {
|
|
400
|
-
const place = () => {
|
|
401
|
-
const r = el.getBoundingClientRect();
|
|
402
|
-
const { left, top } = _clampToViewport(anchorX, anchorY, r.width || fallbackW, r.height || fallbackH);
|
|
403
|
-
el.style.left = left + 'px'; el.style.top = top + 'px';
|
|
404
|
-
};
|
|
405
|
-
// setTimeout(0), not queueMicrotask: the triggering click's own default
|
|
406
|
-
// focus-on-click (moving focus to the clicked <button>) can run AFTER a
|
|
407
|
-
// same-tick microtask, so a queueMicrotask focus() call here was losing
|
|
408
|
-
// the race and leaving focus on the trigger button instead of the
|
|
409
|
-
// dialog -- breaking Escape-to-close (keydown only bubbles from the
|
|
410
|
-
// focused element) for any keyboard user. A macrotask reliably runs
|
|
411
|
-
// after the click's focus settles.
|
|
412
|
-
setTimeout(() => { place(); el.focus(); }, 0);
|
|
413
|
-
const onDown = (e) => { if (!el.contains(e.target)) close(); };
|
|
414
|
-
queueMicrotask(() => document.addEventListener('mousedown', onDown, true));
|
|
415
|
-
return () => document.removeEventListener('mousedown', onDown, true);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// CommandPalette — centered Cmd+K palette with live filter + keyboard nav.
|
|
419
|
-
export function CommandPalette({ open, items = [], onSelect, onClose } = {}) {
|
|
420
|
-
if (!open) return null;
|
|
421
|
-
const list = Array.isArray(items) ? items : [];
|
|
422
|
-
const labelOf = (it) => String(it.label || it.title || it.name || '');
|
|
423
|
-
let active = 0, filterText = '';
|
|
424
|
-
|
|
425
|
-
const matches = () => {
|
|
426
|
-
const q = filterText.trim().toLowerCase();
|
|
427
|
-
return q ? list.filter(it => labelOf(it).toLowerCase().includes(q)) : list.slice();
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
const rowsFor = (filtered) => {
|
|
431
|
-
const out = [];
|
|
432
|
-
let lastGroup = null, flatIdx = 0;
|
|
433
|
-
for (const it of filtered) {
|
|
434
|
-
const grp = it.group != null ? String(it.group) : null;
|
|
435
|
-
if (grp && grp !== lastGroup) {
|
|
436
|
-
out.push(h('div', { class: 'ov-cmd-group', role: 'presentation' }, grp));
|
|
437
|
-
lastGroup = grp;
|
|
438
|
-
}
|
|
439
|
-
const idx = flatIdx++;
|
|
440
|
-
const glyph = it.icon != null ? it.icon : (it.glyph != null ? it.glyph : null);
|
|
441
|
-
const hint = it.hint != null ? it.hint : (it.shortcut != null ? it.shortcut : null);
|
|
442
|
-
out.push(h('button', {
|
|
443
|
-
type: 'button', role: 'option',
|
|
444
|
-
id: 'ov-cmd-item-' + idx,
|
|
445
|
-
'data-idx': String(idx),
|
|
446
|
-
'aria-selected': idx === active ? 'true' : 'false',
|
|
447
|
-
class: 'ov-cmd-item' + (idx === active ? ' is-active' : ''),
|
|
448
|
-
onclick: () => choose(it),
|
|
449
|
-
onmousemove: () => { if (active !== idx) { active = idx; renderInner(); } },
|
|
450
|
-
},
|
|
451
|
-
glyph != null ? h('span', { class: 'ov-cmd-glyph', 'aria-hidden': 'true' }, glyph) : null,
|
|
452
|
-
h('span', { class: 'ov-cmd-label' }, labelOf(it)),
|
|
453
|
-
hint != null ? h('span', { class: 'ov-cmd-hint' }, hint) : null
|
|
454
|
-
));
|
|
455
|
-
}
|
|
456
|
-
return out;
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
let rootEl = null, inputEl = null, listEl = null, flat = [];
|
|
460
|
-
// Remember the element focused before the palette opened so we can return
|
|
461
|
-
// focus there on close (the input steals focus on mount).
|
|
462
|
-
const prevFocus = (typeof document !== 'undefined') ? document.activeElement : null;
|
|
463
|
-
const restoreFocus = () => { if (prevFocus && prevFocus.focus && document.contains(prevFocus)) prevFocus.focus(); };
|
|
464
|
-
const close = () => { restoreFocus(); if (onClose) onClose(); };
|
|
465
|
-
const choose = (it) => { if (it && onSelect) onSelect(it); };
|
|
466
|
-
|
|
467
|
-
const renderInner = () => {
|
|
468
|
-
if (!listEl) return;
|
|
469
|
-
const filtered = matches();
|
|
470
|
-
flat = filtered;
|
|
471
|
-
if (active >= filtered.length) active = Math.max(0, filtered.length - 1);
|
|
472
|
-
webjsx.applyDiff(listEl, h('div', { class: 'ov-cmd-list-inner' },
|
|
473
|
-
filtered.length ? rowsFor(filtered) : h('div', { class: 'ov-cmd-empty' }, 'No results')));
|
|
474
|
-
const sel = listEl.querySelector('.ov-cmd-item.is-active');
|
|
475
|
-
if (sel && sel.scrollIntoView) sel.scrollIntoView({ block: 'nearest' });
|
|
476
|
-
if (inputEl) inputEl.setAttribute('aria-activedescendant', filtered.length ? 'ov-cmd-item-' + active : '');
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
const onKey = (e) => {
|
|
480
|
-
if (e.key === 'Escape') { e.preventDefault(); close(); }
|
|
481
|
-
else if (e.key === 'ArrowDown') { e.preventDefault(); if (flat.length) { active = (active + 1) % flat.length; renderInner(); } }
|
|
482
|
-
else if (e.key === 'ArrowUp') { e.preventDefault(); if (flat.length) { active = (active - 1 + flat.length) % flat.length; renderInner(); } }
|
|
483
|
-
else if (e.key === 'Enter') { e.preventDefault(); if (flat[active]) choose(flat[active]); }
|
|
484
|
-
};
|
|
485
|
-
|
|
486
|
-
return h('div', {
|
|
487
|
-
class: 'ov-cmd-backdrop', role: 'presentation',
|
|
488
|
-
ref: (el) => {
|
|
489
|
-
if (!el || el._ovCmd) return; el._ovCmd = true; rootEl = el;
|
|
490
|
-
el.addEventListener('mousedown', (e) => {
|
|
491
|
-
const panel = el.querySelector('.ov-cmd-panel');
|
|
492
|
-
if (panel && !panel.contains(e.target)) close();
|
|
493
|
-
});
|
|
494
|
-
},
|
|
495
|
-
},
|
|
496
|
-
h('div', { class: 'ov-cmd-panel', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Command palette', onkeydown: onKey },
|
|
497
|
-
h('input', {
|
|
498
|
-
type: 'text', class: 'ov-cmd-input', placeholder: 'Type a command…',
|
|
499
|
-
'aria-label': 'command search',
|
|
500
|
-
role: 'combobox',
|
|
501
|
-
'aria-autocomplete': 'list',
|
|
502
|
-
'aria-expanded': 'true',
|
|
503
|
-
'aria-controls': 'ov-cmd-list',
|
|
504
|
-
'aria-activedescendant': '',
|
|
505
|
-
oninput: (e) => { filterText = e.target.value; active = 0; renderInner(); },
|
|
506
|
-
ref: (el) => { if (!el || el._ovCmdIn) return; el._ovCmdIn = true; inputEl = el; queueMicrotask(() => el.focus()); },
|
|
507
|
-
}),
|
|
508
|
-
h('div', { class: 'ov-cmd-list', id: 'ov-cmd-list', role: 'listbox',
|
|
509
|
-
ref: (el) => { if (!el) return; listEl = el; queueMicrotask(renderInner); } })
|
|
510
|
-
)
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Sanctioned literal-emoji exception: an emoji picker's whole purpose is to
|
|
515
|
-
// present emoji, so the glyph ban does not apply to this data table or the
|
|
516
|
-
// per-emoji <button> labels below. This is intentional product content, not
|
|
517
|
-
// decorative chrome.
|
|
518
|
-
const EMOJI_CATEGORIES = [
|
|
519
|
-
{ id: 'smileys', label: '😀', emoji: [
|
|
520
|
-
['😀', 'grinning smile'], ['😁', 'grinning smile happy'], ['😂', 'joy tears laugh'], ['🤣', 'rofl laugh'],
|
|
521
|
-
['😊', 'smile blush happy'], ['😍', 'heart eyes love'], ['😘', 'kiss'], ['😎', 'cool sunglasses'],
|
|
522
|
-
['🤔', 'thinking'], ['😅', 'sweat smile'], ['😉', 'wink'], ['🙂', 'smile slight'],
|
|
523
|
-
['😇', 'angel innocent'], ['🥳', 'party'], ['😴', 'sleep'], ['🤩', 'starstruck'],
|
|
524
|
-
['😜', 'wink tongue'], ['😢', 'cry sad'], ['😭', 'sob cry'], ['😡', 'angry mad'],
|
|
525
|
-
['😱', 'scream shock'], ['🥺', 'pleading'], ['😤', 'huff'], ['😬', 'grimace'],
|
|
526
|
-
] },
|
|
527
|
-
{ id: 'gestures', label: '👍', emoji: [
|
|
528
|
-
['👍', 'thumbsup yes good'], ['👎', 'thumbsdown no bad'], ['👌', 'ok'], ['✌️', 'peace'],
|
|
529
|
-
['🤞', 'fingers crossed'], ['🙏', 'pray thanks'], ['👏', 'clap'], ['🙌', 'raised hands'],
|
|
530
|
-
['💪', 'muscle strong'], ['👀', 'eyes look'], ['🤝', 'handshake'], ['✋', 'hand stop'],
|
|
531
|
-
['🤙', 'call'], ['👋', 'wave hi bye'], ['🤟', 'love you'], ['☝️', 'point up'],
|
|
532
|
-
] },
|
|
533
|
-
{ id: 'hearts', label: '❤️', emoji: [
|
|
534
|
-
['❤️', 'heart love red'], ['🧡', 'heart orange'], ['💛', 'heart yellow'], ['💚', 'heart green'],
|
|
535
|
-
['💙', 'heart blue'], ['💜', 'heart purple'], ['🖤', 'heart black'], ['🤍', 'heart white'],
|
|
536
|
-
['💔', 'broken heart'], ['💕', 'hearts'], ['💖', 'sparkling heart'], ['💗', 'growing heart'],
|
|
537
|
-
] },
|
|
538
|
-
{ id: 'symbols', label: '✅', emoji: [
|
|
539
|
-
['🔥', 'fire lit'], ['💯', 'hundred'], ['✅', 'check yes done'], ['❌', 'cross no'],
|
|
540
|
-
['⭐', 'star'], ['🎉', 'party tada'], ['🎊', 'confetti'], ['✨', 'sparkles'],
|
|
541
|
-
['💡', 'idea lightbulb'], ['⚡', 'zap lightning'], ['💢', 'anger'], ['💀', 'skull dead'],
|
|
542
|
-
['🚀', 'rocket launch'], ['🏆', 'trophy win'],
|
|
543
|
-
] },
|
|
544
|
-
];
|
|
545
|
-
const ALL_EMOJI = EMOJI_CATEGORIES.flatMap((c) => c.emoji);
|
|
546
|
-
|
|
547
|
-
// EmojiPicker — fixed popover near (anchorX, anchorY) with category tabs + grid.
|
|
548
|
-
// `query`, when non-empty, filters across all categories by name/keyword
|
|
549
|
-
// substring match (case-insensitive) instead of showing the active tab.
|
|
550
|
-
export function EmojiPicker({ open, anchorX = 0, anchorY = 0, onSelect, onClose, query = '' } = {}) {
|
|
551
|
-
if (!open) return null;
|
|
552
|
-
let cat = EMOJI_CATEGORIES[0].id;
|
|
553
|
-
let rootEl = null, gridEl = null;
|
|
554
|
-
const close = () => onClose && onClose();
|
|
555
|
-
|
|
556
|
-
const renderGrid = () => {
|
|
557
|
-
if (!gridEl) return;
|
|
558
|
-
const q = (query || '').trim().toLowerCase();
|
|
559
|
-
const cells = q
|
|
560
|
-
? ALL_EMOJI.filter(([, name]) => name.toLowerCase().includes(q))
|
|
561
|
-
: (EMOJI_CATEGORIES.find(x => x.id === cat) || EMOJI_CATEGORIES[0]).emoji;
|
|
562
|
-
webjsx.applyDiff(gridEl, h('div', { class: 'ov-emoji-grid-inner' },
|
|
563
|
-
cells.length ? cells.map(([ch, name]) => h('button', {
|
|
564
|
-
type: 'button', class: 'ov-emoji-cell', 'aria-label': name || ch, title: name || ch,
|
|
565
|
-
onclick: () => { if (onSelect) onSelect(ch); },
|
|
566
|
-
}, ch)) : h('div', { class: 'ov-emoji-empty' }, 'no emoji found')));
|
|
567
|
-
};
|
|
568
|
-
|
|
569
|
-
const tabNavKey = (e) => {
|
|
570
|
-
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
|
|
571
|
-
const tabs = rootEl ? [...rootEl.querySelectorAll('.ov-emoji-tab')] : [];
|
|
572
|
-
if (!tabs.length) return;
|
|
573
|
-
const idx = tabs.indexOf(document.activeElement);
|
|
574
|
-
if (idx < 0) return;
|
|
575
|
-
e.preventDefault();
|
|
576
|
-
const next = e.key === 'ArrowRight' ? (idx + 1) % tabs.length : (idx - 1 + tabs.length) % tabs.length;
|
|
577
|
-
tabs[next].focus();
|
|
578
|
-
tabs[next].click();
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
return h('div', {
|
|
582
|
-
class: 'ov-emoji-root', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Emoji picker',
|
|
583
|
-
tabindex: '-1',
|
|
584
|
-
onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); return; } tabNavKey(e); if (rootEl) trapTab(rootEl, e); },
|
|
585
|
-
ref: (el) => {
|
|
586
|
-
if (!el) { if (rootEl && rootEl._ovEmojiCleanup) rootEl._ovEmojiCleanup(); return; }
|
|
587
|
-
if (el._ovEmoji) return; el._ovEmoji = true; rootEl = el;
|
|
588
|
-
el._ovEmojiCleanup = _anchoredOverlayLifecycle(el, { anchorX, anchorY, fallbackW: 260, fallbackH: 240, close });
|
|
589
|
-
},
|
|
590
|
-
},
|
|
591
|
-
(query || '').trim() ? null : h('div', { class: 'ov-emoji-tabs', role: 'tablist' },
|
|
592
|
-
...EMOJI_CATEGORIES.map((c) => h('button', {
|
|
593
|
-
type: 'button', class: 'ov-emoji-tab', role: 'tab',
|
|
594
|
-
'aria-selected': c.id === cat ? 'true' : 'false',
|
|
595
|
-
'aria-controls': 'ov-emoji-panel',
|
|
596
|
-
onclick: (e) => {
|
|
597
|
-
cat = c.id;
|
|
598
|
-
const tabs = rootEl.querySelectorAll('.ov-emoji-tab');
|
|
599
|
-
tabs.forEach(t => t.setAttribute('aria-selected', 'false'));
|
|
600
|
-
e.currentTarget.setAttribute('aria-selected', 'true');
|
|
601
|
-
renderGrid();
|
|
602
|
-
},
|
|
603
|
-
}, c.label))),
|
|
604
|
-
h('div', {
|
|
605
|
-
class: 'ov-emoji-grid', id: 'ov-emoji-panel', role: 'tabpanel',
|
|
606
|
-
'aria-label': EMOJI_CATEGORIES.find(c => c.id === cat)?.label || EMOJI_CATEGORIES[0].label,
|
|
607
|
-
ref: (el) => { if (!el) return; gridEl = el; queueMicrotask(renderGrid); } })
|
|
608
|
-
);
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// BootOverlay — full-screen brand/progress overlay with error state.
|
|
612
|
-
export function BootOverlay({ progress = 0, phase = '', errored = false, visible = false } = {}) {
|
|
613
|
-
if (!visible) return null;
|
|
614
|
-
let pct = Number(progress) || 0;
|
|
615
|
-
if (pct <= 1) pct = pct * 100;
|
|
616
|
-
pct = Math.max(0, Math.min(100, pct));
|
|
617
|
-
return h('div', { class: 'ov-boot' + (errored ? ' is-error' : ''), role: errored ? 'alert' : 'status', 'aria-live': 'polite' },
|
|
618
|
-
h('div', { class: 'ov-boot-inner' },
|
|
619
|
-
errored
|
|
620
|
-
? h('div', { class: 'ov-boot-mark ov-boot-mark-error', 'aria-hidden': 'true' }, Icon('warn'))
|
|
621
|
-
: h('div', { class: 'ov-boot-spinner', 'aria-hidden': 'true' }),
|
|
622
|
-
!errored ? h('div', { class: 'ov-boot-bar', role: 'progressbar',
|
|
623
|
-
'aria-valuenow': String(Math.round(pct)), 'aria-valuemin': '0', 'aria-valuemax': '100' },
|
|
624
|
-
h('div', { class: 'ov-boot-bar-fill', style: 'width:' + pct + '%' })) : null,
|
|
625
|
-
h('div', { class: 'ov-boot-phase' }, String(phase || (errored ? 'Error' : 'Loading…')))
|
|
626
|
-
)
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// SettingsPopover — fixed popover with generic section/row control rendering.
|
|
631
|
-
export function SettingsPopover({ title = 'Settings', open, anchorX = 0, anchorY = 0, sections = [], onClose } = {}) {
|
|
632
|
-
if (!open) return null;
|
|
633
|
-
let rootEl = null;
|
|
634
|
-
const close = () => onClose && onClose();
|
|
635
|
-
const secs = Array.isArray(sections) ? sections : [];
|
|
636
|
-
|
|
637
|
-
const renderRow = (row, i) => {
|
|
638
|
-
const label = row.label != null ? row.label : (row.title != null ? row.title : '');
|
|
639
|
-
const kind = row.kind;
|
|
640
|
-
// Give every interactive control a stable id and point the row label's
|
|
641
|
-
// `for` at it, so the visible label is the control's accessible name.
|
|
642
|
-
const ctrlId = 'ov-set-' + i + '-' + kind;
|
|
643
|
-
const labelNode = h('label', { class: 'ov-set-row-label', for: ctrlId }, String(label));
|
|
644
|
-
let control = null;
|
|
645
|
-
if (kind === 'select') {
|
|
646
|
-
const opts = Array.isArray(row.options) ? row.options : [];
|
|
647
|
-
// Controlled via the `value` prop only — per-option `selected` is
|
|
648
|
-
// dropped so the two don't fight (value wins).
|
|
649
|
-
control = h('select', {
|
|
650
|
-
id: ctrlId,
|
|
651
|
-
class: 'ov-set-control', value: row.value != null ? String(row.value) : undefined,
|
|
652
|
-
onchange: (e) => row.onChange && row.onChange(e.target.value),
|
|
653
|
-
}, ...opts.map(o => {
|
|
654
|
-
const v = (o && typeof o === 'object') ? o.value : o;
|
|
655
|
-
const l = (o && typeof o === 'object') ? (o.label != null ? o.label : o.value) : o;
|
|
656
|
-
return h('option', { value: String(v) }, String(l));
|
|
657
|
-
}));
|
|
658
|
-
} else if (kind === 'toggle') {
|
|
659
|
-
control = h('input', {
|
|
660
|
-
id: ctrlId,
|
|
661
|
-
type: 'checkbox', class: 'ov-set-toggle',
|
|
662
|
-
checked: row.value ? 'checked' : undefined,
|
|
663
|
-
onchange: (e) => row.onChange && row.onChange(e.target.checked),
|
|
664
|
-
});
|
|
665
|
-
} else if (kind === 'range') {
|
|
666
|
-
control = h('input', {
|
|
667
|
-
id: ctrlId,
|
|
668
|
-
type: 'range', class: 'ov-set-control',
|
|
669
|
-
min: String(row.min != null ? row.min : 0),
|
|
670
|
-
max: String(row.max != null ? row.max : 100),
|
|
671
|
-
step: String(row.step != null ? row.step : 1),
|
|
672
|
-
value: String(row.value != null ? row.value : 0),
|
|
673
|
-
oninput: (e) => row.onChange && row.onChange(Number(e.target.value)),
|
|
674
|
-
});
|
|
675
|
-
} else if (kind === 'button') {
|
|
676
|
-
control = h('button', { type: 'button', class: 'ov-set-btn',
|
|
677
|
-
onclick: () => row.onClick && row.onClick() }, String(label || 'Action'));
|
|
678
|
-
return h('div', { class: 'ov-set-row', key: i }, control);
|
|
679
|
-
} else {
|
|
680
|
-
control = h('span', { class: 'ov-set-row-value' }, String(row.value != null ? row.value : ''));
|
|
681
|
-
// Non-interactive value row: a plain span label (no `for` target).
|
|
682
|
-
return h('div', { class: 'ov-set-row', key: i }, h('span', { class: 'ov-set-row-label' }, String(label)), control);
|
|
683
|
-
}
|
|
684
|
-
return h('div', { class: 'ov-set-row', key: i }, labelNode, control);
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
return h('div', {
|
|
688
|
-
class: 'ov-set-root', role: 'dialog', 'aria-modal': 'true', 'aria-label': String(title), tabindex: '-1',
|
|
689
|
-
onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); return; } if (rootEl) trapTab(rootEl, e); },
|
|
690
|
-
ref: (el) => {
|
|
691
|
-
if (!el) { if (rootEl && rootEl._ovSetCleanup) rootEl._ovSetCleanup(); return; }
|
|
692
|
-
if (el._ovSet) return; el._ovSet = true; rootEl = el;
|
|
693
|
-
el._ovSetCleanup = _anchoredOverlayLifecycle(el, { anchorX, anchorY, fallbackW: 280, fallbackH: 200, close });
|
|
694
|
-
},
|
|
695
|
-
},
|
|
696
|
-
h('div', { class: 'ov-set-head' }, String(title)),
|
|
697
|
-
h('div', { class: 'ov-set-body' },
|
|
698
|
-
...secs.map((sec, si) => {
|
|
699
|
-
const slabel = sec.label != null ? sec.label : (sec.title != null ? sec.title : '');
|
|
700
|
-
const rows = Array.isArray(sec.rows) ? sec.rows : (Array.isArray(sec.items) ? sec.items : []);
|
|
701
|
-
return h('div', { class: 'ov-set-section', key: si },
|
|
702
|
-
slabel ? h('div', { class: 'ov-set-section-head' }, String(slabel)) : null,
|
|
703
|
-
...rows.map((r, ri) => renderRow(r, ri)));
|
|
704
|
-
}))
|
|
705
|
-
);
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// AuthModal — centered login dialog: extension / generate / import (nsec) modes.
|
|
709
|
-
export function AuthModal({ mode = 'extension', error = '', busy = false, open = false, onModeChange, onConnectExtension, onGenerate, onImport, onClose } = {}) {
|
|
710
|
-
if (!open) return null;
|
|
711
|
-
const close = () => onClose && onClose();
|
|
712
|
-
const modes = [
|
|
713
|
-
{ id: 'extension', label: 'Extension' },
|
|
714
|
-
{ id: 'generate', label: 'Generate' },
|
|
715
|
-
{ id: 'import', label: 'Import key' },
|
|
716
|
-
];
|
|
717
|
-
let nsec = '';
|
|
718
|
-
const body = () => {
|
|
719
|
-
if (mode === 'generate') {
|
|
720
|
-
return [
|
|
721
|
-
h('p', { class: 'ov-auth-hint' }, 'Create a fresh Nostr identity. Back up the key after.'),
|
|
722
|
-
h('button', { type: 'button', class: 'ov-auth-primary', disabled: busy ? true : null,
|
|
723
|
-
onclick: () => onGenerate && onGenerate() }, busy ? 'Working…' : 'Generate new key'),
|
|
724
|
-
];
|
|
725
|
-
}
|
|
726
|
-
if (mode === 'import') {
|
|
727
|
-
return [
|
|
728
|
-
h('p', { class: 'ov-auth-hint' }, 'Paste an existing nsec / hex secret key.'),
|
|
729
|
-
h('input', {
|
|
730
|
-
type: 'password', class: 'ov-auth-input', placeholder: 'nsec1…',
|
|
731
|
-
'aria-label': 'secret key', disabled: busy ? true : null,
|
|
732
|
-
oninput: (e) => { nsec = e.target.value; },
|
|
733
|
-
onkeydown: (e) => { if (e.key === 'Enter') { e.preventDefault(); onImport && onImport(nsec); } },
|
|
734
|
-
}),
|
|
735
|
-
h('button', { type: 'button', class: 'ov-auth-primary', disabled: busy ? true : null,
|
|
736
|
-
onclick: () => onImport && onImport(nsec) }, busy ? 'Working…' : 'Import'),
|
|
737
|
-
];
|
|
738
|
-
}
|
|
739
|
-
return [
|
|
740
|
-
h('p', { class: 'ov-auth-hint' }, 'Connect a NIP-07 browser extension (Alby, nos2x…).'),
|
|
741
|
-
h('button', { type: 'button', class: 'ov-auth-primary', disabled: busy ? true : null,
|
|
742
|
-
onclick: () => onConnectExtension && onConnectExtension() }, busy ? 'Connecting…' : 'Connect extension'),
|
|
743
|
-
];
|
|
744
|
-
};
|
|
745
|
-
return h('div', {
|
|
746
|
-
class: 'ov-auth-backdrop', role: 'presentation',
|
|
747
|
-
ref: (el) => {
|
|
748
|
-
if (!el || el._ovAuth) return; el._ovAuth = true;
|
|
749
|
-
el.addEventListener('mousedown', (e) => {
|
|
750
|
-
const panel = el.querySelector('.ov-auth-panel');
|
|
751
|
-
if (panel && !panel.contains(e.target)) close();
|
|
752
|
-
});
|
|
753
|
-
},
|
|
754
|
-
},
|
|
755
|
-
h('div', {
|
|
756
|
-
class: 'ov-auth-panel', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Sign in',
|
|
757
|
-
onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); } },
|
|
758
|
-
},
|
|
759
|
-
h('div', { class: 'ov-auth-head' },
|
|
760
|
-
h('h2', { class: 'ov-auth-title' }, 'Sign in'),
|
|
761
|
-
h('button', { type: 'button', class: 'ov-auth-x', 'aria-label': 'close', onclick: close }, Icon('x'))
|
|
762
|
-
),
|
|
763
|
-
h('div', { class: 'ov-auth-tabs', role: 'tablist',
|
|
764
|
-
onkeydown: (e) => {
|
|
765
|
-
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
|
|
766
|
-
const panel = e.currentTarget.closest('.ov-auth-panel');
|
|
767
|
-
const tabs = panel ? [...panel.querySelectorAll('.ov-auth-tab')] : [];
|
|
768
|
-
if (!tabs.length) return;
|
|
769
|
-
const idx = tabs.indexOf(document.activeElement);
|
|
770
|
-
if (idx < 0) return;
|
|
771
|
-
e.preventDefault();
|
|
772
|
-
const next = e.key === 'ArrowRight' ? (idx + 1) % tabs.length : (idx - 1 + tabs.length) % tabs.length;
|
|
773
|
-
tabs[next].focus();
|
|
774
|
-
onModeChange && onModeChange(modes[next].id);
|
|
775
|
-
},
|
|
776
|
-
},
|
|
777
|
-
...modes.map(m => h('button', {
|
|
778
|
-
type: 'button', role: 'tab', key: 'am-' + m.id,
|
|
779
|
-
id: 'ov-auth-tab-' + m.id,
|
|
780
|
-
class: 'ov-auth-tab' + (m.id === mode ? ' is-active' : ''),
|
|
781
|
-
'aria-selected': m.id === mode ? 'true' : 'false',
|
|
782
|
-
'aria-controls': 'ov-auth-panel',
|
|
783
|
-
onclick: () => onModeChange && onModeChange(m.id),
|
|
784
|
-
}, m.label))
|
|
785
|
-
),
|
|
786
|
-
h('div', { class: 'ov-auth-body', id: 'ov-auth-panel', role: 'tabpanel',
|
|
787
|
-
'aria-labelledby': 'ov-auth-tab-' + mode }, ...body()),
|
|
788
|
-
error ? h('div', { class: 'ov-auth-error', role: 'alert' }, String(error)) : null
|
|
789
|
-
)
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// MenuButton — icon-trigger select menu: one option carries a checkmark
|
|
794
|
-
// (the active selection), roving keyboard nav mirrors Dropdown's own
|
|
795
|
-
// open/close/outside-click/typeahead wiring, plus a stale/unavailable
|
|
796
|
-
// per-item state that renders as a muted "unavailable — retry" row instead
|
|
797
|
-
// of a normal selectable item (ported from docstudio's model-picker menu,
|
|
798
|
-
// which shows a retry affordance when its option list fails to load).
|
|
799
|
-
// Zero-option and single-option lists degrade gracefully: an empty list
|
|
800
|
-
// renders a static "No options available" row (no crash, no keyboard trap,
|
|
801
|
-
// nothing focusable); roving nav on a single-option list simply refocuses
|
|
802
|
-
// the same item on every Arrow press (wrap-to-self), never throws.
|
|
803
|
-
export function MenuButton({ trigger, items = [], selected, onSelect, onRetry, placement = 'bottom-start', ariaLabel = 'Menu', emptyText = 'No options available' } = {}) {
|
|
804
|
-
const menu = useRovingMenu({ itemSelector: '[role="menuitemradio"]:not([aria-disabled="true"])', items, typeahead: true, placement });
|
|
805
|
-
const select = (it) => { if (it.disabled || it.unavailable) return; if (onSelect) onSelect(it.id, it); menu.close(); };
|
|
806
|
-
const buildMenuEl = () => {
|
|
807
|
-
const el = document.createElement('div');
|
|
808
|
-
el.className = 'ds-popover ov-menubutton-menu';
|
|
809
|
-
el.setAttribute('role', 'menu');
|
|
810
|
-
el.setAttribute('aria-label', ariaLabel);
|
|
811
|
-
const tree = items.length
|
|
812
|
-
? h('div', { class: 'ov-menubutton-list' },
|
|
813
|
-
...items.map((it, i) => it.unavailable
|
|
814
|
-
? h('div', { key: it.id || i, class: 'ov-menubutton-item is-unavailable' },
|
|
815
|
-
h('span', { class: 'ov-menubutton-label' }, it.label || 'Unavailable'),
|
|
816
|
-
h('button', { type: 'button', class: 'ov-menubutton-retry', onclick: () => onRetry && onRetry(it.id, it) }, 'Retry')
|
|
817
|
-
)
|
|
818
|
-
: h('button', {
|
|
819
|
-
key: it.id || i, type: 'button', role: 'menuitemradio',
|
|
820
|
-
'aria-checked': it.id === selected ? 'true' : 'false',
|
|
821
|
-
class: 'ov-menubutton-item' + (it.disabled ? '' : ''),
|
|
822
|
-
'aria-disabled': it.disabled ? 'true' : 'false',
|
|
823
|
-
tabindex: '-1', onclick: () => select(it),
|
|
824
|
-
},
|
|
825
|
-
h('span', { class: 'ov-menubutton-check', 'aria-hidden': 'true' }, it.id === selected ? Icon('check', { size: 14 }) : ''),
|
|
826
|
-
h('span', { class: 'ov-menubutton-label' }, it.label)
|
|
827
|
-
)))
|
|
828
|
-
: h('div', { class: 'ov-menubutton-empty' }, emptyText);
|
|
829
|
-
webjsx.applyDiff(el, tree);
|
|
830
|
-
return el;
|
|
831
|
-
};
|
|
832
|
-
const onTrigClick = () => menu.onTrigClick(buildMenuEl);
|
|
833
|
-
const onTrigKey = (e) => menu.onTrigKey(e, buildMenuEl);
|
|
834
|
-
const refFn = menu.refFn('_ovMenuButton');
|
|
835
|
-
const child = (typeof trigger === 'function') ? trigger() : trigger;
|
|
836
|
-
const wireRef = (el) => { refFn(el); if (el) { el.addEventListener('click', onTrigClick); el.addEventListener('keydown', onTrigKey); } };
|
|
837
|
-
return (child && child.type)
|
|
838
|
-
? webjsx.createElement(child.type, { ...(child.props || {}), ref: wireRef }, ...(child.children || []))
|
|
839
|
-
: h('button', { type: 'button', class: 'ov-menubutton-trigger', ref: wireRef }, child || 'Select');
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// VideoLightbox — fullscreen video player overlay with backdrop dismiss.
|
|
843
|
-
export function VideoLightbox({ src, label = '', open = false, onClose } = {}) {
|
|
844
|
-
if (!open || !src) return null;
|
|
845
|
-
const close = () => onClose && onClose();
|
|
846
|
-
return h('div', {
|
|
847
|
-
class: 'ov-lightbox-backdrop', role: 'dialog', 'aria-modal': 'true', 'aria-label': label || 'Video',
|
|
848
|
-
tabindex: '-1',
|
|
849
|
-
onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); } },
|
|
850
|
-
ref: (el) => { if (el && !el._ovLb) { el._ovLb = true; setTimeout(() => el.focus(), 0); } },
|
|
851
|
-
onmousedown: (e) => { if (e.target === e.currentTarget) close(); },
|
|
852
|
-
},
|
|
853
|
-
h('button', { type: 'button', class: 'ov-lightbox-x', 'aria-label': 'close', onclick: close }, Icon('x')),
|
|
854
|
-
h('div', { class: 'ov-lightbox-stage' },
|
|
855
|
-
h('video', { class: 'ov-lightbox-video', src, controls: true, autoplay: true, playsinline: true }),
|
|
856
|
-
label ? h('div', { class: 'ov-lightbox-label' }, label) : null
|
|
857
|
-
)
|
|
858
|
-
);
|
|
859
|
-
}
|
|
5
|
+
//
|
|
6
|
+
// This module is a barrel: every component lives in a single-responsibility
|
|
7
|
+
// submodule under ./overlay-primitives/, and the public export surface here is
|
|
8
|
+
// unchanged — no consumer import needs to move. `trapTab` and `useRovingMenu`
|
|
9
|
+
// are re-exported too: they are consumed cross-module (shell.js imports
|
|
10
|
+
// trapTab from this path) even though the components.js barrel does not
|
|
11
|
+
// forward them.
|
|
12
|
+
|
|
13
|
+
import { useFloating, useLongPress, withBusy, trapTab } from './overlay-primitives/floating.js';
|
|
14
|
+
import { Tooltip } from './overlay-primitives/tooltip.js';
|
|
15
|
+
import { Popover } from './overlay-primitives/popover.js';
|
|
16
|
+
import { useRovingMenu } from './overlay-primitives/roving-menu.js';
|
|
17
|
+
import { Dropdown, PermissionMenu, MenuButton } from './overlay-primitives/menus.js';
|
|
18
|
+
import { ApprovalPrompt } from './overlay-primitives/approval-prompt.js';
|
|
19
|
+
import { CommandPalette } from './overlay-primitives/command-palette.js';
|
|
20
|
+
import { EmojiPicker } from './overlay-primitives/emoji-picker.js';
|
|
21
|
+
import { SettingsPopover } from './overlay-primitives/settings-popover.js';
|
|
22
|
+
import { AuthModal } from './overlay-primitives/auth-modal.js';
|
|
23
|
+
import { BootOverlay, VideoLightbox } from './overlay-primitives/full-screen.js';
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
useFloating, useLongPress, withBusy, trapTab,
|
|
27
|
+
Tooltip,
|
|
28
|
+
Popover,
|
|
29
|
+
useRovingMenu,
|
|
30
|
+
Dropdown, PermissionMenu, MenuButton,
|
|
31
|
+
ApprovalPrompt,
|
|
32
|
+
CommandPalette,
|
|
33
|
+
EmojiPicker,
|
|
34
|
+
SettingsPopover,
|
|
35
|
+
AuthModal,
|
|
36
|
+
BootOverlay, VideoLightbox,
|
|
37
|
+
};
|