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
|
@@ -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
|
+
}
|