anentrypoint-design 0.0.212 → 0.0.213

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.212",
3
+ "version": "0.0.213",
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",
@@ -215,6 +215,19 @@ function ToolCallNode(p) {
215
215
  // collapse on success unless the caller explicitly overrides with open:true.
216
216
  const defaultOpen = p.open != null ? !!p.open : (status === 'running' || status === 'error');
217
217
  const iconName = status === 'running' ? 'refresh' : (status === 'error' ? 'warn' : 'check');
218
+ const copyText = (txt) => (e) => {
219
+ const b = e.currentTarget;
220
+ const done = () => {
221
+ b.textContent = 'copied';
222
+ b.classList.add('is-copied');
223
+ setTimeout(() => { b.textContent = 'copy'; b.classList.remove('is-copied'); }, 1600);
224
+ };
225
+ if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(txt).then(done).catch(() => {});
226
+ else { try { const t = document.createElement('textarea'); t.value = txt; document.body.appendChild(t); t.select(); document.execCommand('copy'); document.body.removeChild(t); done(); } catch {} }
227
+ };
228
+ const sectionLabel = (text, txt) => h('div', { class: 'chat-tool-section-label' },
229
+ h('span', {}, text),
230
+ h('button', { type: 'button', class: 'chat-code-copy chat-tool-copy', 'aria-label': 'copy ' + text, onclick: copyText(txt) }, 'copy'));
218
231
  return h('details', { class: 'chat-bubble chat-tool tool-' + status, open: defaultOpen },
219
232
  h('summary', { class: 'chat-tool-head' },
220
233
  h('span', { class: 'chat-tool-icon', 'aria-hidden': 'true' }, Icon(iconName, { size: 14 })),
@@ -224,10 +237,10 @@ function ToolCallNode(p) {
224
237
  ),
225
238
  h('div', { class: 'chat-tool-body' },
226
239
  h('div', { class: 'chat-tool-section' },
227
- h('div', { class: 'chat-tool-section-label' }, 'args'),
240
+ sectionLabel('args', argsText),
228
241
  h('pre', { class: 'chat-tool-pre' }, h('code', {}, argsText))),
229
242
  resultText ? h('div', { class: 'chat-tool-section' },
230
- h('div', { class: 'chat-tool-section-label' }, p.error ? 'error' : 'result'),
243
+ sectionLabel(p.error ? 'error' : 'result', resultText),
231
244
  h('pre', { class: 'chat-tool-pre' + (p.error ? ' is-error' : '') }, h('code', {}, resultText)))
232
245
  // A finished tool with no output would otherwise render no result
233
246
  // section, reading identically to a still-running tool. Show an
@@ -43,7 +43,7 @@ function highlightTitle(title, highlight) {
43
43
  return segs;
44
44
  }
45
45
 
46
- export function Row({ code, rank, title, sub, meta, active, state = 'default', onClick, key, style, href, kind, cols, leading, trailing, target, selected, rail, expanded, highlight, actions }) {
46
+ export function Row({ code, rank, title, sub, meta, active, state = 'default', onClick, key, style, href, kind, cols, leading, trailing, target, selected, rail, expanded, highlight, actions, detail }) {
47
47
  // `rank` is an alias for `code` (the leading monospace index); callers use
48
48
  // either name. `rail` renders a thin colour bar at the row's leading edge as
49
49
  // a status indicator (tone: green | purple | flame | <any token>).
@@ -105,12 +105,15 @@ export function Row({ code, rank, title, sub, meta, active, state = 'default', o
105
105
  // state. green is the unremarkable default - announcing "ok" everywhere would
106
106
  // be AT noise - so it emits nothing.
107
107
  const railWord = rail === 'flame' ? 'error' : rail === 'purple' ? 'subagent' : null;
108
+ // `detail` renders as a sibling block AFTER the title/meta children (its own
109
+ // line via flex-basis:100% in .ds-row-detail), not inside the title span.
108
110
  return h(isLink ? 'a' : 'div', props,
109
111
  railWord ? h('span', { class: 'sr-only' }, railWord) : null,
110
112
  leading != null ? leading : (codeVal != null ? h('span', { class: 'code' }, codeVal) : null),
111
113
  h('span', { class: 'title' }, titleNode, sub ? h('span', { class: 'sub' }, sub) : null),
112
114
  trailing != null ? trailing : (meta != null ? h('span', { class: 'meta' }, meta) : null),
113
- actionRow);
115
+ actionRow,
116
+ detail != null ? h('pre', { class: 'ds-row-detail' }, detail) : null);
114
117
  }
115
118
 
116
119
  export function RowLink({ code, title, sub, meta, href = '#', key, target }) {
@@ -55,9 +55,12 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
55
55
  const noAccess = locked || permissions === 'EACCES' || (Array.isArray(permissions) && permissions.length === 0);
56
56
  const readOnly = !noAccess && Array.isArray(permissions) && permissions.indexOf('write') === -1 && permissions.indexOf('read') !== -1;
57
57
  const permTag = noAccess ? 'no access' : (readOnly ? 'read-only' : null);
58
- const meta = [type === 'dir' ? null : fmtFileSize(size), modified || null, permTag].filter(Boolean).join(' · ');
58
+ // permTag is rendered as its own chip (a SHAPE channel, not folded into the
59
+ // muted meta text) - so drop it from the meta join, but keep it in the
60
+ // accessible label so AT still announces the restriction.
61
+ const meta = [type === 'dir' ? null : fmtFileSize(size), modified || null].filter(Boolean).join(' · ');
59
62
  const typeLabel = TYPE_LABELS[type] || 'file';
60
- const accessibleLabel = `${typeLabel}: ${name}${meta ? ` (${meta})` : ''}`;
63
+ const accessibleLabel = `${typeLabel}: ${name}${meta ? ` (${meta})` : ''}${permTag ? ', ' + permTag : ''}`;
61
64
  const canOpen = onOpen && !noAccess && !busy;
62
65
  // Mutation actions on a read-only/no-access row render disabled (with a
63
66
  // 'read-only' title) instead of vanishing, so the affordance reads honestly.
@@ -109,10 +112,13 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
109
112
  'aria-pressed': active ? 'true' : 'false',
110
113
  disabled: canOpen ? null : true,
111
114
  },
112
- code != null ? h('span', { class: 'code', 'aria-label': `code: ${code}` }, code) : null,
113
- FileIcon({ type }),
114
- h('span', { class: 'title' }, name),
115
- h('span', { class: 'ds-file-meta meta', 'aria-label': meta ? `metadata: ${meta}` : null }, meta || '—')
115
+ ...[
116
+ code != null ? h('span', { class: 'code', 'aria-label': `code: ${code}` }, code) : null,
117
+ FileIcon({ type }),
118
+ h('span', { class: 'title' }, name),
119
+ h('span', { class: 'ds-file-meta meta', 'aria-label': meta ? `metadata: ${meta}` : null }, meta || '—'),
120
+ permTag ? h('span', { class: 'ds-file-perm-tag' + (noAccess ? ' is-noaccess' : ''), 'aria-hidden': 'true' }, permTag) : null,
121
+ ].filter(Boolean)
116
122
  ),
117
123
  actionBtns.length ? h('span', { key: 'acts', class: 'ds-file-actions', role: 'group', 'aria-label': `actions for ${name}` },
118
124
  ...actionBtns
@@ -121,6 +127,7 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
121
127
  return h('div', {
122
128
  key,
123
129
  class: 'ds-file-row row' + (active ? ' active' : '') + (noAccess ? ' is-locked' : '')
130
+ + (readOnly ? ' is-restricted' : '')
124
131
  + (marked ? ' is-marked' : '') + (selectable ? ' is-selectable' : ''),
125
132
  'data-file-type': type,
126
133
  'aria-busy': busy ? 'true' : null,
@@ -130,7 +137,7 @@ export function FileRow({ name, type = 'other', size, modified, code, onOpen, on
130
137
  // FileSkeleton — placeholder shimmer rows shown while a directory loads, so the
131
138
  // grid does not flash from a bare spinner to a full list (predictable perceived
132
139
  // perf, the file-manager feel). `rows` controls how many ghost rows render.
133
- export function FileSkeleton({ rows = 8 } = {}) {
140
+ export function FileSkeleton({ rows = 12 } = {}) {
134
141
  return h('div', { class: 'ds-file-grid ds-file-skeleton', 'aria-hidden': 'true' },
135
142
  ...Array.from({ length: Math.max(1, rows) }, (_, i) => h('div', { key: 'sk' + i, class: 'ds-file-row ds-file-row-skeleton' },
136
143
  h('span', { class: 'ds-skel ds-skel-icon' }),
@@ -173,7 +180,7 @@ export function sortFiles(files = [], sort = 'name', dir = 'asc') {
173
180
  // CAP and a "show N more" row, mirroring the History tab's "load N older".
174
181
  const FILE_GRID_CAP = 200;
175
182
 
176
- export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No files here yet',
183
+ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No files here yet', emptyAction,
177
184
  sort, filter, loading = false,
178
185
  shown, onShowMore, actions, busy,
179
186
  // Canonical multi-select contract (shared with
@@ -187,8 +194,14 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
187
194
  // delete / upload round-trip) keeps the rows on screen and dims them -
188
195
  // flashing the whole directory to shimmer rows on every mutation reads as
189
196
  // data loss.
190
- if (loading && !files.length) return FileSkeleton({});
191
- if (!files.length) return EmptyState({ text: emptyText });
197
+ if (loading && !files.length) return FileSkeleton({ rows: 12 });
198
+ // A filtered miss is NOT an empty directory: when the in-grid filter narrows
199
+ // to zero matches, the host still passes an empty `files` array - but we must
200
+ // keep the controls toolbar (the filter input that caused the miss) mounted so
201
+ // the user can clear/edit it to recover. Only a genuinely-empty directory (no
202
+ // active filter) gets the bare cold EmptyState early-return.
203
+ const hasFilter = !!(filter && (filter.value || '').length > 0);
204
+ if (!files.length && !hasFilter) return EmptyState({ text: emptyText, glyph: Icon('folder-open', { size: 28 }), action: emptyAction });
192
205
  const refreshing = loading && files.length > 0;
193
206
  // Cap the rendered rows. `shown` (host-controlled) overrides the default cap
194
207
  // so "show more" can grow it; otherwise default to FILE_GRID_CAP.
@@ -241,12 +254,20 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
241
254
  // it switches presentation of the same content, not panels.
242
255
  const densityCtl = onDensity
243
256
  ? h('div', { key: 'density', class: 'ds-density', role: 'radiogroup', 'aria-label': 'view density' },
244
- ...DENSITIES.map(([k, label]) => h('button', {
257
+ ...DENSITIES.map(([k, label], idx) => h('button', {
245
258
  key: 'd-' + k, type: 'button', role: 'radio',
246
259
  class: 'ds-density-btn' + (density === k ? ' active' : ''),
247
260
  'aria-checked': density === k ? 'true' : 'false',
261
+ // Icon-led, but the density name stays the accessible name
262
+ // (aria-label) + the native tooltip (title) so the control reads
263
+ // dense without losing its label.
264
+ 'aria-label': label, title: label,
265
+ // Single tab stop: the checked radio is tabbable, the rest are
266
+ // roved. Arrow/Home/End move + select (selection follows focus).
267
+ tabindex: density === k ? '0' : '-1',
268
+ onkeydown: (e) => rovingRadio(e, idx, DENSITIES, (tk) => { if (density !== tk) onDensity(tk); }),
248
269
  onclick: () => { if (density !== k) onDensity(k); },
249
- }, label)))
270
+ }, Icon(DENSITY_ICONS[k], { size: 15 }))))
250
271
  : null;
251
272
  // One toolbar baseline: filter + select-all + sort sit left, density is
252
273
  // pushed right by the spread. The filter used to be a separate right-aligned
@@ -266,10 +287,16 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
266
287
  const controls = controlsKids.length
267
288
  ? h('div', { class: 'ds-file-controls' }, ...controlsKids)
268
289
  : null;
290
+ // A filtered miss (zero rows but an active filter) renders the EmptyState
291
+ // INSIDE the listing, below the controls toolbar, so the filter input stays
292
+ // mounted and editable - the user can clear/edit it to recover instead of
293
+ // being stranded with no toolbar (the early-return only fires for a genuinely
294
+ // empty directory). The host passes filter-aware copy via emptyText.
295
+ const filteredEmpty = !files.length && hasFilter;
269
296
  // role=group not listbox: the rows contain real <button> action controls, so
270
297
  // listbox/option semantics are invalid (an option can't host interactive
271
298
  // children). Keyboard nav still works via roving focus over the open buttons.
272
- const grid = h('div', {
299
+ const grid = filteredEmpty ? EmptyState({ text: emptyText, glyph: Icon('folder-open', { size: 28 }) }) : h('div', {
273
300
  class: 'ds-file-grid' + (isThumb ? ' ds-file-grid-thumb' : '') + (refreshing ? ' is-refreshing' : ''),
274
301
  role: 'group', 'aria-label': 'files', tabindex: '0',
275
302
  'aria-busy': refreshing ? 'true' : 'false',
@@ -313,6 +340,25 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
313
340
  }
314
341
 
315
342
  const DENSITIES = [['list', 'list'], ['compact', 'compact'], ['thumb', 'thumbnails']];
343
+ const DENSITY_ICONS = { list: 'rows', compact: 'rows-tight', thumb: 'grid' };
344
+
345
+ // Roving-radiogroup keyboard helper (the WAI-ARIA radio pattern): a radiogroup
346
+ // is a SINGLE tab stop where Arrow/Home/End move AND select among options, with
347
+ // selection following focus. `items` is the ordered [[key, ...], ...] list;
348
+ // `onSelect(targetKey)` is the same handler the onclick fires. Mouse path is
349
+ // unchanged - this only adds keyboard navigation.
350
+ function rovingRadio(e, idx, items, onSelect) {
351
+ let target = -1;
352
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') target = (idx - 1 + items.length) % items.length;
353
+ else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') target = (idx + 1) % items.length;
354
+ else if (e.key === 'Home') target = 0;
355
+ else if (e.key === 'End') target = items.length - 1;
356
+ else return;
357
+ e.preventDefault();
358
+ onSelect(items[target][0]);
359
+ const sib = e.currentTarget.parentNode && e.currentTarget.parentNode.children[target];
360
+ sib && sib.focus();
361
+ }
316
362
 
317
363
  // FileCell — the thumbnail-density tile. Image entries show a real (lazy)
318
364
  // thumbnail through the host's confined thumbUrl; everything else keeps its
@@ -470,10 +516,20 @@ export function UploadProgress({ items = [], onDismiss } = {}) {
470
516
  );
471
517
  }
472
518
 
473
- export function EmptyState({ text = 'nothing here', glyph = Icon('circle') } = {}) {
519
+ export function EmptyState({ text = 'nothing here', glyph = Icon('circle'), action } = {}) {
520
+ // action: { onClick, label } - an optional CTA (e.g. 'go up' / 'upload a
521
+ // file'), mirroring the SessionDashboard emptyAction contract so an empty
522
+ // directory is not a dead end. Children are built as an array + filtered so
523
+ // the keyed Btn never sits beside an unkeyed span (webjsx applyDiff 'key'
524
+ // crash on mixed keyed/unkeyed siblings).
474
525
  return h('div', { class: 'ds-file-empty', role: 'status' },
475
- h('span', { class: 'ds-file-empty-glyph', 'aria-hidden': 'true' }, glyph),
476
- h('span', { class: 'ds-file-empty-text' }, text)
526
+ ...[
527
+ h('span', { key: 'glyph', class: 'ds-file-empty-glyph', 'aria-hidden': 'true' }, glyph),
528
+ h('span', { key: 'text', class: 'ds-file-empty-text' }, text),
529
+ (action && action.onClick)
530
+ ? Btn({ key: 'ea', onClick: action.onClick, children: action.label || 'go up' })
531
+ : null,
532
+ ].filter(Boolean)
477
533
  );
478
534
  }
479
535
 
@@ -296,6 +296,13 @@ export function useKeyboardShortcut(map = {}, { scope = 'global', enabled = true
296
296
 
297
297
  export function ShortcutHint({ combo, kind = 'kbd' } = {}) { return h('kbd', { class: 'ds-kbd ds-kbd-' + kind }, formatShortcut(combo || '')); }
298
298
 
299
+ export function ShortcutList({ shortcuts = [] } = {}) {
300
+ return h('div', { class: 'ds-shortcuts-hint' },
301
+ ...shortcuts.map(s => h('div', { class: 'ds-shortcut-row' },
302
+ h('kbd', { class: 'ds-kbd' }, s.keys || s.combo || ''),
303
+ h('span', { class: 'ds-kbd-label' }, s.desc || s.description || s.label || ''))));
304
+ }
305
+
299
306
  export function useKeyboardShortcutHelp() { return { registry: Array.from(SHORTCUT_REGISTRY) }; }
300
307
  export function ShortcutHelpDialog({ open = false, onClose, registry } = {}) {
301
308
  if (!open) return null;
@@ -198,8 +198,11 @@ export function SessionCard({ session = {}, onStop, onOpen, onView, active = fal
198
198
  const meta = h('div', { class: 'ds-dash-meta' }, ...[
199
199
  s.cwd ? h('span', { class: 'ds-dash-cwd', title: s.cwd }, s.cwd) : null,
200
200
  (statBits.length || costText) ? h('span', { class: 'ds-dash-stat' },
201
- statBits.join(' · '),
202
- costText ? h('span', { class: 'ds-dash-stat-cost' }, (statBits.length ? ' · ' : '') + costText) : null
201
+ ...[
202
+ statBits.length ? statBits.join(' · ') : null,
203
+ (statBits.length && costText) ? ' · ' : null,
204
+ costText ? h('span', { class: 'ds-dash-stat-cost' }, costText) : null,
205
+ ].filter(Boolean)
203
206
  ) : null,
204
207
  activityBits.length ? h('span', { class: 'ds-dash-activity' }, activityBits.join(' · ')) : null,
205
208
  ].filter(Boolean));
@@ -146,6 +146,10 @@ const ICON_PATHS = {
146
146
  // file-browser icons (replace folder/file emoji + arrow glyphs in fs apps)
147
147
  folder: '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
148
148
  'folder-open': '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2H5l-2 9z"/><path d="M3 18l2-9h17l-2 9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
149
+ // density-picker icons (list / compact / thumbnail view modes)
150
+ rows: '<path d="M4 6h16M4 12h16M4 18h16"/>',
151
+ 'rows-tight': '<path d="M4 5h16M4 9h16M4 13h16M4 17h16"/>',
152
+ grid: '<rect x="4" y="4" width="7" height="7" rx="1"/><rect x="13" y="4" width="7" height="7" rx="1"/><rect x="4" y="13" width="7" height="7" rx="1"/><rect x="13" y="13" width="7" height="7" rx="1"/>',
149
153
  'file-image': '<path d="M6 3h8l5 5v13a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><path d="M14 3v5h5"/><circle cx="9.5" cy="12.5" r="1.5"/><path d="M18 19l-4-4-3 3-2-2-3 3"/>',
150
154
  link: '<path d="M10 13a5 5 0 0 0 7 0l2-2a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-2 2a5 5 0 0 0 7 7l1-1"/>',
151
155
  upload: '<path d="M12 16V4M7 9l5-5 5 5"/><path d="M5 20h14"/>',
@@ -318,12 +322,17 @@ function toggleWs(which) {
318
322
 
319
323
  // Column resize: read the current rendered track width and write a clamped inline
320
324
  // --ws-<col>-w on .ws-shell (inline overrides the fluid clamp base), persisted.
321
- // Bounds are derived from the CSS fluid clamp() floors/ceilings in app-shell.css
322
- // (--ws-rail-w clamp(200,16vw,260); sessions clamp(248,22vw,360); pane
323
- // clamp(288,24vw,420)) so a drag/arrow can never shrink a column below its
324
- // designed floor (the collapsed rail is a SEPARATE class, not a resize target)
325
- // nor grow past the ultrawide ceiling.
326
- const WS_RESIZE_CLAMP = { rail: [200, 260], sessions: [248, 360], pane: [288, 420] };
325
+ // Floors match the CSS fluid clamp() floors in app-shell.css (--ws-rail-w
326
+ // clamp(200,16vw,260); sessions clamp(248,22vw,360); pane clamp(288,24vw,420))
327
+ // so a drag/arrow can never shrink a column below its designed minimum (the
328
+ // collapsed rail is a SEPARATE class, not a resize target). The ceilings are
329
+ // INTENTIONALLY raised above the fluid clamp() mid-term ceilings: on wide
330
+ // viewports the 16/22/24vw mid term already pins each column to its clamp
331
+ // ceiling, so a ceiling-equals-clamp bound made the outward drag inert there.
332
+ // The higher resize ceilings let a deliberate drag/arrow grow a column past its
333
+ // auto-fluid width (the inline --ws-<col>-w override pins the chosen width past
334
+ // the clamp base).
335
+ const WS_RESIZE_CLAMP = { rail: [200, 320], sessions: [248, 520], pane: [288, 640] };
327
336
  function wsResize(col, dx, persist = true) {
328
337
  const shell = document.querySelector('.ws-shell');
329
338
  if (!shell) return;
@@ -505,6 +514,15 @@ export function WorkspaceShell({ rail, sessions, main, pane, crumb, status, narr
505
514
  'aria-expanded': 'true', onclick: () => toggleWs('sessions'),
506
515
  }, Icon('chevron-left')) : null,
507
516
  h('div', { class: 'ws-crumb-main' }, crumb),
517
+ // Desktop-only context-pane collapse, on the same crumb-level
518
+ // chrome idiom as the sessions toggle. Hidden on mobile via CSS.
519
+ hasPane ? h('button', {
520
+ class: 'ws-desktop-toggle ws-pane-toggle', type: 'button',
521
+ 'aria-label': paneIsCollapsed ? 'show context pane' : 'hide context pane',
522
+ title: paneIsCollapsed ? 'show context pane' : 'hide context pane',
523
+ 'aria-expanded': paneIsCollapsed ? 'false' : 'true',
524
+ onclick: () => toggleWs('pane'),
525
+ }, Icon(paneIsCollapsed ? 'chevron-left' : 'chevron-right')) : null,
508
526
  hasPane ? h('button', {
509
527
  class: 'ws-drawer-toggle ws-pane-drawer-toggle', type: 'button',
510
528
  'aria-label': 'toggle context pane', 'aria-expanded': 'false',
@@ -514,16 +532,10 @@ export function WorkspaceShell({ rail, sessions, main, pane, crumb, status, narr
514
532
  h('main', { class: 'ws-main' + (narrow ? ' narrow' : '') + (mainFlush ? ' ws-main--flush' : ''), id: 'ws-main', tabindex: '-1' },
515
533
  ...(Array.isArray(main) ? main : [main])),
516
534
  status || null),
517
- // Optional right context pane with its own collapse toggle.
535
+ // Optional right context pane. Its desktop collapse toggle now lives in
536
+ // the crumb cluster, alongside the sessions toggle.
518
537
  hasPane
519
538
  ? h('aside', { class: 'ws-pane', role: 'complementary', 'aria-label': paneLabel },
520
- h('button', {
521
- class: 'ws-pane-toggle', type: 'button',
522
- 'aria-label': paneIsCollapsed ? 'show context pane' : 'hide context pane',
523
- title: paneIsCollapsed ? 'show context pane' : 'hide context pane',
524
- 'aria-expanded': paneIsCollapsed ? 'false' : 'true',
525
- onclick: () => toggleWs('pane'),
526
- }, Icon(paneIsCollapsed ? 'chevron-left' : 'chevron-right')),
527
539
  pane)
528
540
  : null,
529
541
  // Keyboard/pointer column resize handles (desktop only).
package/src/components.js CHANGED
@@ -66,7 +66,7 @@ export {
66
66
 
67
67
  export {
68
68
  useDraggable, useDropTarget, useNumberScrub, usePointerDrag, Reorderable,
69
- useKeyboardShortcut, formatShortcut, ShortcutHint,
69
+ useKeyboardShortcut, formatShortcut, ShortcutHint, ShortcutList,
70
70
  useKeyboardShortcutHelp, ShortcutHelpDialog
71
71
  } from './components/interaction-primitives.js';
72
72