anentrypoint-design 0.0.147 → 0.0.150

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anentrypoint-design",
3
- "version": "0.0.147",
3
+ "version": "0.0.150",
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",
@@ -154,8 +154,15 @@ export function Kpi({ items = [] }) {
154
154
  h('div', { class: 'lbl' }, l))));
155
155
  }
156
156
 
157
- export function Table({ headers = [], rows = [], onRowClick, emptyText = 'nothing here yet' }) {
157
+ export function Table({ headers = [], rows = [], onRowClick, emptyText = 'nothing here yet', rowLabels }) {
158
158
  if (!rows || rows.length === 0) return h('div', { class: 'empty' }, emptyText);
159
+ // rowLabels lets callers supply a plain-text label per row when the first
160
+ // cell is a vnode (so the aria-label is meaningful, not the literal 'row').
161
+ const labelFor = (row, i) => {
162
+ if (Array.isArray(rowLabels) && rowLabels[i] != null) return String(rowLabels[i]);
163
+ const c = row[0];
164
+ return c == null ? 'row' : (typeof c === 'object' ? 'row' : String(c));
165
+ };
159
166
  return h('table', { role: 'table' },
160
167
  h('thead', {}, h('tr', { role: 'row' }, ...headers.map((hd, i) => h('th', { key: i, scope: 'col', role: 'columnheader' }, hd)))),
161
168
  h('tbody', {}, ...rows.map((row, i) => h('tr', {
@@ -163,7 +170,7 @@ export function Table({ headers = [], rows = [], onRowClick, emptyText = 'nothin
163
170
  class: onRowClick ? 'clickable' : '',
164
171
  role: 'row',
165
172
  onclick: onRowClick ? () => onRowClick(i) : null,
166
- ...(onRowClick ? { tabindex: '0', onkeydown: (e) => { if (e.key === 'Enter') onRowClick(i); } } : {})
173
+ ...(onRowClick ? { tabindex: '0', 'aria-label': 'open ' + labelFor(row, i), onkeydown: (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onRowClick(i); } } } : {})
167
174
  }, ...row.map((c, j) => h('td', { key: j, role: 'cell' }, c == null ? '' : (typeof c === 'object' ? c : String(c))))))));
168
175
  }
169
176
 
@@ -235,13 +242,14 @@ export function PageHeader({ title, lede, eyebrow, right }) {
235
242
  );
236
243
  }
237
244
 
238
- export function SearchInput({ value = '', placeholder = 'search…', onInput, onSubmit, name = 'q', key }) {
245
+ export function SearchInput({ value = '', placeholder = 'search…', onInput, onSubmit, name = 'q', key, label }) {
239
246
  return h('input', {
240
247
  key,
241
248
  type: 'search',
242
249
  name,
243
250
  class: 'ds-search-input',
244
251
  placeholder,
252
+ 'aria-label': label || placeholder,
245
253
  value,
246
254
  oninput: onInput ? (e) => onInput(e.target.value, e) : null,
247
255
  onkeydown: onSubmit ? (e) => { if (e.key === 'Enter') onSubmit(e.target.value, e); } : null
@@ -28,6 +28,19 @@ const section = (title, ...children) => Panel({ title, children: children.flat()
28
28
  const noteAlert = (note) => note ? h('div', { class: 'ds-alert ds-alert-' + note.kind, role: 'alert' },
29
29
  h('span', { class: 'ds-alert-icon' }, '!'),
30
30
  h('div', { class: 'ds-alert-content' }, note.msg)) : null;
31
+ // Manual refresh button for non-polling pages — parity with auto-refreshing ones.
32
+ const refreshBtn = (onClick, busy) => Btn({ children: busy ? 'refreshing…' : '↻ refresh', disabled: !!busy, onClick, 'aria-label': 'refresh' });
33
+ // Non-blocking refresh-error banner: keep last-good content, surface the failure.
34
+ const refreshError = (err) => err ? h('div', { class: 'ds-alert ds-alert-warn', role: 'status', 'aria-live': 'polite' },
35
+ h('span', { class: 'ds-alert-icon' }, '!'),
36
+ h('div', { class: 'ds-alert-content' }, 'refresh failed: ' + String(err.message || err))) : null;
37
+ // Polite live region announcing async busy/done state to screen readers.
38
+ const liveRegion = (msg) => h('div', { class: 'fd-sr-live', role: 'status', 'aria-live': 'polite' }, msg || '');
39
+ // Truncate with a title tooltip carrying the full text.
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
+ // Autoscroll a thread only when the user is already near the bottom, so
42
+ // scrolling up to read history is not yanked back down on the next render.
43
+ const stickyScroll = (el) => { if (!el) return; const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80; if (nearBottom) el.scrollTop = el.scrollHeight; };
31
44
 
32
45
  // ---- home ------------------------------------------------------------------
33
46
 
@@ -37,9 +50,10 @@ export const home = makePage((ctx) => {
37
50
  const [health, agents, sessions] = await Promise.all([
38
51
  api('/api/health').catch(() => null),
39
52
  api('/api/agents').catch(() => null),
40
- api('/api/sessions').catch(() => []),
53
+ api('/api/sessions').catch((e) => ({ _err: e })),
41
54
  ]);
42
- ctx.set({ loading: false, health, agents, sessions: Array.isArray(sessions) ? sessions : [], error: null });
55
+ const sessFailed = sessions && sessions._err;
56
+ ctx.set({ loading: false, health, agents, sessions: Array.isArray(sessions) ? sessions : [], sessFailed, error: null });
43
57
  } catch (e) { ctx.set({ loading: false, error: e }); }
44
58
  }
45
59
  load();
@@ -61,12 +75,14 @@ export const home = makePage((ctx) => {
61
75
  [agents.count ?? 0, 'active agents'],
62
76
  ] }),
63
77
  section('recent sessions',
64
- sessions.length
65
- ? Table({
66
- headers: ['session', 'platform', 'updated'],
67
- rows: sessions.slice(0, 8).map(x => [x.title || x.id, x.platform || '—', fmtAgo(x.updated_at)]),
68
- })
69
- : emptyState('no sessions yet')),
78
+ s.sessFailed
79
+ ? errorState(new Error('could not load sessions'))
80
+ : sessions.length
81
+ ? Table({
82
+ headers: ['session', 'platform', 'updated'],
83
+ rows: sessions.slice(0, 8).map(x => { const t = trunc(x.title || x.id, 60); return [h('span', { title: t.title }, t.text), x.platform || '—', fmtAgo(x.updated_at)]; }),
84
+ })
85
+ : emptyState('no sessions yet')),
70
86
  section('health',
71
87
  s.health ? Table({ headers: ['check', 'status'], rows: Object.entries(s.health).map(([k, v]) => [k, typeof v === 'object' ? JSON.stringify(v) : String(v)]) })
72
88
  : emptyState('health endpoint unavailable')),
@@ -96,8 +112,9 @@ export const chat = makePage((ctx) => {
96
112
  const s = ctx.state;
97
113
  return h('div', { class: 'fd-chat' },
98
114
  PageHeader({ eyebrow: 'freddie', title: 'chat', lede: 'one-shot agent turns · POST /api/chat' }),
115
+ liveRegion(s.sending ? 'waiting for assistant reply' : ''),
99
116
  h('div', { class: 'chat-thread fd-chat-thread', role: 'log', 'aria-label': 'chat messages',
100
- ref: (el) => { if (el) el.scrollTop = el.scrollHeight; } },
117
+ ref: stickyScroll },
101
118
  s.messages.length ? s.messages.map((m, i) => ChatMessage({ ...m, key: i }))
102
119
  : emptyState('send a prompt to start', '✎'),
103
120
  s.sending ? ChatMessage({ role: 'assistant', typing: true, key: '_typing' }) : null),
@@ -114,11 +131,25 @@ export const chat = makePage((ctx) => {
114
131
  // ---- voice -----------------------------------------------------------------
115
132
 
116
133
  export const voice = makePage((ctx) => {
117
- Object.assign(ctx.state, { loading: false });
118
- return () => [
119
- PageHeader({ eyebrow: 'freddie', title: 'voice', lede: 'voice surfaces' }),
120
- section('status', emptyState('no voice backend wired in this build. configure a transcription/tts plugin to enable.', '🎙')),
121
- ];
134
+ async function load() {
135
+ // Probe for a voice backend; the endpoint is optional, so a 404/!ok
136
+ // means "not wired" rather than an error to surface.
137
+ try { const v = await api('/api/voice').catch(() => null); ctx.set({ loading: false, voice: v, error: null }); }
138
+ catch (e) { ctx.set({ loading: false, error: e }); }
139
+ }
140
+ load();
141
+ return () => {
142
+ const s = ctx.state;
143
+ if (s.loading) return loadingState();
144
+ const v = s.voice;
145
+ const enabled = v && (v.enabled || v.transcription || v.tts);
146
+ return [
147
+ PageHeader({ eyebrow: 'freddie', title: 'voice', lede: 'voice surfaces', right: enabled ? Chip({ tone: 'ok', children: 'enabled' }) : Chip({ tone: 'neutral', children: 'not configured' }) }),
148
+ enabled
149
+ ? section('backends', Table({ headers: ['capability', 'status'], rows: [['transcription', v.transcription ? Chip({ tone: 'ok', children: 'on' }) : Chip({ tone: 'neutral', children: 'off' })], ['tts', v.tts ? Chip({ tone: 'ok', children: 'on' }) : Chip({ tone: 'neutral', children: 'off' })]] }))
150
+ : section('status', emptyState('no voice backend wired in this build. configure a transcription/tts plugin to enable.', '🎙')),
151
+ ];
152
+ };
122
153
  });
123
154
 
124
155
  // ---- sessions --------------------------------------------------------------
@@ -134,6 +165,7 @@ export const sessions = makePage((ctx) => {
134
165
  try { ctx.set({ loading: false, list: await api('/api/search?q=' + encodeURIComponent(q)), error: null }); }
135
166
  catch (e) { ctx.set({ loading: false, error: e }); }
136
167
  }
168
+ async function refresh() { ctx.set({ refreshing: true }); try { ctx.set({ list: await api('/api/sessions'), error: null }); } catch (e) { ctx.set({ error: e }); } ctx.set({ refreshing: false }); }
137
169
  async function open(id) {
138
170
  ctx.set({ selected: id, msgLoading: true });
139
171
  try { ctx.set({ messages: await api('/api/sessions/' + encodeURIComponent(id) + '/messages'), msgLoading: false }); }
@@ -146,12 +178,14 @@ export const sessions = makePage((ctx) => {
146
178
  if (s.error && !s.list) return errorState(s.error, load);
147
179
  const list = Array.isArray(s.list) ? s.list : [];
148
180
  return [
149
- PageHeader({ eyebrow: 'freddie', title: 'sessions', lede: list.length + ' sessions' }),
150
- SearchInput({ value: s.q, placeholder: 'search messages…', onInput: (v) => { s.q = v; }, onSubmit: (v) => search(v) }),
181
+ PageHeader({ eyebrow: 'freddie', title: 'sessions', lede: list.length + ' sessions', right: refreshBtn(refresh, s.refreshing) }),
182
+ s.error && s.list ? refreshError(s.error) : null,
183
+ SearchInput({ value: s.q, label: 'search sessions', placeholder: 'search messages…', onInput: (v) => { s.q = v; }, onSubmit: (v) => search(v) }),
151
184
  section('sessions',
152
185
  list.length
153
186
  ? Table({ headers: ['session', 'platform', 'updated'], onRowClick: (i) => open(list[i].id),
154
- rows: list.map(x => [x.title || x.id, x.platform || '—', fmtAgo(x.updated_at)]) })
187
+ rowLabels: list.map(x => x.title || x.id),
188
+ rows: list.map(x => { const t = trunc(x.title || x.id, 60); return [h('span', { title: t.title }, t.text), x.platform || '—', fmtAgo(x.updated_at)]; }) })
155
189
  : emptyState('no sessions match')),
156
190
  s.selected ? section('messages · ' + s.selected,
157
191
  s.msgLoading ? loadingState()
@@ -282,6 +316,7 @@ export const models = makePage((ctx) => {
282
316
  const status = s.sampler?.status || {};
283
317
  return [
284
318
  PageHeader({ eyebrow: 'freddie', title: 'models', lede: providers.length + ' providers', right: Btn({ primary: true, disabled: s.discovering, children: s.discovering ? 'discovering…' : 'discover', onClick: discover }) }),
319
+ liveRegion(s.discovering ? 'discovering models' : ''),
285
320
  section('providers', providers.length ? Table({
286
321
  headers: ['provider', 'sampler', 'cached models'],
287
322
  rows: providers.map(p => {
@@ -381,11 +416,16 @@ export const config = makePage((ctx) => {
381
416
  if (s.error) return errorState(s.error, load);
382
417
  const cfg = s.cfg || {};
383
418
  const flat = Object.entries(cfg).filter(([, v]) => typeof v !== 'object' || v === null);
419
+ const nested = Object.entries(cfg).filter(([, v]) => typeof v === 'object' && v !== null);
384
420
  const skinList = Array.isArray(s.skins) ? s.skins : (s.skins?.skins || s.skins?.available || []);
385
421
  const activeSkin = cfg.skin || s.skins?.active || '';
386
422
  return [
387
423
  PageHeader({ eyebrow: 'freddie', title: 'config', lede: 'runtime configuration' }),
388
424
  noteAlert(s.note),
425
+ liveRegion(s.busy ? 'saving configuration' : ''),
426
+ nested.length ? h('div', { class: 'ds-alert ds-alert-info', role: 'note' },
427
+ h('span', { class: 'ds-alert-icon' }, 'i'),
428
+ h('div', { class: 'ds-alert-content' }, nested.length + ' nested config ' + (nested.length === 1 ? 'object is' : 'objects are') + ' read-only here (' + nested.map(([k]) => k).join(', ') + ') — edit via the config file or raw view below.')) : null,
389
429
  skinList.length ? section('skin',
390
430
  Select({ label: 'active skin', value: activeSkin, options: skinList, onChange: (v) => setSkin(v) })
391
431
  ) : null,
@@ -463,7 +503,19 @@ export const batch = makePage((ctx) => {
463
503
  TextField({ label: 'prompts (one per line)', value: s.prompts, multiline: true, rows: 6, onInput: (v) => { s.prompts = v; } }),
464
504
  TextField({ label: 'concurrency', type: 'number', value: String(s.concurrency), onInput: (v) => { s.concurrency = v; } }),
465
505
  Btn({ primary: true, disabled: s.busy, children: s.busy ? 'running…' : 'run batch', onClick: run })),
466
- s.result ? section('result', h('pre', { class: 'fd-pre' }, JSON.stringify(s.result, null, 2))) : null,
506
+ s.result ? section('result', (() => {
507
+ const r = s.result;
508
+ const items = Array.isArray(r.results) ? r.results : (Array.isArray(r) ? r : null);
509
+ if (!items) return h('pre', { class: 'fd-pre' }, JSON.stringify(r, null, 2));
510
+ return [
511
+ Kpi({ items: [[items.length, 'prompts'], [items.filter(x => !x.error).length, 'ok'], [items.filter(x => x.error).length, 'errors']] }),
512
+ Table({ headers: ['#', 'prompt', 'status', 'output'], rows: items.map((x, i) => {
513
+ const p = trunc(x.prompt || x.input || '', 50);
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)];
516
+ }) }),
517
+ ];
518
+ })()) : null,
467
519
  ];
468
520
  };
469
521
  });
@@ -55,6 +55,11 @@ html, body {
55
55
  .os-menubar > *, .os-taskbar > * { flex-shrink: 0; }
56
56
  .os-menubar .os-spacer { flex: 1 1 auto; min-width: 0; }
57
57
  .os-menubar .os-tray { margin-left: auto; }
58
+ /* Many open windows must scroll within the bar, not overflow it. Mobile
59
+ already had this; promote it to all widths so a crowded desktop taskbar
60
+ scrolls horizontally instead of pushing buttons off-screen. */
61
+ .os-taskbar { overflow-x: auto; overflow-y: hidden; scrollbar-width: none; }
62
+ .os-taskbar::-webkit-scrollbar { display: none; }
58
63
 
59
64
  .os-brand {
60
65
  color: var(--os-fg);
@@ -738,3 +743,26 @@ html.ds-247420 { touch-action: pan-x pan-y; overscroll-behavior: none; -webkit-t
738
743
  .ds-247420 .tb-sessions-card-actions button:hover { background: color-mix(in oklab, var(--fg, #1a1a1a) 6%, transparent); }
739
744
  .ds-247420 .tb-sessions-card-actions button.danger:hover { background: #d33; color: #fff; border-color: #d33; }
740
745
  .ds-247420 .tb-sessions-empty-mid { padding: 40px; text-align: center; opacity: 0.6; font-size: 13px; }
746
+
747
+ /* ---- prefers-contrast: more — strengthen borders + focus so the translucent
748
+ chrome stays legible under forced/high-contrast user settings. ---- */
749
+ @media (prefers-contrast: more) {
750
+ .ds-247420 .wm-win { border-color: var(--fg, #1a1a1a); }
751
+ .ds-247420 .wm-bar { border-bottom: 1px solid var(--fg, #1a1a1a); }
752
+ .ds-247420 .os-menubar,
753
+ .ds-247420 .os-taskbar { border-color: var(--fg, #1a1a1a); }
754
+ .ds-247420 .tb-sess-chip,
755
+ .ds-247420 .tb-sessions-card,
756
+ .ds-247420 .tb-sessions-btn { border-color: var(--fg, #1a1a1a); }
757
+ .ds-247420 :focus-visible { outline: 2px solid var(--fg, #1a1a1a); outline-offset: 2px; }
758
+ }
759
+
760
+ /* ---- print: a web-OS desktop has no meaningful print form. Suppress the
761
+ live chrome so a Ctrl+P does not waste pages on translucent panels. ---- */
762
+ @media print {
763
+ .os-menubar, .os-taskbar, .wm-snap-preview,
764
+ .tb-sess-overlay, .tb-switching, .wm-switcher { display: none !important; }
765
+ .wm-root, .wm-canvas { position: static !important; transform: none !important; overflow: visible !important; }
766
+ .wm-win { position: static !important; box-shadow: none !important; border: 1px solid #000 !important; page-break-inside: avoid; margin: 0 0 12px; width: auto !important; height: auto !important; }
767
+ .wm-win.wm-min { display: none !important; }
768
+ }
@@ -25,6 +25,12 @@
25
25
  overflow: hidden;
26
26
  }
27
27
  .wm-win.wm-focused { box-shadow: inset 4px 0 0 var(--os-accent); }
28
+ /* Promote to a compositor layer only for the duration of a drag/resize so the
29
+ per-frame left/top/width/height writes don't force a main-thread layout each
30
+ pointermove. Released on pointerup (class removed) to avoid the permanent
31
+ will-change memory/perf anti-pattern. */
32
+ .wm-win.wm-dragging { will-change: left, top; }
33
+ .wm-win.wm-resizing { will-change: width, height; }
28
34
 
29
35
  .wm-bar {
30
36
  display: flex;