@joewinke/jatui 0.26.0 → 0.27.0

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 (73) hide show
  1. package/CLAUDE.md +106 -1
  2. package/dist/actions/keyboardScope.d.ts +58 -0
  3. package/dist/actions/keyboardScope.js +110 -0
  4. package/dist/actions/listNav.d.ts +48 -3
  5. package/dist/actions/listNav.js +267 -77
  6. package/dist/actions/longpress.d.ts +37 -0
  7. package/dist/actions/longpress.js +105 -0
  8. package/dist/actions/stickToBottom.d.ts +66 -0
  9. package/dist/actions/stickToBottom.js +155 -0
  10. package/dist/actions/swipe.d.ts +64 -0
  11. package/dist/actions/swipe.js +179 -0
  12. package/dist/components/BottomSheet.svelte +280 -0
  13. package/dist/components/BottomSheet.svelte.d.ts +27 -0
  14. package/dist/components/ConfirmDialog.svelte +21 -0
  15. package/dist/components/ConfirmDialog.svelte.d.ts +11 -1
  16. package/dist/components/ConfirmModal.svelte +15 -0
  17. package/dist/components/ConfirmModal.svelte.d.ts +5 -0
  18. package/dist/components/Drawer.svelte +18 -1
  19. package/dist/components/Drawer.svelte.d.ts +9 -1
  20. package/dist/components/InputDialog.svelte +21 -0
  21. package/dist/components/InputDialog.svelte.d.ts +11 -1
  22. package/dist/components/Modal.svelte +18 -1
  23. package/dist/components/Modal.svelte.d.ts +8 -0
  24. package/dist/components/basic/Empty.svelte +173 -9
  25. package/dist/components/basic/Empty.svelte.d.ts +50 -1
  26. package/dist/components/basic/ErrorState.svelte +128 -0
  27. package/dist/components/basic/ErrorState.svelte.d.ts +46 -0
  28. package/dist/components/basic/JumpToLatest.svelte +60 -0
  29. package/dist/components/basic/JumpToLatest.svelte.d.ts +9 -0
  30. package/dist/components/basic/PageHeader.svelte +216 -0
  31. package/dist/components/basic/PageHeader.svelte.d.ts +52 -0
  32. package/dist/components/basic/ProgressButton.svelte +221 -0
  33. package/dist/components/basic/ProgressButton.svelte.d.ts +63 -0
  34. package/dist/components/graph/GraphApiBridge.svelte +36 -0
  35. package/dist/components/graph/GraphApiBridge.svelte.d.ts +9 -0
  36. package/dist/components/graph/GraphCanvas.svelte +212 -27
  37. package/dist/components/graph/GraphCanvas.svelte.d.ts +113 -19
  38. package/dist/components/graph/api.d.ts +52 -0
  39. package/dist/components/graph/api.js +66 -0
  40. package/dist/components/graph/index.d.ts +2 -0
  41. package/dist/components/graph/index.js +1 -0
  42. package/dist/components/graph/theme.js +13 -0
  43. package/dist/components/messaging/MessageItem.svelte +5 -0
  44. package/dist/components/messaging/MessageItem.svelte.d.ts +7 -0
  45. package/dist/components/table/DataTable.svelte +452 -78
  46. package/dist/components/table/DataTable.svelte.d.ts +2 -2
  47. package/dist/components/table/ManageColumnsDropdown.svelte +466 -0
  48. package/dist/components/table/ManageColumnsDropdown.svelte.d.ts +26 -0
  49. package/dist/components/table/columnStorage.d.ts +29 -0
  50. package/dist/components/table/columnStorage.js +61 -0
  51. package/dist/components/table/groupRows.d.ts +72 -0
  52. package/dist/components/table/groupRows.js +118 -0
  53. package/dist/components/table/index.d.ts +7 -1
  54. package/dist/components/table/index.js +3 -0
  55. package/dist/components/table/types.d.ts +121 -20
  56. package/dist/index.d.ts +25 -2
  57. package/dist/index.js +32 -1
  58. package/dist/utils/ansiToHtml.d.ts +83 -0
  59. package/dist/utils/ansiToHtml.js +543 -0
  60. package/dist/utils/keyboardShortcutRegistry.d.ts +332 -11
  61. package/dist/utils/keyboardShortcutRegistry.js +1070 -76
  62. package/dist/utils/pollBackoff.d.ts +83 -0
  63. package/dist/utils/pollBackoff.js +151 -0
  64. package/dist/utils/snapFlick.d.ts +59 -0
  65. package/dist/utils/snapFlick.js +66 -0
  66. package/dist/video/advisors/qr.d.ts +1 -1
  67. package/dist/virtual/createRowWindow.d.ts +75 -0
  68. package/dist/virtual/createRowWindow.js +129 -0
  69. package/dist/virtual/index.d.ts +4 -0
  70. package/dist/virtual/index.js +5 -0
  71. package/dist/virtual/rowWindowCore.d.ts +202 -0
  72. package/dist/virtual/rowWindowCore.js +510 -0
  73. package/package.json +11 -1
package/CLAUDE.md CHANGED
@@ -56,6 +56,7 @@ Then import:
56
56
  | Component | Purpose | Key Props |
57
57
  |-----------|---------|-----------|
58
58
  | `Button` | Styled button with variants | `variant`, `size`, `loading`, `disabled`, `onclick` |
59
+ | `ProgressButton` | Multi-phase async submit button (upstreamed from the JAT IDE): walks through the **named phases** of the work (`phases: [{key,label}]` in order) instead of a blank spinner — spinner + current phase label with shimmer, a fill bar that advances to `(index+1)/N`, disabled while busy, green check-flash on `success`. Caller owns the state machine (`phase` / `success`). Renders through the shared `Button`, so `class` composes with join patterns. | `phase?` (null = idle), `phases?: Phase[]`, `idleLabel?`, `success?`, `successLabel?`, `variant?`, `size?`, `disabled?`, `type?`, `class?`, `onclick?`, `children` |
59
60
  | `Badge` | Status/label badge | `variant`, `size`, `children` (snippet) |
60
61
  | `StatusBadge` | Status indicator with dot | `status: StatusValue`, `label` |
61
62
  | `Card` | Content card container | `title`, `children` (snippet) |
@@ -65,7 +66,10 @@ Then import:
65
66
  | `LoadingSpinner` | Spinner animation | `size`, `color` |
66
67
  | `Sparkline` | Inline mini chart | `data: number[]`, `width`, `height`, `color` |
67
68
  | `LazyImage` | Image with lazy loading + placeholder | `src`, `alt`, `placeholder` |
69
+ | `Empty` | Zero-state surface — v1 thin copy/children block still works; pass `title` for the v2 hero layout (upstreamed from the IDE's EmptyState): named `glyph` (11 curated Heroicons), `size` (sm/md/lg), `pulse` (reduced-motion gated, tinted with `--color-primary`), `icon` snippet override, CTA `actions` snippet, `description`, extra `children` below. | `title` (opts into v2), `description`, `glyph`, `size`, `pulse`, `icon`/`actions` (snippets), `copy`, `class`, `children` |
70
+ | `ErrorState` | Standard "this failed" surface (upstreamed from the IDE's ErrorState) — error-token themed icon, optional Retry button, extra `actions`, collapsible `details` block for raw error output. Centered card mirroring `Empty` v2. | `title`, `message`, `details`, `retryLabel`, `onRetry`, `size`, `actions` (snippet) |
68
71
  | `HunkDiffView` | Side-by-side diff with line numbers, red/green backgrounds, strikethrough on removed lines, inline word-level highlights via the `diff` lib. Used for reviewing YAML/text changes per hunk. | `oldText: string \| null`, `newText: string \| null`, `unified?: boolean` |
72
+ | `JumpToLatest` | "Jump to latest" pill for streaming transcripts (pairs with `createStickToBottom`) — shows when the reader has scrolled away from the live edge, badges the unread arrival count, jumps + resumes following on click. Position inside a `relative` wrapper that also holds the scroll element. | `visible`, `unreadCount`, `label`, `onclick` |
69
73
  | `ContextMenu` | Floating right-click (or programmatic) action menu, positioned via `use:floating` at a cursor point. Real `<button>`s, per-item `icon`/`variant`/`disabled`, dividers, click-outside + Escape dismiss, arrow-key/Home/End roving nav (focus starts on the first item). Items may declare `submit: SubmitConfig` to POST to a SvelteKit form action instead of (or in addition to) `onSelect` — a hidden `<form>` is built, requestSubmit()ed, then removed, so `use:enhance` fires exactly like a regular form submission. **Items also support nested `submenu: ContextMenuItem[]`** (right-side popover on hover/focus/ArrowRight, chevron indicator, ArrowLeft/Escape closes just the submenu — submenus open on parent-item focus **without stealing focus into the submenu**; ArrowRight is the explicit "enter the submenu" gesture) **and inline `content: Snippet`** (replaces the default icon+label row with an arbitrary widget — the wrapping button still closes the menu, so widgets that shouldn't must `event.stopPropagation()`). `autoFocus` (default true) can be set false to suppress auto-focus of the first item on open. | `open` (bindable), `x`, `y`, `items: ContextMenuItem[]`, `header?`, `zIndex?`, `autoFocus?`, `onClose?` |
70
74
  | `ContextMenuTrigger` | Wraps content with the standard right-click + "⋯" touch affordance, backed by `ContextMenu`. Use instead of hand-rolling `oncontextmenu` + a kebab button per row/card. | `items: ContextMenuItem[]`, `children` (snippet), `header?`, `triggerLabel?`, `hideButton?`, `zIndex?`, `class?` |
71
75
  | `Tooltip` | Component-shaped wrapper around the shared `use:tooltip` action — for cases where you can't easily add an attribute to the target element (wrap a chip / badge / icon in the tooltip instead). Warm/cool window + `aria-describedby` are shared with the action. Prefer `use:tooltip` directly when you own the element. | `content: string`, `placement?`, `delayMs?`, `disabled?`, `display?`, `children` (snippet) |
@@ -75,6 +79,7 @@ Then import:
75
79
  | `BatchJobTable` | Generic queue runner (harvested from vid-gen's BatchGenerator) — `<BatchJobTable generics T>` over bindable `JobRow<T>[]`; host supplies async `run(data, {signal, attempt, reportProgress})`. Concurrency cap, optional per-run `processLimit` (quota budget), rolling-average ETA, per-row status badge / progress / elapsed / retry / skip / cancel / reorder — each cell a snippet override (`statusCell`, `progressCell`, `elapsedCell`, `actionsCell`), host data columns via `headers` + `cells`, extra toolbar content via `toolbar`. Uses `showToast` / `showConfirm` (mount `ToastHost` + `ConfirmDialog`). Exposes `start()`/`pause()`/`cancel()`/`retryFailed()`/`clearFinished()`. Pure scheduling helpers exported too (`nextJobsToStart`, `moveJob`, `summarizeJobs`, `toJobRows`). | `jobs` (bindable), `run`, `concurrencyLimit?` (3), `processLimit?`, `autoStart?`, `notify?`, `confirmDestructive?`, `onJobDone?`, `onJobFailed?`, `onBatchDone?` |
76
80
  | `QuotaMeter` | "312 / 500 credits · resets in 4h 10m" bar — warning tint at `warnAt`, error at 100%. Presentational; the host owns the ledger. | `used`, `limit`, `resetsAt?`, `label?`, `unit?`, `warnAt?` (0.8), `size?` |
77
81
  | `VariantMatrixPicker` | Category/option chip grid for building a combinatorial variant matrix (harvested from vid-gen's FrameVariantGenerator, presets stripped — caller-supplied `categories`). Each category tab shows a live selection count and an additive/multiplicative badge: multiplicative selections cartesian-multiply the total, additive ones stack flat onto every combination. Live combination count in the footer; algebra exported separately as pure functions (`generateCombinations`, `countCombinations`, `variantCategoryMode`, `findVariantOption`, `generateVariantFilename`). | `categories: VariantCategory[]`, `selected` (bindable `VariantSelection`), `forceMode?`, `showCount?` (true), `countLabel?`, `onChange?`, `class?` |
82
+ | `BottomSheet` | Mobile bottom sheet (upstreamed from the JAT IDE — jatui-rl7w6.2): drag handle with pointer capture, backdrop close, optional header. `snap='dual'` enables the inspector pattern — peek → expanded snap heights with flick-momentum release (fast flick ≥ 0.5 px/ms wins over position; flick down from peek dismisses) and `visualViewport` keyboard avoidance so content clears the soft keyboard. | `open` (bindable), `title?`, `zIndex?` (70), `fullHeight?`, `snap?` ('single'\|'dual'), `peekFraction?` (0.4), `expandedFraction?` (0.9), `onclose?`, `children` (snippet) |
78
83
 
79
84
  ### Tables (subpath: `jatui/table`)
80
85
 
@@ -82,7 +87,15 @@ Requires peer `@tanstack/table-core`. `import { DataTable, flexRender, columnRes
82
87
 
83
88
  | Component | Purpose | Key Props |
84
89
  |-----------|---------|-----------|
85
- | `DataTable` | TanStack-backed table (generic over the row type): sorting, selection, client pagination, skeleton. Grid-mode hooks per jat `docs/design/datatable-virtualisation-group-rows.md` §3.3: `getRowId` (keys selection/expansion/api by id, renders `data-row-id`); controlled sort — `bind:sorting`, `initialSorting`, `manualSorting` (caller pre-sorts `data`), `onSortingChange(next, {field, shiftKey})`; `rowProps(row, item)` → `{class, attrs, onclick, oncontextmenu, ondragover, ondrop}` (`attrs` for `data-nav-id`, aria); `onRowClick(row, e)` (skips clicks on interactive children); `headerProps(columnId)`; `expandedRow` snippet `(row, {colspan})` — click/Enter toggles unless `isExpanded(row)` makes it controlled; `empty` snippet; `variant` `zebra`/`plain`/`unstyled` + `tableClass`; `bind:api` → `orderedRowIds()`, `scrollToRow(id)`. Sortable headers are keyboard-operable. | `data`, `columns`, `sortable`, `selectable`, `paginated`, `getRowId?`, `sorting?` (bindable), `rowProps?`, `onRowClick?`, `expandedRow?`, `empty?`, `variant?`, `api?` (bindable) |
90
+ | `DataTable` | TanStack-backed table (generic over the row type): sorting, selection, client pagination, skeleton. Grid-mode hooks per jat `docs/design/datatable-virtualisation-group-rows.md` §3.3: `getRowId` (keys selection/expansion/api by id, renders `data-row-id`); controlled sort — `bind:sorting`, `initialSorting`, `manualSorting` (caller pre-sorts `data`), `onSortingChange(next, {field, shiftKey})`; `rowProps(row, item)` → `{class, attrs, onclick, oncontextmenu, ondragover, ondrop}` (`attrs` for `data-nav-id`, aria); `onRowClick(row, e)` (skips clicks on interactive children); `headerProps(columnId)`; `expandedRow` snippet `(row, {colspan})` — click/Enter toggles unless `isExpanded(row)` makes it controlled; `empty` snippet; `variant` `zebra`/`plain`/`unstyled` + `tableClass`; `bind:api` → `orderedRowIds()`, `scrollToRow(id)`. Sortable headers are keyboard-operable. **Grid mode** (all opt-in; default markup is snapshot-gated against 0.26.0): `groupBy: GroupLevel[]` renders `groupRows` header rows (`data-group-key` = `::`-joined path), `bind:collapsed` / `onCollapsedChange`, `groupLabel`, `groupHeader` snippet `(group, {toggle, colspan, collapsed})` renders the header CELLS; `stickyHeader` + `stickyGroupHeaders` (offset by the thead height and `--dt-group-row-h`); `maxHeight` makes the table its own scroller (else sticky clips horizontal overflow); `virtual={{ createRowWindow, estimateRowSize, overscan, threshold, scrollRoot }}` windows the body with spacer rows that keep zebra parity — pass `createRowWindow` from `jatui/virtual` so `jatui/table` never needs the virtual-core peer; read once at mount; `pinnedRowIds`; `ariaGrid` → `grid`/`treegrid`, `aria-selected`, `aria-level`, `aria-rowindex`/`rowcount` (windowed tables always get the rowindex pair); `ariaLabel`. One TanStack instance per mount, updated with `setOptions` — a selection or sort change re-renders no cell. | `data`, `columns`, `sortable`, `selectable`, `paginated`, `getRowId?`, `sorting?` (bindable), `rowProps?`, `onRowClick?`, `expandedRow?`, `empty?`, `variant?`, `api?` (bindable), `groupBy?`, `collapsed?` (bindable), `groupHeader?`, `stickyHeader?`, `stickyGroupHeaders?`, `maxHeight?`, `virtual?`, `pinnedRowIds?`, `ariaGrid?` |
91
+
92
+ ### Graph (subpath: `jatui/graph`)
93
+
94
+ Requires peer `@xyflow/svelte` (install it — the IDE's build breaks on `import … from 'jatui/graph'` without it). `import { GraphCanvas, JatuiNode, type GraphCanvasApi } from 'jatui/graph'`.
95
+
96
+ | Component | Purpose | Key Props |
97
+ |-----------|---------|-----------|
98
+ | `GraphCanvas` | Svelte Flow wrapper with the DaisyUI → `--xy-*` theme bridge — read-only status graphs AND node editors. Props mirror `SvelteFlow`'s own names, so xyflow docs apply. Custom `nodeTypes` merge over `{ jatuiNode }` (**keep the object stable** — xyflow re-mounts every node when its identity changes); `edgeTypes`/`defaultEdgeOptions`; editing (`isValidConnection`, `onconnect`, `onbeforedelete`/`ondelete`, `deleteKey`, `snapToGrid`/`snapGrid`, `connectionMode`, key options, `zoomOnDoubleClick`); events forwarded verbatim (`onnodeclick`/`onNodeClick` alias, `onnodecontextmenu`, `onnodedragstart`/`stop`, `onedgeclick`, `onpaneclick`, `onselectionchange`, `onmoveend`). `bind:viewport` stays live even when seeded `undefined`. `children` renders INSIDE `<SvelteFlow>` (Panels, anything calling `useSvelteFlow()`). `bind:this` exposes `api: GraphCanvasApi` (`fitView`, `zoomTo` — centre-anchored, `getZoom`, `setCenter`, `screenToFlowPosition`, `getViewport`; all no-ops before init) plus the same methods (except `fitView`, whose name the prop owns) directly. The no-at-rest-shadow rule strips the node **wrapper** shadow inside `.graph-canvas` only — draw status glows on an inner element. xyflow has no `onnodedoubleclick`: handle `ondblclick` inside the node component. Use `$state.raw` for nodes/edges. | `nodes`/`edges` (bindable), `viewport` (bindable), `nodeTypes?`, `edgeTypes?`, `readonly?`, `fitView?`, `minimap?: boolean \| MiniMapProps`, `background?: false \| BackgroundProps`, `controls?`, `minZoom?`/`maxZoom?`, `children?` |
86
99
 
87
100
  ### Media
88
101
 
@@ -104,6 +117,20 @@ Requires peer `@tanstack/table-core`. `import { DataTable, flexRender, columnRes
104
117
 
105
118
  **Types:** `VideoChapter`, `VideoSpeakerTurn`, `VideoTranscriptSegment`, `VideoBookmark`, `VideoWaveform`, `VideoSpeakerTalkTime`. `VideoWaveform.peaks` normalise against their own max, so 0–1, 0–99 and 0–255 arrays all work.
106
119
 
120
+ ### Table (subpath: `jatui/table`)
121
+
122
+ `DataTable` (TanStack table-core), `flexRender`, `columnResize` and two column-management companions (upstreamed from the jat IDE, audit jat-1alfp U9):
123
+
124
+ | Component / util | Purpose | Key Props |
125
+ |-----------|---------|-----------|
126
+ | `ManageColumnsDropdown` | Show/hide + drag-reorder table columns, with a search filter and a last-visible-column lock (can't hide the final column). Panel renders with `position:fixed` viewport coordinates by default (`appendToBody`, SearchDropdown pattern) so it can never be clipped by an `overflow-x:auto`/`overflow:hidden` ancestor — the clipping bug the IDE copy still has (jat-hwt1h). Consumer owns the state: pass `columnOrder` + `hiddenColumns` and apply the `onReorder`/`onToggleVisibility` callbacks; `onReset` adds a "Reset to default" footer. | `columnOrder`, `hiddenColumns: Set<string>`, `getLabel`, `onReorder`, `onToggleVisibility`, `onReset?`, `label?`, `appendToBody?` (default true) |
127
+ | `columnStorage` | localStorage persistence for column settings — `saveColumnSettings(key, settings)` / `loadColumnSettings(key, allColumnIds)`. Load merges newly added column ids at the end and drops removed ones; returns null when nothing saved (or corrupt/absent localStorage — SSR-safe silent no-ops). | — |
128
+
129
+ ```typescript
130
+ import type { ColumnSettings } from 'jatui/table'
131
+ import { saveColumnSettings, loadColumnSettings } from 'jatui/table'
132
+ ```
133
+
107
134
  ### Meetings (subpath: `jatui/meet`)
108
135
 
109
136
  Real-time video-meeting UI. `livekit-client` is an **OPTIONAL peer dep**, lazily
@@ -145,12 +172,30 @@ import { loadLiveKit, isLiveKitAvailable, LiveKitNotInstalledError } from 'jatui
145
172
 
146
173
  | Component | Purpose | Key Props |
147
174
  |-----------|---------|-----------|
175
+ | `PageHeader` | The standard route header (upstreamed from the JAT IDE): uppercase/tracked "instrument label" title + slots for a count chip, subtitle, leading `icon` snippet, right-aligned `actions` cluster, `meta` metrics strip and content below the header row. `sticky` pins it to the top of the scroll container; `size='lg'` for landing screens. All color via `--color-*` tokens; entrance animation reduced-motion gated (`animate`, default off). | `title`, `subtitle?`, `count?`, `size?` ('md'\|'lg'), `sticky?`, `animate?`, `icon?`/`actions?`/`meta?`/`children` (snippets) |
148
176
  | `ResizablePanel` | Resizable container | `minWidth`, `maxWidth`, `defaultWidth` |
149
177
  | `ResizableDivider` | Drag handle between panels | `onResize` |
150
178
  | `InfiniteGrid` | Cursor-paged infinite-scroll grid (from chimaro). `loadMore(cursor, {signal})` resolves `{items, nextCursor, total?}` (`nextCursor: null` = end); cells render through the `renderItem(item, i)` snippet. Change `resetKey` to start over — the in-flight page is aborted and a late page for the old key is dropped. One request in flight; items de-duped by `getKey`; short pages auto-chain until the sentinel leaves the screen; distinct skeleton / empty / exhausted / error(+Retry) states; non-item rows are `overflow-anchor: none` so scroll anchoring can't runaway-load. Scroll root = `root` or nearest scrollable ancestor. Exposes `reset()`/`retry()`. Not virtualized. Pure core: `createInfiniteLoader`. | `loadMore`, `renderItem` (snippet), `getKey?`, `resetKey?`, `columns?` / `minItemWidth?`, `gap?`, `root?`, `loadAhead?` (400px), `skeleton?`/`empty?`/`end?`/`error?` (snippets), `emptyText?`, `endText?`, `onStateChange?` |
151
179
  | `InfiniteSearchGrid` | Search box + `InfiniteGrid`. Query is debounced, then becomes the grid's `resetKey` (Enter commits now, Escape clears). Below `minQueryLength` no request is made and `prompt` shows. All other `InfiniteGrid` props pass through. | `search(query, cursor, {signal})`, `renderItem` (snippet), `query` (bindable), `debounceMs?` (300), `minQueryLength?` (0), `placeholder?`, `noResultsText?`/`noResults?`, `prompt?`/`promptText?`, `toolbar?` (snippet) |
152
180
  | `SectionNav` | Sticky in-page section index for long single-column pages — vertical rail beside the content on xl+, wrapping chip row above it below xl. Built-in scroll-spy (omit `activeId` and it just works; `bind:activeId` to observe/control — the spy writes back to it): on every observer callback + rAF-throttled scroll/resize it measures ALL sections and activates the last one whose top is at/above the activation line (`activationRatio`, default 0.3 of the scroller height); at the bottom of the page the last section wins. Clicking an item activates it immediately and holds the spy off until the smooth scroll lands (`scrollend`, or 800ms after the last scroll event); reduced-motion scrolls instantly. Pure decision fn exported as `computeActiveSection(rects, viewport, scroll, ratio?)`. Amber dot per `dirtyIds` marks sections holding unsaved edits. `go()` scrolls AND moves focus so keyboard/SR users land where the click did. j/k/Enter via `listNav`. Layout contract: parent is a `flex flex-wrap gap-8 items-start` row, content column `flex-1 min-w-0`; sections need `id` + `scroll-mt-*`. | `sections: {id,label}[]`, `activeId?` (bindable), `dirtyIds?: Set<string>`, `ariaLabel?`, `spy?`, `activationRatio?` (0.3) |
153
181
 
182
+ **Windowing (subpath: `jatui/virtual`, optional peer `@tanstack/virtual-core`).** `createRowWindow(opts)` is headless variable-height windowing built on **spacer rows**, not absolute positioning, so it works inside a `<table>` and keeps `table-layout: fixed` and sticky `<thead>`s. It returns reactive `items` (`{index, key, start, size, end, gapBefore, sticky, pinned}`), `paddingTop`, `paddingBottom`, `totalSize` and `windowed`, plus `use:win.anchor` (on the `<tbody>` or list container, whose top is item 0's top) and `use:win.measure={item.key}` (on each item; a shared ResizeObserver re-measures it). Reading a row's height for the first time, or later re-measuring one above the viewport, shifts `scrollTop` by the delta so nothing jumps. The anchor gets `overflow-anchor: none` so native scroll anchoring doesn't correct the shift a second time. The options are reactive getters: `count`, `getKey`, `estimateSize`, `pinned()` (keys kept rendered, e.g. an open editor or exiting row), `stickyIndices()` (a flat list, or one array per level `[projectHdrs, epicHdrs]`; the active header of each level stays rendered, and an inner header never outlives its outer group), and `threshold` (render everything below N; pass it as a getter to make it reactive). Also `scrollRoot` (`'auto'` = the nearest ancestor that really scrolls, else the window; it re-resolves on resize), `overscan` (6), `scrollPaddingStart/End` and `initialViewportSize` (800; used for SSR and first paint). Methods: `scrollToIndex`/`scrollToKey(…, {align})` (queued until mount), `indexOf`, `remeasure()`. Render `item.gapBefore` as a spacer before the item; it is non-zero only when a pinned or sticky item sits outside the slice. Pure policy helpers are exported too (`extractWindowIndices`, `activeStickyIndices`, `layoutWindow`, `findScrollRoot`, `RowWindowEngine`). Demo: `/components/row-window`.
183
+
184
+ ```svelte
185
+ <script>
186
+ import { createRowWindow } from 'jatui/virtual'
187
+ const win = createRowWindow({ count: () => rows.length, getKey: (i) => rows[i].id, estimateSize: () => 44 })
188
+ </script>
189
+ <tbody use:win.anchor>
190
+ <tr style:height="{win.paddingTop}px"></tr>
191
+ {#each win.items as it (it.key)}
192
+ {#if it.gapBefore}<tr style:height="{it.gapBefore}px"></tr>{/if}
193
+ <tr use:win.measure={it.key}>…rows[it.index]…</tr>
194
+ {/each}
195
+ <tr style:height="{win.paddingBottom}px"></tr>
196
+ </tbody>
197
+ ```
198
+
154
199
  ### Audio & Voice
155
200
 
156
201
  | Component | Purpose | Key Props |
@@ -237,6 +282,8 @@ const callbacks: AnnotationCallbacks = {
237
282
 
238
283
  **Shortcut registry** (`createShortcutRegistry()`, or the lazy `getShortcutRegistry()` singleton) — the data source for the overlay, so apps stop hand-wiring a keydown handler plus a separate list for the overlay. Shortcuts register against a **context** and return an unregister fn (return it from `$effect`). Contexts form a stack: `global` is the bottom, `pushContext(name, { modal?, label? })` returns a pop fn, the top context shadows the same combo lower down, and a `modal` context suppresses everything beneath it (including `global`). Handled for you: no firing while typing in text inputs/textarea/select/contenteditable (opt in per shortcut with `allowInInput`), IME composition and `defaultPrevented` events are skipped, `mod` = ⌘ on macOS / Ctrl elsewhere, exact modifier matching (`?` doesn't need `shift`), and macOS Option combos fall back to `event.code`. Per shortcut: `enabled()`, `allowRepeat`, `preventDefault`, `group` (overlay section), `hidden`, `label` (keycap override).
239
284
 
285
+ **keyboardScope** (`use:keyboardScope({ 'mod+s': save, … })` or bare `createKeyboardScope(container)`) — the *container-local* complement to the registry's context stack (upstreamed from the JAT IDE): embed-safe shortcuts for a self-contained widget (pane, drawer, embedded panel) inside a busy host page. Ownership is decided from DOM engagement, not a pushed context: a shortcut fires when focus is inside the container, or when the target is body/html AND the most recent pointerdown/focusin (capture-phase) landed inside — the "engaged" heuristic, so a pane owns Delete/mod+Z right after a click without permanently stealing the keys. Never fires while typing in a text-entry element (shares the registry's `isTypingTarget`), even for modifier combos. `comboFor(event)` normalizes a keydown to the handler-map key (fixed `shift,alt,mod` order; `mod` = ⌘/Ctrl; backspace ≡ delete). Use the registry for app keymaps; use keyboardScope inside an embeddable widget that must own a few keys.
286
+
240
287
  ```svelte
241
288
  <script>
242
289
  import { createShortcutRegistry, KeyboardShortcutsOverlay, SIDEBAR_TOGGLE_KEY } from "jatui"
@@ -252,6 +299,14 @@ const callbacks: AnnotationCallbacks = {
252
299
  <KeyboardShortcutsOverlay registry={shortcuts} />
253
300
  ```
254
301
 
302
+ **Rebinding.** Give a definition an `id` and its `key` (a string or an array of combos) becomes the *default*. The live binding is the user's override: pass `bindings: localStorageBindingStore('app.shortcuts', { migrate? })` (or any `BindingStore`) to `createShortcutRegistry`, then `registry.rebind(id, 'ctrl+shift+n' | [] | null, { onConflict: 'reject' | 'swap' | 'allow' })`, `resetBinding(id)`, `resetAllBindings()`. Stored entries that can't be parsed are never dropped silently: they are warned once and listed by `rejectedBindings()` as `{ id, raw, reason }`. Registered entries re-resolve on every change, so components never re-register. `declare([...])` catalogues ids before their component mounts, and `bindings()` / `binding(id)` list them for a settings page. `await registry.ready` for an async store. Combos are stored canonically. `normalizeCombo('Super+Up')` → `meta+arrowup`; it accepts the IDE's legacy spellings (`Command`/`Win`/`Super`, `Space`, `Up`…). `comboFromEvent(e)` records a binding from a keydown, and `validateCombo(k, { requireModifier })` checks one the user typed. Duplicate `id` in one context throws in dev.
303
+
304
+ **Conflicts.** `findConflicts(keys, { id?, context? })` lists what a binding would collide with, and `allConflicts({ minSeverity? })` lists every pair among registrations + declarations once (newer side as `source`) — run it with `minSeverity: 'error'` in a test over your catalogue. Kinds: `same-context` (error — one of them is dead), `shadows` / `shadowed-by` (warning — co-active contexts, the upper wins), `modal-blocked` (info — the upper context is modal), `reserved` (browser combos: `mod+t/w/n` error, copy/paste/save/find/… and `mod+b` warning; override with `reserved`, default `BROWSER_RESERVED_COMBOS`). Which contexts can be stacked comes from `contextGraph: { ctx: [contexts it sits above] }` (transitive; `global` is beneath all; without it, non-global contexts only meet `global`). Contexts are modal once pushed `{ modal: true }` or when listed in `modalContexts`. `rebind()` rejects any error-severity conflict (a reserved block even under `swap`), and a successful rebind returns the remaining `conflicts` for display.
305
+
306
+ **Beyond combos.** `attach()` listens for keydown, keyup and blur. `onRelease(e)` on a definition fires on the keyup of the same *physical* key (`event.code`) after it fired, whatever modifiers remain (push-to-talk); a blur drops pending releases without calling them. `onModifierChange({ alt, ctrl, shift, meta })` (registry option) reports held-modifier changes and resets on blur, so an Alt-held hint layer can't stick. `registerHandler({ context?, handle(e) → boolean, allowInInput?, preventDefault?, describe? })` adds a raw handler (count prefixes, `m<letter>` chords) that runs inside the context walk *after* that context's combos, newest first; returning false continues the walk, a modal suppresses it, it is never rebindable or conflict-checked, and `describe()` rows reach `toSections`. `isTypingTarget: (target, event) => boolean` replaces the typing test (compose with the exported `isTypingTarget`). `pushContext(name, { layer })` — frames sort by layer, then push order, so an app-level surface at `layer: 1` stays above routes that mount later. `pushContext(name, { modal: true, passthrough: ['global.sidebar', 'mod+\\'] })` lets listed ids/combos from beneath still fire (raw handlers never pass). `formatBinding(id)` → live keycap or null (for hints, tooltips, menu accelerators). `toSections({ includeDeclared: true })` also lists `declare()`d ids nothing has registered yet. `handleKeyup(e)` / `handleBlur()` dispatch by hand.
307
+
308
+ **Dialogs join the stack.** `Modal`, `Drawer`, `BottomSheet`, `ConfirmModal`, `ConfirmDialog` and `InputDialog` take `shortcutRegistry` (+ optional `shortcutContext` name); while open they push a modal context so page shortcuts don't fire behind them.
309
+
255
310
  **Sidebar convention: `SIDEBAR_TOGGLE_KEY` = `mod+\`** (Ctrl+\ / ⌘\). Do NOT use Ctrl+B: Chrome reserves it (bookmarks bar). Also avoid Ctrl+T/W/N, which the browser keeps. The overlay owns `?` by default, so don't register it — but if a host app already has its own global `?` handler registered first, the overlay respects `event.defaultPrevented` and stays out of the way (one overlay per keypress). Helpers: `formatShortcut(def | "mod+k", platform?)` → `⌘K` / `Ctrl+K`, `isTypingTarget(el)`, `parseShortcutCombo`, `detectShortcutPlatform`.
256
311
 
257
312
  ### Maps (Google Maps)
@@ -299,6 +354,39 @@ if (await showConfirm({ title: "Delete?", body: warnBody, danger: true })) { ...
299
354
  ## Utilities
300
355
 
301
356
  ```typescript
357
+ // Streaming-transcript auto-scroll (upstreamed from the JAT IDE — jatui-rl7w6.3).
358
+ // Reader-aware follow: follows the live edge ONLY while the reader is at it;
359
+ // upward scroll/wheel/keys/selection suspend following; coalesces sticks per
360
+ // frame; surfaces an unread count for a JumpToLatest pill.
361
+ import { createStickToBottom, type StickState, type StickController } from "jatui"
362
+ import { JumpToLatest } from "jatui"
363
+ // const scroller = createStickToBottom({ onState: (s) => (stick = s) })
364
+ // <div use:scroller.action class="overflow-y-auto">…</div>
365
+ // <JumpToLatest visible={!stick.atBottom} unreadCount={stick.unread} onclick={scroller.jumpToLatest} />
366
+ // scroller.requestStick() // gated follow (streamed content)
367
+ // scroller.requestStick(true) // force (the reader's own send, errors)
368
+
369
+ // ANSI terminal output → HTML (upstreamed from the JAT IDE — jatui-rl7w6.3).
370
+ // SGR colors incl. 24-bit/256-color, bold/dim/italic/underline/reverse, WCAG
371
+ // contrast auto-foreground on backgrounds, URL linkification, ECMA-48 strict
372
+ // stripAnsi (text after bare two-byte escapes survives), incremental stateful
373
+ // parsing and per-line hierarchy classes for <pre>-based terminal surfaces.
374
+ import { ansiToHtml, ansiToHtmlStateful, ansiToHtmlWithLinks, ansiToHtmlWithHierarchy, linkifyUrls, stripAnsi, classifyLine, parseOutputLines, createDefaultAnsiState } from "jatui"
375
+ import type { AnsiState, ParsedLine } from "jatui"
376
+
377
+ // Mobile gesture kit (upstreamed from the JAT IDE — jatui-rl7w6.2).
378
+ // swipe: one-finger horizontal drag with deadzone/directional-ratio gating,
379
+ // rubber-banded offset emission and distance-or-velocity commit.
380
+ import { swipe } from "jatui" // use:swipe={{ onSwipeLeft, onSwipeRight, onOffsetChange, allowLeft, allowRight, enabled }}
381
+ import type { SwipeOptions } from "jatui"
382
+ // longpress: press-and-hold (touch, haptic where supported) + right-click (mouse)
383
+ // context affordance; cancels on movement, swallows the synthesized tap after firing.
384
+ import { longpress } from "jatui" // use:longpress={{ onLongPress(detail), duration? (450), moveTolerance? (10), enabled? }}
385
+ import type { LongPressOptions, LongPressDetail } from "jatui"
386
+ // snapFlick: pure snap-point + flick-momentum math behind BottomSheet dual mode.
387
+ import { resolveSnapTarget, keyboardInsetPx, clampSnapHeight, FLICK_VELOCITY_PX_MS } from "jatui"
388
+ import type { SnapName, SnapHeight, SnapInput, SnapResult } from "jatui"
389
+
302
390
  // Video playback advisors (ported from extension-library — mos-5lzns.27).
303
391
  // Pure functions: no DOM, no settings reads, never write playbackRate. The
304
392
  // player's rate composer / resume-at-sentence / audio-graph tasks consume
@@ -322,6 +410,16 @@ import { pollOperation, PollTimeoutError } from "jatui"
322
410
  // { intervalMs, timeoutMs, backoff, signal, onProgress }) — rejects on timeout / abort / thrown check
323
411
  import { formatElapsed, rollingAverage, estimateRemainingMs } from "jatui"
324
412
 
413
+ // Recurring poll with coordinated exponential backoff (upstreamed from the JAT IDE — jatui-rl7w6.5).
414
+ import { createPollLoop } from "jatui"
415
+ // const poller = createPollLoop({ name: 'agents', intervalMs: 3000, maxIntervalMs: 60_000,
416
+ // tick: () => store.fetch() }) // resolve false / throw = failure → backoff; true/void = ok
417
+ // poller.start(); poller.stop(); poller.reset()
418
+ // Skips ticks while document.hidden (resumes the moment the tab is visible), never
419
+ // overlaps ticks, backs off exponentially with ±jitter capped at maxIntervalMs.
420
+ // Framework-free; the recurring-poll companion to pollOperation (one-shot).
421
+ import { computeBackoffDelayMs } from "jatui" // pure delay math (doubles, cap 2^6×, floored at intervalMs)
422
+
325
423
  // Variant combination algebra (jatui-m4uz3.8) — pure functions behind VariantMatrixPicker.
326
424
  import { generateCombinations, countCombinations, type VariantCategory, type VariantSelection } from "jatui"
327
425
  // VariantCategory — { id, label, icon?, mode?: 'multiplicative' | 'additive', options: VariantOption[] }
@@ -329,6 +427,13 @@ import { generateCombinations, countCombinations, type VariantCategory, type Var
329
427
  // VariantSelection — { [categoryId]: string[] } (selected option ids per category)
330
428
  // generateCombinations(categories, selected, { forceMode? }) → [{ modifierIds, prompt, options }]
331
429
 
430
+ // Grouped render list (jatui-u6d4e.2) — pure, also on 'jatui/table'. Flattens filtered+sorted rows
431
+ // into header|row items for a table/row window; rows keep input order, groups ordered by level.compare.
432
+ // collapsed = Set of "::"-joined path keys ("jat::jat-vte6e"); showHeader=false renders rows ungrouped.
433
+ import { groupRows, groupPathKey, type GroupLevel, type GroupedItem } from "jatui"
434
+ // groupRows(rows, [{ key: t => t.project }, { key: t => t.epic }], { getRowId, collapsed, label? })
435
+ // → { kind:'group', key, path, depth, label, rows, count, collapsed } | { kind:'row', row, id, depth, groupPath, position }
436
+
332
437
  // Date formatting
333
438
  import { formatRelativeTime, formatDate, formatShortDate, formatDateTime } from "jatui"
334
439
 
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Embed-safe keyboard shortcuts for a self-contained widget (pane, drawer,
3
+ * embedded panel) living inside a busy host page. Upstreamed from the JAT
4
+ * IDE (adoption audit U6, jatui-rl7w6.5; originally jat-nvxkt in JST).
5
+ *
6
+ * Modeled on wayflow's `dom/src/keyboard.ts` `createKeyboardScope`: a shortcut
7
+ * fires when focus is inside the container, OR when the event target is
8
+ * body/html (the state right after clicking empty space, or before anything
9
+ * has ever been focused) AND the most recent pointerdown/focusin landed
10
+ * inside the container. That "engaged" heuristic means a pane owns
11
+ * Delete/mod+Z immediately after a click, without permanently stealing
12
+ * those keys from the rest of the host page.
13
+ *
14
+ * Relationship to the shortcut registry (`createShortcutRegistry`): the
15
+ * registry resolves shortcuts against a *named context stack* for page-level
16
+ * keymaps and carries the `?` overlay. keyboardScope is the container-local
17
+ * complement — it decides ownership from DOM engagement (what was last
18
+ * clicked/focused), not from a pushed context, so a self-contained widget
19
+ * needs no cooperation from its host. Use the registry for app keymaps;
20
+ * use keyboardScope inside an embeddable widget that must own a few keys.
21
+ *
22
+ * Usage — Svelte action, handler map keyed by normalized combo:
23
+ *
24
+ * <div use:keyboardScope={{
25
+ * 'mod+s': () => save(),
26
+ * 'mod+e': () => toggleEdit(),
27
+ * 'escape': () => close(),
28
+ * }}>...</div>
29
+ *
30
+ * Usage — bare factory, for code that isn't a Svelte action (e.g. wiring the
31
+ * predicate into an existing keydown handler):
32
+ *
33
+ * const scope = createKeyboardScope(container)
34
+ * if (scope.isInScope(event.target)) { ... }
35
+ * scope.destroy()
36
+ *
37
+ * Combo strings combine modifiers in the fixed order shift, alt, mod, then
38
+ * the base key, joined by "+" (e.g. "shift+mod+z"). 'mod' resolves to Cmd on
39
+ * Mac, Ctrl elsewhere. Backspace normalizes to the same combo as Delete.
40
+ *
41
+ * Matching wayflow's own `interaction.ts` dispatcher, shortcuts never fire
42
+ * while the event target is a text-entry element (input/textarea/select
43
+ * /contenteditable — non-text input types like checkbox/button are exempt)
44
+ * — even for modifier combos — so typing in an input never triggers the
45
+ * widget's shortcuts.
46
+ */
47
+ export interface KeyboardScope {
48
+ isInScope: (target: EventTarget | null) => boolean;
49
+ destroy: () => void;
50
+ }
51
+ export declare function createKeyboardScope(container: HTMLElement): KeyboardScope;
52
+ export type KeyboardScopeHandlers = Record<string, (event: KeyboardEvent) => void>;
53
+ /** Normalizes a keydown event into a combo string, e.g. "mod+z", "delete". */
54
+ export declare function comboFor(event: KeyboardEvent): string;
55
+ export declare function keyboardScope(node: HTMLElement, handlers: KeyboardScopeHandlers): {
56
+ update(next: KeyboardScopeHandlers): void;
57
+ destroy(): void;
58
+ };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Embed-safe keyboard shortcuts for a self-contained widget (pane, drawer,
3
+ * embedded panel) living inside a busy host page. Upstreamed from the JAT
4
+ * IDE (adoption audit U6, jatui-rl7w6.5; originally jat-nvxkt in JST).
5
+ *
6
+ * Modeled on wayflow's `dom/src/keyboard.ts` `createKeyboardScope`: a shortcut
7
+ * fires when focus is inside the container, OR when the event target is
8
+ * body/html (the state right after clicking empty space, or before anything
9
+ * has ever been focused) AND the most recent pointerdown/focusin landed
10
+ * inside the container. That "engaged" heuristic means a pane owns
11
+ * Delete/mod+Z immediately after a click, without permanently stealing
12
+ * those keys from the rest of the host page.
13
+ *
14
+ * Relationship to the shortcut registry (`createShortcutRegistry`): the
15
+ * registry resolves shortcuts against a *named context stack* for page-level
16
+ * keymaps and carries the `?` overlay. keyboardScope is the container-local
17
+ * complement — it decides ownership from DOM engagement (what was last
18
+ * clicked/focused), not from a pushed context, so a self-contained widget
19
+ * needs no cooperation from its host. Use the registry for app keymaps;
20
+ * use keyboardScope inside an embeddable widget that must own a few keys.
21
+ *
22
+ * Usage — Svelte action, handler map keyed by normalized combo:
23
+ *
24
+ * <div use:keyboardScope={{
25
+ * 'mod+s': () => save(),
26
+ * 'mod+e': () => toggleEdit(),
27
+ * 'escape': () => close(),
28
+ * }}>...</div>
29
+ *
30
+ * Usage — bare factory, for code that isn't a Svelte action (e.g. wiring the
31
+ * predicate into an existing keydown handler):
32
+ *
33
+ * const scope = createKeyboardScope(container)
34
+ * if (scope.isInScope(event.target)) { ... }
35
+ * scope.destroy()
36
+ *
37
+ * Combo strings combine modifiers in the fixed order shift, alt, mod, then
38
+ * the base key, joined by "+" (e.g. "shift+mod+z"). 'mod' resolves to Cmd on
39
+ * Mac, Ctrl elsewhere. Backspace normalizes to the same combo as Delete.
40
+ *
41
+ * Matching wayflow's own `interaction.ts` dispatcher, shortcuts never fire
42
+ * while the event target is a text-entry element (input/textarea/select
43
+ * /contenteditable — non-text input types like checkbox/button are exempt)
44
+ * — even for modifier combos — so typing in an input never triggers the
45
+ * widget's shortcuts.
46
+ */
47
+ import { isTypingTarget } from '../utils/keyboardShortcutRegistry';
48
+ export function createKeyboardScope(container) {
49
+ const contains = (target) => target instanceof Node && container.contains(target);
50
+ // Capture phase so scoping is decided before any handler can stopPropagation.
51
+ let engaged = false;
52
+ const track = (event) => {
53
+ engaged = contains(event.target);
54
+ };
55
+ document.addEventListener('pointerdown', track, true);
56
+ document.addEventListener('focusin', track, true);
57
+ return {
58
+ isInScope: (target) => contains(target) ||
59
+ ((target === document.body || target === document.documentElement) && engaged),
60
+ destroy() {
61
+ document.removeEventListener('pointerdown', track, true);
62
+ document.removeEventListener('focusin', track, true);
63
+ },
64
+ };
65
+ }
66
+ const isMac = typeof navigator !== 'undefined' && /Mac/.test(navigator.platform ?? '');
67
+ const KEY_ALIASES = {
68
+ backspace: 'delete',
69
+ };
70
+ /** Normalizes a keydown event into a combo string, e.g. "mod+z", "delete". */
71
+ export function comboFor(event) {
72
+ const parts = [];
73
+ if (event.shiftKey)
74
+ parts.push('shift');
75
+ if (event.altKey)
76
+ parts.push('alt');
77
+ if (isMac ? event.metaKey : event.ctrlKey)
78
+ parts.push('mod');
79
+ const rawKey = event.key.toLowerCase();
80
+ parts.push(KEY_ALIASES[rawKey] ?? rawKey);
81
+ return parts.join('+');
82
+ }
83
+ export function keyboardScope(node, handlers) {
84
+ let current = handlers;
85
+ const scope = createKeyboardScope(node);
86
+ // Bound to document, not the node: the body/html-targeted branch of
87
+ // isInScope only ever sees events whose target IS document.body/.html,
88
+ // which never bubble through the container.
89
+ function onKeydown(event) {
90
+ if (!scope.isInScope(event.target))
91
+ return;
92
+ if (isTypingTarget(event.target))
93
+ return;
94
+ const handler = current[comboFor(event)];
95
+ if (!handler)
96
+ return;
97
+ event.preventDefault();
98
+ handler(event);
99
+ }
100
+ document.addEventListener('keydown', onKeydown);
101
+ return {
102
+ update(next) {
103
+ current = next;
104
+ },
105
+ destroy() {
106
+ document.removeEventListener('keydown', onKeydown);
107
+ scope.destroy();
108
+ },
109
+ };
110
+ }
@@ -61,6 +61,29 @@
61
61
  * if (nav.handleKeydown(e)) return;
62
62
  * // ...page-specific shortcuts
63
63
  * }
64
+ *
65
+ * Windowed / virtualised lists (`getOrderedIds` + `revealItem`):
66
+ *
67
+ * A windowed list only renders the rows near the viewport, so a DOM query
68
+ * stops j/k at the window edge and `*` misses every unrendered row. Pass
69
+ * `getOrderedIds` and the controller navigates over that id list instead;
70
+ * the DOM query (or `getItems`) is then only used to find the elements that
71
+ * ARE rendered, so the focus/selection classes can be painted on them.
72
+ * When the cursor lands on an id with no rendered element, `revealItem(id)`
73
+ * is called to scroll it into the window; the focus class is painted once
74
+ * the row mounts (same frame if the reveal renders synchronously, otherwise
75
+ * on a following animation frame, or on the next `refresh()`).
76
+ *
77
+ * <div use:listNav={{
78
+ * getOrderedIds: () => table.orderedRowIds(),
79
+ * revealItem: (id) => table.scrollToRow(id),
80
+ * onSelect: (el) => openDetail(el.dataset.navId!),
81
+ * }}>
82
+ * <!-- only the windowed rows, each with data-nav-id -->
83
+ * </div>
84
+ *
85
+ * Enter/Space need a rendered element (`onSelect` receives it), so they
86
+ * pass through when the focused row has been scrolled out of the window.
64
87
  */
65
88
  import type { Action } from 'svelte/action';
66
89
  export interface ListNavOptions {
@@ -76,8 +99,13 @@ export interface ListNavOptions {
76
99
  onSelect?: (item: HTMLElement, index: number) => void;
77
100
  /** Fired when Escape is pressed. Default behaviour: clears focus. */
78
101
  onEscape?: () => void;
79
- /** Fired whenever the focused index changes (including cleared focus). */
80
- onFocusChange?: (item: HTMLElement | null, index: number) => void;
102
+ /**
103
+ * Fired whenever the focused index changes (including cleared focus). With
104
+ * {@link getOrderedIds}, `item` is null while the focused row is not
105
+ * rendered (index is still >= 0); it fires again with the element once the
106
+ * revealed row mounts. `id` is the focused item's id (null when cleared).
107
+ */
108
+ onFocusChange?: (item: HTMLElement | null, index: number, id: string | null) => void;
81
109
  /** When true, j at the last item wraps to first (and vice-versa). Default: true. */
82
110
  wraparound?: boolean;
83
111
  /** Class applied to the currently focused item. Default: `jk-focused`. */
@@ -100,6 +128,21 @@ export interface ListNavOptions {
100
128
  * your items use a different attribute (e.g. `data-rule-nav-id`).
101
129
  */
102
130
  getItemId?: (el: HTMLElement) => string | null | undefined;
131
+ /**
132
+ * Return every navigable item id in list order, rendered or not (e.g. a
133
+ * DataTable's `orderedRowIds()`). When given, it replaces the DOM query as
134
+ * the source of positions: j/k/G/counts/selection all range over these ids,
135
+ * and `getItems` / {@link itemSelector} only locate the rendered subset
136
+ * (matched by {@link getItemId}). Called on every navigation event.
137
+ */
138
+ getOrderedIds?: () => readonly string[];
139
+ /**
140
+ * Bring an unrendered item into the rendered window (e.g. a DataTable's
141
+ * `scrollToRow(id)`). Called before an id from {@link getOrderedIds} is
142
+ * focused when no element for it is rendered. Rendered items are scrolled
143
+ * with `scrollIntoView` as usual.
144
+ */
145
+ revealItem?: (id: string) => void;
103
146
  }
104
147
  export interface KeyboardShortcut {
105
148
  key: string;
@@ -115,6 +158,8 @@ export interface ListNavController {
115
158
  clear(): void;
116
159
  /** Re-apply the focused class after items change (e.g. after a filter). */
117
160
  refresh(): void;
161
+ /** Id of the focused item (null when nothing is focused or it has no id). */
162
+ focusedId(): string | null;
118
163
  /** Read-only snapshot of selected item ids. */
119
164
  selectedIds(): Set<string>;
120
165
  /** Toggle selection. With no arg, toggles the focused item. */
@@ -123,7 +168,7 @@ export interface ListNavController {
123
168
  setSelection(ids: Iterable<string>): void;
124
169
  /** Drop the selection and exit visual mode. */
125
170
  clearSelection(): void;
126
- /** Toggle select-all of the currently visible items. */
171
+ /** Select every item (every id from `getOrderedIds` when given, else the rendered items). */
127
172
  selectAll(): void;
128
173
  /** True while visual-mode (V) is active. */
129
174
  isSelecting(): boolean;