anentrypoint-design 0.0.196 → 0.0.198

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.
@@ -277,6 +277,195 @@ export function AppShell({ topbar, crumb, side, main, status, narrow } = {}) {
277
277
  );
278
278
  }
279
279
 
280
+ // Toggle a named WorkspaceShell column (left rail or right pane). Pure-DOM like
281
+ // toggleSide: WorkspaceShell is stateless chrome, the collapsed class lives on
282
+ // .ws-shell and is read by both CSS and the toggle buttons' aria-expanded.
283
+ function toggleWs(which) {
284
+ const shell = document.querySelector('.ws-shell');
285
+ if (!shell) return;
286
+ const cls = which === 'pane' ? 'ws-pane-collapsed' : 'ws-rail-collapsed';
287
+ const nowCollapsed = shell.classList.toggle(cls);
288
+ const btn = document.querySelector('.ws-' + which + '-toggle');
289
+ if (btn) btn.setAttribute('aria-expanded', nowCollapsed ? 'false' : 'true');
290
+ try {
291
+ localStorage.setItem('ds.ws.' + which, nowCollapsed ? 'collapsed' : 'open');
292
+ } catch (_) {}
293
+ }
294
+
295
+ // Toggle a mobile WorkspaceShell DRAWER (sessions or pane). Distinct from the
296
+ // desktop width-collapse (toggleWs): on mobile the columns are fixed overlays
297
+ // revealed by .ws-sessions-open / .ws-pane-open. Opening one closes the other
298
+ // (only one drawer at a time over the content). Esc + scrim dismiss call this
299
+ // with open=false. Pure-DOM, matching the AppShell toggleSide pattern.
300
+ function toggleWsDrawer(which, open) {
301
+ const shell = document.querySelector('.ws-shell');
302
+ if (!shell) return;
303
+ const cls = which === 'pane' ? 'ws-pane-open' : 'ws-sessions-open';
304
+ const other = which === 'pane' ? 'ws-sessions-open' : 'ws-pane-open';
305
+ const next = open != null ? open : !shell.classList.contains(cls);
306
+ shell.classList.toggle(cls, next);
307
+ if (next) shell.classList.remove(other);
308
+ const btn = document.querySelector('.ws-' + which + '-drawer-toggle');
309
+ if (btn) btn.setAttribute('aria-expanded', next ? 'true' : 'false');
310
+ // When opening, move focus into the drawer and arm an Esc-to-close once.
311
+ if (next) {
312
+ const drawer = shell.querySelector(which === 'pane' ? '.ws-pane' : '.ws-sessions');
313
+ const focusable = drawer && drawer.querySelector('button, a, input, [tabindex]');
314
+ if (focusable) try { focusable.focus(); } catch (_) {}
315
+ const onKey = (e) => { if (e.key === 'Escape') { toggleWsDrawer(which, false); document.removeEventListener('keydown', onKey); if (btn) try { btn.focus(); } catch (_) {} } };
316
+ document.addEventListener('keydown', onKey);
317
+ }
318
+ }
319
+ function closeWsDrawers() {
320
+ const shell = document.querySelector('.ws-shell');
321
+ if (!shell) return;
322
+ shell.classList.remove('ws-sessions-open', 'ws-pane-open');
323
+ document.querySelectorAll('.ws-sessions-drawer-toggle, .ws-pane-drawer-toggle').forEach((b) => b.setAttribute('aria-expanded', 'false'));
324
+ }
325
+
326
+ // Read persisted collapse state for a WorkspaceShell column so the layout is
327
+ // predictable across reloads (Claude-Desktop keeps the rail where you left it).
328
+ function wsCollapsed(which, fallback) {
329
+ try {
330
+ const v = localStorage.getItem('ds.ws.' + which);
331
+ if (v === 'collapsed') return true;
332
+ if (v === 'open') return false;
333
+ } catch (_) {}
334
+ return !!fallback;
335
+ }
336
+
337
+ // WorkspaceShell — a Claude-Desktop / cowork three-(or four-)column app shell.
338
+ //
339
+ // rail : the persistent left workspace nav (icon+label items, collapsible
340
+ // to icon-only). Pass the result of WorkspaceRail() or any vnode.
341
+ // sessions: an OPTIONAL second column (a conversation/session list) shown
342
+ // between the rail and the main content. Null hides it.
343
+ // main : the primary content column (chat thread, files view, dashboard...).
344
+ // pane : an OPTIONAL right context pane (per-conversation context, file
345
+ // preview...). Null hides it; collapsible when present.
346
+ // crumb : an optional thin top chrome bar (breadcrumb + status), spanning
347
+ // the content area only (the rail has its own header).
348
+ // status : an optional footer.
349
+ // narrow : caller's isNarrow() — drives the mobile single-column collapse.
350
+ // railCollapsed / paneCollapsed : initial collapse (persisted state wins).
351
+ //
352
+ // Pure stateless chrome (props in, vnode out). Collapse is DOM-class + a
353
+ // persisted flag, so the host does not have to thread collapse state through its
354
+ // own store. Visual styling lives in app-shell.css (.ws-*).
355
+ export function WorkspaceShell({ rail, sessions, main, pane, crumb, status, narrow,
356
+ railCollapsed = false, paneCollapsed = false,
357
+ railLabel = 'workspace navigation',
358
+ paneLabel = 'context' } = {}) {
359
+ const hasSessions = Boolean(sessions);
360
+ const hasPane = Boolean(pane);
361
+ const railIsCollapsed = wsCollapsed('rail', railCollapsed);
362
+ const paneIsCollapsed = hasPane ? wsCollapsed('pane', paneCollapsed) : true;
363
+ const shellCls = 'ws-shell'
364
+ + (railIsCollapsed ? ' ws-rail-collapsed' : '')
365
+ + (hasPane ? '' : ' ws-no-pane')
366
+ + (hasPane && paneIsCollapsed ? ' ws-pane-collapsed' : '')
367
+ + (hasSessions ? '' : ' ws-no-sessions')
368
+ + (narrow ? ' narrow' : '');
369
+ return h('div', { class: shellCls },
370
+ h('a', { href: '#ws-main', class: 'skip-link' }, 'skip to main content'),
371
+ // Left rail column. Its own toggle collapses it to icon-only.
372
+ h('nav', { class: 'ws-rail', role: 'navigation', 'aria-label': railLabel },
373
+ h('button', {
374
+ class: 'ws-rail-toggle', type: 'button',
375
+ // Label reflects the ACTION the click performs (expand when
376
+ // collapsed, collapse when expanded), not a static word - a
377
+ // stale "collapse navigation" on an already-collapsed rail
378
+ // mis-announces the control to AT.
379
+ 'aria-label': railIsCollapsed ? 'expand navigation' : 'collapse navigation',
380
+ title: railIsCollapsed ? 'expand navigation' : 'collapse navigation',
381
+ 'aria-expanded': railIsCollapsed ? 'false' : 'true',
382
+ onclick: () => toggleWs('rail'),
383
+ }, Icon('menu')),
384
+ rail || null),
385
+ // Tap-scrim behind an open mobile drawer; click anywhere dismisses.
386
+ h('div', { class: 'ws-scrim', 'aria-hidden': 'true', onclick: () => closeWsDrawers() }),
387
+ // Optional sessions column. On mobile it is a drawer; selecting a row
388
+ // (any button click inside) auto-closes it, mirroring AppShell.
389
+ hasSessions
390
+ ? h('div', { class: 'ws-sessions', role: 'complementary', 'aria-label': 'conversations',
391
+ onclick: (e) => { if (narrow && e.target.closest('button, a')) closeWsDrawers(); } }, sessions)
392
+ : null,
393
+ // Primary content column, with an optional thin crumb bar on top. On
394
+ // mobile the crumb hosts the drawer toggles (sessions on the left, pane
395
+ // on the right) so both overlay columns are reachable - without them the
396
+ // conversation list and context pane are dead on <=900px.
397
+ h('div', { class: 'ws-content' },
398
+ crumb
399
+ ? h('div', { class: 'ws-crumb' },
400
+ hasSessions ? h('button', {
401
+ class: 'ws-drawer-toggle ws-sessions-drawer-toggle', type: 'button',
402
+ 'aria-label': 'toggle conversations', 'aria-expanded': 'false',
403
+ onclick: () => toggleWsDrawer('sessions'),
404
+ }, Icon('thread')) : null,
405
+ h('div', { class: 'ws-crumb-main' }, crumb),
406
+ hasPane ? h('button', {
407
+ class: 'ws-drawer-toggle ws-pane-drawer-toggle', type: 'button',
408
+ 'aria-label': 'toggle context pane', 'aria-expanded': 'false',
409
+ onclick: () => toggleWsDrawer('pane'),
410
+ }, Icon('page')) : null)
411
+ : null,
412
+ h('main', { class: 'ws-main' + (narrow ? ' narrow' : ''), id: 'ws-main', tabindex: '-1' },
413
+ ...(Array.isArray(main) ? main : [main])),
414
+ status || null),
415
+ // Optional right context pane with its own collapse toggle.
416
+ hasPane
417
+ ? h('aside', { class: 'ws-pane', role: 'complementary', 'aria-label': paneLabel },
418
+ h('button', {
419
+ class: 'ws-pane-toggle', type: 'button',
420
+ 'aria-label': paneIsCollapsed ? 'show context pane' : 'hide context pane',
421
+ title: paneIsCollapsed ? 'show context pane' : 'hide context pane',
422
+ 'aria-expanded': paneIsCollapsed ? 'false' : 'true',
423
+ onclick: () => toggleWs('pane'),
424
+ }, Icon(paneIsCollapsed ? 'chevron-left' : 'chevron-right')),
425
+ pane)
426
+ : null,
427
+ );
428
+ }
429
+
430
+ // WorkspaceRail — the contents of the WorkspaceShell left rail: a brand/header,
431
+ // a primary action (New chat), and a list of nav items. Each item collapses to
432
+ // an icon when the rail is collapsed (the label is kept in the DOM for AT and
433
+ // shown via CSS when expanded).
434
+ //
435
+ // brand : short product name shown in the rail header.
436
+ // action : { label, icon, onClick } a prominent primary button (New chat).
437
+ // items : [{ key, label, icon, active, count, onClick }] nav entries.
438
+ // footer : optional vnode pinned to the rail bottom (e.g. settings/theme).
439
+ export function WorkspaceRail({ brand = '247420', action, items = [], footer } = {}) {
440
+ return h('div', { class: 'ws-rail-inner' },
441
+ h('div', { class: 'ws-rail-head' },
442
+ h('span', { class: 'ws-rail-brand' }, brand)),
443
+ action
444
+ ? h('button', {
445
+ class: 'ws-rail-action', type: 'button',
446
+ 'aria-label': action.label,
447
+ onclick: action.onClick || null,
448
+ }, action.icon ? Icon(action.icon) : null, h('span', { class: 'ws-rail-action-label' }, action.label))
449
+ : null,
450
+ h('ul', { class: 'ws-rail-nav', role: 'list' },
451
+ ...items.map((it) => h('li', { key: it.key || it.label, role: 'listitem' },
452
+ h('button', {
453
+ type: 'button',
454
+ class: 'ws-rail-item' + (it.active ? ' active' : ''),
455
+ 'aria-current': it.active ? 'page' : null,
456
+ 'aria-label': it.label + (it.count ? ' (' + it.count + ')' : ''),
457
+ title: it.label,
458
+ onclick: it.onClick || null,
459
+ },
460
+ it.icon ? Icon(it.icon) : h('span', { class: 'ws-rail-item-glyph', 'aria-hidden': 'true' }),
461
+ h('span', { class: 'ws-rail-item-label' }, it.label),
462
+ (it.count != null && it.count !== 0 && it.count !== '0')
463
+ ? h('span', { class: 'ws-rail-item-count', 'aria-hidden': 'true' }, String(it.count))
464
+ : null)))),
465
+ footer ? h('div', { class: 'ws-rail-foot' }, footer) : null,
466
+ );
467
+ }
468
+
280
469
  export function Heading({ level = 1, children, style = '', 'aria-level': ariaLevel }) {
281
470
  return h('h' + level, { style, 'aria-level': ariaLevel != null ? String(ariaLevel) : null }, children);
282
471
  }
package/src/components.js CHANGED
@@ -6,6 +6,7 @@ export const h = webjsx.createElement;
6
6
  export {
7
7
  Brand, Chip, Btn, Glyph, Icon, IconButton, Badge,
8
8
  Topbar, Crumb, Side, Status, AppShell,
9
+ WorkspaceShell, WorkspaceRail,
9
10
  Heading, Lede, Dot, Rail
10
11
  } from './components/shell.js';
11
12
 
@@ -26,9 +27,15 @@ export {
26
27
 
27
28
  export { AgentChat } from './components/agent-chat.js';
28
29
 
30
+ export {
31
+ ConversationList, SessionCard, SessionDashboard
32
+ } from './components/sessions.js';
33
+
34
+ export { ContextPane } from './components/context-pane.js';
35
+
29
36
  export {
30
37
  fileGlyph, fmtFileSize,
31
- FileIcon, FileRow, FileGrid, FileToolbar,
38
+ FileIcon, FileRow, FileGrid, FileSkeleton, sortFiles, FileToolbar,
32
39
  DropZone, UploadProgress, EmptyState, BreadcrumbPath
33
40
  } from './components/files.js';
34
41
 
@@ -1307,3 +1307,43 @@ html.ds-247420 { touch-action: pan-x pan-y; overscroll-behavior: none; -webkit-t
1307
1307
  line-height: 1.2;
1308
1308
  margin: 0;
1309
1309
  }
1310
+
1311
+ /* gm memory browser — recall hits rendered as interactive, prunable cards
1312
+ (thebird's gm app, docs/apps.js recall panel). */
1313
+ .ds-247420 .gm-recall-hits {
1314
+ display: flex;
1315
+ flex-direction: column;
1316
+ gap: 6px;
1317
+ margin: 8px 0;
1318
+ }
1319
+ .ds-247420 .gm-recall-hit {
1320
+ border: 1px solid color-mix(in oklab, var(--fg) 14%, transparent);
1321
+ border-radius: 6px;
1322
+ padding: 8px 10px;
1323
+ background: color-mix(in oklab, var(--bg) 92%, var(--fg) 2%);
1324
+ }
1325
+ .ds-247420 .gm-recall-hit.is-pruned {
1326
+ opacity: 0.45;
1327
+ text-decoration: line-through;
1328
+ }
1329
+ .ds-247420 .gm-recall-meta {
1330
+ font-family: var(--os-mono, ui-monospace, monospace);
1331
+ font-size: 11px;
1332
+ color: color-mix(in oklab, var(--fg) 60%, transparent);
1333
+ margin-bottom: 4px;
1334
+ }
1335
+ .ds-247420 .gm-recall-text {
1336
+ font-size: 13px;
1337
+ line-height: 1.4;
1338
+ white-space: pre-wrap;
1339
+ word-break: break-word;
1340
+ }
1341
+ .ds-247420 .gm-recall-acts {
1342
+ display: flex;
1343
+ justify-content: flex-end;
1344
+ margin-top: 6px;
1345
+ }
1346
+ .ds-247420 .gm-recall-forget {
1347
+ font-size: 11px;
1348
+ padding: 2px 8px;
1349
+ }