anentrypoint-design 0.0.438 → 0.0.440

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.
Files changed (60) hide show
  1. package/dist/247420.css +78 -23
  2. package/dist/247420.js +23 -23
  3. package/dist/colors_and_type.css +86 -42
  4. package/dist/preview/buttons.html +5 -2
  5. package/dist/preview/colors-core.html +4 -1
  6. package/dist/preview/colors-lore.html +4 -1
  7. package/dist/preview/colors-semantic.html +4 -1
  8. package/dist/preview/dateline.html +4 -1
  9. package/dist/preview/dropzone.html +4 -1
  10. package/dist/preview/file-grid.html +4 -1
  11. package/dist/preview/file-row.html +4 -1
  12. package/dist/preview/file-toolbar.html +4 -1
  13. package/dist/preview/file-viewer.html +7 -1
  14. package/dist/preview/header.html +5 -2
  15. package/dist/preview/icons-unicode.html +26 -20
  16. package/dist/preview/index-row.html +4 -1
  17. package/dist/preview/inputs.html +4 -1
  18. package/dist/preview/manifesto.html +4 -1
  19. package/dist/preview/rules.html +4 -1
  20. package/dist/preview/spacing.html +17 -12
  21. package/dist/preview/stamps-lore.html +4 -1
  22. package/dist/preview/stamps.html +4 -1
  23. package/dist/preview/theme-ink.html +4 -1
  24. package/dist/preview/type-display.html +4 -1
  25. package/dist/preview/type-mono.html +4 -1
  26. package/dist/preview/type-prose.html +5 -2
  27. package/dist/preview/type-scale.html +40 -15
  28. package/dist/preview/wordmarks.html +18 -22
  29. package/dist/src/components/content.js +2 -1
  30. package/dist/src/components.js +21 -5
  31. package/dist/src/kits/os/theme.css +38 -5
  32. package/dist/ui_kits/aicat/app.js +1 -1
  33. package/dist/ui_kits/gallery/app.js +27 -7
  34. package/dist/ui_kits/signin/app.js +51 -10
  35. package/dist/ui_kits/system_primer/app.js +189 -3
  36. package/dist/ui_kits/terminal/app.js +56 -10
  37. package/package.json +1 -1
  38. package/src/components/content/cli.js +7 -1
  39. package/src/components/content/panel.js +7 -2
  40. package/src/components/shell/app-shell.js +7 -2
  41. package/src/components/shell/icons.js +4 -1
  42. package/src/css/app-shell/kits-appended.css +58 -21
  43. package/src/css/app-shell/panel-row.css +14 -0
  44. package/src/css/app-shell/responsive2-workspace.css +5 -1
  45. package/src/css/app-shell/topbar.css +1 -1
  46. package/src/kits/os/files-app.js +9 -2
  47. package/src/kits/os/icons.js +5 -0
  48. package/src/kits/os/launcher.css +7 -2
  49. package/src/kits/os/launcher.js +27 -3
  50. package/src/kits/os/monitor-app.js +8 -2
  51. package/src/kits/os/shell.js +55 -12
  52. package/src/kits/os/theme.css +20 -2
  53. package/src/kits/os/wm.js +41 -11
  54. package/types/components.d.ts +2 -0
  55. package/dist/slides/deck-stage-overlay.js +0 -63
  56. package/dist/slides/deck-stage-state.js +0 -81
  57. package/dist/slides/deck-stage-style.js +0 -117
  58. package/dist/slides/deck-stage.js +0 -159
  59. package/dist/src/kits/os/freddie/helpers.js +0 -59
  60. package/dist/src/kits/os/validator-app.js +0 -55
@@ -1,5 +1,5 @@
1
1
  import * as webjsx from 'webjsx';
2
- import { Topbar, Crumb, Status, Side, AppShell, Panel, PageHeader, Chip, Btn, ThemeToggle } from 'ds/components.js';
2
+ import { Topbar, Crumb, Status, Side, AppShell, Panel, PageHeader, Chip, Btn, ThemeToggle, Slider, Progress, InputOTP, HoverCard, DatePicker, formatDate, DateRangePicker, Carousel, Menubar, AspectRatio, LiveCursorOverlay, RemoteSelectionRings, RecentEditHighlightFlash, AgentPresenceChip, PresenceBar, ContextMeter, ContextTreemap, ContextXRayPanel } from 'ds/components.js';
3
3
  import { mountKit } from 'ds/bootstrap.js';
4
4
  const h = webjsx.createElement;
5
5
 
@@ -94,6 +94,50 @@ function TypeScalePanel() {
94
94
  // click, on the page whose whole job is showing how controls behave.
95
95
  const primState = { pressed: null };
96
96
 
97
+ // Same interactive-demonstration standard as primState above, for the 2026
98
+ // restyle's new primitives (Calendar/DatePicker/Slider/InputOTP/HoverCard):
99
+ // each control is genuinely wired to state and re-renders on change, not a
100
+ // static screenshot of the component.
101
+ const restyleState = {
102
+ sliderValue: 40,
103
+ otpValue: '',
104
+ otpComplete: null,
105
+ datePickerValue: null,
106
+ datePickerOpen: false,
107
+ hoverCardOpen: false,
108
+ };
109
+
110
+ // The remaining backfilled primitives. Same live-wired standard: every control
111
+ // that looks interactive owns real state and re-renders on change. The three
112
+ // collab-ui overlays and the context-pane trio are DATA-driven rather than
113
+ // input-driven, so their specimens supply representative fixture data — the
114
+ // honest demonstration for a component whose input is a remote peer's cursor
115
+ // position or a token accounting breakdown, neither of which a primer page can
116
+ // manufacture by being clicked.
117
+ const moreState = {
118
+ rangeValue: { from: null, to: null },
119
+ rangeOpen: false,
120
+ menuOpenIndex: null,
121
+ menuPicked: null,
122
+ xrayOpenId: null,
123
+ };
124
+
125
+ const COLLAB_USERS = [
126
+ { userId: 'u1', label: 'ana', color: 'var(--purple)', status: 'active' },
127
+ { userId: 'u2', label: 'blake', color: 'var(--green)', status: 'idle' },
128
+ { userId: 'u3', label: 'agent', color: 'var(--mascot)', status: 'active' },
129
+ ];
130
+
131
+ // `value` (not `tokens`) and a `tone` class suffix are the real field names
132
+ // ContextMeter/ContextTreemap/ContextXRayPanel all read — a `tokens` key renders
133
+ // zero-width bars and an empty treemap rather than erroring, so it has to match.
134
+ const CONTEXT_SEGMENTS = [
135
+ { id: 'sys', label: 'system', value: 1840, tone: 'system' },
136
+ { id: 'files', label: 'files', value: 7320, tone: 'files' },
137
+ { id: 'chat', label: 'chat', value: 4210, tone: 'chat' },
138
+ { id: 'tools', label: 'tool defs', value: 1130, tone: 'other' },
139
+ ];
140
+
97
141
  function PrimitivesPanel() {
98
142
  return Panel({ id: 'primitives', title: 'primitives', class: 'ds-panel-gap', children:
99
143
  h('div', { class: 'ds-prim-panel' },
@@ -120,6 +164,144 @@ function PrimitivesPanel() {
120
164
  });
121
165
  }
122
166
 
167
+ // New primitives added in the 2026 webjsx-toolkit-look restyle. Live-wired
168
+ // (not static screenshots), same standard as PrimitivesPanel above.
169
+ function RestylePanel() {
170
+ return Panel({ id: 'restyle', title: 'restyle 2026 — new primitives', class: 'ds-panel-gap', children:
171
+ h('div', { class: 'ds-prim-panel' },
172
+ h('div', { class: 'ds-prim-row' },
173
+ h('span', { class: 'ds-prim-label' }, 'slider'),
174
+ Slider({
175
+ value: restyleState.sliderValue, min: 0, max: 100,
176
+ onChange: (v) => { restyleState.sliderValue = v; kit.render(); },
177
+ label: 'demo slider',
178
+ }),
179
+ h('span', { class: 'ds-prim-label' }, String(restyleState.sliderValue))
180
+ ),
181
+ h('div', { class: 'ds-prim-row' },
182
+ h('span', { class: 'ds-prim-label' }, 'progress'),
183
+ Progress({ value: restyleState.sliderValue, max: 100, label: 'demo progress' })
184
+ ),
185
+ h('div', { class: 'ds-prim-row' },
186
+ h('span', { class: 'ds-prim-label' }, 'otp input'),
187
+ InputOTP({
188
+ length: 6, value: restyleState.otpValue,
189
+ onChange: (v) => { restyleState.otpValue = v; kit.render(); },
190
+ onComplete: (v) => { restyleState.otpComplete = v; kit.render(); },
191
+ label: 'demo code',
192
+ }),
193
+ h('span', { class: 'ds-prim-label' },
194
+ restyleState.otpComplete ? 'complete: ' + restyleState.otpComplete : 'type 6 digits')
195
+ ),
196
+ h('div', { class: 'ds-prim-row' },
197
+ h('span', { class: 'ds-prim-label' }, 'hover card'),
198
+ HoverCard({
199
+ trigger: Btn({ children: 'hover me' }),
200
+ content: h('div', {}, 'HoverCard content — composes Popover with a delayed hover trigger.'),
201
+ open: restyleState.hoverCardOpen,
202
+ onOpenChange: (v) => { restyleState.hoverCardOpen = v; kit.render(); },
203
+ })
204
+ ),
205
+ h('div', { class: 'ds-prim-row' },
206
+ h('span', { class: 'ds-prim-label' }, 'date picker'),
207
+ DatePicker({
208
+ value: restyleState.datePickerValue,
209
+ onChange: (v) => { restyleState.datePickerValue = v; kit.render(); },
210
+ open: restyleState.datePickerOpen,
211
+ onOpenChange: (v) => { restyleState.datePickerOpen = v; kit.render(); },
212
+ placeholder: 'pick a date',
213
+ }),
214
+ h('span', { class: 'ds-prim-label' },
215
+ restyleState.datePickerValue ? formatDate(restyleState.datePickerValue) : 'none selected')
216
+ )
217
+ )
218
+ });
219
+ }
220
+
221
+ // The rest of the backfilled surface: range picking, the two overlay
222
+ // compositions, the aspect-ratio wrapper, the collab-ui overlays and the
223
+ // context-pane trio. Split from RestylePanel rather than appended to it to keep
224
+ // each function a single readable specimen group.
225
+ function BackfillPanel() {
226
+ const totalTokens = CONTEXT_SEGMENTS.reduce((n, s) => n + s.value, 0);
227
+ return Panel({ id: 'backfill', title: 'backfill — range, overlays, collab, context', class: 'ds-panel-gap', children:
228
+ h('div', { class: 'ds-prim-panel' },
229
+ h('div', { class: 'ds-prim-row' },
230
+ h('span', { class: 'ds-prim-label' }, 'date range'),
231
+ DateRangePicker({
232
+ value: moreState.rangeValue,
233
+ onChange: (v) => { moreState.rangeValue = v; kit.render(); },
234
+ open: moreState.rangeOpen,
235
+ onOpenChange: (v) => { moreState.rangeOpen = v; kit.render(); },
236
+ placeholder: 'pick a range',
237
+ name: 'primer-drp',
238
+ }),
239
+ h('span', { class: 'ds-prim-label' },
240
+ moreState.rangeValue.from
241
+ ? formatDate(moreState.rangeValue.from) + ' -> ' + (moreState.rangeValue.to ? formatDate(moreState.rangeValue.to) : '...')
242
+ : 'none selected')
243
+ ),
244
+ h('div', { class: 'ds-prim-row' },
245
+ h('span', { class: 'ds-prim-label' }, 'menubar'),
246
+ Menubar({
247
+ openIndex: moreState.menuOpenIndex,
248
+ onOpenIndexChange: (i) => { moreState.menuOpenIndex = i; kit.render(); },
249
+ ariaLabel: 'primer demo menubar',
250
+ menus: [
251
+ { id: 'file', label: 'file', items: [{ id: 'new', label: 'new' }, { id: 'open', label: 'open' }], onSelect: (id) => { moreState.menuPicked = 'file/' + id; kit.render(); } },
252
+ { id: 'edit', label: 'edit', items: [{ id: 'copy', label: 'copy' }, { separator: true }, { id: 'del', label: 'delete', danger: true }], onSelect: (id) => { moreState.menuPicked = 'edit/' + id; kit.render(); } },
253
+ ],
254
+ }),
255
+ h('span', { class: 'ds-prim-label' },
256
+ moreState.menuPicked ? 'chose: ' + moreState.menuPicked : 'nothing chosen yet')
257
+ ),
258
+ h('div', { class: 'ds-prim-row' },
259
+ h('span', { class: 'ds-prim-label' }, 'carousel'),
260
+ Carousel({
261
+ label: 'primer demo carousel',
262
+ items: ['one', 'two', 'three', 'four'],
263
+ renderItem: (item) => h('div', { class: 'ds-prim-label' }, item),
264
+ })
265
+ ),
266
+ h('div', { class: 'ds-prim-row' },
267
+ h('span', { class: 'ds-prim-label' }, 'aspect ratio'),
268
+ AspectRatio({ ratio: '16 / 9', children: h('div', { class: 'ds-prim-label' }, '16 / 9') })
269
+ ),
270
+ h('div', { class: 'ds-prim-row' },
271
+ h('span', { class: 'ds-prim-label' }, 'presence'),
272
+ PresenceBar({ users: COLLAB_USERS }),
273
+ AgentPresenceChip({ userId: 'u9', label: 'solo agent', color: 'var(--purple-2)', status: 'active' })
274
+ ),
275
+ h('div', { class: 'ds-prim-row' },
276
+ h('span', { class: 'ds-prim-label' }, 'collab overlays'),
277
+ // LiveCursorOverlay takes flat x/y, but the ring and flash
278
+ // overlays read a NESTED `rect` ({top,left,width,height}) --
279
+ // passing flat coords there throws on `s.rect.left`, taking the
280
+ // whole kit's mount down rather than just blanking one specimen.
281
+ LiveCursorOverlay({ cursors: [{ userId: 'u1', label: 'ana', color: 'var(--purple)', x: 40, y: 18 }, { userId: 'u3', label: 'agent', color: 'var(--mascot)', x: 120, y: 44 }] }),
282
+ RemoteSelectionRings({ selections: [{ userId: 'u2', color: 'var(--green)', rect: { left: 20, top: 12, width: 90, height: 18 } }] }),
283
+ RecentEditHighlightFlash({ edits: [{ timestamp: 1, color: 'var(--purple)', rect: { left: 12, top: 60, width: 70, height: 16 } }] })
284
+ ),
285
+ h('div', { class: 'ds-prim-row' },
286
+ h('span', { class: 'ds-prim-label' }, 'context meter'),
287
+ ContextMeter({ used: totalTokens, total: 32000, segments: CONTEXT_SEGMENTS })
288
+ ),
289
+ h('div', { class: 'ds-prim-row' },
290
+ h('span', { class: 'ds-prim-label' }, 'context treemap'),
291
+ ContextTreemap({ items: CONTEXT_SEGMENTS, width: 280, height: 160 })
292
+ ),
293
+ h('div', { class: 'ds-prim-row' },
294
+ h('span', { class: 'ds-prim-label' }, 'context x-ray'),
295
+ ContextXRayPanel({
296
+ segments: CONTEXT_SEGMENTS,
297
+ openId: moreState.xrayOpenId,
298
+ onOpenIdChange: (id) => { moreState.xrayOpenId = id; kit.render(); },
299
+ })
300
+ )
301
+ )
302
+ });
303
+ }
304
+
123
305
  function App() {
124
306
  return AppShell({
125
307
  topbar: Topbar({
@@ -137,7 +319,9 @@ function App() {
137
319
  { glyph: '-', label: 'palette', key: 'p', href: '#palette' },
138
320
  { glyph: '-', label: 'semantic', key: 's', href: '#semantic' },
139
321
  { glyph: '-', label: 'type scale', key: 't', href: '#type-scale' },
140
- { glyph: '-', label: 'primitives', key: 'r', href: '#primitives' }
322
+ { glyph: '-', label: 'primitives', key: 'r', href: '#primitives' },
323
+ { glyph: '-', label: 'restyle 2026', key: 'x', href: '#restyle' },
324
+ { glyph: '-', label: 'backfill', key: 'b', href: '#backfill' }
141
325
  ] }
142
326
  ]
143
327
  }),
@@ -156,7 +340,9 @@ function App() {
156
340
  PaletteGrid(),
157
341
  SemanticGrid(),
158
342
  TypeScalePanel(),
159
- PrimitivesPanel()
343
+ PrimitivesPanel(),
344
+ RestylePanel(),
345
+ BackfillPanel()
160
346
  )
161
347
  ],
162
348
  status: Status({
@@ -1,6 +1,7 @@
1
1
  import * as webjsx from 'webjsx';
2
2
  import { Topbar, Crumb, Status, Side, AppShell, Panel, Heading, Lede, Chip, ThemeToggle } from 'ds/components.js';
3
3
  import { mountKit } from 'ds/bootstrap.js';
4
+ import { run as runCommand, complete as completeLine } from 'ds/shell.js';
4
5
  const h = webjsx.createElement;
5
6
 
6
7
  const root = document.getElementById('root');
@@ -8,10 +9,15 @@ const root = document.getElementById('root');
8
9
  // Live terminal — instant output, no fake reveal animation. This is the
9
10
  // usable surface; anything typed appears immediately, anything emitted by
10
11
  // the (stubbed) backend appears immediately.
12
+ // Seeded by really running the commands through the interpreter at load, not
13
+ // by hand-writing their output. A canned transcript would go stale the moment
14
+ // the filesystem or a command changed, and it would show a shell that cannot
15
+ // be told apart from one that executes nothing -- which is exactly what this
16
+ // kit used to be.
11
17
  const liveTranscript = [
12
- { kind: 'cmt', text: '# live session · ~/dev/design' },
13
- { kind: 'cmd', text: 'echo "ready"' },
14
- { kind: 'out', text: 'ready' }
18
+ // No leading '#' in the text: Line() renders the cmt prompt mark itself,
19
+ // so writing one here produces a doubled '# #' on screen.
20
+ { kind: 'cmt', text: 'live shell — really executes. type `help` for commands.' },
15
21
  ];
16
22
  const live = { input: '', cwd: '~/dev/design', phase: 'ready' };
17
23
  const PHASES = ['ready', 'loading', 'empty', 'error'];
@@ -76,6 +82,28 @@ const demo = { visible: reduced ? demoScript.slice() : [], looping: !reduced };
76
82
  const history = [];
77
83
  let historyIdx = -1;
78
84
 
85
+ // The interpreter's session state. cwd is an array of path segments owned here
86
+ // and mutated in place by `cd`, so the prompt and the shell never disagree
87
+ // about where the session is.
88
+ const shellCwd = [];
89
+ const shellPath = () => '~/' + shellCwd.join('/');
90
+ const shellCtx = {
91
+ cwd: shellCwd,
92
+ clear: () => clearScrollback(),
93
+ // `theme dark` drives the same data-theme attribute ThemeToggle writes, so
94
+ // the command and the toggle stay one mechanism rather than two.
95
+ setTheme: (t) => document.documentElement.setAttribute('data-theme', t),
96
+ };
97
+
98
+ // Run a line for real and append both the command and its output, so the
99
+ // opening scrollback is produced by the same path a typed command takes.
100
+ function seed(line) {
101
+ liveTranscript.push({ kind: 'cmd', text: line });
102
+ for (const out of runCommand(line, shellCtx)) liveTranscript.push(out);
103
+ }
104
+ seed('whoami');
105
+ seed('ls');
106
+
79
107
  // Empties the scrollback and drops the shell into its own empty state, which is
80
108
  // the honest reading of a cleared shell (the empty panel explains what lands
81
109
  // there). Both the sidebar item and Cmd/Ctrl+K call this.
@@ -149,7 +177,10 @@ function App() {
149
177
  // rather than documenting a keystroke and doing nothing when
150
178
  // clicked. Both are also bound to the keys they advertise.
151
179
  { group: 'shortcuts', items: [
152
- { glyph: '·', label: 'clear (⌘k)', key: 'c',
180
+ // The handler binds metaKey OR ctrlKey, so the label names
181
+ // both. The Command symbol is also the one glyph the mono
182
+ // stack has no coverage for -- it rendered as tofu here.
183
+ { glyph: '·', label: 'clear (ctrl/cmd k)', key: 'c',
153
184
  onClick: (e) => { e.preventDefault(); clearScrollback(); } },
154
185
  { glyph: '·', label: 'history (up)', key: 'h',
155
186
  onClick: (e) => { e.preventDefault(); recallHistory(); } }
@@ -162,7 +193,7 @@ function App() {
162
193
  h('div', {}, Heading({ level: 1, children: 'terminal' })),
163
194
  ThemeToggle()
164
195
  ),
165
- Lede({ children: 'two surfaces — a live shell (instant, no fake reveal) and a decorative demo loop that plays the .cli row primitives. respects prefers-reduced-motion.' }),
196
+ Lede({ children: 'a working shell ls, cd, cat, echo, theme and more run against a real in-memory tree, with tab completion and history. below it, a decorative demo loop plays the .cli row primitives. respects prefers-reduced-motion.' }),
166
197
 
167
198
  // Live terminal — usable, no reveal delays.
168
199
  Panel({
@@ -179,17 +210,31 @@ function App() {
179
210
  h('span', { class: 'prompt' }, '$'),
180
211
  h('input', {
181
212
  value: live.input,
182
- placeholder: 'type a command and press enter…',
213
+ placeholder: 'try `help`, `ls`, `cat readme.md`…',
183
214
  class: 'ds-term-input',
184
215
  oninput: (e) => { live.input = e.target.value; },
185
216
  onkeydown: (e) => {
186
217
  if (e.key === 'Enter' && live.input.trim()) {
187
- liveTranscript.push({ kind: 'cmd', text: live.input });
188
- liveTranscript.push({ kind: 'out', text: '(stub) ran: ' + live.input });
189
- history.push(live.input);
218
+ const line = live.input;
219
+ liveTranscript.push({ kind: 'cmd', text: line });
220
+ // Real execution: the interpreter in
221
+ // ds/shell.js owns the filesystem and the
222
+ // command table, so an unknown command
223
+ // reports a genuine error instead of the
224
+ // old '(stub) ran: ...' echo that claimed
225
+ // success for anything typed.
226
+ for (const out of runCommand(line, shellCtx)) liveTranscript.push(out);
227
+ live.cwd = shellPath();
228
+ history.push(line);
190
229
  historyIdx = -1;
191
230
  live.input = '';
192
231
  kit.render();
232
+ } else if (e.key === 'Tab') {
233
+ // Completion has to pre-empt the browser's
234
+ // focus move, so preventDefault comes first.
235
+ e.preventDefault();
236
+ live.input = completeLine(live.input, shellCwd);
237
+ kit.render();
193
238
  } else if (e.key === 'ArrowUp') {
194
239
  // The sidebar advertises 'history (up)';
195
240
  // the key it names has to do it too.
@@ -216,7 +261,8 @@ function App() {
216
261
  Panel({ title: 'about this kit', class: 'ds-panel-gap', children: h('div', { class: 'ds-pattern-notes' },
217
262
  h('p', {}, '· ', Chip({ tone: 'accent', children: '.cli' }), ' rows pair ', h('code', {}, '.prompt'), ' + ', h('code', {}, '.cmd'), '.'),
218
263
  h('p', {}, '· six line kinds: ', Chip({ tone: 'dim', children: 'cmt' }), ' ', Chip({ tone: 'dim', children: 'cmd' }), ' ', Chip({ tone: 'dim', children: 'out' }), ' ', Chip({ tone: 'accent', children: 'ok' }), ' ', Chip({ tone: '', children: 'warn' }), ' ', Chip({ tone: 'dim', children: 'log' }), '.'),
219
- h('p', {}, '· live panel is instant; demo panel reveals lines on a loop for the showcase only never fake-animate output a user is waiting on.')
264
+ h('p', {}, '· live panel is instant and really executes ', h('code', {}, 'help'), ' lists the commands, tab completes, up walks history.'),
265
+ h('p', {}, '· demo panel reveals lines on a loop for the showcase only — never fake-animate output a user is waiting on.')
220
266
  ) })
221
267
  )
222
268
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anentrypoint-design",
3
- "version": "0.0.438",
3
+ "version": "0.0.440",
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",
@@ -5,13 +5,19 @@
5
5
 
6
6
  import * as webjsx from '../../../vendor/webjsx/index.js';
7
7
  import { Panel } from './panel.js';
8
+ import { Btn } from '../shell.js';
8
9
  const h = webjsx.createElement;
9
10
 
10
11
  export function Install({ cmd, copied, onCopy }) {
11
12
  return h('div', { class: 'cli' },
12
13
  h('span', { class: 'prompt' }, '$'),
13
14
  h('span', { class: 'cmd' }, cmd),
14
- h('span', { class: 'copy', onclick: () => onCopy && onCopy(cmd) }, copied ? 'copied' : 'copy')
15
+ Btn({
16
+ class: 'copy', size: 'sm',
17
+ onClick: () => onCopy && onCopy(cmd),
18
+ 'aria-label': copied ? 'copied to clipboard' : 'copy install command',
19
+ children: h('span', { 'aria-live': 'polite' }, copied ? 'copied' : 'copy')
20
+ })
15
21
  );
16
22
  }
17
23
 
@@ -7,11 +7,16 @@ import * as webjsx from '../../../vendor/webjsx/index.js';
7
7
  import { RowLink } from './row.js';
8
8
  const h = webjsx.createElement;
9
9
 
10
- export function Panel({ title, count, right, style = '', class: className = '', children, kind, id }) {
10
+ export function Panel({ title, count, right, style = '', class: className = '', children, kind, id, headingLevel = 2 }) {
11
11
  const cls = 'panel' + (kind ? ' panel-' + kind : '') + (className ? ' ' + className : '');
12
+ // title renders as a real heading (h2 by default; `headingLevel` lets a
13
+ // caller nest a Panel under an existing h2/h3 without skipping a level) so
14
+ // a screen-reader heading-jump can actually find each section, not just
15
+ // the page's single top-level h1.
16
+ const headingTag = 'h' + headingLevel;
12
17
  return h('div', { class: cls, style, ...(id ? { id } : {}) },
13
18
  title != null ? h('div', { class: 'panel-head' },
14
- h('span', {}, title),
19
+ h(headingTag, { class: 'panel-title' }, title),
15
20
  right != null ? right : (count != null ? h('span', {}, String(count)) : null)
16
21
  ) : null,
17
22
  h('div', { class: 'panel-body' }, ...(Array.isArray(children) ? children : [children]))
@@ -93,7 +93,7 @@ export function Side({ sections = [] } = {}) {
93
93
  return h('div', { class: 'app-side-group', key: sec.group, role: 'group', 'aria-labelledby': groupId },
94
94
  h('h2', { class: 'group', id: groupId }, sec.group),
95
95
  ...sec.items.map((item, i) => {
96
- const { glyph, label, href, active, count, color, onClick } = item;
96
+ const { glyph, label, href, active, count, color, onClick, ariaLabel } = item;
97
97
  const countLabel = (count != null && count !== 0 && count !== '0') ? ` (${count})` : '';
98
98
  // An item with neither href nor onClick is not a control, and
99
99
  // must not look like one. href used to default to '#', so a
@@ -118,7 +118,12 @@ export function Side({ sections = [] } = {}) {
118
118
  ...(isControl ? { href: href != null ? href : '#' } : {}),
119
119
  class: active ? 'active' : '',
120
120
  'aria-current': active ? 'page' : null,
121
- 'aria-label': label + countLabel,
121
+ // ariaLabel lets a caller pass the untruncated source string
122
+ // as the accessible name when `label` itself is a shortened
123
+ // display string (e.g. a preset prompt cut to fit the row) —
124
+ // otherwise screen-reader users lose the exact same
125
+ // distinguishing text sighted users lose to the ellipsis.
126
+ 'aria-label': (ariaLabel != null ? ariaLabel : label) + countLabel,
122
127
  onclick: onClick,
123
128
  onkeydown: isControl ? onSideLinkKeyDown : null
124
129
  },
@@ -89,7 +89,10 @@ export const ICON_PATHS = {
89
89
  clipboard: '<rect x="8" y="4" width="8" height="4" rx="1"/><path d="M8 6H6a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-2"/>',
90
90
  // Live-cursor pointer arrow — collab.js's LiveCursorOverlay renders one
91
91
  // per remote collaborator, filled with that collaborator's color.
92
- cursor: '<path d="M5 3l14 8-6.5 1.5L11 20z"/>'
92
+ cursor: '<path d="M5 3l14 8-6.5 1.5L11 20z"/>',
93
+ // Password-visibility toggle — eye / eye-off pair.
94
+ eye: '<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/>',
95
+ 'eye-off': '<path d="M2 12s3.5-7 10-7c1.7 0 3.2.4 4.5 1.1M22 12s-3.5 7-10 7c-1.7 0-3.2-.4-4.5-1.1"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2"/><path d="m4 4 16 16"/>'
93
96
  };
94
97
 
95
98
  // The single SVG attribute contract (viewBox/stroke/linecap…) shared by both
@@ -289,23 +289,12 @@
289
289
  .ds-gallery-tile:hover { transform: translateY(-2px); box-shadow: 0 2px 8px color-mix(in oklab, var(--fg) 10%, transparent); }
290
290
  .ds-gallery-tile:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
291
291
  .ds-gallery-tile--tight { min-height: clamp(96px, 14vw, 120px); }
292
- .ds-lightbox {
293
- position: fixed; inset: 0; background: var(--scrim);
294
- display: flex; align-items: center; justify-content: center;
295
- padding: clamp(var(--space-2-75), 4vw, var(--space-5));
296
- padding-bottom: max(clamp(var(--space-2-75), 4vw, var(--space-5)), env(safe-area-inset-bottom));
297
- z-index: var(--z-modal);
298
- }
299
- .ds-lightbox-card {
300
- /* 28px ceiling has no tier (--space-4 is 24, --space-5 is 32); it is the
301
- lightbox card inset tuned against the min(520px,100%) card width. */
302
- background: var(--panel-0); border-radius: var(--r-2); padding: clamp(var(--space-3), 4vw, 28px);
303
- width: min(520px, 100%); min-width: 0;
304
- max-height: calc(100vh - 24px);
305
- max-height: calc(100dvh - 24px);
306
- overflow: auto;
307
- display: flex; flex-direction: column; gap: 14px; /* off-scale, matches the card inset above */
308
- }
292
+ /* The lightbox scrim/card chrome itself is now Dialog's (.ds-ep-dialog /
293
+ .ds-ep-dialog-backdrop in editor-primitives.css) Dialog already ships
294
+ role="dialog", aria-modal, Tab-trap and focus-restore-on-close, which the
295
+ former hand-rolled .ds-lightbox/.ds-lightbox-card overlay had none of. Only
296
+ the gallery-specific content rules (head row, tag, carousel sizing, tile
297
+ preview) remain here. */
309
298
  .ds-lightbox-preview {
310
299
  /* 36px ceiling has no tier (--space-5 is 32, --space-6 is 48). */
311
300
  padding: clamp(var(--space-3), 6vw, 36px); border-radius: var(--r-1); text-align: center;
@@ -511,11 +500,24 @@
511
500
  .ds-auth-otp-wrap { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); }
512
501
  .ds-auth-field { display: flex; flex-direction: column; gap: var(--space-1); }
513
502
  /* .ds-auth-field-label joins the kit small-label voice above, no overrides. */
503
+ .ds-auth-field-input-row { position: relative; display: flex; align-items: center; }
504
+ /* Reserves room for the absolutely-positioned toggle: its --space-1 inset plus
505
+ the --ds-icon-btn-sm 26px box, so the input text never sits under it. */
506
+ .ds-auth-field-input-row .input { flex: 1 1 auto; padding-right: calc(26px + var(--space-2)); }
507
+ .ds-auth-pw-toggle { position: absolute; right: var(--space-1); }
514
508
  .ds-auth-row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); flex-wrap: wrap; }
515
509
  .ds-auth-remember { display: flex; align-items: center; gap: var(--space-1-75); cursor: pointer; }
516
510
  .ds-auth-remember-text { color: var(--panel-text-2); font-size: var(--fs-tiny); }
517
511
  .ds-auth-forgot { font-size: var(--fs-tiny); }
518
- .ds-auth-error { padding: var(--space-2) var(--space-2-5); background: var(--panel-1); border-radius: var(--r-0); color: var(--danger); font-size: var(--fs-tiny); overflow-wrap: anywhere; }
512
+ /* Error and status share layout but not color: a color-only distinction
513
+ between role="alert" (something is wrong) and role="status" (informational)
514
+ fails WCAG 1.4.1, so each also carries a distinct ASCII marker via ::before —
515
+ never rely on the reader noticing the role attribute alone. */
516
+ .ds-auth-error, .ds-auth-status { padding: var(--space-2) var(--space-2-5); background: var(--panel-1); border-radius: var(--r-0); font-size: var(--fs-tiny); overflow-wrap: anywhere; display: flex; gap: var(--space-1-75); align-items: flex-start; }
517
+ .ds-auth-error { color: var(--danger); }
518
+ .ds-auth-error::before { content: '[!]'; flex: none; }
519
+ .ds-auth-status { color: var(--panel-text-2); }
520
+ .ds-auth-status::before { content: '[i]'; flex: none; }
519
521
  .ds-auth-providers { display: flex; gap: var(--space-2); flex-wrap: wrap; }
520
522
  .ds-auth-provider-btn { flex: 1 1 100px; min-width: 0; display: flex; align-items: center; justify-content: center; gap: var(--space-2); padding: var(--space-2-5); }
521
523
  .ds-auth-provider-btn--loading { opacity: 0.7; pointer-events: none; }
@@ -541,8 +543,12 @@
541
543
  .ds-gal-swatch-meta { display: flex; justify-content: space-between; gap: var(--space-1-75); flex-wrap: wrap; font-family: var(--ff-mono); font-size: var(--fs-nano); min-width: 0; }
542
544
  .ds-gal-swatch-name { color: var(--panel-text); }
543
545
  .ds-gal-swatch-hint { color: var(--panel-text-3); overflow-wrap: anywhere; }
544
- .ds-lightbox-head { display: flex; justify-content: space-between; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
546
+ .ds-lightbox-head { display: flex; justify-content: space-between; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-2-75); }
545
547
  /* .ds-lightbox-tag joins the kit small-label voice above, no overrides. */
548
+ /* Dialog's .ds-ep-dialog-body has no default child gap (unlike the former
549
+ hand-rolled .ds-lightbox-card, which was `display:flex; gap:14px`), so the
550
+ carousel block gets its own top gutter to keep head/carousel/hint spaced. */
551
+ .ds-lightbox-card--carousel { margin-bottom: var(--space-2-75); }
546
552
 
547
553
  /* --- error_404 kit --- */
548
554
  .ds-err-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; justify-content: center; margin-top: var(--space-1-75); }
@@ -600,6 +606,20 @@
600
606
  .ds-term-input-row { margin-top: var(--space-1-75); }
601
607
  .ds-term-input { flex: 1; min-width: 0; background: transparent; border: 0; outline: 0; font-family: var(--ff-mono); font-size: var(--fs-micro); color: var(--paper); caret-color: var(--acid); }
602
608
  .ds-term-input::placeholder { color: var(--paper-3-dark); }
609
+ /* Adding type="text" (for the a11y fix below) makes the global
610
+ input[type="text"] rule in states-interactions.css match this element for
611
+ the first time, at equal-or-higher specificity than this file's load
612
+ order would otherwise win. Re-assert the borderless prompt-line chrome so
613
+ the shared form-control skin (bg, border, radius, fixed height) can't leak
614
+ onto what is a terminal prompt, not a form field. */
615
+ .ds-term-input[type='text'] { background: transparent; border: 0; border-radius: 0; box-shadow: none; height: auto; }
616
+ /* The row-level `outline: 0` above kills the browser default, so the control
617
+ needs its own :focus-visible ring back — otherwise a keyboard user gets no
618
+ visual confirmation focus landed on the one control they type into. */
619
+ .ds-term-input:focus-visible {
620
+ outline: var(--focus-w) solid var(--focus-color);
621
+ outline-offset: var(--focus-offset);
622
+ }
603
623
  /* The input itself is a prompt line, not a form control, so it is NOT floored
604
624
  to 44px (that would break the terminal's line rhythm). Instead the whole
605
625
  prompt ROW becomes the tap target, and the font goes to 16px so focusing it
@@ -622,8 +642,25 @@
622
642
  .ds-chat-layout > .chat { min-width: 0; }
623
643
  .ds-chat-detail { display: none; }
624
644
  @media (min-width: 1100px) {
625
- .ds-chat-layout { display: grid; grid-template-columns: minmax(0, 1fr) 280px; align-items: start; gap: var(--space-4); }
626
- .ds-chat-detail { display: block; position: sticky; top: 0; }
645
+ /* Two compounding grid quirks, not one:
646
+ 1. align-items:start sizes the row to its tallest item's natural content
647
+ height instead of stretching items to fill it — align-items must stay
648
+ the grid default (stretch).
649
+ 2. Even with stretch, an implicit row with no explicit sizing (the
650
+ default here) ALSO sizes itself to content first, then stretches
651
+ items to that content-derived height — which can exceed the row's
652
+ actual allotment from this grid's own flex parent (.chat-kit-page,
653
+ flex:1/min-height:0). .chat then renders taller than .ds-chat-layout's
654
+ own box, so it overflows past the grid container instead of being
655
+ clipped by .chat-thread's internal scroll — visually burying the
656
+ pattern-notes panel that follows this grid in the DOM. Explicit
657
+ `grid-template-rows: minmax(0, 1fr)` makes the row take exactly the
658
+ container's height (from the flex chain above) as the ceiling, so
659
+ .chat's min-height:0 has a real, smaller-than-content value to clip
660
+ against. .ds-chat-detail opts out via align-self so its own sticky
661
+ positioning still works without forcing the row back to content-sized. */
662
+ .ds-chat-layout { display: grid; grid-template-columns: minmax(0, 1fr) 280px; grid-template-rows: minmax(0, 1fr); gap: var(--space-4); }
663
+ .ds-chat-detail { display: block; position: sticky; top: 0; align-self: start; }
627
664
  }
628
665
 
629
666
  /* Avatar — generic identity disc primitive (content.js Avatar). Sized
@@ -53,8 +53,22 @@
53
53
  font-weight: 500; color: var(--fg-3);
54
54
  font-family: var(--ff-mono); letter-spacing: 0;
55
55
  }
56
+ /* Panel titles render as a real heading (h2 by default) for AT heading-jump
57
+ navigation, but must read identically to the old plain span — reset the
58
+ UA heading margin/size/weight so it inherits .panel-head's own type rules
59
+ instead of the browser's h2 stylesheet. */
60
+ .panel-title {
61
+ margin: 0; font: inherit; font-size: inherit; font-weight: inherit;
62
+ letter-spacing: inherit; text-transform: inherit; color: inherit;
63
+ }
56
64
  .panel-body { padding: var(--space-2) var(--space-3) var(--space-3); }
57
65
  .panel-body > * { margin: 0; }
66
+ /* One-line explanatory caption under a panel's title — e.g. distinguishing
67
+ two visually-similar panels (lore palette vs semantic tokens) in words
68
+ instead of leaving the reader to infer it from the title alone. */
69
+ .ds-panel-caption {
70
+ font-size: var(--fs-sm); color: var(--fg-3); margin: 0 0 var(--space-2);
71
+ }
58
72
  /* Inner sibling rhythm scales with density like the outer panel rhythm, so a
59
73
  compact/spacious surface stays proportional instead of only its outer
60
74
  intervals moving while the panel interiors hold still. Kept strictly below
@@ -126,7 +126,11 @@
126
126
  /* Embedded shell (freddie WM window) fills its container, not the viewport. */
127
127
  .fd-root .app { height: 100%; overflow: visible; }
128
128
  .app-body { min-height: 0; flex: 1 1 auto; }
129
- .app-side-shell { overflow-y: auto; min-height: 0; }
129
+ /* scrollbar-gutter mirrors .app-main's own (topbar.css) without it the
130
+ thumb draws flush against the sidebar's nav text with zero breathing
131
+ room, reading as a mark stuck to the content rather than a scrollbar
132
+ anchored to the shell's own edge. */
133
+ .app-side-shell { overflow-y: auto; overflow-x: hidden; min-height: 0; scrollbar-gutter: stable; }
130
134
  .app-main { overflow-y: auto; min-height: 0; }
131
135
  }
132
136
 
@@ -164,7 +164,7 @@ body.canvas-host { background: transparent !important; }
164
164
 
165
165
  .app-side {
166
166
  display: flex; flex-direction: column; gap: var(--space-4);
167
- padding: var(--space-5) var(--space-3);
167
+ padding: var(--space-5) var(--space-2) var(--space-5) var(--space-3);
168
168
  font-size: var(--fs-sm);
169
169
  }
170
170
  /* Sidebar section wrapper emitted by Side() (src/components/shell/app-shell.js).
@@ -11,6 +11,13 @@ export function renderFilesApp(opts = {}) {
11
11
 
12
12
  let preview = null;
13
13
  let selected = null;
14
+ function renderError(err) {
15
+ node.innerHTML = '';
16
+ const errRow = document.createElement('div');
17
+ errRow.className = 'row-error';
18
+ errRow.textContent = 'could not load files: ' + (err && err.message ? err.message : String(err));
19
+ node.appendChild(errRow);
20
+ }
14
21
  async function refresh() {
15
22
  const items = await list();
16
23
  preview = null;
@@ -40,8 +47,8 @@ export function renderFilesApp(opts = {}) {
40
47
  }
41
48
 
42
49
  let timer = null;
43
- refresh().catch(() => {});
44
- if (pollMs > 0) timer = setInterval(() => refresh().catch(() => {}), pollMs);
50
+ refresh().catch(renderError);
51
+ if (pollMs > 0) timer = setInterval(() => refresh().catch(renderError), pollMs);
45
52
 
46
53
  return {
47
54
  node,