anentrypoint-design 0.0.207 → 0.0.209

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.
@@ -211,7 +211,7 @@ export const sessions = makePage((ctx) => {
211
211
  s.msgLoading ? loadingState('loading messages…')
212
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 }))
213
213
  : emptyState('no messages')) : null,
214
- ];
214
+ ].filter(Boolean);
215
215
  };
216
216
  });
217
217
 
@@ -249,13 +249,13 @@ export const projects = makePage((ctx) => {
249
249
  active: p.name === activeName,
250
250
  trailing: h('span', { class: 'fd-row-actions' },
251
251
  p.name !== activeName ? Btn({ children: 'activate', onClick: () => activate(p.name) }) : Chip({ tone: 'ok', children: 'active' }),
252
- p.name !== 'default' ? Btn({ danger: true, children: 'delete', onClick: () => del(p.name) }) : null),
252
+ p.name !== 'default' ? Btn({ variant: 'danger', children: 'delete', onClick: () => del(p.name) }) : null),
253
253
  })) : emptyState('no projects')),
254
254
  section('new project',
255
255
  TextField({ label: 'name', value: s.newName, onInput: (v) => { s.newName = v; }, placeholder: 'my-project' }),
256
256
  TextField({ label: 'path (optional)', value: s.newPath, onInput: (v) => { s.newPath = v; }, placeholder: 'C:/path/to/dir' }),
257
- Btn({ primary: true, disabled: s.busy, children: s.busy ? 'working…' : 'create', onClick: create })),
258
- ];
257
+ Btn({ variant: 'primary', disabled: s.busy, children: s.busy ? 'working…' : 'create', onClick: create })),
258
+ ].filter(Boolean);
259
259
  };
260
260
  });
261
261
 
@@ -274,7 +274,7 @@ export const agents = makePage((ctx) => {
274
274
  s.error && s.data ? refreshError(s.error) : null,
275
275
  Kpi({ items: [[d.count ?? 0, 'active'], [d.turns ?? 0, 'total turns'], [d.last_activity ? fmtAgo(d.last_activity) : '—', 'last activity']] }),
276
276
  section('detail', Table({ headers: ['field', 'value'], rows: Object.entries(d).map(([k, v]) => [k, String(v)]) })),
277
- ];
277
+ ].filter(Boolean);
278
278
  };
279
279
  });
280
280
 
@@ -303,7 +303,7 @@ export const analytics = makePage((ctx) => {
303
303
  s.error && (s.sampler || s.avail) ? refreshError(s.error) : null,
304
304
  Kpi({ items: [[ok + '/' + samp.length, 'providers up'], [sum.total_models ?? '—', 'models'], [sum.usable_in_any_mode ?? '—', 'usable']] }),
305
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')),
306
- ];
306
+ ].filter(Boolean);
307
307
  };
308
308
  });
309
309
 
@@ -337,7 +337,7 @@ export const models = makePage((ctx) => {
337
337
  const cached = s.cached || {};
338
338
  const status = s.sampler?.status || {};
339
339
  return [
340
- PageHeader({ eyebrow: 'freddie', title: 'models', lede: providers.length + ' providers', right: Btn({ primary: true, disabled: s.discovering, children: s.discovering ? 'discovering…' : 'discover', onClick: discover }) }),
340
+ PageHeader({ eyebrow: 'freddie', title: 'models', lede: providers.length + ' providers', right: Btn({ variant: 'primary', disabled: s.discovering, children: s.discovering ? 'discovering…' : 'discover', onClick: discover }) }),
341
341
  liveRegion(s.discovering ? 'discovering models' : ''),
342
342
  section('providers', providers.length ? Table({
343
343
  headers: ['provider', 'sampler', 'cached models'],
@@ -377,12 +377,12 @@ export const cron = makePage((ctx) => {
377
377
  noteAlert(s.note),
378
378
  section('jobs', list.length ? list.map((j, i) => Row({
379
379
  key: i, code: j.enabled ? Icon('play') : Icon('pause'), title: j.cron, sub: trunc(j.prompt, TRUNC_SUB).text,
380
- trailing: Btn({ danger: true, children: 'delete', onClick: () => del(j.id) }),
380
+ trailing: Btn({ variant: 'danger', children: 'delete', onClick: () => del(j.id) }),
381
381
  })) : emptyState('no cron jobs')),
382
382
  section('new job',
383
383
  TextField({ label: 'cron expression', value: s.expr, onInput: (v) => { s.expr = v; }, placeholder: '0 9 * * *' }),
384
384
  TextField({ label: 'prompt', value: s.prompt, multiline: true, onInput: (v) => { s.prompt = v; }, placeholder: 'what to run…' }),
385
- Btn({ primary: true, disabled: s.busy, children: s.busy ? 'working…' : 'add job', onClick: add })),
385
+ Btn({ variant: 'primary', disabled: s.busy, children: s.busy ? 'working…' : 'add job', onClick: add })),
386
386
  ];
387
387
  };
388
388
  });
@@ -405,7 +405,7 @@ export const skills = makePage((ctx) => {
405
405
  onClick: () => ctx.set({ open: s.open === i ? null : i }), active: s.open === i }),
406
406
  s.open === i ? h('pre', { class: 'fd-pre fd-skill-body' }, sk.body || sk.content || '(no body)') : null,
407
407
  )) : emptyState('no skills')),
408
- ];
408
+ ].filter(Boolean);
409
409
  };
410
410
  });
411
411
 
@@ -456,8 +456,8 @@ export const config = makePage((ctx) => {
456
456
  ) : emptyState('no scalar config keys')),
457
457
  section('raw', h('pre', { class: 'fd-pre' }, JSON.stringify(cfg, null, 2))),
458
458
  section('actions',
459
- Btn({ primary: true, disabled: s.busy || !Object.keys(s.edited).length, children: s.busy ? 'saving…' : 'save changes', onClick: save })),
460
- ];
459
+ Btn({ variant: 'primary', disabled: s.busy || !Object.keys(s.edited).length, children: s.busy ? 'saving…' : 'save changes', onClick: save })),
460
+ ].filter(Boolean);
461
461
  };
462
462
  });
463
463
 
@@ -506,11 +506,11 @@ export const env = makePage((ctx) => {
506
506
  trailing: h('span', { class: 'fd-row-actions' },
507
507
  a.set ? Chip({ tone: 'ok', children: 'set' }) : Chip({ tone: 'neutral', children: 'unset' }),
508
508
  TextField({ type: 'password', value: s.draft[a.provider] || '', onInput: (v) => { s.draft[a.provider] = v; }, placeholder: 'paste key', 'aria-label': 'key for ' + a.provider }),
509
- Btn({ primary: true, disabled: s.busy === a.provider, children: s.busy === a.provider ? '…' : 'save', onClick: () => setKey(a.provider) }),
510
- (a.set && a.source === 'stored') ? Btn({ danger: true, disabled: s.busy === a.provider, children: 'remove', onClick: () => removeKey(a.provider) }) : null),
509
+ Btn({ variant: 'primary', disabled: s.busy === a.provider, children: s.busy === a.provider ? '…' : 'save', onClick: () => setKey(a.provider) }),
510
+ (a.set && a.source === 'stored') ? Btn({ variant: 'danger', disabled: s.busy === a.provider, children: 'remove', onClick: () => removeKey(a.provider) }) : null),
511
511
  })) : emptyState('no providers')),
512
512
  otherRows.length ? section('other environment', Table({ headers: ['key', 'status'], rows: otherRows })) : null,
513
- ];
513
+ ].filter(Boolean);
514
514
  };
515
515
  });
516
516
 
@@ -536,7 +536,7 @@ export const tools = makePage((ctx) => {
536
536
  ctx.state.open === t.name ? h('pre', { class: 'fd-pre' }, JSON.stringify(t.schema || t, null, 2)) : null,
537
537
  )))),
538
538
  list.length ? null : emptyState('no tools match'),
539
- ];
539
+ ].filter(Boolean);
540
540
  };
541
541
  });
542
542
 
@@ -560,7 +560,7 @@ export const batch = makePage((ctx) => {
560
560
  section('prompts',
561
561
  TextField({ label: 'prompts (one per line)', value: s.prompts, multiline: true, rows: 6, onInput: (v) => { s.prompts = v; } }),
562
562
  TextField({ label: 'concurrency', type: 'number', min: 1, 'aria-label': 'batch concurrency', value: String(s.concurrency), onInput: (v) => { s.concurrency = v; } }),
563
- Btn({ primary: true, disabled: s.busy, children: s.busy ? 'running…' : 'run batch', onClick: run })),
563
+ Btn({ variant: 'primary', disabled: s.busy, children: s.busy ? 'running…' : 'run batch', onClick: run })),
564
564
  s.result ? section('result', (() => {
565
565
  const r = s.result;
566
566
  const items = Array.isArray(r.results) ? r.results : (Array.isArray(r) ? r : null);
@@ -572,7 +572,7 @@ export const batch = makePage((ctx) => {
572
572
  }) }),
573
573
  ];
574
574
  })()) : null,
575
- ];
575
+ ].filter(Boolean);
576
576
  };
577
577
  });
578
578
 
@@ -592,7 +592,7 @@ export const gateway = makePage((ctx) => {
592
592
  PageHeader({ eyebrow: 'freddie', title: 'gateway', lede: 'messaging platform status' }),
593
593
  s.error && s.data ? refreshError(s.error) : null,
594
594
  section('platforms', rows.length ? Table({ headers: ['platform', 'status'], rows }) : emptyState('no platforms configured')),
595
- ];
595
+ ].filter(Boolean);
596
596
  };
597
597
  });
598
598
 
@@ -632,14 +632,14 @@ export const chains = makePage((ctx) => {
632
632
  noteAlert(s.note),
633
633
  section('chains', Array.isArray(chainsList) && chainsList.length ? chainsList.map((c, i) => Row({
634
634
  key: i, title: c.name || c, sub: Array.isArray(c.links) ? c.links.join(' -> ') : '',
635
- trailing: Btn({ danger: true, children: 'delete', onClick: () => del(c.name || c) }),
635
+ trailing: Btn({ variant: 'danger', children: 'delete', onClick: () => del(c.name || c) }),
636
636
  })) : emptyState('no chains defined')),
637
637
  section('new chain',
638
638
  TextField({ label: 'name', value: s.name, onInput: (v) => { s.name = v; } }),
639
639
  TextField({ label: 'links (comma-separated models)', value: s.links, onInput: (v) => { s.links = v; }, placeholder: 'mistral/large, openrouter/auto' }),
640
- Btn({ primary: true, disabled: s.busy, children: s.busy ? 'working…' : 'create chain', onClick: create })),
640
+ Btn({ variant: 'primary', disabled: s.busy, children: s.busy ? 'working…' : 'create chain', onClick: create })),
641
641
  s.cfg ? section('config', h('pre', { class: 'fd-pre' }, JSON.stringify(s.cfg, null, 2))) : null,
642
- ];
642
+ ].filter(Boolean);
643
643
  };
644
644
  });
645
645
 
@@ -661,7 +661,7 @@ export const machines = makePage((ctx) => {
661
661
  headers: ['kind', 'key', 'state'],
662
662
  rows: list.map(m => [m.kind || '—', m.key || m.machine_id || '—', m.state || m.value || truncJson(m)]),
663
663
  }) : emptyState('no live machines')),
664
- ];
664
+ ].filter(Boolean);
665
665
  };
666
666
  });
667
667
 
@@ -689,7 +689,7 @@ export const health = makePage((ctx) => {
689
689
  s.error && (s.health || s.providers) ? refreshError(s.error) : null,
690
690
  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')),
691
691
  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,
692
- ];
692
+ ].filter(Boolean);
693
693
  };
694
694
  });
695
695
 
@@ -716,7 +716,7 @@ export const debug = makePage((ctx) => {
716
716
  key: i, title: name, onClick: () => loadLogs(name), active: s.sub === name,
717
717
  })) : emptyState('no debug subsystems')),
718
718
  s.sub ? section('logs · ' + s.sub, h('pre', { class: 'fd-pre' }, JSON.stringify(s.logs, null, 2))) : null,
719
- ];
719
+ ].filter(Boolean);
720
720
  };
721
721
  });
722
722
 
@@ -199,11 +199,11 @@ export function SessionCard({ session = {}, onStop, onOpen, onView, active = fal
199
199
  activityBits.length ? h('span', { class: 'ds-dash-activity' }, activityBits.join(' · ')) : null,
200
200
  ].filter(Boolean));
201
201
  const actions = h('div', { class: 'ds-dash-actions', role: 'group', 'aria-label': 'session actions' }, ...[
202
- onOpen ? Btn({ key: 'open', primary: true, 'aria-label': 'open session', onClick: () => onOpen(s),
202
+ onOpen ? Btn({ key: 'open', variant: 'primary', 'aria-label': 'open session', onClick: () => onOpen(s),
203
203
  children: [Icon('external-link', { size: 14 }), h('span', {}, 'open')] }) : null,
204
204
  onView ? Btn({ key: 'view', 'aria-label': s.external ? 'open in history' : 'view events', onClick: () => onView(s),
205
205
  children: [Icon('file-text', { size: 14 }), h('span', {}, s.external ? 'history' : 'events')] }) : null,
206
- (onStop && !s.external) ? Btn({ key: 'stop', danger: true, disabled: !!s.stopping, 'aria-label': 'stop session',
206
+ (onStop && !s.external) ? Btn({ key: 'stop', variant: 'danger', disabled: !!s.stopping, 'aria-label': 'stop session',
207
207
  onClick: () => !s.stopping && onStop(s),
208
208
  children: [Icon('square', { size: 14 }), h('span', {}, s.stopping ? 'stopping…' : 'stop')] }) : null,
209
209
  ].filter(Boolean));
@@ -244,7 +244,7 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
244
244
  sort, filter, errorsOnly = false, onErrorsOnly,
245
245
  selectable = false, selected, onToggleSelect, onSelectAll, onClearSelection,
246
246
  activeSid, streamState,
247
- emptyText = 'No live sessions', offline = false } = {}) {
247
+ emptyText = 'No live sessions', emptyAction, offline = false } = {}) {
248
248
  if (offline) {
249
249
  return h('div', { class: 'ds-dash-state ds-dash-state-error', role: 'status' }, 'Backend offline — live sessions unavailable');
250
250
  }
@@ -297,12 +297,11 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
297
297
  onErrorsOnly ? h('button', { key: 'eo', type: 'button', class: 'ds-dash-errors-toggle' + (errorsOnly ? ' active' : ''),
298
298
  'aria-pressed': errorsOnly ? 'true' : 'false', onclick: () => onErrorsOnly(!errorsOnly) }, 'errors only') : null)
299
299
  : null;
300
- if (!sessions.length) {
301
- return h('div', { class: 'ds-dash' },
302
- h('div', { class: 'ds-dash-header', role: 'group', 'aria-label': 'live session controls' },
303
- ...[h('span', { key: 'cnt', class: 'ds-dash-count', role: 'status', 'aria-live': 'polite' }, '0 running'), streamLine].filter(Boolean)),
304
- h('div', { class: 'ds-dash-state', role: 'status' }, emptyText));
305
- }
300
+ // NOTE: no separate empty-branch return. The empty state renders as a KEYED
301
+ // child of the same stable body wrapper the populated states use - swapping
302
+ // an unkeyed .ds-dash-state for keyed group children used to crash webjsx
303
+ // applyDiff (reading 'key') the moment the first session appeared, leaving a
304
+ // half-applied DOM ('1 running' header over 'No live sessions' body).
306
305
  // Tri-state select-all over the selectable (non-external) sessions.
307
306
  const selectableSids = sessions.filter((s) => !s.external).map((s) => s.sid);
308
307
  const selOfVisible = selectableSids.filter((sid) => selSet.has(sid)).length;
@@ -318,16 +317,16 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
318
317
  ? h('button', { key: 'selclr', type: 'button', class: 'ds-dash-clear', onclick: () => onClearSelection() }, 'clear')
319
318
  : null;
320
319
  const stopBtn = stoppingCount > 0 && (onStopSelected || onStopAll)
321
- ? Btn({ key: 'stopbusy', danger: true, disabled: true, children: 'stopping ' + stoppingCount + '…' })
320
+ ? Btn({ key: 'stopbusy', variant: 'danger', disabled: true, children: 'stopping ' + stoppingCount + '…' })
322
321
  : (selectable && selCount && onStopSelected
323
322
  ? (onArmStopSelected && !confirmingStopSelected
324
- ? Btn({ key: 'stopsel', danger: true, onClick: () => onArmStopSelected([...selSet]), children: 'stop selected' })
325
- : Btn({ key: 'stopsel', danger: true, className: confirmingStopSelected ? 'is-armed' : null, onClick: () => onStopSelected([...selSet]),
323
+ ? Btn({ key: 'stopsel', variant: 'danger', onClick: () => onArmStopSelected([...selSet]), children: 'stop selected' })
324
+ : Btn({ key: 'stopsel', variant: 'danger', className: confirmingStopSelected ? 'is-armed' : null, onClick: () => onStopSelected([...selSet]),
326
325
  children: confirmingStopSelected ? 'stop ' + selCount + ' sessions - press again' : 'stop selected' }))
327
326
  : (onStopAll
328
327
  ? (onArmStopAll && !confirmingStopAll
329
- ? Btn({ key: 'stopall', danger: true, onClick: () => onArmStopAll(sessions), children: 'stop all' })
330
- : Btn({ key: 'stopall', danger: true, className: confirmingStopAll ? 'is-armed' : null, onClick: () => onStopAll(sessions),
328
+ ? Btn({ key: 'stopall', variant: 'danger', onClick: () => onArmStopAll(sessions), children: 'stop all' })
329
+ : Btn({ key: 'stopall', variant: 'danger', className: confirmingStopAll ? 'is-armed' : null, onClick: () => onStopAll(sessions),
331
330
  children: confirmingStopAll ? 'stop ' + sessions.length + ' sessions - press again' : 'stop all' }))
332
331
  : null));
333
332
  // Build header children as a filtered array: webjsx applyDiff crashes
@@ -336,10 +335,14 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
336
335
  const headerKids = [
337
336
  selectable && selCount
338
337
  ? h('span', { key: 'cnt', class: 'ds-dash-count', role: 'status', 'aria-live': 'polite' }, selCount + ' selected')
339
- : (breakdown || h('span', { key: 'cnt', class: 'ds-dash-count', role: 'status', 'aria-live': 'polite' }, sessions.length + ' running')),
338
+ : (breakdown || h('span', { key: 'cnt', class: 'ds-dash-count', role: 'status', 'aria-live': 'polite' },
339
+ sessions.length ? sessions.length + ' running' : '0 running')),
340
340
  selectAllCtl, clearCtl, streamLine,
341
341
  h('span', { key: 'spread', class: 'spread' }),
342
- stopBtn, toolbar,
342
+ // No stop control without a session to stop; the empty dashboard keeps
343
+ // only the count, heartbeat, and (when wired) filter/sort chrome.
344
+ sessions.length ? stopBtn : null,
345
+ toolbar,
343
346
  ].filter(Boolean);
344
347
  const header = h('div', { class: 'ds-dash-header', role: 'group', 'aria-label': 'live session controls' }, ...headerKids);
345
348
  // Status-bucketed command center: when sorting by status (the default), the
@@ -350,20 +353,31 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
350
353
  const cardOf = (s) => h('div', { key: s.sid, role: 'listitem' },
351
354
  SessionCard({ session: s, onStop, onOpen, onView, active: s.sid === activeSid,
352
355
  selectable, selected: selSet.has(s.sid), onToggleSelect }));
353
- let body;
354
- if (grouped) {
356
+ // ONE stable body wrapper across every state (empty / grouped / flat), with
357
+ // KEYED children - the ConversationList stable-keyed-body rule. Diffing
358
+ // happens on the children, never by swapping the container's shape.
359
+ let bodyKids;
360
+ if (!sessions.length) {
361
+ bodyKids = [h('div', { key: 'empty', class: 'ds-dash-state', role: 'status' },
362
+ ...[
363
+ h('span', { key: 'et' }, emptyText),
364
+ (emptyAction && emptyAction.onClick)
365
+ ? Btn({ key: 'ea', onClick: emptyAction.onClick, children: emptyAction.label || 'start a chat' })
366
+ : null,
367
+ ].filter(Boolean))];
368
+ } else if (grouped) {
355
369
  const buckets = [
356
370
  { key: 'error', label: 'Errored', rows: sessions.filter((s) => !s.external && s.status === 'error') },
357
371
  { key: 'running', label: 'Running', rows: sessions.filter((s) => !s.external && s.status !== 'error' && s.status !== 'stale') },
358
372
  { key: 'idle', label: 'Idle', rows: sessions.filter((s) => !s.external && s.status === 'stale') },
359
373
  { key: 'external', label: 'External', rows: sessions.filter((s) => s.external) },
360
374
  ].filter((b) => b.rows.length);
361
- body = h('div', { class: 'ds-dash-groups' },
362
- ...buckets.map((b) => h('div', { key: 'grp' + b.key, class: 'ds-dash-group', role: 'group', 'aria-label': b.label + ' sessions' },
363
- h('div', { class: 'ds-dash-group-label' }, b.label + ' · ' + b.rows.length),
364
- h('div', { class: 'ds-dash-grid', role: 'list', 'aria-label': b.label + ' sessions' }, ...b.rows.map(cardOf)))));
375
+ bodyKids = buckets.map((b) => h('div', { key: 'grp' + b.key, class: 'ds-dash-group', role: 'group', 'aria-label': b.label + ' sessions' },
376
+ h('div', { key: 'gl', class: 'ds-dash-group-label' }, b.label + ' · ' + b.rows.length),
377
+ h('div', { key: 'gg', class: 'ds-dash-grid', role: 'list', 'aria-label': b.label + ' sessions' }, ...b.rows.map(cardOf))));
365
378
  } else {
366
- body = h('div', { class: 'ds-dash-grid', role: 'list', 'aria-label': 'live sessions' }, ...sessions.map(cardOf));
379
+ bodyKids = [h('div', { key: 'flat', class: 'ds-dash-grid', role: 'list', 'aria-label': 'live sessions' }, ...sessions.map(cardOf))];
367
380
  }
381
+ const body = h('div', { key: 'body', class: 'ds-dash-groups' }, ...bodyKids);
368
382
  return h('div', { class: 'ds-dash' }, header, body);
369
383
  }
@@ -15,7 +15,7 @@ export function Chip({ tone = '', children }) {
15
15
  return h('span', { class: 'chip' + (tone ? ' tone-' + tone : '') }, children);
16
16
  }
17
17
 
18
- export function Btn({ href, variant = 'default', children, onClick, 'aria-label': ariaLabel, primary, ghost, danger, disabled, className }) {
18
+ export function Btn({ href, variant = 'default', children, onClick, 'aria-label': ariaLabel, primary, ghost, danger, disabled, className, key }) {
19
19
  // Support legacy primary/ghost props for backward compatibility, but prefer variant
20
20
  const resolvedVariant = variant !== 'default' ? variant : (primary ? 'primary' : (ghost ? 'ghost' : (danger ? 'danger' : 'default')));
21
21
  const cls = (resolvedVariant === 'primary' ? 'btn-primary' : (resolvedVariant === 'ghost' ? 'btn-ghost' : (resolvedVariant === 'danger' ? 'btn-primary danger' : 'btn')))
@@ -37,6 +37,7 @@ export function Btn({ href, variant = 'default', children, onClick, 'aria-label'
37
37
  const isLink = href != null && href !== '' && href !== '#';
38
38
  if (isLink) {
39
39
  return h('a', {
40
+ key,
40
41
  class: cls, href,
41
42
  'aria-label': ariaName,
42
43
  'aria-disabled': disabled ? 'true' : null,
@@ -45,6 +46,7 @@ export function Btn({ href, variant = 'default', children, onClick, 'aria-label'
45
46
  }, ...kids);
46
47
  }
47
48
  return h('button', {
49
+ key,
48
50
  type: 'button', class: cls,
49
51
  disabled: disabled ? true : null,
50
52
  'aria-label': ariaName,
@@ -159,6 +161,9 @@ const ICON_PATHS = {
159
161
  // Icon(); use innerHTML = iconMarkup(name). Keeps the icon paths upstream so
160
162
  // raw-DOM call sites never reintroduce decorative glyph literals.
161
163
  export function iconMarkup(name, { size = 16 } = {}) {
164
+ // Accept the props-object shape too - every sibling component takes a
165
+ // single object, so Icon({name}) is what the barrel trains consumers to try.
166
+ if (name && typeof name === 'object') ({ name, size = 16 } = name);
162
167
  const inner = ICON_PATHS[name];
163
168
  if (!inner) return '';
164
169
  return '<svg class="ds-icon ds-icon-' + name + '" width="' + size + '" height="' + size +
@@ -166,6 +171,7 @@ export function iconMarkup(name, { size = 16 } = {}) {
166
171
  ' stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + inner + '</svg>';
167
172
  }
168
173
  export function Icon(name, { size = 16 } = {}) {
174
+ if (name && typeof name === 'object') ({ name, size = 16 } = name);
169
175
  const inner = ICON_PATHS[name];
170
176
  if (!inner) return h('span', { class: 'glyph', 'aria-hidden': 'true' }, '');
171
177
  return h('svg', {
@@ -34,19 +34,26 @@ export function ThemeToggle({ compact = false, onChange } = {}) {
34
34
  const current = getTheme();
35
35
 
36
36
  if (compact) {
37
- const resolved = resolvedTheme();
38
- const label = current === 'auto' ? `auto (${resolved})` : (current === 'ink' ? 'dark' : 'light');
37
+ // Plain words only - 'ink'/'paper' are internal theme codenames a user
38
+ // never chose; the resolved scheme rides in the title, not the label.
39
+ const resolvedWord = resolvedTheme() === 'ink' ? 'dark' : 'light';
40
+ const word = current === 'auto' ? 'auto' : (current === 'ink' ? 'dark' : 'light');
41
+ const label = 'theme: ' + word;
39
42
  return h('button', {
40
43
  class: 'btn ds-theme-toggle',
41
44
  type: 'button',
42
- 'aria-label': 'theme: ' + label,
43
- title: 'theme: ' + label + ' — click to cycle',
45
+ 'aria-label': label,
46
+ title: label + (current === 'auto' ? ' (currently ' + resolvedWord + ')' : '') + ' — click to cycle',
44
47
  onclick: () => {
45
48
  const next = current === 'auto' ? 'paper' : (current === 'paper' ? 'ink' : 'auto');
46
49
  applyTheme(next);
47
50
  if (onChange) try { onChange(next); } catch {}
48
51
  }
49
- }, label);
52
+ },
53
+ // CSS-drawn half-disc so the control still reads as the theme switch
54
+ // when the label is hidden (icon-only rail strip).
55
+ h('span', { class: 'ds-theme-disc', 'aria-hidden': 'true' }),
56
+ h('span', { class: 'ds-theme-toggle-label' }, label));
50
57
  }
51
58
 
52
59
  return h('div', {
package/src/components.js CHANGED
@@ -36,7 +36,7 @@ export { ContextPane } from './components/context-pane.js';
36
36
  export {
37
37
  fileGlyph, fmtFileSize,
38
38
  FileIcon, FileRow, FileGrid, FileSkeleton, sortFiles, FileToolbar, RootsPicker,
39
- DropZone, UploadProgress, EmptyState, BreadcrumbPath
39
+ DropZone, UploadProgress, EmptyState, BreadcrumbPath, BulkBar
40
40
  } from './components/files.js';
41
41
 
42
42
  export {