anentrypoint-design 0.0.172 → 0.0.174
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.js +22 -17
- package/package.json +1 -1
- package/src/components/content.js +52 -11
- package/src/components/files-modals.js +80 -65
- package/src/components/files.js +26 -20
- package/src/components/form-primitives.js +5 -2
- package/src/components/freddie/runtime.js +1 -1
- package/src/components/freddie.js +65 -41
- package/src/components/interaction-primitives.js +25 -2
- package/src/components/overlay-primitives.js +75 -31
- package/src/components/shell.js +11 -7
- package/src/page-html.js +17 -3
|
@@ -38,6 +38,17 @@ const refreshError = (err) => err ? h('div', { class: 'ds-alert ds-alert-warn',
|
|
|
38
38
|
const liveRegion = (msg) => h('div', { class: 'fd-sr-live', role: 'status', 'aria-live': 'polite' }, msg || '');
|
|
39
39
|
// Truncate with a title tooltip carrying the full text.
|
|
40
40
|
const trunc = (s, n = 90) => { const str = String(s || ''); return str.length > n ? { text: str.slice(0, n) + '…', title: str } : { text: str, title: null }; };
|
|
41
|
+
// Named truncation widths so list pages cap display consistently (and any
|
|
42
|
+
// raw .slice(0,N) on user text routes through trunc() for ellipsis + tooltip).
|
|
43
|
+
const TRUNC_TITLE = 60; // session/skill/tool titles
|
|
44
|
+
const TRUNC_SUB = 80; // row sub-text (prompts, descriptions)
|
|
45
|
+
const TRUNC_OUTPUT = 70; // batch output cells
|
|
46
|
+
const TRUNC_DESC = 90; // long descriptions
|
|
47
|
+
const TRUNC_PROMPT = 50; // batch prompt cells
|
|
48
|
+
// Render trunc() result as a span carrying the full text in its title tooltip.
|
|
49
|
+
const truncSpan = (s, n) => { const t = trunc(s, n); return h('span', { title: t.title }, t.text); };
|
|
50
|
+
// Cap a raw JSON dump for an inline table cell without losing the data via tooltip.
|
|
51
|
+
const truncJson = (v, n = TRUNC_TITLE) => truncSpan(JSON.stringify(v), n);
|
|
41
52
|
// Autoscroll a thread only when the user is already near the bottom, so
|
|
42
53
|
// scrolling up to read history is not yanked back down on the next render.
|
|
43
54
|
const stickyScroll = (el) => { if (!el) return; const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80; if (nearBottom) el.scrollTop = el.scrollHeight; };
|
|
@@ -47,13 +58,22 @@ const stickyScroll = (el) => { if (!el) return; const nearBottom = el.scrollHeig
|
|
|
47
58
|
export const home = makePage((ctx) => {
|
|
48
59
|
async function load() {
|
|
49
60
|
try {
|
|
50
|
-
|
|
61
|
+
// tools/skills counts come from the host when injected; otherwise
|
|
62
|
+
// fall back to the same /api/* endpoints the tools/skills pages use
|
|
63
|
+
// so the home KPIs never render an em-dash placeholder.
|
|
64
|
+
const needTools = ctx.host?.pi?.tools?.size == null;
|
|
65
|
+
const needSkills = ctx.host?.pi?.skills?.size == null;
|
|
66
|
+
const [health, agents, sessions, toolsList, skillsList] = await Promise.all([
|
|
51
67
|
api('/api/health').catch(() => null),
|
|
52
68
|
api('/api/agents').catch(() => null),
|
|
53
69
|
api('/api/sessions').catch((e) => ({ _err: e })),
|
|
70
|
+
needTools ? api('/api/tools').catch(() => null) : Promise.resolve(null),
|
|
71
|
+
needSkills ? api('/api/skills').catch(() => null) : Promise.resolve(null),
|
|
54
72
|
]);
|
|
73
|
+
const toolsCount = needTools ? (Array.isArray(toolsList) ? toolsList.length : (toolsList?.tools?.length ?? null)) : null;
|
|
74
|
+
const skillsCount = needSkills ? (Array.isArray(skillsList) ? skillsList.length : (skillsList?.skills?.length ?? null)) : null;
|
|
55
75
|
const sessFailed = sessions && sessions._err;
|
|
56
|
-
ctx.set({ loading: false, health, agents, sessions: Array.isArray(sessions) ? sessions : [], sessFailed, error: null });
|
|
76
|
+
ctx.set({ loading: false, health, agents, sessions: Array.isArray(sessions) ? sessions : [], sessFailed, toolsCount, skillsCount, error: null });
|
|
57
77
|
} catch (e) { ctx.set({ loading: false, error: e }); }
|
|
58
78
|
}
|
|
59
79
|
load();
|
|
@@ -64,8 +84,8 @@ export const home = makePage((ctx) => {
|
|
|
64
84
|
if (s.error) return errorState(s.error, load);
|
|
65
85
|
const sessions = s.sessions || [];
|
|
66
86
|
const agents = s.agents || {};
|
|
67
|
-
const tools = ctx.host?.pi?.tools?.size ?? '—';
|
|
68
|
-
const skills = ctx.host?.pi?.skills?.size ?? '—';
|
|
87
|
+
const tools = ctx.host?.pi?.tools?.size ?? s.toolsCount ?? '—';
|
|
88
|
+
const skills = ctx.host?.pi?.skills?.size ?? s.skillsCount ?? '—';
|
|
69
89
|
return [
|
|
70
90
|
PageHeader({ eyebrow: 'freddie', title: 'dashboard', lede: 'agent harness · live overview' }),
|
|
71
91
|
Kpi({ items: [
|
|
@@ -80,11 +100,11 @@ export const home = makePage((ctx) => {
|
|
|
80
100
|
: sessions.length
|
|
81
101
|
? Table({
|
|
82
102
|
headers: ['session', 'platform', 'updated'],
|
|
83
|
-
rows: sessions.slice(0, 8).map(x =>
|
|
103
|
+
rows: sessions.slice(0, 8).map(x => [truncSpan(x.title || x.id, TRUNC_TITLE), x.platform || '—', fmtAgo(x.updated_at)]),
|
|
84
104
|
})
|
|
85
105
|
: emptyState('no sessions yet')),
|
|
86
106
|
section('health',
|
|
87
|
-
s.health ? Table({ headers: ['check', 'status'], rows: Object.entries(s.health).map(([k, v]) => [k, typeof v === 'object' ?
|
|
107
|
+
s.health ? Table({ headers: ['check', 'status'], rows: Object.entries(s.health).map(([k, v]) => [k, typeof v === 'object' ? truncJson(v) : String(v)]) })
|
|
88
108
|
: emptyState('health endpoint unavailable')),
|
|
89
109
|
];
|
|
90
110
|
};
|
|
@@ -140,7 +160,7 @@ export const voice = makePage((ctx) => {
|
|
|
140
160
|
load();
|
|
141
161
|
return () => {
|
|
142
162
|
const s = ctx.state;
|
|
143
|
-
if (s.loading) return loadingState();
|
|
163
|
+
if (s.loading) return loadingState('loading voice config…');
|
|
144
164
|
const v = s.voice;
|
|
145
165
|
const enabled = v && (v.enabled || v.transcription || v.tts);
|
|
146
166
|
return [
|
|
@@ -174,7 +194,7 @@ export const sessions = makePage((ctx) => {
|
|
|
174
194
|
load();
|
|
175
195
|
return () => {
|
|
176
196
|
const s = ctx.state;
|
|
177
|
-
if (s.loading) return loadingState();
|
|
197
|
+
if (s.loading) return loadingState('loading sessions…');
|
|
178
198
|
if (s.error && !s.list) return errorState(s.error, load);
|
|
179
199
|
const list = Array.isArray(s.list) ? s.list : [];
|
|
180
200
|
return [
|
|
@@ -185,10 +205,10 @@ export const sessions = makePage((ctx) => {
|
|
|
185
205
|
list.length
|
|
186
206
|
? Table({ headers: ['session', 'platform', 'updated'], onRowClick: (i) => open(list[i].id),
|
|
187
207
|
rowLabels: list.map(x => x.title || x.id),
|
|
188
|
-
rows: list.map(x =>
|
|
208
|
+
rows: list.map(x => [truncSpan(x.title || x.id, TRUNC_TITLE), x.platform || '—', fmtAgo(x.updated_at)]) })
|
|
189
209
|
: emptyState('no sessions match')),
|
|
190
210
|
s.selected ? section('messages · ' + s.selected,
|
|
191
|
-
s.msgLoading ? loadingState()
|
|
211
|
+
s.msgLoading ? loadingState('loading messages…')
|
|
192
212
|
: (s.messages || []).length ? (s.messages).map((m, i) => ChatMessage({ role: m.role, text: m.content || m.text || '', time: m.ts ? fmtTime(m.ts) : '', key: i }))
|
|
193
213
|
: emptyState('no messages')) : null,
|
|
194
214
|
];
|
|
@@ -216,7 +236,7 @@ export const projects = makePage((ctx) => {
|
|
|
216
236
|
load();
|
|
217
237
|
return () => {
|
|
218
238
|
const s = ctx.state;
|
|
219
|
-
if (s.loading) return loadingState();
|
|
239
|
+
if (s.loading) return loadingState('loading projects…');
|
|
220
240
|
if (s.error && !s.data) return errorState(s.error, load);
|
|
221
241
|
const d = s.data || {}; const list = d.projects || [];
|
|
222
242
|
const activeName = (d.active && d.active.name) || d.active || 'default';
|
|
@@ -246,11 +266,12 @@ export const agents = makePage((ctx) => {
|
|
|
246
266
|
load(); ctx.interval(load, 5000);
|
|
247
267
|
return () => {
|
|
248
268
|
const s = ctx.state;
|
|
249
|
-
if (s.loading) return loadingState();
|
|
250
|
-
if (s.error) return errorState(s.error, load);
|
|
269
|
+
if (s.loading) return loadingState('loading agents…');
|
|
270
|
+
if (s.error && !s.data) return errorState(s.error, load);
|
|
251
271
|
const d = s.data || {};
|
|
252
272
|
return [
|
|
253
273
|
PageHeader({ eyebrow: 'freddie', title: 'agents', lede: 'live agent activity' }),
|
|
274
|
+
s.error && s.data ? refreshError(s.error) : null,
|
|
254
275
|
Kpi({ items: [[d.count ?? 0, 'active'], [d.turns ?? 0, 'total turns'], [d.last_activity ? fmtAgo(d.last_activity) : '—', 'last activity']] }),
|
|
255
276
|
section('detail', Table({ headers: ['field', 'value'], rows: Object.entries(d).map(([k, v]) => [k, String(v)]) })),
|
|
256
277
|
];
|
|
@@ -272,13 +293,14 @@ export const analytics = makePage((ctx) => {
|
|
|
272
293
|
load(); ctx.interval(load, 15000);
|
|
273
294
|
return () => {
|
|
274
295
|
const s = ctx.state;
|
|
275
|
-
if (s.loading) return loadingState();
|
|
276
|
-
if (s.error) return errorState(s.error, load);
|
|
296
|
+
if (s.loading) return loadingState('loading analytics…');
|
|
297
|
+
if (s.error && !s.sampler && !s.avail) return errorState(s.error, load);
|
|
277
298
|
const samp = s.sampler?.status ? Object.values(s.sampler.status) : [];
|
|
278
299
|
const ok = samp.filter(x => x && x.available !== false).length;
|
|
279
300
|
const sum = s.avail?.summary || {};
|
|
280
301
|
return [
|
|
281
302
|
PageHeader({ eyebrow: 'freddie', title: 'analytics', lede: 'provider availability & sampler health' }),
|
|
303
|
+
s.error && (s.sampler || s.avail) ? refreshError(s.error) : null,
|
|
282
304
|
Kpi({ items: [[ok + '/' + samp.length, 'providers up'], [sum.total_models ?? '—', 'models'], [sum.usable_in_any_mode ?? '—', 'usable']] }),
|
|
283
305
|
section('sampler', samp.length ? Table({ headers: ['provider', 'available', 'fails'], rows: Object.entries(s.sampler.status).map(([k, v]) => [k, v.available === false ? 'no' : 'yes', String(v.failCount ?? 0)]) }) : emptyState('no sampler data')),
|
|
284
306
|
];
|
|
@@ -309,7 +331,7 @@ export const models = makePage((ctx) => {
|
|
|
309
331
|
load();
|
|
310
332
|
return () => {
|
|
311
333
|
const s = ctx.state;
|
|
312
|
-
if (s.loading) return loadingState();
|
|
334
|
+
if (s.loading) return loadingState('loading models…');
|
|
313
335
|
if (s.error && !s.providers) return errorState(s.error, load);
|
|
314
336
|
const providers = Array.isArray(s.providers) ? s.providers : [];
|
|
315
337
|
const cached = s.cached || {};
|
|
@@ -347,14 +369,14 @@ export const cron = makePage((ctx) => {
|
|
|
347
369
|
load();
|
|
348
370
|
return () => {
|
|
349
371
|
const s = ctx.state;
|
|
350
|
-
if (s.loading) return loadingState();
|
|
372
|
+
if (s.loading) return loadingState('loading cron jobs…');
|
|
351
373
|
if (s.error && !s.list) return errorState(s.error, load);
|
|
352
374
|
const list = Array.isArray(s.list) ? s.list : [];
|
|
353
375
|
return [
|
|
354
376
|
PageHeader({ eyebrow: 'freddie', title: 'cron', lede: list.length + ' scheduled jobs' }),
|
|
355
377
|
noteAlert(s.note),
|
|
356
378
|
section('jobs', list.length ? list.map((j, i) => Row({
|
|
357
|
-
key: i, code: j.enabled ? Icon('play') : Icon('pause'), title: j.cron, sub: (j.prompt
|
|
379
|
+
key: i, code: j.enabled ? Icon('play') : Icon('pause'), title: j.cron, sub: trunc(j.prompt, TRUNC_SUB).text,
|
|
358
380
|
trailing: Btn({ danger: true, children: 'delete', onClick: () => del(j.id) }),
|
|
359
381
|
})) : emptyState('no cron jobs')),
|
|
360
382
|
section('new job',
|
|
@@ -373,13 +395,13 @@ export const skills = makePage((ctx) => {
|
|
|
373
395
|
load();
|
|
374
396
|
return () => {
|
|
375
397
|
const s = ctx.state;
|
|
376
|
-
if (s.loading) return loadingState();
|
|
398
|
+
if (s.loading) return loadingState('loading skills…');
|
|
377
399
|
if (s.error) return errorState(s.error, load);
|
|
378
400
|
const list = Array.isArray(s.list) ? s.list : (s.list?.skills || []);
|
|
379
401
|
return [
|
|
380
402
|
PageHeader({ eyebrow: 'freddie', title: 'skills', lede: list.length + ' skills' }),
|
|
381
403
|
section('skills', list.length ? list.map((sk, i) => h('div', { key: i },
|
|
382
|
-
Row({ code: (sk.source || 'fs').slice(0, 3), title: sk.name, sub: (sk.description
|
|
404
|
+
Row({ code: (sk.source || 'fs').slice(0, 3), title: sk.name, sub: trunc(sk.description, TRUNC_DESC).text,
|
|
383
405
|
onClick: () => ctx.set({ open: s.open === i ? null : i }), active: s.open === i }),
|
|
384
406
|
s.open === i ? h('pre', { class: 'fd-pre fd-skill-body' }, sk.body || sk.content || '(no body)') : null,
|
|
385
407
|
)) : emptyState('no skills')),
|
|
@@ -412,7 +434,7 @@ export const config = makePage((ctx) => {
|
|
|
412
434
|
load();
|
|
413
435
|
return () => {
|
|
414
436
|
const s = ctx.state;
|
|
415
|
-
if (s.loading) return loadingState();
|
|
437
|
+
if (s.loading) return loadingState('loading config…');
|
|
416
438
|
if (s.error) return errorState(s.error, load);
|
|
417
439
|
const cfg = s.cfg || {};
|
|
418
440
|
const flat = Object.entries(cfg).filter(([, v]) => typeof v !== 'object' || v === null);
|
|
@@ -433,7 +455,8 @@ export const config = makePage((ctx) => {
|
|
|
433
455
|
TextField({ key: i, label: k, value: String(ctx.state.edited[k] ?? v ?? ''), onInput: (val) => { ctx.state.edited[k] = val; } })
|
|
434
456
|
) : emptyState('no scalar config keys')),
|
|
435
457
|
section('raw', h('pre', { class: 'fd-pre' }, JSON.stringify(cfg, null, 2))),
|
|
436
|
-
|
|
458
|
+
section('actions',
|
|
459
|
+
Btn({ primary: true, disabled: s.busy || !Object.keys(s.edited).length, children: s.busy ? 'saving…' : 'save changes', onClick: save })),
|
|
437
460
|
];
|
|
438
461
|
};
|
|
439
462
|
});
|
|
@@ -445,10 +468,10 @@ export const env = makePage((ctx) => {
|
|
|
445
468
|
load();
|
|
446
469
|
return () => {
|
|
447
470
|
const s = ctx.state;
|
|
448
|
-
if (s.loading) return loadingState();
|
|
471
|
+
if (s.loading) return loadingState('loading environment…');
|
|
449
472
|
if (s.error) return errorState(s.error, load);
|
|
450
473
|
const d = s.data || {};
|
|
451
|
-
const rows = Object.entries(d).map(([k, v]) => [k, v === true || v === 'set' ? Chip({ tone: 'ok', children: 'set' }) : (v ?
|
|
474
|
+
const rows = Object.entries(d).map(([k, v]) => [k, v === true || v === 'set' ? Chip({ tone: 'ok', children: 'set' }) : (v ? truncSpan(v, TRUNC_SUB) : Chip({ tone: 'neutral', children: 'unset' }))]);
|
|
452
475
|
return [
|
|
453
476
|
PageHeader({ eyebrow: 'freddie', title: 'env', lede: 'environment / key presence' }),
|
|
454
477
|
section('variables', rows.length ? Table({ headers: ['key', 'status'], rows }) : emptyState('no env data')),
|
|
@@ -464,7 +487,7 @@ export const tools = makePage((ctx) => {
|
|
|
464
487
|
load();
|
|
465
488
|
return () => {
|
|
466
489
|
const s = ctx.state;
|
|
467
|
-
if (s.loading) return loadingState();
|
|
490
|
+
if (s.loading) return loadingState('loading tools…');
|
|
468
491
|
if (s.error) return errorState(s.error, load);
|
|
469
492
|
let list = Array.isArray(s.list) ? s.list : (s.list?.tools || []);
|
|
470
493
|
if (s.q) list = list.filter(t => (t.name || '').includes(s.q));
|
|
@@ -472,9 +495,9 @@ export const tools = makePage((ctx) => {
|
|
|
472
495
|
for (const t of list) { const g = t.toolset || 'core'; (groups[g] = groups[g] || []).push(t); }
|
|
473
496
|
return [
|
|
474
497
|
PageHeader({ eyebrow: 'freddie', title: 'tools', lede: list.length + ' tools' }),
|
|
475
|
-
SearchInput({ value: s.q, placeholder: 'filter tools…', onInput: (v) => ctx.set({ q: v }) }),
|
|
498
|
+
SearchInput({ value: s.q, label: 'filter tools', placeholder: 'filter tools…', onInput: (v) => ctx.set({ q: v }) }),
|
|
476
499
|
...Object.entries(groups).map(([g, ts]) => section(g + ' · ' + ts.length, ts.map((t, i) => h('div', { key: i },
|
|
477
|
-
Row({ title: t.name, sub: (t.schema?.description || t.description
|
|
500
|
+
Row({ title: t.name, sub: trunc(t.schema?.description || t.description, TRUNC_DESC).text, onClick: () => ctx.set({ open: ctx.state.open === t.name ? null : t.name }), active: ctx.state.open === t.name }),
|
|
478
501
|
ctx.state.open === t.name ? h('pre', { class: 'fd-pre' }, JSON.stringify(t.schema || t, null, 2)) : null,
|
|
479
502
|
)))),
|
|
480
503
|
list.length ? null : emptyState('no tools match'),
|
|
@@ -501,7 +524,7 @@ export const batch = makePage((ctx) => {
|
|
|
501
524
|
noteAlert(s.note),
|
|
502
525
|
section('prompts',
|
|
503
526
|
TextField({ label: 'prompts (one per line)', value: s.prompts, multiline: true, rows: 6, onInput: (v) => { s.prompts = v; } }),
|
|
504
|
-
TextField({ label: 'concurrency', type: 'number', value: String(s.concurrency), onInput: (v) => { s.concurrency = v; } }),
|
|
527
|
+
TextField({ label: 'concurrency', type: 'number', min: 1, 'aria-label': 'batch concurrency', value: String(s.concurrency), onInput: (v) => { s.concurrency = v; } }),
|
|
505
528
|
Btn({ primary: true, disabled: s.busy, children: s.busy ? 'running…' : 'run batch', onClick: run })),
|
|
506
529
|
s.result ? section('result', (() => {
|
|
507
530
|
const r = s.result;
|
|
@@ -510,9 +533,7 @@ export const batch = makePage((ctx) => {
|
|
|
510
533
|
return [
|
|
511
534
|
Kpi({ items: [[items.length, 'prompts'], [items.filter(x => !x.error).length, 'ok'], [items.filter(x => x.error).length, 'errors']] }),
|
|
512
535
|
Table({ headers: ['#', 'prompt', 'status', 'output'], rows: items.map((x, i) => {
|
|
513
|
-
|
|
514
|
-
const out = trunc(x.error || x.result || x.content || x.output || '', 70);
|
|
515
|
-
return [String(i + 1), h('span', { title: p.title }, p.text), x.error ? Chip({ tone: 'miss', children: 'error' }) : Chip({ tone: 'ok', children: 'ok' }), h('span', { title: out.title }, out.text)];
|
|
536
|
+
return [String(i + 1), truncSpan(x.prompt || x.input || '', TRUNC_PROMPT), x.error ? Chip({ tone: 'miss', children: 'error' }) : Chip({ tone: 'ok', children: 'ok' }), truncSpan(x.error || x.result || x.content || x.output || '', TRUNC_OUTPUT)];
|
|
516
537
|
}) }),
|
|
517
538
|
];
|
|
518
539
|
})()) : null,
|
|
@@ -527,13 +548,14 @@ export const gateway = makePage((ctx) => {
|
|
|
527
548
|
load(); ctx.interval(load, 10000);
|
|
528
549
|
return () => {
|
|
529
550
|
const s = ctx.state;
|
|
530
|
-
if (s.loading) return loadingState();
|
|
531
|
-
if (s.error) return errorState(s.error, load);
|
|
551
|
+
if (s.loading) return loadingState('loading gateway…');
|
|
552
|
+
if (s.error && !s.data) return errorState(s.error, load);
|
|
532
553
|
const d = s.data || {};
|
|
533
554
|
const platforms = d.platforms || d;
|
|
534
555
|
const rows = Object.entries(platforms).map(([k, v]) => [k, typeof v === 'object' ? (v.running || v.up ? Chip({ tone: 'ok', children: 'up' }) : Chip({ tone: 'miss', children: 'down' })) : String(v)]);
|
|
535
556
|
return [
|
|
536
557
|
PageHeader({ eyebrow: 'freddie', title: 'gateway', lede: 'messaging platform status' }),
|
|
558
|
+
s.error && s.data ? refreshError(s.error) : null,
|
|
537
559
|
section('platforms', rows.length ? Table({ headers: ['platform', 'status'], rows }) : emptyState('no platforms configured')),
|
|
538
560
|
];
|
|
539
561
|
};
|
|
@@ -566,7 +588,7 @@ export const chains = makePage((ctx) => {
|
|
|
566
588
|
load();
|
|
567
589
|
return () => {
|
|
568
590
|
const s = ctx.state;
|
|
569
|
-
if (s.loading) return loadingState();
|
|
591
|
+
if (s.loading) return loadingState('loading chains…');
|
|
570
592
|
if (s.error && !s.cfg && !s.health) return errorState(s.error, load);
|
|
571
593
|
const chainsList = s.list?.chains || s.list || [];
|
|
572
594
|
const up = s.health && (s.health.ok || s.health.status === 'ok' || s.health.healthy);
|
|
@@ -593,15 +615,16 @@ export const machines = makePage((ctx) => {
|
|
|
593
615
|
load(); ctx.interval(load, 8000);
|
|
594
616
|
return () => {
|
|
595
617
|
const s = ctx.state;
|
|
596
|
-
if (s.loading) return loadingState();
|
|
597
|
-
if (s.error) return errorState(s.error, load);
|
|
618
|
+
if (s.loading) return loadingState('loading machines…');
|
|
619
|
+
if (s.error && !s.data) return errorState(s.error, load);
|
|
598
620
|
const d = s.data || {};
|
|
599
621
|
const list = Array.isArray(d) ? d : (d.machines || Object.entries(d).map(([kind, v]) => ({ kind, ...(typeof v === 'object' ? v : { value: v }) })));
|
|
600
622
|
return [
|
|
601
623
|
PageHeader({ eyebrow: 'freddie', title: 'machines', lede: 'persisted xstate machine census' }),
|
|
624
|
+
s.error && s.data ? refreshError(s.error) : null,
|
|
602
625
|
section('machines', list.length ? Table({
|
|
603
626
|
headers: ['kind', 'key', 'state'],
|
|
604
|
-
rows: list.map(m => [m.kind || '—', m.key || m.machine_id || '—', m.state || m.value ||
|
|
627
|
+
rows: list.map(m => [m.kind || '—', m.key || m.machine_id || '—', m.state || m.value || truncJson(m)]),
|
|
605
628
|
}) : emptyState('no live machines')),
|
|
606
629
|
];
|
|
607
630
|
};
|
|
@@ -622,13 +645,14 @@ export const health = makePage((ctx) => {
|
|
|
622
645
|
load(); ctx.interval(load, 15000);
|
|
623
646
|
return () => {
|
|
624
647
|
const s = ctx.state;
|
|
625
|
-
if (s.loading) return loadingState();
|
|
626
|
-
if (s.error) return errorState(s.error, load);
|
|
648
|
+
if (s.loading) return loadingState('loading health…');
|
|
649
|
+
if (s.error && !s.health && !s.providers) return errorState(s.error, load);
|
|
627
650
|
const hd = s.health || {};
|
|
628
651
|
const provs = Array.isArray(s.providers) ? s.providers : (s.providers?.providers || []);
|
|
629
652
|
return [
|
|
630
653
|
PageHeader({ eyebrow: 'freddie', title: 'health', lede: 'system & provider health', right: hd.ok ? Chip({ tone: 'ok', children: 'healthy' }) : Chip({ tone: 'miss', children: 'degraded' }) }),
|
|
631
|
-
|
|
654
|
+
s.error && (s.health || s.providers) ? refreshError(s.error) : null,
|
|
655
|
+
section('checks', Object.keys(hd).length ? Table({ headers: ['check', 'status'], rows: Object.entries(hd).map(([k, v]) => [k, typeof v === 'object' ? truncJson(v) : (v === true ? Chip({ tone: 'ok', children: 'ok' }) : v === false ? Chip({ tone: 'miss', children: 'no' }) : String(v))]) }) : emptyState('no health data')),
|
|
632
656
|
provs.length ? section('providers', Table({ headers: ['provider', 'status'], rows: provs.map(p => { const n = typeof p === 'string' ? p : p.name || p.id; const ok = typeof p === 'object' ? (p.ok ?? p.available) : null; return [n, ok == null ? '—' : (ok ? Chip({ tone: 'ok', children: 'up' }) : Chip({ tone: 'miss', children: 'down' }))]; }) })) : null,
|
|
633
657
|
];
|
|
634
658
|
};
|
|
@@ -647,7 +671,7 @@ export const debug = makePage((ctx) => {
|
|
|
647
671
|
load();
|
|
648
672
|
return () => {
|
|
649
673
|
const s = ctx.state;
|
|
650
|
-
if (s.loading) return loadingState();
|
|
674
|
+
if (s.loading) return loadingState('loading debug snapshots…');
|
|
651
675
|
if (s.error) return errorState(s.error, load);
|
|
652
676
|
const d = s.data || {};
|
|
653
677
|
const subsystems = d.subsystems || Object.keys(d);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Interaction primitives — pointer drag/drop + keyboard shortcuts.
|
|
2
2
|
// Pointer Events only (touch+mouse). Visuals via editor-primitives.css.
|
|
3
3
|
import * as webjsx from '../../vendor/webjsx/index.js';
|
|
4
|
+
import { Icon } from './shell.js';
|
|
4
5
|
const h = webjsx.createElement;
|
|
5
6
|
const DRAG_THRESHOLD = 5;
|
|
6
7
|
const IS_MAC = (typeof navigator !== 'undefined') && /Mac|iPhone|iPad/.test(navigator.platform || '');
|
|
@@ -146,7 +147,7 @@ export function Reorderable({ items = [], getKey, renderItem, onReorder, axis =
|
|
|
146
147
|
h('button', {
|
|
147
148
|
type: 'button', class: 'ds-reorder-handle',
|
|
148
149
|
'aria-label': 'Reorder', tabindex: '0',
|
|
149
|
-
}, '
|
|
150
|
+
}, Icon('more-horizontal')),
|
|
150
151
|
renderItem ? renderItem(item, i) : null
|
|
151
152
|
);
|
|
152
153
|
})
|
|
@@ -203,8 +204,30 @@ export function ShortcutHelpDialog({ open = false, onClose, registry } = {}) {
|
|
|
203
204
|
const list = registry || Array.from(SHORTCUT_REGISTRY);
|
|
204
205
|
const groups = {};
|
|
205
206
|
list.forEach(r => { (groups[r.scope] = groups[r.scope] || []).push(r); });
|
|
207
|
+
// Escape-to-close, Tab focus trap, and autofocus on open — wired through a
|
|
208
|
+
// ref so teardown runs on the webjsx ref(null) unmount branch.
|
|
209
|
+
const dialogRef = (el) => {
|
|
210
|
+
if (!el) { if (ShortcutHelpDialog._teardown) { ShortcutHelpDialog._teardown(); ShortcutHelpDialog._teardown = null; } return; }
|
|
211
|
+
const focusables = () => el.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])');
|
|
212
|
+
const onKey = (e) => {
|
|
213
|
+
if (e.key === 'Escape') { e.preventDefault(); if (onClose) onClose(); return; }
|
|
214
|
+
if (e.key === 'Tab') {
|
|
215
|
+
const f = focusables();
|
|
216
|
+
if (!f.length) { e.preventDefault(); return; }
|
|
217
|
+
const first = f[0], last = f[f.length - 1];
|
|
218
|
+
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
|
219
|
+
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
el.addEventListener('keydown', onKey);
|
|
223
|
+
ShortcutHelpDialog._teardown = () => el.removeEventListener('keydown', onKey);
|
|
224
|
+
// The dialog itself is focusable (tabindex=-1) so it always has a home
|
|
225
|
+
// for focus even when it contains no interactive controls.
|
|
226
|
+
const f = focusables();
|
|
227
|
+
(f[0] || el).focus();
|
|
228
|
+
};
|
|
206
229
|
return h('div', { class: 'ds-ep-dialog-backdrop', onmousedown: (e) => { if (e.target === e.currentTarget && onClose) onClose(); } },
|
|
207
|
-
h('div', { class: 'ds-ep-dialog', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Keyboard shortcuts' },
|
|
230
|
+
h('div', { class: 'ds-ep-dialog', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Keyboard shortcuts', tabindex: '-1', ref: dialogRef },
|
|
208
231
|
h('h2', null, 'Keyboard shortcuts'),
|
|
209
232
|
...Object.entries(groups).map(([scope, rows]) =>
|
|
210
233
|
h('section', { class: 'ds-kbd-group' },
|
|
@@ -9,6 +9,16 @@ const h = webjsx.createElement;
|
|
|
9
9
|
const kids = (c) => c == null ? [] : (Array.isArray(c) ? c : [c]);
|
|
10
10
|
const FOCUSABLE_SEL = 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
11
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
|
+
const FLOAT_OFFSET_TOOLTIP = 6;
|
|
19
|
+
const FLOAT_OFFSET_POPOVER = 6;
|
|
20
|
+
const FLOAT_OFFSET_DROPDOWN = 4;
|
|
21
|
+
|
|
12
22
|
// useFloating — compute left/top + auto-flip; re-runs on resize/scroll.
|
|
13
23
|
export function useFloating(anchorEl, contentEl, { placement = 'bottom-start', offset = 8 } = {}) {
|
|
14
24
|
if (!anchorEl || !contentEl) return { update() {}, dispose() {}, finalPlacement: placement };
|
|
@@ -30,8 +40,8 @@ export function useFloating(anchorEl, contentEl, { placement = 'bottom-start', o
|
|
|
30
40
|
x = s === 'right' ? a.right + offset : a.left - offset - c.width;
|
|
31
41
|
y = align === 'start' ? a.top : align === 'end' ? a.bottom - c.height : a.top + (a.height - c.height) / 2;
|
|
32
42
|
}
|
|
33
|
-
x = Math.max(
|
|
34
|
-
y = Math.max(
|
|
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));
|
|
35
45
|
contentEl.style.left = x + 'px';
|
|
36
46
|
contentEl.style.top = y + 'px';
|
|
37
47
|
finalPlacement = s + '-' + align;
|
|
@@ -86,7 +96,7 @@ function _showTip(trigger, label, placement, kind) {
|
|
|
86
96
|
_tipEl.id = 'ds-tip-' + (++_tipId);
|
|
87
97
|
trigger.setAttribute('aria-describedby', _tipEl.id);
|
|
88
98
|
if (_tipFloat) _tipFloat.dispose();
|
|
89
|
-
_tipFloat = useFloating(trigger, _tipEl, { placement, offset:
|
|
99
|
+
_tipFloat = useFloating(trigger, _tipEl, { placement, offset: FLOAT_OFFSET_TOOLTIP });
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
export function Tooltip({ children, label, placement = 'top', delay = 350, kind = 'default' } = {}) {
|
|
@@ -126,7 +136,7 @@ export function Popover({ open, anchorEl, onClose, placement = 'bottom-start', c
|
|
|
126
136
|
el.tabIndex = -1;
|
|
127
137
|
document.body.appendChild(el);
|
|
128
138
|
webjsx.applyDiff(el, h('div', { class: 'ds-popover-inner' }, ...kids(children)));
|
|
129
|
-
const floating = useFloating(anchorEl, el, { placement, offset:
|
|
139
|
+
const floating = useFloating(anchorEl, el, { placement, offset: FLOAT_OFFSET_POPOVER });
|
|
130
140
|
const close = () => onClose && onClose();
|
|
131
141
|
const onDown = (e) => { if (el.contains(e.target) || anchorEl.contains(e.target)) return; close(); };
|
|
132
142
|
const onKey = (e) => {
|
|
@@ -203,7 +213,7 @@ export function Dropdown({ trigger, items = [], onSelect, placement = 'bottom-st
|
|
|
203
213
|
webjsx.applyDiff(menuEl, tree);
|
|
204
214
|
document.body.appendChild(menuEl);
|
|
205
215
|
menuEl.addEventListener('keydown', onMenuKey);
|
|
206
|
-
floating = useFloating(triggerEl, menuEl, { placement, offset:
|
|
216
|
+
floating = useFloating(triggerEl, menuEl, { placement, offset: FLOAT_OFFSET_DROPDOWN });
|
|
207
217
|
document.addEventListener('mousedown', onDown, true);
|
|
208
218
|
triggerEl.setAttribute('aria-expanded', 'true');
|
|
209
219
|
if (focusFirst) queueMicrotask(() => focusItem(0));
|
|
@@ -225,7 +235,7 @@ export function Dropdown({ trigger, items = [], onSelect, placement = 'bottom-st
|
|
|
225
235
|
}
|
|
226
236
|
|
|
227
237
|
// Clamp a fixed-position box to the viewport given desired top-left coords.
|
|
228
|
-
function _clampToViewport(x, y, w, h, margin =
|
|
238
|
+
function _clampToViewport(x, y, w, h, margin = CLAMP_MARGIN) {
|
|
229
239
|
const vw = (typeof window !== 'undefined' ? window.innerWidth : 1024);
|
|
230
240
|
const vh = (typeof window !== 'undefined' ? window.innerHeight : 768);
|
|
231
241
|
return {
|
|
@@ -234,6 +244,35 @@ function _clampToViewport(x, y, w, h, margin = 8) {
|
|
|
234
244
|
};
|
|
235
245
|
}
|
|
236
246
|
|
|
247
|
+
// Tab focus trap for a dialog root — keeps Tab/Shift+Tab cycling inside `el`.
|
|
248
|
+
// Call from an onkeydown handler; returns true if it handled the event.
|
|
249
|
+
function _trapTab(el, e) {
|
|
250
|
+
if (e.key !== 'Tab') return false;
|
|
251
|
+
const nodes = el.querySelectorAll(FOCUSABLE_SEL);
|
|
252
|
+
if (!nodes.length) { e.preventDefault(); return true; }
|
|
253
|
+
const first = nodes[0], last = nodes[nodes.length - 1], a = document.activeElement;
|
|
254
|
+
if (e.shiftKey && a === first) { e.preventDefault(); last.focus(); return true; }
|
|
255
|
+
if (!e.shiftKey && a === last) { e.preventDefault(); first.focus(); return true; }
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Shared lifecycle for fixed anchor-positioned popovers (EmojiPicker,
|
|
260
|
+
// SettingsPopover): on mount, place+clamp near (anchorX, anchorY), focus the
|
|
261
|
+
// root, and wire an outside-mousedown close. Returns a cleanup fn the ref(null)
|
|
262
|
+
// branch must call. Both consumers deduped through this so the
|
|
263
|
+
// queueMicrotask/place/clamp/outside-close dance is authored once.
|
|
264
|
+
function _anchoredOverlayLifecycle(el, { anchorX, anchorY, fallbackW, fallbackH, close }) {
|
|
265
|
+
const place = () => {
|
|
266
|
+
const r = el.getBoundingClientRect();
|
|
267
|
+
const { left, top } = _clampToViewport(anchorX, anchorY, r.width || fallbackW, r.height || fallbackH);
|
|
268
|
+
el.style.left = left + 'px'; el.style.top = top + 'px';
|
|
269
|
+
};
|
|
270
|
+
queueMicrotask(() => { place(); el.focus(); });
|
|
271
|
+
const onDown = (e) => { if (!el.contains(e.target)) close(); };
|
|
272
|
+
queueMicrotask(() => document.addEventListener('mousedown', onDown, true));
|
|
273
|
+
return () => document.removeEventListener('mousedown', onDown, true);
|
|
274
|
+
}
|
|
275
|
+
|
|
237
276
|
// CommandPalette — centered Cmd+K palette with live filter + keyboard nav.
|
|
238
277
|
export function CommandPalette({ open, items = [], onSelect, onClose } = {}) {
|
|
239
278
|
if (!open) return null;
|
|
@@ -275,7 +314,11 @@ export function CommandPalette({ open, items = [], onSelect, onClose } = {}) {
|
|
|
275
314
|
};
|
|
276
315
|
|
|
277
316
|
let rootEl = null, inputEl = null, listEl = null, flat = [];
|
|
278
|
-
|
|
317
|
+
// Remember the element focused before the palette opened so we can return
|
|
318
|
+
// focus there on close (the input steals focus on mount).
|
|
319
|
+
const prevFocus = (typeof document !== 'undefined') ? document.activeElement : null;
|
|
320
|
+
const restoreFocus = () => { if (prevFocus && prevFocus.focus && document.contains(prevFocus)) prevFocus.focus(); };
|
|
321
|
+
const close = () => { restoreFocus(); if (onClose) onClose(); };
|
|
279
322
|
const choose = (it) => { if (it && onSelect) onSelect(it); };
|
|
280
323
|
|
|
281
324
|
const renderInner = () => {
|
|
@@ -320,6 +363,10 @@ export function CommandPalette({ open, items = [], onSelect, onClose } = {}) {
|
|
|
320
363
|
);
|
|
321
364
|
}
|
|
322
365
|
|
|
366
|
+
// Sanctioned literal-emoji exception: an emoji picker's whole purpose is to
|
|
367
|
+
// present emoji, so the glyph ban does not apply to this data table or the
|
|
368
|
+
// per-emoji <button> labels below. This is intentional product content, not
|
|
369
|
+
// decorative chrome.
|
|
323
370
|
const EMOJI_CATEGORIES = [
|
|
324
371
|
{ id: 'smileys', label: '😀', emoji: ['😀','😁','😂','🤣','😊','😍','😘','😎','🤔','😅','😉','🙂','😇','🥳','😴','🤩','😜','😢','😭','😡','😱','🥺','😤','😬'] },
|
|
325
372
|
{ id: 'gestures', label: '👍', emoji: ['👍','👎','👌','✌️','🤞','🙏','👏','🙌','💪','👀','🤝','✋','🤙','👋','🤟','☝️'] },
|
|
@@ -347,19 +394,11 @@ export function EmojiPicker({ open, anchorX = 0, anchorY = 0, onSelect, onClose
|
|
|
347
394
|
return h('div', {
|
|
348
395
|
class: 'ov-emoji-root', role: 'dialog', 'aria-label': 'Emoji picker',
|
|
349
396
|
tabindex: '-1',
|
|
350
|
-
onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); } },
|
|
397
|
+
onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); return; } if (rootEl) _trapTab(rootEl, e); },
|
|
351
398
|
ref: (el) => {
|
|
352
399
|
if (!el) { if (rootEl && rootEl._ovEmojiCleanup) rootEl._ovEmojiCleanup(); return; }
|
|
353
400
|
if (el._ovEmoji) return; el._ovEmoji = true; rootEl = el;
|
|
354
|
-
|
|
355
|
-
const r = el.getBoundingClientRect();
|
|
356
|
-
const { left, top } = _clampToViewport(anchorX, anchorY, r.width || 260, r.height || 240);
|
|
357
|
-
el.style.left = left + 'px'; el.style.top = top + 'px';
|
|
358
|
-
};
|
|
359
|
-
queueMicrotask(() => { place(); el.focus(); });
|
|
360
|
-
const onDown = (e) => { if (!el.contains(e.target)) close(); };
|
|
361
|
-
queueMicrotask(() => document.addEventListener('mousedown', onDown, true));
|
|
362
|
-
el._ovEmojiCleanup = () => document.removeEventListener('mousedown', onDown, true);
|
|
401
|
+
el._ovEmojiCleanup = _anchoredOverlayLifecycle(el, { anchorX, anchorY, fallbackW: 260, fallbackH: 240, close });
|
|
363
402
|
},
|
|
364
403
|
},
|
|
365
404
|
h('div', { class: 'ov-emoji-tabs', role: 'tablist' },
|
|
@@ -401,32 +440,41 @@ export function BootOverlay({ progress = 0, phase = '', errored = false, visible
|
|
|
401
440
|
// SettingsPopover — fixed popover with generic section/row control rendering.
|
|
402
441
|
export function SettingsPopover({ title = 'Settings', open, anchorX = 0, anchorY = 0, sections = [], onClose } = {}) {
|
|
403
442
|
if (!open) return null;
|
|
443
|
+
let rootEl = null;
|
|
404
444
|
const close = () => onClose && onClose();
|
|
405
445
|
const secs = Array.isArray(sections) ? sections : [];
|
|
406
446
|
|
|
407
447
|
const renderRow = (row, i) => {
|
|
408
448
|
const label = row.label != null ? row.label : (row.title != null ? row.title : '');
|
|
409
449
|
const kind = row.kind;
|
|
410
|
-
|
|
450
|
+
// Give every interactive control a stable id and point the row label's
|
|
451
|
+
// `for` at it, so the visible label is the control's accessible name.
|
|
452
|
+
const ctrlId = 'ov-set-' + i + '-' + kind;
|
|
453
|
+
const labelNode = h('label', { class: 'ov-set-row-label', for: ctrlId }, String(label));
|
|
411
454
|
let control = null;
|
|
412
455
|
if (kind === 'select') {
|
|
413
456
|
const opts = Array.isArray(row.options) ? row.options : [];
|
|
457
|
+
// Controlled via the `value` prop only — per-option `selected` is
|
|
458
|
+
// dropped so the two don't fight (value wins).
|
|
414
459
|
control = h('select', {
|
|
460
|
+
id: ctrlId,
|
|
415
461
|
class: 'ov-set-control', value: row.value != null ? String(row.value) : undefined,
|
|
416
462
|
onchange: (e) => row.onChange && row.onChange(e.target.value),
|
|
417
463
|
}, ...opts.map(o => {
|
|
418
464
|
const v = (o && typeof o === 'object') ? o.value : o;
|
|
419
465
|
const l = (o && typeof o === 'object') ? (o.label != null ? o.label : o.value) : o;
|
|
420
|
-
return h('option', { value: String(v)
|
|
466
|
+
return h('option', { value: String(v) }, String(l));
|
|
421
467
|
}));
|
|
422
468
|
} else if (kind === 'toggle') {
|
|
423
469
|
control = h('input', {
|
|
470
|
+
id: ctrlId,
|
|
424
471
|
type: 'checkbox', class: 'ov-set-toggle',
|
|
425
472
|
checked: row.value ? 'checked' : undefined,
|
|
426
473
|
onchange: (e) => row.onChange && row.onChange(e.target.checked),
|
|
427
474
|
});
|
|
428
475
|
} else if (kind === 'range') {
|
|
429
476
|
control = h('input', {
|
|
477
|
+
id: ctrlId,
|
|
430
478
|
type: 'range', class: 'ov-set-control',
|
|
431
479
|
min: String(row.min != null ? row.min : 0),
|
|
432
480
|
max: String(row.max != null ? row.max : 100),
|
|
@@ -440,23 +488,19 @@ export function SettingsPopover({ title = 'Settings', open, anchorX = 0, anchorY
|
|
|
440
488
|
return h('div', { class: 'ov-set-row', key: i }, control);
|
|
441
489
|
} else {
|
|
442
490
|
control = h('span', { class: 'ov-set-row-value' }, String(row.value != null ? row.value : ''));
|
|
491
|
+
// Non-interactive value row: a plain span label (no `for` target).
|
|
492
|
+
return h('div', { class: 'ov-set-row', key: i }, h('span', { class: 'ov-set-row-label' }, String(label)), control);
|
|
443
493
|
}
|
|
444
494
|
return h('div', { class: 'ov-set-row', key: i }, labelNode, control);
|
|
445
495
|
};
|
|
446
496
|
|
|
447
497
|
return h('div', {
|
|
448
498
|
class: 'ov-set-root', role: 'dialog', 'aria-label': String(title), tabindex: '-1',
|
|
449
|
-
onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); } },
|
|
499
|
+
onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); return; } if (rootEl) _trapTab(rootEl, e); },
|
|
450
500
|
ref: (el) => {
|
|
451
|
-
if (!el
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const { left, top } = _clampToViewport(anchorX, anchorY, r.width || 280, r.height || 200);
|
|
455
|
-
el.style.left = left + 'px'; el.style.top = top + 'px';
|
|
456
|
-
};
|
|
457
|
-
queueMicrotask(() => { place(); el.focus(); });
|
|
458
|
-
const onDown = (e) => { if (!el.contains(e.target)) close(); };
|
|
459
|
-
queueMicrotask(() => document.addEventListener('mousedown', onDown, true));
|
|
501
|
+
if (!el) { if (rootEl && rootEl._ovSetCleanup) rootEl._ovSetCleanup(); return; }
|
|
502
|
+
if (el._ovSet) return; el._ovSet = true; rootEl = el;
|
|
503
|
+
el._ovSetCleanup = _anchoredOverlayLifecycle(el, { anchorX, anchorY, fallbackW: 280, fallbackH: 200, close });
|
|
460
504
|
},
|
|
461
505
|
},
|
|
462
506
|
h('div', { class: 'ov-set-head' }, String(title)),
|
|
@@ -524,7 +568,7 @@ export function AuthModal({ mode = 'extension', error = '', busy = false, open =
|
|
|
524
568
|
},
|
|
525
569
|
h('div', { class: 'ov-auth-head' },
|
|
526
570
|
h('h2', { class: 'ov-auth-title' }, 'Sign in'),
|
|
527
|
-
h('button', { type: 'button', class: 'ov-auth-x', 'aria-label': 'close', onclick: close }, '
|
|
571
|
+
h('button', { type: 'button', class: 'ov-auth-x', 'aria-label': 'close', onclick: close }, Icon('x'))
|
|
528
572
|
),
|
|
529
573
|
h('div', { class: 'ov-auth-tabs', role: 'tablist' },
|
|
530
574
|
...modes.map(m => h('button', {
|
|
@@ -551,7 +595,7 @@ export function VideoLightbox({ src, label = '', open = false, onClose } = {}) {
|
|
|
551
595
|
ref: (el) => { if (el && !el._ovLb) { el._ovLb = true; queueMicrotask(() => el.focus()); } },
|
|
552
596
|
onmousedown: (e) => { if (e.target === e.currentTarget) close(); },
|
|
553
597
|
},
|
|
554
|
-
h('button', { type: 'button', class: 'ov-lightbox-x', 'aria-label': 'close', onclick: close }, '
|
|
598
|
+
h('button', { type: 'button', class: 'ov-lightbox-x', 'aria-label': 'close', onclick: close }, Icon('x')),
|
|
555
599
|
h('div', { class: 'ov-lightbox-stage' },
|
|
556
600
|
h('video', { class: 'ov-lightbox-video', src, controls: true, autoplay: true, playsinline: true }),
|
|
557
601
|
label ? h('div', { class: 'ov-lightbox-label' }, label) : null
|