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,38 @@
1
+ // ApprovalPrompt — an inline, in-thread tool-permission card (as opposed to
2
+ // PermissionMenu's settings-style dropdown): shows the tool name + an
3
+ // optional args preview, an optional free-text note the user can attach to
4
+ // their decision (auto-focused, since the note is usually the primary
5
+ // reason to open this card at all), and up to four resolution actions
6
+ // (once/session/all/deny). Mirrors docstudio's chat-approval-prompts.js
7
+ // buildApprovalPrompt shape. The note textarea is entirely optional -
8
+ // omitting `onDecision`'s use of the note arg keeps existing simpler
9
+ // once/deny-only call sites unaffected.
10
+
11
+ import * as webjsx from '../../../vendor/webjsx/index.js';
12
+ import { Icon } from '../shell.js';
13
+ const h = webjsx.createElement;
14
+
15
+ export function ApprovalPrompt({ toolName, categoryLabel, argsPreview, onDecision, autoFocusNote = true } = {}) {
16
+ let noteEl = null;
17
+ const noteRef = (el) => {
18
+ if (!el || noteEl === el) return;
19
+ noteEl = el;
20
+ if (autoFocusNote) queueMicrotask(() => noteEl && noteEl.focus());
21
+ };
22
+ const decide = (kind) => { if (onDecision) onDecision(kind, (noteEl && noteEl.value || '').trim()); };
23
+ return h('div', { class: 'ov-approval', role: 'group', 'aria-label': toolName ? `Permission requested: ${toolName}` : 'Permission requested' },
24
+ h('div', { class: 'ov-approval-head' },
25
+ h('span', { class: 'ov-approval-icon' }, Icon('lock', { size: 16 })),
26
+ h('strong', { class: 'ov-approval-tool' }, toolName || ''),
27
+ categoryLabel ? h('span', { class: 'ov-approval-cat' }, '- ' + categoryLabel) : null),
28
+ argsPreview ? h('pre', { class: 'ov-approval-args' }, argsPreview) : null,
29
+ h('textarea', {
30
+ class: 'ov-approval-note', ref: noteRef,
31
+ placeholder: 'Add instructions for the assistant (optional)...',
32
+ }),
33
+ h('div', { class: 'ov-approval-actions' },
34
+ h('button', { type: 'button', class: 'ov-approval-btn ov-approval-btn-primary', onclick: () => decide('once') }, 'Allow once'),
35
+ h('button', { type: 'button', class: 'ov-approval-btn ov-approval-btn-soft', onclick: () => decide('session') }, 'Allow for session'),
36
+ h('button', { type: 'button', class: 'ov-approval-btn', onclick: () => decide('all') }, 'Allow all'),
37
+ h('button', { type: 'button', class: 'ov-approval-btn ov-approval-btn-deny', onclick: () => decide('deny') }, 'Deny')));
38
+ }
@@ -0,0 +1,89 @@
1
+ // AuthModal — centered login dialog: extension / generate / import (nsec) modes.
2
+
3
+ import * as webjsx from '../../../vendor/webjsx/index.js';
4
+ import { Icon } from '../shell.js';
5
+ const h = webjsx.createElement;
6
+
7
+ export function AuthModal({ mode = 'extension', error = '', busy = false, open = false, onModeChange, onConnectExtension, onGenerate, onImport, onClose } = {}) {
8
+ if (!open) return null;
9
+ const close = () => onClose && onClose();
10
+ const modes = [
11
+ { id: 'extension', label: 'Extension' },
12
+ { id: 'generate', label: 'Generate' },
13
+ { id: 'import', label: 'Import key' },
14
+ ];
15
+ let nsec = '';
16
+ const body = () => {
17
+ if (mode === 'generate') {
18
+ return [
19
+ h('p', { class: 'ov-auth-hint' }, 'Create a fresh Nostr identity. Back up the key after.'),
20
+ h('button', { type: 'button', class: 'ov-auth-primary', disabled: busy ? true : null,
21
+ onclick: () => onGenerate && onGenerate() }, busy ? 'Working…' : 'Generate new key'),
22
+ ];
23
+ }
24
+ if (mode === 'import') {
25
+ return [
26
+ h('p', { class: 'ov-auth-hint' }, 'Paste an existing nsec / hex secret key.'),
27
+ h('input', {
28
+ type: 'password', class: 'ov-auth-input', placeholder: 'nsec1…',
29
+ 'aria-label': 'secret key', disabled: busy ? true : null,
30
+ oninput: (e) => { nsec = e.target.value; },
31
+ onkeydown: (e) => { if (e.key === 'Enter') { e.preventDefault(); onImport && onImport(nsec); } },
32
+ }),
33
+ h('button', { type: 'button', class: 'ov-auth-primary', disabled: busy ? true : null,
34
+ onclick: () => onImport && onImport(nsec) }, busy ? 'Working…' : 'Import'),
35
+ ];
36
+ }
37
+ return [
38
+ h('p', { class: 'ov-auth-hint' }, 'Connect a NIP-07 browser extension (Alby, nos2x…).'),
39
+ h('button', { type: 'button', class: 'ov-auth-primary', disabled: busy ? true : null,
40
+ onclick: () => onConnectExtension && onConnectExtension() }, busy ? 'Connecting…' : 'Connect extension'),
41
+ ];
42
+ };
43
+ return h('div', {
44
+ class: 'ov-auth-backdrop', role: 'presentation',
45
+ ref: (el) => {
46
+ if (!el || el._ovAuth) return; el._ovAuth = true;
47
+ el.addEventListener('mousedown', (e) => {
48
+ const panel = el.querySelector('.ov-auth-panel');
49
+ if (panel && !panel.contains(e.target)) close();
50
+ });
51
+ },
52
+ },
53
+ h('div', {
54
+ class: 'ov-auth-panel', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Sign in',
55
+ onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); } },
56
+ },
57
+ h('div', { class: 'ov-auth-head' },
58
+ h('h2', { class: 'ov-auth-title' }, 'Sign in'),
59
+ h('button', { type: 'button', class: 'ov-auth-x', 'aria-label': 'close', onclick: close }, Icon('x'))
60
+ ),
61
+ h('div', { class: 'ov-auth-tabs', role: 'tablist',
62
+ onkeydown: (e) => {
63
+ if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
64
+ const panel = e.currentTarget.closest('.ov-auth-panel');
65
+ const tabs = panel ? [...panel.querySelectorAll('.ov-auth-tab')] : [];
66
+ if (!tabs.length) return;
67
+ const idx = tabs.indexOf(document.activeElement);
68
+ if (idx < 0) return;
69
+ e.preventDefault();
70
+ const next = e.key === 'ArrowRight' ? (idx + 1) % tabs.length : (idx - 1 + tabs.length) % tabs.length;
71
+ tabs[next].focus();
72
+ onModeChange && onModeChange(modes[next].id);
73
+ },
74
+ },
75
+ ...modes.map(m => h('button', {
76
+ type: 'button', role: 'tab', key: 'am-' + m.id,
77
+ id: 'ov-auth-tab-' + m.id,
78
+ class: 'ov-auth-tab' + (m.id === mode ? ' is-active' : ''),
79
+ 'aria-selected': m.id === mode ? 'true' : 'false',
80
+ 'aria-controls': 'ov-auth-panel',
81
+ onclick: () => onModeChange && onModeChange(m.id),
82
+ }, m.label))
83
+ ),
84
+ h('div', { class: 'ov-auth-body', id: 'ov-auth-panel', role: 'tabpanel',
85
+ 'aria-labelledby': 'ov-auth-tab-' + mode }, ...body()),
86
+ error ? h('div', { class: 'ov-auth-error', role: 'alert' }, String(error)) : null
87
+ )
88
+ );
89
+ }
@@ -0,0 +1,101 @@
1
+ // CommandPalette — centered Cmd+K palette with live filter + keyboard nav.
2
+ // The result list is rendered imperatively (applyDiff into the list element)
3
+ // so filter/active-index changes repaint only the list, not the whole page.
4
+
5
+ import * as webjsx from '../../../vendor/webjsx/index.js';
6
+ const h = webjsx.createElement;
7
+
8
+ export function CommandPalette({ open, items = [], onSelect, onClose } = {}) {
9
+ if (!open) return null;
10
+ const list = Array.isArray(items) ? items : [];
11
+ const labelOf = (it) => String(it.label || it.title || it.name || '');
12
+ let active = 0, filterText = '';
13
+
14
+ const matches = () => {
15
+ const q = filterText.trim().toLowerCase();
16
+ return q ? list.filter(it => labelOf(it).toLowerCase().includes(q)) : list.slice();
17
+ };
18
+
19
+ const rowsFor = (filtered) => {
20
+ const out = [];
21
+ let lastGroup = null, flatIdx = 0;
22
+ for (const it of filtered) {
23
+ const grp = it.group != null ? String(it.group) : null;
24
+ if (grp && grp !== lastGroup) {
25
+ out.push(h('div', { class: 'ov-cmd-group', role: 'presentation' }, grp));
26
+ lastGroup = grp;
27
+ }
28
+ const idx = flatIdx++;
29
+ const glyph = it.icon != null ? it.icon : (it.glyph != null ? it.glyph : null);
30
+ const hint = it.hint != null ? it.hint : (it.shortcut != null ? it.shortcut : null);
31
+ out.push(h('button', {
32
+ type: 'button', role: 'option',
33
+ id: 'ov-cmd-item-' + idx,
34
+ 'data-idx': String(idx),
35
+ 'aria-selected': idx === active ? 'true' : 'false',
36
+ class: 'ov-cmd-item' + (idx === active ? ' is-active' : ''),
37
+ onclick: () => choose(it),
38
+ onmousemove: () => { if (active !== idx) { active = idx; renderInner(); } },
39
+ },
40
+ glyph != null ? h('span', { class: 'ov-cmd-glyph', 'aria-hidden': 'true' }, glyph) : null,
41
+ h('span', { class: 'ov-cmd-label' }, labelOf(it)),
42
+ hint != null ? h('span', { class: 'ov-cmd-hint' }, hint) : null
43
+ ));
44
+ }
45
+ return out;
46
+ };
47
+
48
+ let rootEl = null, inputEl = null, listEl = null, flat = [];
49
+ // Remember the element focused before the palette opened so we can return
50
+ // focus there on close (the input steals focus on mount).
51
+ const prevFocus = (typeof document !== 'undefined') ? document.activeElement : null;
52
+ const restoreFocus = () => { if (prevFocus && prevFocus.focus && document.contains(prevFocus)) prevFocus.focus(); };
53
+ const close = () => { restoreFocus(); if (onClose) onClose(); };
54
+ const choose = (it) => { if (it && onSelect) onSelect(it); };
55
+
56
+ const renderInner = () => {
57
+ if (!listEl) return;
58
+ const filtered = matches();
59
+ flat = filtered;
60
+ if (active >= filtered.length) active = Math.max(0, filtered.length - 1);
61
+ webjsx.applyDiff(listEl, h('div', { class: 'ov-cmd-list-inner' },
62
+ filtered.length ? rowsFor(filtered) : h('div', { class: 'ov-cmd-empty' }, 'No results')));
63
+ const sel = listEl.querySelector('.ov-cmd-item.is-active');
64
+ if (sel && sel.scrollIntoView) sel.scrollIntoView({ block: 'nearest' });
65
+ if (inputEl) inputEl.setAttribute('aria-activedescendant', filtered.length ? 'ov-cmd-item-' + active : '');
66
+ };
67
+
68
+ const onKey = (e) => {
69
+ if (e.key === 'Escape') { e.preventDefault(); close(); }
70
+ else if (e.key === 'ArrowDown') { e.preventDefault(); if (flat.length) { active = (active + 1) % flat.length; renderInner(); } }
71
+ else if (e.key === 'ArrowUp') { e.preventDefault(); if (flat.length) { active = (active - 1 + flat.length) % flat.length; renderInner(); } }
72
+ else if (e.key === 'Enter') { e.preventDefault(); if (flat[active]) choose(flat[active]); }
73
+ };
74
+
75
+ return h('div', {
76
+ class: 'ov-cmd-backdrop', role: 'presentation',
77
+ ref: (el) => {
78
+ if (!el || el._ovCmd) return; el._ovCmd = true; rootEl = el;
79
+ el.addEventListener('mousedown', (e) => {
80
+ const panel = el.querySelector('.ov-cmd-panel');
81
+ if (panel && !panel.contains(e.target)) close();
82
+ });
83
+ },
84
+ },
85
+ h('div', { class: 'ov-cmd-panel', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Command palette', onkeydown: onKey },
86
+ h('input', {
87
+ type: 'text', class: 'ov-cmd-input', placeholder: 'Type a command…',
88
+ 'aria-label': 'command search',
89
+ role: 'combobox',
90
+ 'aria-autocomplete': 'list',
91
+ 'aria-expanded': 'true',
92
+ 'aria-controls': 'ov-cmd-list',
93
+ 'aria-activedescendant': '',
94
+ oninput: (e) => { filterText = e.target.value; active = 0; renderInner(); },
95
+ ref: (el) => { if (!el || el._ovCmdIn) return; el._ovCmdIn = true; inputEl = el; queueMicrotask(() => el.focus()); },
96
+ }),
97
+ h('div', { class: 'ov-cmd-list', id: 'ov-cmd-list', role: 'listbox',
98
+ ref: (el) => { if (!el) return; listEl = el; queueMicrotask(renderInner); } })
99
+ )
100
+ );
101
+ }
@@ -0,0 +1,103 @@
1
+ // EmojiPicker — fixed popover near (anchorX, anchorY) with category tabs +
2
+ // grid, plus the emoji dataset it presents.
3
+
4
+ import * as webjsx from '../../../vendor/webjsx/index.js';
5
+ import { trapTab, _anchoredOverlayLifecycle } from './floating.js';
6
+ const h = webjsx.createElement;
7
+
8
+ // Sanctioned literal-emoji exception: an emoji picker's whole purpose is to
9
+ // present emoji, so the glyph ban does not apply to this data table or the
10
+ // per-emoji <button> labels below. This is intentional product content, not
11
+ // decorative chrome.
12
+ const EMOJI_CATEGORIES = [
13
+ { id: 'smileys', label: '😀', emoji: [
14
+ ['😀', 'grinning smile'], ['😁', 'grinning smile happy'], ['😂', 'joy tears laugh'], ['🤣', 'rofl laugh'],
15
+ ['😊', 'smile blush happy'], ['😍', 'heart eyes love'], ['😘', 'kiss'], ['😎', 'cool sunglasses'],
16
+ ['🤔', 'thinking'], ['😅', 'sweat smile'], ['😉', 'wink'], ['🙂', 'smile slight'],
17
+ ['😇', 'angel innocent'], ['🥳', 'party'], ['😴', 'sleep'], ['🤩', 'starstruck'],
18
+ ['😜', 'wink tongue'], ['😢', 'cry sad'], ['😭', 'sob cry'], ['😡', 'angry mad'],
19
+ ['😱', 'scream shock'], ['🥺', 'pleading'], ['😤', 'huff'], ['😬', 'grimace'],
20
+ ] },
21
+ { id: 'gestures', label: '👍', emoji: [
22
+ ['👍', 'thumbsup yes good'], ['👎', 'thumbsdown no bad'], ['👌', 'ok'], ['✌️', 'peace'],
23
+ ['🤞', 'fingers crossed'], ['🙏', 'pray thanks'], ['👏', 'clap'], ['🙌', 'raised hands'],
24
+ ['💪', 'muscle strong'], ['👀', 'eyes look'], ['🤝', 'handshake'], ['✋', 'hand stop'],
25
+ ['🤙', 'call'], ['👋', 'wave hi bye'], ['🤟', 'love you'], ['☝️', 'point up'],
26
+ ] },
27
+ { id: 'hearts', label: '❤️', emoji: [
28
+ ['❤️', 'heart love red'], ['🧡', 'heart orange'], ['💛', 'heart yellow'], ['💚', 'heart green'],
29
+ ['💙', 'heart blue'], ['💜', 'heart purple'], ['🖤', 'heart black'], ['🤍', 'heart white'],
30
+ ['💔', 'broken heart'], ['💕', 'hearts'], ['💖', 'sparkling heart'], ['💗', 'growing heart'],
31
+ ] },
32
+ { id: 'symbols', label: '✅', emoji: [
33
+ ['🔥', 'fire lit'], ['💯', 'hundred'], ['✅', 'check yes done'], ['❌', 'cross no'],
34
+ ['⭐', 'star'], ['🎉', 'party tada'], ['🎊', 'confetti'], ['✨', 'sparkles'],
35
+ ['💡', 'idea lightbulb'], ['⚡', 'zap lightning'], ['💢', 'anger'], ['💀', 'skull dead'],
36
+ ['🚀', 'rocket launch'], ['🏆', 'trophy win'],
37
+ ] },
38
+ ];
39
+ const ALL_EMOJI = EMOJI_CATEGORIES.flatMap((c) => c.emoji);
40
+
41
+ // EmojiPicker — fixed popover near (anchorX, anchorY) with category tabs + grid.
42
+ // `query`, when non-empty, filters across all categories by name/keyword
43
+ // substring match (case-insensitive) instead of showing the active tab.
44
+ export function EmojiPicker({ open, anchorX = 0, anchorY = 0, onSelect, onClose, query = '' } = {}) {
45
+ if (!open) return null;
46
+ let cat = EMOJI_CATEGORIES[0].id;
47
+ let rootEl = null, gridEl = null;
48
+ const close = () => onClose && onClose();
49
+
50
+ const renderGrid = () => {
51
+ if (!gridEl) return;
52
+ const q = (query || '').trim().toLowerCase();
53
+ const cells = q
54
+ ? ALL_EMOJI.filter(([, name]) => name.toLowerCase().includes(q))
55
+ : (EMOJI_CATEGORIES.find(x => x.id === cat) || EMOJI_CATEGORIES[0]).emoji;
56
+ webjsx.applyDiff(gridEl, h('div', { class: 'ov-emoji-grid-inner' },
57
+ cells.length ? cells.map(([ch, name]) => h('button', {
58
+ type: 'button', class: 'ov-emoji-cell', 'aria-label': name || ch, title: name || ch,
59
+ onclick: () => { if (onSelect) onSelect(ch); },
60
+ }, ch)) : h('div', { class: 'ov-emoji-empty' }, 'no emoji found')));
61
+ };
62
+
63
+ const tabNavKey = (e) => {
64
+ if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
65
+ const tabs = rootEl ? [...rootEl.querySelectorAll('.ov-emoji-tab')] : [];
66
+ if (!tabs.length) return;
67
+ const idx = tabs.indexOf(document.activeElement);
68
+ if (idx < 0) return;
69
+ e.preventDefault();
70
+ const next = e.key === 'ArrowRight' ? (idx + 1) % tabs.length : (idx - 1 + tabs.length) % tabs.length;
71
+ tabs[next].focus();
72
+ tabs[next].click();
73
+ };
74
+
75
+ return h('div', {
76
+ class: 'ov-emoji-root', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Emoji picker',
77
+ tabindex: '-1',
78
+ onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); return; } tabNavKey(e); if (rootEl) trapTab(rootEl, e); },
79
+ ref: (el) => {
80
+ if (!el) { if (rootEl && rootEl._ovEmojiCleanup) rootEl._ovEmojiCleanup(); return; }
81
+ if (el._ovEmoji) return; el._ovEmoji = true; rootEl = el;
82
+ el._ovEmojiCleanup = _anchoredOverlayLifecycle(el, { anchorX, anchorY, fallbackW: 260, fallbackH: 240, close });
83
+ },
84
+ },
85
+ (query || '').trim() ? null : h('div', { class: 'ov-emoji-tabs', role: 'tablist' },
86
+ ...EMOJI_CATEGORIES.map((c) => h('button', {
87
+ type: 'button', class: 'ov-emoji-tab', role: 'tab',
88
+ 'aria-selected': c.id === cat ? 'true' : 'false',
89
+ 'aria-controls': 'ov-emoji-panel',
90
+ onclick: (e) => {
91
+ cat = c.id;
92
+ const tabs = rootEl.querySelectorAll('.ov-emoji-tab');
93
+ tabs.forEach(t => t.setAttribute('aria-selected', 'false'));
94
+ e.currentTarget.setAttribute('aria-selected', 'true');
95
+ renderGrid();
96
+ },
97
+ }, c.label))),
98
+ h('div', {
99
+ class: 'ov-emoji-grid', id: 'ov-emoji-panel', role: 'tabpanel',
100
+ 'aria-label': EMOJI_CATEGORIES.find(c => c.id === cat)?.label || EMOJI_CATEGORIES[0].label,
101
+ ref: (el) => { if (!el) return; gridEl = el; queueMicrotask(renderGrid); } })
102
+ );
103
+ }
@@ -0,0 +1,146 @@
1
+ // Overlay positioning core — the shared geometry/lifecycle every overlay in
2
+ // this group builds on: useFloating (anchored placement with auto-flip +
3
+ // viewport clamp), useLongPress, withBusy, trapTab, plus the internal
4
+ // _clampToViewport / _anchoredOverlayLifecycle helpers used by the fixed
5
+ // anchored popovers (EmojiPicker, SettingsPopover). No inline styles except
6
+ // runtime left/top. CSS classes scoped to .ds-247420 (see
7
+ // editor-primitives.css).
8
+
9
+ export const FOCUSABLE_SEL = 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';
10
+ export const kids = (c) => c == null ? [] : (Array.isArray(c) ? c : [c]);
11
+
12
+ // Shared viewport-clamp margins (px). Previously scattered as bare 8/4/6
13
+ // literals across useFloating + _clampToViewport. CLAMP_MARGIN is the gap a
14
+ // fixed box keeps from the viewport edge; FLOAT_EDGE is the useFloating edge
15
+ // gap; FLOAT_OFFSET_* are anchor-to-content offsets per overlay kind.
16
+ const CLAMP_MARGIN = 8;
17
+ const FLOAT_EDGE = 4;
18
+ export const FLOAT_OFFSET_TOOLTIP = 6;
19
+ export const FLOAT_OFFSET_POPOVER = 6;
20
+ export const FLOAT_OFFSET_DROPDOWN = 4;
21
+
22
+ // useFloating — compute left/top + auto-flip; re-runs on resize/scroll.
23
+ export function useFloating(anchorEl, contentEl, { placement = 'bottom-start', offset = 8 } = {}) {
24
+ if (!anchorEl || !contentEl) return { update() {}, dispose() {}, finalPlacement: placement };
25
+ let finalPlacement = placement;
26
+ const compute = () => {
27
+ const a = anchorEl.getBoundingClientRect(), c = contentEl.getBoundingClientRect();
28
+ const vw = window.innerWidth, vh = window.innerHeight;
29
+ const [side, align = 'start'] = placement.split('-');
30
+ let s = side;
31
+ if (s === 'bottom' && a.bottom + offset + c.height > vh && a.top - offset - c.height >= 0) s = 'top';
32
+ else if (s === 'top' && a.top - offset - c.height < 0 && a.bottom + offset + c.height <= vh) s = 'bottom';
33
+ else if (s === 'right' && a.right + offset + c.width > vw && a.left - offset - c.width >= 0) s = 'left';
34
+ else if (s === 'left' && a.left - offset - c.width < 0 && a.right + offset + c.width <= vw) s = 'right';
35
+ let x = 0, y = 0;
36
+ if (s === 'bottom' || s === 'top') {
37
+ y = s === 'bottom' ? a.bottom + offset : a.top - offset - c.height;
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
+ // Clamp a fixed-position box to the viewport given desired top-left coords.
103
+ function _clampToViewport(x, y, w, h, margin = CLAMP_MARGIN) {
104
+ const vw = (typeof window !== 'undefined' ? window.innerWidth : 1024);
105
+ const vh = (typeof window !== 'undefined' ? window.innerHeight : 768);
106
+ return {
107
+ left: Math.max(margin, Math.min(vw - w - margin, x)),
108
+ top: Math.max(margin, Math.min(vh - h - margin, y)),
109
+ };
110
+ }
111
+
112
+ // Tab focus trap for a dialog root — keeps Tab/Shift+Tab cycling inside `el`.
113
+ // Call from an onkeydown handler; returns true if it handled the event.
114
+ export function trapTab(el, e) {
115
+ if (e.key !== 'Tab') return false;
116
+ const nodes = el.querySelectorAll(FOCUSABLE_SEL);
117
+ if (!nodes.length) { e.preventDefault(); return true; }
118
+ const first = nodes[0], last = nodes[nodes.length - 1], a = document.activeElement;
119
+ if (e.shiftKey && a === first) { e.preventDefault(); last.focus(); return true; }
120
+ if (!e.shiftKey && a === last) { e.preventDefault(); first.focus(); return true; }
121
+ return false;
122
+ }
123
+
124
+ // Shared lifecycle for fixed anchor-positioned popovers (EmojiPicker,
125
+ // SettingsPopover): on mount, place+clamp near (anchorX, anchorY), focus the
126
+ // root, and wire an outside-mousedown close. Returns a cleanup fn the ref(null)
127
+ // branch must call. Both consumers deduped through this so the
128
+ // queueMicrotask/place/clamp/outside-close dance is authored once.
129
+ export function _anchoredOverlayLifecycle(el, { anchorX, anchorY, fallbackW, fallbackH, close }) {
130
+ const place = () => {
131
+ const r = el.getBoundingClientRect();
132
+ const { left, top } = _clampToViewport(anchorX, anchorY, r.width || fallbackW, r.height || fallbackH);
133
+ el.style.left = left + 'px'; el.style.top = top + 'px';
134
+ };
135
+ // setTimeout(0), not queueMicrotask: the triggering click's own default
136
+ // focus-on-click (moving focus to the clicked <button>) can run AFTER a
137
+ // same-tick microtask, so a queueMicrotask focus() call here was losing
138
+ // the race and leaving focus on the trigger button instead of the
139
+ // dialog -- breaking Escape-to-close (keydown only bubbles from the
140
+ // focused element) for any keyboard user. A macrotask reliably runs
141
+ // after the click's focus settles.
142
+ setTimeout(() => { place(); el.focus(); }, 0);
143
+ const onDown = (e) => { if (!el.contains(e.target)) close(); };
144
+ queueMicrotask(() => document.addEventListener('mousedown', onDown, true));
145
+ return () => document.removeEventListener('mousedown', onDown, true);
146
+ }
@@ -0,0 +1,45 @@
1
+ // Full-screen overlays — BootOverlay (brand/progress splash with an error
2
+ // state) and VideoLightbox (fullscreen video player with backdrop dismiss).
3
+ // Both cover the whole viewport rather than anchoring to a trigger.
4
+
5
+ import * as webjsx from '../../../vendor/webjsx/index.js';
6
+ import { Icon } from '../shell.js';
7
+ const h = webjsx.createElement;
8
+
9
+ // BootOverlay — full-screen brand/progress overlay with error state.
10
+ export function BootOverlay({ progress = 0, phase = '', errored = false, visible = false } = {}) {
11
+ if (!visible) return null;
12
+ let pct = Number(progress) || 0;
13
+ if (pct <= 1) pct = pct * 100;
14
+ pct = Math.max(0, Math.min(100, pct));
15
+ return h('div', { class: 'ov-boot' + (errored ? ' is-error' : ''), role: errored ? 'alert' : 'status', 'aria-live': 'polite' },
16
+ h('div', { class: 'ov-boot-inner' },
17
+ errored
18
+ ? h('div', { class: 'ov-boot-mark ov-boot-mark-error', 'aria-hidden': 'true' }, Icon('warn'))
19
+ : h('div', { class: 'ov-boot-spinner', 'aria-hidden': 'true' }),
20
+ !errored ? h('div', { class: 'ov-boot-bar', role: 'progressbar',
21
+ 'aria-valuenow': String(Math.round(pct)), 'aria-valuemin': '0', 'aria-valuemax': '100' },
22
+ h('div', { class: 'ov-boot-bar-fill', style: 'width:' + pct + '%' })) : null,
23
+ h('div', { class: 'ov-boot-phase' }, String(phase || (errored ? 'Error' : 'Loading…')))
24
+ )
25
+ );
26
+ }
27
+
28
+ // VideoLightbox — fullscreen video player overlay with backdrop dismiss.
29
+ export function VideoLightbox({ src, label = '', open = false, onClose } = {}) {
30
+ if (!open || !src) return null;
31
+ const close = () => onClose && onClose();
32
+ return h('div', {
33
+ class: 'ov-lightbox-backdrop', role: 'dialog', 'aria-modal': 'true', 'aria-label': label || 'Video',
34
+ tabindex: '-1',
35
+ onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); } },
36
+ ref: (el) => { if (el && !el._ovLb) { el._ovLb = true; setTimeout(() => el.focus(), 0); } },
37
+ onmousedown: (e) => { if (e.target === e.currentTarget) close(); },
38
+ },
39
+ h('button', { type: 'button', class: 'ov-lightbox-x', 'aria-label': 'close', onclick: close }, Icon('x')),
40
+ h('div', { class: 'ov-lightbox-stage' },
41
+ h('video', { class: 'ov-lightbox-video', src, controls: true, autoplay: true, playsinline: true }),
42
+ label ? h('div', { class: 'ov-lightbox-label' }, label) : null
43
+ )
44
+ );
45
+ }