anentrypoint-design 0.0.365 → 0.0.366
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 +1 -0
- package/dist/247420.js +26 -26
- package/package.json +1 -1
- package/src/components/chat.js +2 -1
- package/src/components/community.js +6 -6
- package/src/components/content.js +67 -5
- package/src/components/freddie/runtime.js +17 -0
- package/src/components/freddie.js +1 -5
- package/src/components/overlay-primitives.js +91 -142
- package/src/components.js +6 -2
- package/src/css/app-shell/kits-appended.css +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.366",
|
|
4
4
|
"description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/247420.js",
|
package/src/components/chat.js
CHANGED
|
@@ -10,6 +10,7 @@ import { fmtFileSize } from './files.js';
|
|
|
10
10
|
import { EmojiPicker } from './overlay-primitives.js';
|
|
11
11
|
import { t } from '../i18n.js';
|
|
12
12
|
import { renderMessagePart as sharedRenderMessagePart, safeUrl as sharedSafeUrl, renderInline as sharedRenderInline, injectCodeCopy as sharedInjectCodeCopy } from './chat-message-parts.js';
|
|
13
|
+
import { avatarInitial } from './content.js';
|
|
13
14
|
|
|
14
15
|
// Matches a trailing `:keyword` at the end of the composer draft (optionally
|
|
15
16
|
// preceded by whitespace/start-of-string) so typing `:smile` opens an inline
|
|
@@ -119,7 +120,7 @@ export function ChatMessage({ role, who = 'them', avatar, text, parts, time, typ
|
|
|
119
120
|
const cls = 'chat-msg ' + resolvedWho + (aicat && resolvedWho === 'them' ? ' aicat' : '') + (isCentered ? ' centered' : '') + (isFlat ? ' chat-msg-flat' : '');
|
|
120
121
|
const fallbackAvatar = avatar != null
|
|
121
122
|
? avatar
|
|
122
|
-
: (resolvedWho === 'you' ? 'u' : (name
|
|
123
|
+
: (resolvedWho === 'you' ? 'u' : avatarInitial(name));
|
|
123
124
|
const av = h('span', { class: 'chat-avatar' }, fallbackAvatar);
|
|
124
125
|
let bodyNodes;
|
|
125
126
|
if (typing) bodyNodes = [h('div', { class: 'chat-bubble', key: 'typb' }, h('span', { class: 'chat-typing' }, h('span'), h('span'), h('span')))];
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as webjsx from '../../vendor/webjsx/index.js';
|
|
4
4
|
import { Icon } from './shell.js';
|
|
5
|
+
import { Avatar, avatarInitial } from './content.js';
|
|
5
6
|
import { sanitizeHtml } from '../markdown.js';
|
|
6
7
|
const h = webjsx.createElement;
|
|
7
8
|
|
|
@@ -10,7 +11,6 @@ const h = webjsx.createElement;
|
|
|
10
11
|
const clampCount = (n) => { const v = Number(n) || 0; return v > 99 ? '99+' : String(v); };
|
|
11
12
|
|
|
12
13
|
export function ServerIcon({ id, name, icon, active, badge, onClick } = {}) {
|
|
13
|
-
const initials = (name || '?').slice(0, 2).toUpperCase();
|
|
14
14
|
return h('div', {
|
|
15
15
|
class: 'cm-server-icon' + (active ? ' active' : ''),
|
|
16
16
|
onclick: onClick,
|
|
@@ -27,7 +27,7 @@ export function ServerIcon({ id, name, icon, active, badge, onClick } = {}) {
|
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
h('span', { class: 'cm-server-pill' }),
|
|
30
|
-
icon ? h('img', { src: icon, alt: name }) :
|
|
30
|
+
icon ? h('img', { src: icon, alt: name }) : Avatar({ name, shape: 'square', initialsCount: 2 }),
|
|
31
31
|
badge ? h('span', { class: 'cm-server-badge' }, badge > 99 ? '99+' : String(badge)) : null
|
|
32
32
|
);
|
|
33
33
|
}
|
|
@@ -88,7 +88,7 @@ export function ChannelItem({ id, name, type = 'text', active, voiceActive, voic
|
|
|
88
88
|
),
|
|
89
89
|
voiceActive && participants.length ? h('div', { class: 'cm-ch-voice-users' },
|
|
90
90
|
...participants.map(p => h('div', { class: 'cm-ch-voice-user' + (p.speaking ? ' speaking' : '') },
|
|
91
|
-
h('div', { class: 'cm-ch-voice-user-avatar', style: p.color ? `--avatar-bg:${p.color}` : null }, (p.identity
|
|
91
|
+
h('div', { class: 'cm-ch-voice-user-avatar', style: p.color ? `--avatar-bg:${p.color}` : null }, avatarInitial(p.identity)),
|
|
92
92
|
h('span', { class: 'cm-ch-voice-user-name' }, p.identity)
|
|
93
93
|
))
|
|
94
94
|
) : null
|
|
@@ -120,7 +120,7 @@ export function ChannelCategory({ id, name, channels = [], collapsed, activeId,
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
export function VoiceUser({ identity, speaking, color } = {}) {
|
|
123
|
-
const initial = (identity
|
|
123
|
+
const initial = avatarInitial(identity);
|
|
124
124
|
return h('div', { class: 'cm-voice-user' + (speaking ? ' speaking' : '') },
|
|
125
125
|
h('div', { class: 'cm-voice-user-avatar', style: color ? `--avatar-bg:${color}` : null }, initial),
|
|
126
126
|
h('span', { class: 'cm-voice-user-name' }, identity)
|
|
@@ -128,7 +128,7 @@ export function VoiceUser({ identity, speaking, color } = {}) {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
export function UserPanel({ name, tag, color, muted, deafened, onMute, onDeafen, onSettings } = {}) {
|
|
131
|
-
const initial = (name
|
|
131
|
+
const initial = avatarInitial(name);
|
|
132
132
|
const handleSettings = (e) => {
|
|
133
133
|
e.preventDefault();
|
|
134
134
|
if (onSettings) {
|
|
@@ -204,7 +204,7 @@ export function ChannelSidebar({ serverName, channels = [], categories = [], act
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
export function MemberItem({ identity, name, color, status = 'online' } = {}) {
|
|
207
|
-
const initial = (name || identity
|
|
207
|
+
const initial = avatarInitial(name || identity);
|
|
208
208
|
return h('div', { class: 'cm-member-item' },
|
|
209
209
|
h('div', { class: 'cm-member-avatar', style: color ? `--avatar-bg:${color}` : null },
|
|
210
210
|
h('span', { class: 'cm-member-status' + (status === 'online' ? ' online' : '') }),
|
|
@@ -3,17 +3,33 @@
|
|
|
3
3
|
// ProjectView, Form. Pure factories.
|
|
4
4
|
|
|
5
5
|
import * as webjsx from '../../vendor/webjsx/index.js';
|
|
6
|
-
import { Btn, Heading, Lede, Dot, Icon } from './shell.js';
|
|
6
|
+
import { Btn, Heading, Lede, Dot, Icon, Chip } from './shell.js';
|
|
7
7
|
const h = webjsx.createElement;
|
|
8
8
|
|
|
9
9
|
// Avatar — generic identity disc: an image when `src` resolves, else a
|
|
10
10
|
// letter fallback derived from `name`/`fallback`. Kit previously only had
|
|
11
11
|
// scoped one-offs (chat.js `.chat-avatar`, community.js `.cm-user-avatar`)
|
|
12
12
|
// duplicating this same letter-fallback logic; this is the reusable version.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
// avatarInitial — the single shared letter-fallback computation behind
|
|
14
|
+
// Avatar and every custom-colored avatar wrapper (community.js's voice/user/
|
|
15
|
+
// member rows use their own `--avatar-bg` CSS-variable styling and can't
|
|
16
|
+
// drop in the Avatar element directly, but still want the SAME fallback
|
|
17
|
+
// text, not their own independently-drifting .slice(0,n).toUpperCase()).
|
|
18
|
+
// Empty-guards to '?' identically everywhere it's used.
|
|
19
|
+
export function avatarInitial(name, count = 1) {
|
|
20
|
+
return name ? String(name).trim().slice(0, count).toUpperCase() || '?' : '?';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Avatar — the single letter-fallback/image avatar primitive. `initialsCount`
|
|
24
|
+
// (default 1) controls how many leading characters of `name` become the
|
|
25
|
+
// fallback letters when no `src`/`fallback` is given (community.js's
|
|
26
|
+
// pill-shaped ServerIcon wants 2); `shape` ('circle' default, or 'square')
|
|
27
|
+
// covers non-circular consumers without each hand-rolling its own
|
|
28
|
+
// .slice(0,n).toUpperCase() (previously duplicated across 5+ call sites in
|
|
29
|
+
// community.js and chat.js with drifting char-counts/empty-guards).
|
|
30
|
+
export function Avatar({ name, src, fallback, size = 'md', shape = 'circle', initialsCount = 1, key } = {}) {
|
|
31
|
+
const letter = fallback != null ? fallback : avatarInitial(name, initialsCount);
|
|
32
|
+
const cls = 'ds-avatar ds-avatar-' + size + (shape === 'square' ? ' ds-avatar-square' : '');
|
|
17
33
|
if (src) return h('img', { key, class: cls, src, alt: name || '', loading: 'lazy' });
|
|
18
34
|
return h('span', { key, class: cls, 'aria-hidden': !!name, role: name ? 'img' : undefined, 'aria-label': name || undefined }, letter);
|
|
19
35
|
}
|
|
@@ -474,6 +490,52 @@ export function Table({ headers = [], rows = [], onRowClick, emptyText = 'nothin
|
|
|
474
490
|
}, ...row.map((c, j) => h('td', { key: j }, c == null ? '' : (typeof c === 'object' ? c : String(c)))))))));
|
|
475
491
|
}
|
|
476
492
|
|
|
493
|
+
// HealthTable — generic health-check table: given an arbitrary
|
|
494
|
+
// `{checkName: value}` object, infers a Chip tone per row from the value's
|
|
495
|
+
// own type (boolean true/false -> ok/miss Chip, object -> truncated JSON
|
|
496
|
+
// string, else the raw value stringified) so a new backend health check
|
|
497
|
+
// appears with zero per-check hardcoding at the call site. Ported from
|
|
498
|
+
// freddie's health page (src/components/freddie.js's `health` page), which
|
|
499
|
+
// hand-rolled this exact inference inline; promoted here as a reusable
|
|
500
|
+
// primitive for any dashboard/monitoring surface with a health-check map
|
|
501
|
+
// (agentgui's Live tab included). `okLabel`/`missLabel` let a caller
|
|
502
|
+
// localize the two Chip strings; `jsonTruncate` caps the JSON.stringify
|
|
503
|
+
// length for object-shaped values (matches freddie's own truncation width).
|
|
504
|
+
export function HealthTable({ checks = {}, emptyText = 'no health data', okLabel = 'ok', missLabel = 'no', jsonTruncate = 60 } = {}) {
|
|
505
|
+
const entries = Object.entries(checks);
|
|
506
|
+
if (!entries.length) return h('div', { class: 'empty' }, emptyText);
|
|
507
|
+
const rows = entries.map(([name, v]) => {
|
|
508
|
+
let cell;
|
|
509
|
+
if (typeof v === 'object' && v !== null) {
|
|
510
|
+
const s = JSON.stringify(v);
|
|
511
|
+
cell = h('span', { title: s.length > jsonTruncate ? s : null }, s.length > jsonTruncate ? s.slice(0, jsonTruncate) + '…' : s);
|
|
512
|
+
} else if (v === true) cell = Chip({ tone: 'ok', children: okLabel });
|
|
513
|
+
else if (v === false) cell = Chip({ tone: 'miss', children: missLabel });
|
|
514
|
+
else cell = String(v);
|
|
515
|
+
return [name, cell];
|
|
516
|
+
});
|
|
517
|
+
return Table({ headers: ['check', 'status'], rows });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ProcessRegistryTable — generic long-lived-process registry table: given a
|
|
521
|
+
// list of `{kind, key, state}`-shaped rows (any in-flight process the host
|
|
522
|
+
// tracks — an xstate machine, an ACP/direct-runner session, a background
|
|
523
|
+
// job), renders a uniform table. Ported from freddie's `machines` page
|
|
524
|
+
// (src/components/freddie.js), which used this exact shape for its xstate
|
|
525
|
+
// machine census; generalized here since the shape (kind/key/state) applies
|
|
526
|
+
// equally to agentgui's Live tab listing in-flight agent runner processes.
|
|
527
|
+
// `extraColumns` lets a caller append columns beyond the base three (e.g. a
|
|
528
|
+
// stop-action button) without forking the whole table.
|
|
529
|
+
export function ProcessRegistryTable({ processes = [], emptyText = 'no live processes', extraColumns = [] } = {}) {
|
|
530
|
+
if (!processes.length) return h('div', { class: 'empty' }, emptyText);
|
|
531
|
+
const headers = ['kind', 'key', 'state', ...extraColumns.map(c => c.header)];
|
|
532
|
+
const rows = processes.map(p => [
|
|
533
|
+
p.kind || '—', p.key || '—', p.state || '—',
|
|
534
|
+
...extraColumns.map(c => c.render(p))
|
|
535
|
+
]);
|
|
536
|
+
return Table({ headers, rows });
|
|
537
|
+
}
|
|
538
|
+
|
|
477
539
|
export function HomeView({ state = {}, onNav, onToggleWork, works = [], posts = [], manifesto = [], currentlyShipping } = {}) {
|
|
478
540
|
return [
|
|
479
541
|
Hero({
|
|
@@ -117,3 +117,20 @@ export function emptyState(text = 'nothing here yet', glyph = Icon('circle')) {
|
|
|
117
117
|
h('div', { class: 'fd-empty-glyph', 'aria-hidden': 'true' }, glyph),
|
|
118
118
|
h('div', { class: 'dim' }, text));
|
|
119
119
|
}
|
|
120
|
+
|
|
121
|
+
// refreshError — the never-blank-on-refresh-error convention: a monitoring
|
|
122
|
+
// page whose poll fails AFTER already having last-good data keeps that data
|
|
123
|
+
// visible and shows this non-blocking banner instead of falling back to
|
|
124
|
+
// errorState's full-page replacement. errorState (above) is still correct
|
|
125
|
+
// for the FIRST failed poll (no last-good data exists yet to keep showing) -
|
|
126
|
+
// callers gate on `err && !data` -> errorState, `err && data` -> refreshError
|
|
127
|
+
// alongside the still-rendered data, exactly as every freddie.js page does.
|
|
128
|
+
// Previously a private per-file const; promoted here so any monitoring
|
|
129
|
+
// surface (agentgui's Live tab included) can reuse the same convention
|
|
130
|
+
// instead of re-deriving its own banner markup.
|
|
131
|
+
export function refreshError(err) {
|
|
132
|
+
if (!err) return null;
|
|
133
|
+
return h('div', { class: 'ds-alert ds-alert-warn', role: 'status', 'aria-live': 'polite' },
|
|
134
|
+
h('span', { class: 'ds-alert-icon' }, '!'),
|
|
135
|
+
h('div', { class: 'ds-alert-content' }, 'refresh failed: ' + String(err.message || err)));
|
|
136
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// the single maintenance point for freddie GUI per the dynamic-stack contract.
|
|
6
6
|
|
|
7
7
|
import * as webjsx from '../../vendor/webjsx/index.js';
|
|
8
|
-
import { makePage, api, loadingState, errorState, emptyState } from './freddie/runtime.js';
|
|
8
|
+
import { makePage, api, loadingState, errorState, emptyState, refreshError } from './freddie/runtime.js';
|
|
9
9
|
import { getRecentPaths, saveRecentPath, skillLabel, renderChatMessages } from './freddie/helpers.js';
|
|
10
10
|
import { Panel, Row, Table, Kpi, PageHeader, SearchInput, TextField, Select } from './content.js';
|
|
11
11
|
import { Chip, Btn, Icon } from './shell.js';
|
|
@@ -28,10 +28,6 @@ const noteAlert = (note) => note ? h('div', { class: 'ds-alert ds-alert-' + note
|
|
|
28
28
|
h('div', { class: 'ds-alert-content' }, note.msg)) : null;
|
|
29
29
|
// Manual refresh button for non-polling pages — parity with auto-refreshing ones.
|
|
30
30
|
const refreshBtn = (onClick, busy) => Btn({ children: busy ? 'refreshing…' : [Icon('refresh'), ' refresh'], disabled: !!busy, onClick, 'aria-label': 'refresh' });
|
|
31
|
-
// Non-blocking refresh-error banner: keep last-good content, surface the failure.
|
|
32
|
-
const refreshError = (err) => err ? h('div', { class: 'ds-alert ds-alert-warn', role: 'status', 'aria-live': 'polite' },
|
|
33
|
-
h('span', { class: 'ds-alert-icon' }, '!'),
|
|
34
|
-
h('div', { class: 'ds-alert-content' }, 'refresh failed: ' + String(err.message || err))) : null;
|
|
35
31
|
// Polite live region announcing async busy/done state to screen readers.
|
|
36
32
|
const liveRegion = (msg) => h('div', { class: 'fd-sr-live', role: 'status', 'aria-live': 'polite' }, msg || '');
|
|
37
33
|
// Truncate with a title tooltip carrying the full text.
|
|
@@ -189,11 +189,25 @@ export function Popover({ open, anchorEl, onClose, placement = 'bottom-start', c
|
|
|
189
189
|
return null;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
//
|
|
193
|
-
|
|
192
|
+
// useRovingMenu — the shared open/close/outside-click/roving-nav/typeahead
|
|
193
|
+
// state machine behind Dropdown, PermissionMenu, and MenuButton. All three
|
|
194
|
+
// previously reimplemented an identical ~70-line skeleton (byte-identical
|
|
195
|
+
// close() teardown, near-identical onMenuKey); this factors it into one
|
|
196
|
+
// place so a fix/feature (e.g. typeahead) lands for every consumer instead
|
|
197
|
+
// of drifting per-copy. `itemSelector` picks the live focusable items inside
|
|
198
|
+
// the rendered menu (each consumer uses a different role: menuitem /
|
|
199
|
+
// menuitemcheckbox / menuitemradio); `getLabel(item)` + `items` enable
|
|
200
|
+
// typeahead when `typeahead` is true (Dropdown/MenuButton have it,
|
|
201
|
+
// PermissionMenu's categories aren't typically typeahead-searched so it
|
|
202
|
+
// defaults off but can opt in). Returns { refFn, onTrigClick, onTrigKey,
|
|
203
|
+
// openMenu, close, focusItem, isOpen } — the caller still owns rendering the
|
|
204
|
+
// menu's DOM/CSS (role/class per consumer stays distinct) and wires
|
|
205
|
+
// `menuEl.addEventListener('keydown', onMenuKey)` itself via the returned
|
|
206
|
+
// `onMenuKey`, since only the caller knows when its menuEl exists.
|
|
207
|
+
export function useRovingMenu({ itemSelector, items = [], getLabel = (it) => it.label, typeahead = false, placement = 'bottom-start', onOpenChange } = {}) {
|
|
194
208
|
let triggerEl = null, open = false, menuEl = null, floating = null, typeBuf = '', typeTimer = null;
|
|
195
|
-
const
|
|
196
|
-
const focusItem = (idx) => { const b =
|
|
209
|
+
const liveItems = () => menuEl ? [...menuEl.querySelectorAll(itemSelector)] : [];
|
|
210
|
+
const focusItem = (idx) => { const b = liveItems(); if (!b.length) return; b[((idx % b.length) + b.length) % b.length].focus(); };
|
|
197
211
|
const onDown = (e) => { if (menuEl && menuEl.contains(e.target)) return; if (triggerEl && triggerEl.contains(e.target)) return; close(false); };
|
|
198
212
|
const close = (restore = true) => {
|
|
199
213
|
if (!open) return; open = false;
|
|
@@ -203,32 +217,58 @@ export function Dropdown({ trigger, items = [], onSelect, placement = 'bottom-st
|
|
|
203
217
|
document.removeEventListener('mousedown', onDown, true);
|
|
204
218
|
if (triggerEl) triggerEl.setAttribute('aria-expanded', 'false');
|
|
205
219
|
if (restore && triggerEl) triggerEl.focus();
|
|
220
|
+
if (onOpenChange) onOpenChange(false);
|
|
206
221
|
};
|
|
207
|
-
const select = (it) => { if (it.disabled || it.separator) return; if (onSelect) onSelect(it.id, it); close(); };
|
|
208
222
|
const onMenuKey = (e) => {
|
|
209
|
-
const b =
|
|
223
|
+
const b = liveItems(), idx = b.indexOf(document.activeElement);
|
|
210
224
|
if (e.key === 'Escape') { e.preventDefault(); close(); }
|
|
211
225
|
else if (e.key === 'ArrowDown') { e.preventDefault(); focusItem(idx + 1); }
|
|
212
226
|
else if (e.key === 'ArrowUp') { e.preventDefault(); focusItem(idx - 1); }
|
|
213
227
|
else if (e.key === 'Home') { e.preventDefault(); focusItem(0); }
|
|
214
228
|
else if (e.key === 'End') { e.preventDefault(); focusItem(b.length - 1); }
|
|
215
229
|
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (idx >= 0) b[idx].click(); }
|
|
216
|
-
else if (e.key.length === 1 && /\S/.test(e.key)) {
|
|
230
|
+
else if (typeahead && e.key.length === 1 && /\S/.test(e.key)) {
|
|
217
231
|
typeBuf += e.key.toLowerCase();
|
|
218
232
|
if (typeTimer) clearTimeout(typeTimer);
|
|
219
233
|
typeTimer = setTimeout(() => { typeBuf = ''; }, 600);
|
|
220
|
-
const
|
|
221
|
-
|
|
234
|
+
const selectable = items.filter(it => !it.separator && !it.disabled && !it.unavailable);
|
|
235
|
+
const m = selectable.findIndex(it => (getLabel(it) || '').toLowerCase().startsWith(typeBuf));
|
|
236
|
+
if (m >= 0) focusItem(m);
|
|
222
237
|
}
|
|
223
238
|
};
|
|
224
|
-
const openMenu = (focusFirst = true) => {
|
|
239
|
+
const openMenu = (buildMenuEl, focusFirst = true) => {
|
|
225
240
|
if (open || !triggerEl) return;
|
|
226
241
|
open = true;
|
|
227
|
-
menuEl =
|
|
228
|
-
menuEl.className = 'ds-popover ds-dropdown-menu';
|
|
229
|
-
menuEl.setAttribute('role', 'menu');
|
|
230
|
-
if (ariaLabel) menuEl.setAttribute('aria-label', ariaLabel);
|
|
242
|
+
menuEl = buildMenuEl();
|
|
231
243
|
menuEl.tabIndex = -1;
|
|
244
|
+
document.body.appendChild(menuEl);
|
|
245
|
+
menuEl.addEventListener('keydown', onMenuKey);
|
|
246
|
+
floating = useFloating(triggerEl, menuEl, { placement, offset: FLOAT_OFFSET_DROPDOWN });
|
|
247
|
+
document.addEventListener('mousedown', onDown, true);
|
|
248
|
+
triggerEl.setAttribute('aria-expanded', 'true');
|
|
249
|
+
if (focusFirst && liveItems().length) queueMicrotask(() => focusItem(0));
|
|
250
|
+
if (onOpenChange) onOpenChange(true);
|
|
251
|
+
};
|
|
252
|
+
const onTrigClick = (buildMenuEl) => { if (open) close(false); else openMenu(buildMenuEl, true); };
|
|
253
|
+
const onTrigKey = (e, buildMenuEl) => { if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (!open) openMenu(buildMenuEl, true); else focusItem(0); } };
|
|
254
|
+
const refFn = (dsFlag) => (el) => {
|
|
255
|
+
if (!el || el[dsFlag]) return;
|
|
256
|
+
el[dsFlag] = true; triggerEl = el;
|
|
257
|
+
el.setAttribute('aria-haspopup', 'menu');
|
|
258
|
+
el.setAttribute('aria-expanded', 'false');
|
|
259
|
+
};
|
|
260
|
+
return { refFn, onTrigClick, onTrigKey, openMenu, close, focusItem, isOpen: () => open };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Dropdown — button trigger + portaled menu.
|
|
264
|
+
export function Dropdown({ trigger, items = [], onSelect, placement = 'bottom-start', ariaLabel } = {}) {
|
|
265
|
+
const menu = useRovingMenu({ itemSelector: '[role="menuitem"]:not([aria-disabled="true"])', items, typeahead: true, placement });
|
|
266
|
+
const select = (it) => { if (it.disabled || it.separator) return; if (onSelect) onSelect(it.id, it); menu.close(); };
|
|
267
|
+
const buildMenuEl = () => {
|
|
268
|
+
const el = document.createElement('div');
|
|
269
|
+
el.className = 'ds-popover ds-dropdown-menu';
|
|
270
|
+
el.setAttribute('role', 'menu');
|
|
271
|
+
if (ariaLabel) el.setAttribute('aria-label', ariaLabel);
|
|
232
272
|
const tree = h('div', { class: 'ds-dropdown-list' },
|
|
233
273
|
...items.map((it, i) => it.separator
|
|
234
274
|
? h('div', { key: 'sep' + i, class: 'ds-dropdown-separator', role: 'separator' })
|
|
@@ -241,28 +281,17 @@ export function Dropdown({ trigger, items = [], onSelect, placement = 'bottom-st
|
|
|
241
281
|
it.glyph != null ? h('span', { class: 'ds-dropdown-glyph', 'aria-hidden': 'true' }, it.glyph) : null,
|
|
242
282
|
h('span', { class: 'ds-dropdown-label' }, it.label)
|
|
243
283
|
)));
|
|
244
|
-
webjsx.applyDiff(
|
|
245
|
-
|
|
246
|
-
menuEl.addEventListener('keydown', onMenuKey);
|
|
247
|
-
floating = useFloating(triggerEl, menuEl, { placement, offset: FLOAT_OFFSET_DROPDOWN });
|
|
248
|
-
document.addEventListener('mousedown', onDown, true);
|
|
249
|
-
triggerEl.setAttribute('aria-expanded', 'true');
|
|
250
|
-
if (focusFirst) queueMicrotask(() => focusItem(0));
|
|
251
|
-
};
|
|
252
|
-
const onTrigClick = () => { if (open) close(false); else openMenu(true); };
|
|
253
|
-
const onTrigKey = (e) => { if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (!open) openMenu(true); else focusItem(0); } };
|
|
254
|
-
const refFn = (el) => {
|
|
255
|
-
if (!el || el._dsDropdown) return;
|
|
256
|
-
el._dsDropdown = true; triggerEl = el;
|
|
257
|
-
el.addEventListener('click', onTrigClick);
|
|
258
|
-
el.addEventListener('keydown', onTrigKey);
|
|
259
|
-
el.setAttribute('aria-haspopup', 'menu');
|
|
260
|
-
el.setAttribute('aria-expanded', 'false');
|
|
284
|
+
webjsx.applyDiff(el, tree);
|
|
285
|
+
return el;
|
|
261
286
|
};
|
|
287
|
+
const onTrigClick = () => menu.onTrigClick(buildMenuEl);
|
|
288
|
+
const onTrigKey = (e) => menu.onTrigKey(e, buildMenuEl);
|
|
289
|
+
const refFn = menu.refFn('_dsDropdown');
|
|
262
290
|
const child = (typeof trigger === 'function') ? trigger() : trigger;
|
|
291
|
+
const wireRef = (el) => { refFn(el); if (el) { el.addEventListener('click', onTrigClick); el.addEventListener('keydown', onTrigKey); } };
|
|
263
292
|
return (child && child.type)
|
|
264
|
-
? webjsx.createElement(child.type, { ...(child.props || {}), ref:
|
|
265
|
-
: h('button', { type: 'button', class: 'ds-dropdown-trigger', ref:
|
|
293
|
+
? webjsx.createElement(child.type, { ...(child.props || {}), ref: wireRef }, ...(child.children || []))
|
|
294
|
+
: h('button', { type: 'button', class: 'ds-dropdown-trigger', ref: wireRef }, child || 'Menu');
|
|
266
295
|
}
|
|
267
296
|
|
|
268
297
|
// PermissionMenu — a role=menu of role=menuitemcheckbox rows, one per
|
|
@@ -272,31 +301,14 @@ export function Dropdown({ trigger, items = [], onSelect, placement = 'bottom-st
|
|
|
272
301
|
// element, a document-level mousedown listener, focus restored to the
|
|
273
302
|
// trigger on close) rather than reimplementing that plumbing.
|
|
274
303
|
export function PermissionMenu({ trigger, categories = [], approved = [], onToggle, onToggleAll, placement = 'bottom-start', ariaLabel = 'Permissions' } = {}) {
|
|
275
|
-
let triggerEl = null, open = false, menuEl = null, floating = null;
|
|
276
304
|
const isApproved = (id) => approved.indexOf(id) !== -1;
|
|
277
|
-
const
|
|
278
|
-
const focusItem = (idx) => { const items = liveItems(); if (!items.length) return; items[((idx % items.length) + items.length) % items.length].focus(); };
|
|
279
|
-
const onDown = (e) => { if (menuEl && menuEl.contains(e.target)) return; if (triggerEl && triggerEl.contains(e.target)) return; close(false); };
|
|
280
|
-
const close = (restore = true) => {
|
|
281
|
-
if (!open) return; open = false;
|
|
282
|
-
if (floating) { floating.dispose(); floating = null; }
|
|
283
|
-
if (menuEl && menuEl.parentNode) menuEl.parentNode.removeChild(menuEl);
|
|
284
|
-
menuEl = null;
|
|
285
|
-
document.removeEventListener('mousedown', onDown, true);
|
|
286
|
-
if (triggerEl) triggerEl.setAttribute('aria-expanded', 'false');
|
|
287
|
-
if (restore && triggerEl) triggerEl.focus();
|
|
288
|
-
};
|
|
305
|
+
const menu = useRovingMenu({ itemSelector: '[role="menuitemcheckbox"]', items: categories, getLabel: (cat) => cat.label || cat.id, typeahead: true, placement });
|
|
289
306
|
const toggle = (cat) => { if (onToggle) onToggle(cat.id, !isApproved(cat.id)); };
|
|
290
|
-
const
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
else if (e.key === 'Home') { e.preventDefault(); focusItem(0); }
|
|
296
|
-
else if (e.key === 'End') { e.preventDefault(); focusItem(items.length - 1); }
|
|
297
|
-
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (idx >= 0) items[idx].click(); }
|
|
298
|
-
};
|
|
299
|
-
const renderMenu = () => {
|
|
307
|
+
const buildMenuEl = () => {
|
|
308
|
+
const el = document.createElement('div');
|
|
309
|
+
el.className = 'ds-popover ov-perm-menu';
|
|
310
|
+
el.setAttribute('role', 'menu');
|
|
311
|
+
el.setAttribute('aria-label', ariaLabel);
|
|
300
312
|
const rows = categories.map((cat, i) => h('button', {
|
|
301
313
|
key: cat.id || i, type: 'button', role: 'menuitemcheckbox',
|
|
302
314
|
'aria-checked': isApproved(cat.id) ? 'true' : 'false',
|
|
@@ -307,38 +319,17 @@ export function PermissionMenu({ trigger, categories = [], approved = [], onTogg
|
|
|
307
319
|
const actionsRow = h('div', { class: 'ov-perm-actions' },
|
|
308
320
|
h('button', { type: 'button', class: 'ov-perm-action', onclick: () => onToggleAll && onToggleAll(true) }, 'Approve all'),
|
|
309
321
|
h('button', { type: 'button', class: 'ov-perm-action', onclick: () => onToggleAll && onToggleAll(false) }, 'Revoke all'));
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const openMenu = (focusFirst = true) => {
|
|
313
|
-
if (open || !triggerEl) return;
|
|
314
|
-
open = true;
|
|
315
|
-
menuEl = document.createElement('div');
|
|
316
|
-
menuEl.className = 'ds-popover ov-perm-menu';
|
|
317
|
-
menuEl.setAttribute('role', 'menu');
|
|
318
|
-
menuEl.setAttribute('aria-label', ariaLabel);
|
|
319
|
-
menuEl.tabIndex = -1;
|
|
320
|
-
webjsx.applyDiff(menuEl, renderMenu());
|
|
321
|
-
document.body.appendChild(menuEl);
|
|
322
|
-
menuEl.addEventListener('keydown', onMenuKey);
|
|
323
|
-
floating = useFloating(triggerEl, menuEl, { placement, offset: FLOAT_OFFSET_DROPDOWN });
|
|
324
|
-
document.addEventListener('mousedown', onDown, true);
|
|
325
|
-
triggerEl.setAttribute('aria-expanded', 'true');
|
|
326
|
-
if (focusFirst) queueMicrotask(() => focusItem(0));
|
|
327
|
-
};
|
|
328
|
-
const onTrigClick = () => { if (open) close(false); else openMenu(true); };
|
|
329
|
-
const onTrigKey = (e) => { if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (!open) openMenu(true); else focusItem(0); } };
|
|
330
|
-
const refFn = (el) => {
|
|
331
|
-
if (!el || el._dsPermMenu) return;
|
|
332
|
-
el._dsPermMenu = true; triggerEl = el;
|
|
333
|
-
el.addEventListener('click', onTrigClick);
|
|
334
|
-
el.addEventListener('keydown', onTrigKey);
|
|
335
|
-
el.setAttribute('aria-haspopup', 'menu');
|
|
336
|
-
el.setAttribute('aria-expanded', 'false');
|
|
322
|
+
webjsx.applyDiff(el, h('div', { class: 'ov-perm-list' }, ...rows, actionsRow));
|
|
323
|
+
return el;
|
|
337
324
|
};
|
|
325
|
+
const onTrigClick = () => menu.onTrigClick(buildMenuEl);
|
|
326
|
+
const onTrigKey = (e) => menu.onTrigKey(e, buildMenuEl);
|
|
327
|
+
const refFn = menu.refFn('_dsPermMenu');
|
|
338
328
|
const child = (typeof trigger === 'function') ? trigger() : trigger;
|
|
329
|
+
const wireRef = (el) => { refFn(el); if (el) { el.addEventListener('click', onTrigClick); el.addEventListener('keydown', onTrigKey); } };
|
|
339
330
|
return (child && child.type)
|
|
340
|
-
? webjsx.createElement(child.type, { ...(child.props || {}), ref:
|
|
341
|
-
: h('button', { type: 'button', class: 'ov-perm-trigger', ref:
|
|
331
|
+
? webjsx.createElement(child.type, { ...(child.props || {}), ref: wireRef }, ...(child.children || []))
|
|
332
|
+
: h('button', { type: 'button', class: 'ov-perm-trigger', ref: wireRef }, child || 'Permissions');
|
|
342
333
|
}
|
|
343
334
|
|
|
344
335
|
// ApprovalPrompt — an inline, in-thread tool-permission card (as opposed to
|
|
@@ -800,44 +791,13 @@ export function AuthModal({ mode = 'extension', error = '', busy = false, open =
|
|
|
800
791
|
// nothing focusable); roving nav on a single-option list simply refocuses
|
|
801
792
|
// the same item on every Arrow press (wrap-to-self), never throws.
|
|
802
793
|
export function MenuButton({ trigger, items = [], selected, onSelect, onRetry, placement = 'bottom-start', ariaLabel = 'Menu', emptyText = 'No options available' } = {}) {
|
|
803
|
-
|
|
804
|
-
const
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
if (menuEl && menuEl.parentNode) menuEl.parentNode.removeChild(menuEl);
|
|
811
|
-
menuEl = null;
|
|
812
|
-
document.removeEventListener('mousedown', onDown, true);
|
|
813
|
-
if (triggerEl) triggerEl.setAttribute('aria-expanded', 'false');
|
|
814
|
-
if (restore && triggerEl) triggerEl.focus();
|
|
815
|
-
};
|
|
816
|
-
const select = (it) => { if (it.disabled || it.unavailable) return; if (onSelect) onSelect(it.id, it); close(); };
|
|
817
|
-
const onMenuKey = (e) => {
|
|
818
|
-
const b = liveBtns(), idx = b.indexOf(document.activeElement);
|
|
819
|
-
if (e.key === 'Escape') { e.preventDefault(); close(); }
|
|
820
|
-
else if (e.key === 'ArrowDown') { e.preventDefault(); focusItem(idx + 1); }
|
|
821
|
-
else if (e.key === 'ArrowUp') { e.preventDefault(); focusItem(idx - 1); }
|
|
822
|
-
else if (e.key === 'Home') { e.preventDefault(); focusItem(0); }
|
|
823
|
-
else if (e.key === 'End') { e.preventDefault(); focusItem(b.length - 1); }
|
|
824
|
-
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (idx >= 0) b[idx].click(); }
|
|
825
|
-
else if (e.key.length === 1 && /\S/.test(e.key)) {
|
|
826
|
-
typeBuf += e.key.toLowerCase();
|
|
827
|
-
if (typeTimer) clearTimeout(typeTimer);
|
|
828
|
-
typeTimer = setTimeout(() => { typeBuf = ''; }, 600);
|
|
829
|
-
const m = items.findIndex(it => !it.disabled && !it.unavailable && (it.label || '').toLowerCase().startsWith(typeBuf));
|
|
830
|
-
if (m >= 0) focusItem(items.slice(0, m).filter(it => !it.disabled && !it.unavailable).length);
|
|
831
|
-
}
|
|
832
|
-
};
|
|
833
|
-
const openMenu = (focusFirst = true) => {
|
|
834
|
-
if (open || !triggerEl) return;
|
|
835
|
-
open = true;
|
|
836
|
-
menuEl = document.createElement('div');
|
|
837
|
-
menuEl.className = 'ds-popover ov-menubutton-menu';
|
|
838
|
-
menuEl.setAttribute('role', 'menu');
|
|
839
|
-
menuEl.setAttribute('aria-label', ariaLabel);
|
|
840
|
-
menuEl.tabIndex = -1;
|
|
794
|
+
const menu = useRovingMenu({ itemSelector: '[role="menuitemradio"]:not([aria-disabled="true"])', items, typeahead: true, placement });
|
|
795
|
+
const select = (it) => { if (it.disabled || it.unavailable) return; if (onSelect) onSelect(it.id, it); menu.close(); };
|
|
796
|
+
const buildMenuEl = () => {
|
|
797
|
+
const el = document.createElement('div');
|
|
798
|
+
el.className = 'ds-popover ov-menubutton-menu';
|
|
799
|
+
el.setAttribute('role', 'menu');
|
|
800
|
+
el.setAttribute('aria-label', ariaLabel);
|
|
841
801
|
const tree = items.length
|
|
842
802
|
? h('div', { class: 'ov-menubutton-list' },
|
|
843
803
|
...items.map((it, i) => it.unavailable
|
|
@@ -856,28 +816,17 @@ export function MenuButton({ trigger, items = [], selected, onSelect, onRetry, p
|
|
|
856
816
|
h('span', { class: 'ov-menubutton-label' }, it.label)
|
|
857
817
|
)))
|
|
858
818
|
: h('div', { class: 'ov-menubutton-empty' }, emptyText);
|
|
859
|
-
webjsx.applyDiff(
|
|
860
|
-
|
|
861
|
-
menuEl.addEventListener('keydown', onMenuKey);
|
|
862
|
-
floating = useFloating(triggerEl, menuEl, { placement, offset: FLOAT_OFFSET_DROPDOWN });
|
|
863
|
-
document.addEventListener('mousedown', onDown, true);
|
|
864
|
-
triggerEl.setAttribute('aria-expanded', 'true');
|
|
865
|
-
if (focusFirst && items.length) queueMicrotask(() => focusItem(0));
|
|
866
|
-
};
|
|
867
|
-
const onTrigClick = () => { if (open) close(false); else openMenu(true); };
|
|
868
|
-
const onTrigKey = (e) => { if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (!open) openMenu(true); else focusItem(0); } };
|
|
869
|
-
const refFn = (el) => {
|
|
870
|
-
if (!el || el._ovMenuButton) return;
|
|
871
|
-
el._ovMenuButton = true; triggerEl = el;
|
|
872
|
-
el.addEventListener('click', onTrigClick);
|
|
873
|
-
el.addEventListener('keydown', onTrigKey);
|
|
874
|
-
el.setAttribute('aria-haspopup', 'menu');
|
|
875
|
-
el.setAttribute('aria-expanded', 'false');
|
|
819
|
+
webjsx.applyDiff(el, tree);
|
|
820
|
+
return el;
|
|
876
821
|
};
|
|
822
|
+
const onTrigClick = () => menu.onTrigClick(buildMenuEl);
|
|
823
|
+
const onTrigKey = (e) => menu.onTrigKey(e, buildMenuEl);
|
|
824
|
+
const refFn = menu.refFn('_ovMenuButton');
|
|
877
825
|
const child = (typeof trigger === 'function') ? trigger() : trigger;
|
|
826
|
+
const wireRef = (el) => { refFn(el); if (el) { el.addEventListener('click', onTrigClick); el.addEventListener('keydown', onTrigKey); } };
|
|
878
827
|
return (child && child.type)
|
|
879
|
-
? webjsx.createElement(child.type, { ...(child.props || {}), ref:
|
|
880
|
-
: h('button', { type: 'button', class: 'ov-menubutton-trigger', ref:
|
|
828
|
+
? webjsx.createElement(child.type, { ...(child.props || {}), ref: wireRef }, ...(child.children || []))
|
|
829
|
+
: h('button', { type: 'button', class: 'ov-menubutton-trigger', ref: wireRef }, child || 'Select');
|
|
881
830
|
}
|
|
882
831
|
|
|
883
832
|
// VideoLightbox — fullscreen video player overlay with backdrop dismiss.
|
package/src/components.js
CHANGED
|
@@ -14,9 +14,9 @@ export {
|
|
|
14
14
|
Panel, Card, Row, RowLink, PanelFromItems,
|
|
15
15
|
Hero, HeroFromPageData, Marquee, Install, CliBlock, Receipt, Changelog,
|
|
16
16
|
WorksList, WritingList, Manifesto, Section, PageHeader,
|
|
17
|
-
Kpi, Sparkline, BarChart, Table, SearchInput, TextField, Select, EventList,
|
|
17
|
+
Kpi, Sparkline, BarChart, Table, HealthTable, ProcessRegistryTable, SearchInput, TextField, Select, EventList,
|
|
18
18
|
HomeView, ProjectView, Form,
|
|
19
|
-
Spinner, Skeleton, Alert, FilterPills, Avatar
|
|
19
|
+
Spinner, Skeleton, Alert, FilterPills, Avatar, avatarInitial
|
|
20
20
|
} from './components/content.js';
|
|
21
21
|
|
|
22
22
|
export {
|
|
@@ -114,4 +114,8 @@ export {
|
|
|
114
114
|
skillLabel, getRecentPaths, saveRecentPath, renderChatMessages
|
|
115
115
|
} from './components/freddie.js';
|
|
116
116
|
|
|
117
|
+
export {
|
|
118
|
+
makePage, api, loadingState, errorState, emptyState, refreshError
|
|
119
|
+
} from './components/freddie/runtime.js';
|
|
120
|
+
|
|
117
121
|
export { mountCommunityApp } from './community-app.js';
|
|
@@ -385,6 +385,7 @@
|
|
|
385
385
|
.ds-avatar-sm { width: 24px; height: 24px; font-size: 10px; }
|
|
386
386
|
.ds-avatar-md { width: 36px; height: 36px; font-size: 12px; }
|
|
387
387
|
.ds-avatar-lg { width: 48px; height: 48px; font-size: 15px; }
|
|
388
|
+
.ds-avatar-square { border-radius: var(--r-2, 6px); }
|
|
388
389
|
|
|
389
390
|
/* SpreadsheetPreview — tabbed inline spreadsheet/CSV viewer (docstudio
|
|
390
391
|
xls-preview.js port). Tabs reuse the same neutral-at-rest pattern as
|