@joewinke/jatui 0.24.0 → 0.26.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.
- package/CLAUDE.md +10 -2
- package/dist/components/KeyboardShortcutsOverlay.svelte +5 -0
- package/dist/components/Modal.svelte +32 -0
- package/dist/components/session-nav/transcriptModel.d.ts +46 -0
- package/dist/components/session-nav/transcriptModel.js +143 -19
- package/dist/components/table/DataTable.svelte +188 -27
- package/dist/components/table/DataTable.svelte.d.ts +23 -2
- package/dist/components/table/index.d.ts +1 -1
- package/dist/components/table/types.d.ts +106 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -59,7 +59,7 @@ Then import:
|
|
|
59
59
|
| `Badge` | Status/label badge | `variant`, `size`, `children` (snippet) |
|
|
60
60
|
| `StatusBadge` | Status indicator with dot | `status: StatusValue`, `label` |
|
|
61
61
|
| `Card` | Content card container | `title`, `children` (snippet) |
|
|
62
|
-
| `Modal` | Dialog overlay. `zIndex` accepts `number` OR any CSS `z-index` string, including `"var(--z-lightbox)"`. | `open` (bindable), `title`, `children` (snippet), `zIndex?: string \| number` |
|
|
62
|
+
| `Modal` | Dialog overlay. `zIndex` accepts `number` OR any CSS `z-index` string, including `"var(--z-lightbox)"`. **Escape is owned by the dialog element itself** (not the window handler) — Modal focuses the dialog on open, so `trapKeys`' stopPropagation would otherwise swallow Esc before the window listener sees it; a Modal opened inside a host Drawer closes before the drawer. The component ships its own open-state `.modal-box` opacity rule, so it renders under DaisyUI 5 with no host global override. | `open` (bindable), `title`, `children` (snippet), `zIndex?: string \| number`, `trapKeys?`, `closeOnEscape?` |
|
|
63
63
|
| `ConfirmModal` | Yes/no confirmation dialog | `open` (bindable), `title`, `message`, `onConfirm`, `onCancel` |
|
|
64
64
|
| `Drawer` | Slide-out side panel | `open` (bindable), `side`, `children` (snippet) |
|
|
65
65
|
| `LoadingSpinner` | Spinner animation | `size`, `color` |
|
|
@@ -76,6 +76,14 @@ Then import:
|
|
|
76
76
|
| `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
77
|
| `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?` |
|
|
78
78
|
|
|
79
|
+
### Tables (subpath: `jatui/table`)
|
|
80
|
+
|
|
81
|
+
Requires peer `@tanstack/table-core`. `import { DataTable, flexRender, columnResize } from 'jatui/table'`.
|
|
82
|
+
|
|
83
|
+
| Component | Purpose | Key Props |
|
|
84
|
+
|-----------|---------|-----------|
|
|
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) |
|
|
86
|
+
|
|
79
87
|
### Media
|
|
80
88
|
|
|
81
89
|
| Component | Purpose | Key Props |
|
|
@@ -244,7 +252,7 @@ const callbacks: AnnotationCallbacks = {
|
|
|
244
252
|
<KeyboardShortcutsOverlay registry={shortcuts} />
|
|
245
253
|
```
|
|
246
254
|
|
|
247
|
-
**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
|
|
255
|
+
**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`.
|
|
248
256
|
|
|
249
257
|
### Maps (Google Maps)
|
|
250
258
|
|
|
@@ -133,6 +133,11 @@
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
function onKeydown(e: KeyboardEvent) {
|
|
136
|
+
// Another listener already claimed this keypress (host app's own '?'
|
|
137
|
+
// handler, an open modal, …) — bail so one keypress can't toggle two
|
|
138
|
+
// overlays at once. Covers the '?' and Escape branches alike.
|
|
139
|
+
if (e.defaultPrevented) return;
|
|
140
|
+
|
|
136
141
|
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
137
142
|
|
|
138
143
|
if (e.key === '?') {
|
|
@@ -124,6 +124,21 @@
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
function handleKeyDown(event: KeyboardEvent) {
|
|
127
|
+
// Escape is owned HERE on the dialog element, not by <svelte:window>.
|
|
128
|
+
// Modal focuses the dialog (tabindex=-1) on open, so in the default state
|
|
129
|
+
// keydown dispatches from the dialog itself — and the trapKeys
|
|
130
|
+
// stopPropagation below (which under Svelte 5 delegated events also
|
|
131
|
+
// blocks every window listener) would swallow Escape before the window
|
|
132
|
+
// handler could act: Esc did nothing unless focus was in a text field.
|
|
133
|
+
// Handling it at element level also guarantees a Modal opened inside a
|
|
134
|
+
// host Drawer closes before the drawer does.
|
|
135
|
+
if (open && event.key === 'Escape' && closeOnEscape) {
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
event.stopPropagation();
|
|
138
|
+
close();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
127
142
|
// Stop keydown events from bubbling to window while the modal is open.
|
|
128
143
|
// This prevents page-level keyboard navigation (listNav-style handlers
|
|
129
144
|
// on window) from reacting to arrow keys, j/k, vim chords, digit counts,
|
|
@@ -247,4 +262,21 @@
|
|
|
247
262
|
align-items: center;
|
|
248
263
|
justify-content: center;
|
|
249
264
|
}
|
|
265
|
+
|
|
266
|
+
/*
|
|
267
|
+
* DaisyUI 5 puts .modal-box at opacity:0 (layer daisyui.l1.l2.l3) and only
|
|
268
|
+
* restores it from `.modal[open] .modal-box { opacity: 1 }` in the outer
|
|
269
|
+
* layer — a rule that assumes daisyUI's canonical markup where the box is a
|
|
270
|
+
* direct grid child of the dialog. jatui nests the box inside
|
|
271
|
+
* .modal-backdrop, and the restore rule loses the cascade: the panel stays
|
|
272
|
+
* invisible unless the host adds a global override (the jat IDE had to in
|
|
273
|
+
* app.css, jat-vte6e.17). Un-layered author styles beat @layer rules, so
|
|
274
|
+
* declaring the open state here makes the component render correctly on
|
|
275
|
+
* its own. Entrance motion stays owned by the in:fly transition on the box.
|
|
276
|
+
*/
|
|
277
|
+
:global(dialog.modal[open] .modal-box) {
|
|
278
|
+
opacity: 1;
|
|
279
|
+
translate: 0;
|
|
280
|
+
scale: 1;
|
|
281
|
+
}
|
|
250
282
|
</style>
|
|
@@ -34,6 +34,8 @@ export interface TranscriptItem {
|
|
|
34
34
|
srcLines: number;
|
|
35
35
|
/** id of the signal node that owns this item (assigned after parse) */
|
|
36
36
|
signalId: string | null;
|
|
37
|
+
/** ISO write time of the first source message (null on line-parsed/live items) */
|
|
38
|
+
timestamp?: string | null;
|
|
37
39
|
}
|
|
38
40
|
export interface FileTouchedEntry {
|
|
39
41
|
path: string;
|
|
@@ -65,11 +67,46 @@ export interface SignalNode {
|
|
|
65
67
|
/** Net lines changed across all diffs for this block */
|
|
66
68
|
linesAdded: number;
|
|
67
69
|
linesRemoved: number;
|
|
70
|
+
/**
|
|
71
|
+
* Original signal JSON payload (full data for rich card rendering). Always
|
|
72
|
+
* set by the builders below; optional so SignalNodes constructed by hand
|
|
73
|
+
* (pre-0.25 callers) still type-check.
|
|
74
|
+
*/
|
|
75
|
+
payload?: Record<string, unknown> | null;
|
|
76
|
+
/**
|
|
77
|
+
* True when this node represents the OPEN, in-progress turn — either a
|
|
78
|
+
* server-marked open block (`WorkBlockClient.open === true`) or the
|
|
79
|
+
* synthesized live node owning the trailing items past the last block.
|
|
80
|
+
* SessionNavigator uses this to render a ● live affordance. Optional so
|
|
81
|
+
* existing SignalNode consumers are unaffected.
|
|
82
|
+
*/
|
|
83
|
+
isLive?: boolean;
|
|
68
84
|
}
|
|
69
85
|
export interface LinkedSession {
|
|
70
86
|
items: TranscriptItem[];
|
|
71
87
|
signals: SignalNode[];
|
|
72
88
|
}
|
|
89
|
+
/** Tone drives the marker's hue; deliberately NOT a session-state key. */
|
|
90
|
+
export type MarkerTone = 'success' | 'error' | 'warn' | 'neutral' | 'info';
|
|
91
|
+
/** What a card detector declares when its event deserves a rail marker. */
|
|
92
|
+
export interface TimelineMarkerSpec {
|
|
93
|
+
/** glyph shown on the rail — keep it non-emoji so state glyphs stay distinct */
|
|
94
|
+
glyph: string;
|
|
95
|
+
/** short rail label, e.g. "subtask completed" */
|
|
96
|
+
label: string;
|
|
97
|
+
/** longer text for the title/tooltip */
|
|
98
|
+
detail?: string;
|
|
99
|
+
tone: MarkerTone;
|
|
100
|
+
}
|
|
101
|
+
/** A content event promoted onto the signal timeline, anchored to its row. */
|
|
102
|
+
export interface TimelineMarker extends TimelineMarkerSpec {
|
|
103
|
+
/** rail id — "mrk-<itemId>" */
|
|
104
|
+
id: string;
|
|
105
|
+
/** transcript item this marker anchors to (its `[data-row]`) */
|
|
106
|
+
itemId: number;
|
|
107
|
+
/** signal owning that item — clicking a marker inks this signal's ribbon */
|
|
108
|
+
signalId: string | null;
|
|
109
|
+
}
|
|
73
110
|
export declare function rawTypeToState(raw: string): string;
|
|
74
111
|
/** Build the linked model from rendered transcript lines. */
|
|
75
112
|
export declare function buildLinkedSession(lines: string[]): LinkedSession;
|
|
@@ -99,6 +136,14 @@ export interface WorkBlockClient {
|
|
|
99
136
|
}>;
|
|
100
137
|
linesAdded?: number;
|
|
101
138
|
linesRemoved?: number;
|
|
139
|
+
/**
|
|
140
|
+
* True when the server marks this block as the OPEN in-progress turn
|
|
141
|
+
* (populated by the navigator/sessionWork layer — sibling task jat-difet).
|
|
142
|
+
* Read defensively: may be undefined on blocks from older payloads. When
|
|
143
|
+
* true on the last block, that block's node becomes the live node (it
|
|
144
|
+
* absorbs any trailing items instead of synthesizing a separate one).
|
|
145
|
+
*/
|
|
146
|
+
open?: boolean;
|
|
102
147
|
}
|
|
103
148
|
export interface WorkTranscriptItemClient {
|
|
104
149
|
id: number;
|
|
@@ -107,6 +152,7 @@ export interface WorkTranscriptItemClient {
|
|
|
107
152
|
detail: string[];
|
|
108
153
|
srcLines: number;
|
|
109
154
|
blockId: string | null;
|
|
155
|
+
timestamp?: string | null;
|
|
110
156
|
}
|
|
111
157
|
/**
|
|
112
158
|
* Build a LinkedSession from the WorkBlock substrate (B1).
|
|
@@ -41,10 +41,36 @@ function parseSignalBody(body) {
|
|
|
41
41
|
if (!m)
|
|
42
42
|
return null;
|
|
43
43
|
let label = '';
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
let payload = null;
|
|
45
|
+
// Try to extract and parse the JSON payload from the signal call
|
|
46
|
+
const jsonMatch = body.match(/jat-signal\s+\w+\s+'(\{[\s\S]*?\})'\s*$/) ||
|
|
47
|
+
body.match(/jat-signal\s+\w+\s+"(\{[\s\S]*?\})"\s*$/) ||
|
|
48
|
+
body.match(/jat-signal\s+\w+\s+(\{[\s\S]*?\})\s*$/);
|
|
49
|
+
if (jsonMatch) {
|
|
50
|
+
try {
|
|
51
|
+
payload = JSON.parse(jsonMatch[1]);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// ignore parse errors — payload stays null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (payload) {
|
|
58
|
+
// Extract label from full payload
|
|
59
|
+
const p = payload;
|
|
60
|
+
const candidate = (Array.isArray(p.summary) ? p.summary[0] : undefined) ??
|
|
61
|
+
(typeof p.approach === 'string' ? p.approach : undefined) ??
|
|
62
|
+
(typeof p.taskTitle === 'string' ? p.taskTitle : undefined) ??
|
|
63
|
+
(typeof p.question === 'string' ? p.question : undefined) ??
|
|
64
|
+
(typeof p.currentStep === 'string' ? p.currentStep : undefined);
|
|
65
|
+
if (candidate)
|
|
66
|
+
label = candidate.slice(0, 90);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const pick = body.match(/"(?:approach|question|reviewFocus|currentStep|summary|reason|taskTitle)"\s*:\s*"([^"]{3,90})/);
|
|
70
|
+
if (pick)
|
|
71
|
+
label = pick[1];
|
|
72
|
+
}
|
|
73
|
+
return { rawType: m[1], label, payload };
|
|
48
74
|
}
|
|
49
75
|
/** Build the linked model from rendered transcript lines. */
|
|
50
76
|
export function buildLinkedSession(lines) {
|
|
@@ -64,7 +90,14 @@ export function buildLinkedSession(lines) {
|
|
|
64
90
|
} // blank ends a block
|
|
65
91
|
if (line.startsWith('› ')) {
|
|
66
92
|
flush();
|
|
67
|
-
|
|
93
|
+
const headline = line.slice(2);
|
|
94
|
+
// Claude Code's TUI draws a blank "› " input box while idle between
|
|
95
|
+
// turns; a capture that lands mid-frame leaks that empty prompt line
|
|
96
|
+
// into the transcript. Treat it like a blank line (below) rather than
|
|
97
|
+
// a zero-content prompt item — it never carries real content.
|
|
98
|
+
if (!headline.trim())
|
|
99
|
+
continue;
|
|
100
|
+
cur = { kind: 'prompt', headline, detail: [], srcLines: 1 };
|
|
68
101
|
continue;
|
|
69
102
|
}
|
|
70
103
|
if (line.startsWith('⏺ ')) {
|
|
@@ -73,7 +106,7 @@ export function buildLinkedSession(lines) {
|
|
|
73
106
|
if (sig) {
|
|
74
107
|
// signal emission = boundary; consume it, don't render as an item
|
|
75
108
|
flush();
|
|
76
|
-
emits.push({ afterIdx: items.length - 1, rawType: sig.rawType, label: sig.label });
|
|
109
|
+
emits.push({ afterIdx: items.length - 1, rawType: sig.rawType, label: sig.label, payload: sig.payload });
|
|
77
110
|
continue;
|
|
78
111
|
}
|
|
79
112
|
flush();
|
|
@@ -113,13 +146,33 @@ export function buildLinkedSession(lines) {
|
|
|
113
146
|
}
|
|
114
147
|
flush();
|
|
115
148
|
// Build signal nodes from emission boundaries.
|
|
149
|
+
// Ownership is FORWARD: each signal owns the items that come AFTER it, up to
|
|
150
|
+
// (but not including) the next signal's emission point. The first signal also
|
|
151
|
+
// absorbs any preamble items that appear before it.
|
|
152
|
+
//
|
|
153
|
+
// Let B[i] = emits[i].afterIdx = last item index before signal[i].
|
|
154
|
+
//
|
|
155
|
+
// Partition (non-overlapping):
|
|
156
|
+
// sig[0] → [0 .. B[1]] preamble + first forward region
|
|
157
|
+
// sig[i>0] → [B[i]+1 .. B[i+1]] region after sig[i], before sig[i+1]
|
|
158
|
+
// sig[last] → [B[last]+1 .. N-1] region after last signal
|
|
159
|
+
//
|
|
160
|
+
// This ensures output printed AFTER a signal emission (e.g. the READY FOR
|
|
161
|
+
// REVIEW banner after `jat-signal review`) is attributed to that signal, not
|
|
162
|
+
// the subsequent one.
|
|
116
163
|
const signals = [];
|
|
117
|
-
|
|
164
|
+
const N = items.length;
|
|
118
165
|
emits.forEach((e, i) => {
|
|
119
|
-
|
|
120
|
-
|
|
166
|
+
// rowStart: first signal starts at 0 (absorbs preamble items before it);
|
|
167
|
+
// later signals start at the item immediately after this signal's emission.
|
|
168
|
+
const rowStart = i === 0 ? 0 : e.afterIdx + 1;
|
|
169
|
+
// naturalEnd: extends to the next signal's pre-boundary, or to the last item.
|
|
170
|
+
const naturalEnd = i + 1 < emits.length ? emits[i + 1].afterIdx : N - 1;
|
|
171
|
+
// rowEnd: capped at N-1 (never out of bounds); may be < rowStart when this
|
|
172
|
+
// signal has no items after it (e.g. last line is the signal itself).
|
|
173
|
+
const rowEnd = Math.min(naturalEnd, N - 1);
|
|
121
174
|
let lineCount = 0;
|
|
122
|
-
for (let k = rowStart; k <= rowEnd && k <
|
|
175
|
+
for (let k = rowStart; k <= rowEnd && k < N; k++)
|
|
123
176
|
lineCount += items[k].srcLines;
|
|
124
177
|
signals.push({
|
|
125
178
|
id: `sig-${i}`,
|
|
@@ -135,9 +188,9 @@ export function buildLinkedSession(lines) {
|
|
|
135
188
|
filesTouched: [],
|
|
136
189
|
commits: [],
|
|
137
190
|
linesAdded: 0,
|
|
138
|
-
linesRemoved: 0
|
|
191
|
+
linesRemoved: 0,
|
|
192
|
+
payload: e.payload
|
|
139
193
|
});
|
|
140
|
-
prevBoundary = e.afterIdx;
|
|
141
194
|
});
|
|
142
195
|
for (const sig of signals) {
|
|
143
196
|
for (let k = sig.rowStart; k <= sig.rowEnd && k < items.length; k++) {
|
|
@@ -168,9 +221,11 @@ export function buildLinkedSessionFromWork(blocks, items) {
|
|
|
168
221
|
headline: it.headline,
|
|
169
222
|
detail: it.detail,
|
|
170
223
|
srcLines: it.srcLines,
|
|
171
|
-
signalId: null // filled in below
|
|
224
|
+
signalId: null, // filled in below
|
|
225
|
+
timestamp: it.timestamp ?? null
|
|
172
226
|
}));
|
|
173
|
-
// Map WorkBlock → SignalNode
|
|
227
|
+
// Map WorkBlock → SignalNode. `isLive` is seeded from the server's open flag
|
|
228
|
+
// (read defensively — jat-difet adds it; may be undefined on older payloads).
|
|
174
229
|
const signals = blocks.map((blk) => ({
|
|
175
230
|
id: blk.id,
|
|
176
231
|
state: rawTypeToState(blk.signal.type),
|
|
@@ -185,11 +240,80 @@ export function buildLinkedSessionFromWork(blocks, items) {
|
|
|
185
240
|
filesTouched: blk.filesTouched,
|
|
186
241
|
commits: blk.commits ?? [],
|
|
187
242
|
linesAdded: blk.linesAdded ?? 0,
|
|
188
|
-
linesRemoved: blk.linesRemoved ?? 0
|
|
243
|
+
linesRemoved: blk.linesRemoved ?? 0,
|
|
244
|
+
payload: blk.signal.payload,
|
|
245
|
+
isLive: blk.open === true
|
|
189
246
|
}));
|
|
190
|
-
|
|
247
|
+
const N = transcriptItems.length;
|
|
248
|
+
// Helper: synthesize the live node owning the open in-progress turn.
|
|
249
|
+
const makeLiveNode = (start, end, label) => {
|
|
250
|
+
let lineCount = 0;
|
|
251
|
+
for (let k = start; k <= end && k < N; k++)
|
|
252
|
+
lineCount += transcriptItems[k].srcLines;
|
|
253
|
+
return {
|
|
254
|
+
id: 'sig-live',
|
|
255
|
+
state: rawTypeToState('working'),
|
|
256
|
+
rawType: 'working',
|
|
257
|
+
label,
|
|
258
|
+
rowStart: start,
|
|
259
|
+
rowEnd: end,
|
|
260
|
+
lineCount: Math.max(1, lineCount),
|
|
261
|
+
blockId: null,
|
|
262
|
+
summary: null,
|
|
263
|
+
filesTouched: [],
|
|
264
|
+
commits: [],
|
|
265
|
+
linesAdded: 0,
|
|
266
|
+
linesRemoved: 0,
|
|
267
|
+
payload: null,
|
|
268
|
+
isLive: true
|
|
269
|
+
};
|
|
270
|
+
};
|
|
271
|
+
if (N > 0) {
|
|
272
|
+
if (signals.length === 0) {
|
|
273
|
+
// No blocks but live items exist → the whole transcript is the open
|
|
274
|
+
// turn. Give it a single live node so nothing is left unsignaled.
|
|
275
|
+
signals.push(makeLiveNode(0, N - 1, 'current'));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// (#1 gap-fill) Guarantee a contiguous [0..N-1] partition so EVERY
|
|
279
|
+
// item ends up owning a signal:
|
|
280
|
+
// (a) first signal absorbs any preamble before its range
|
|
281
|
+
signals[0].rowStart = 0;
|
|
282
|
+
// (b) extend each signal's rowEnd to close the gap before the next
|
|
283
|
+
// signal's range (no-op when ranges already touch/overlap)
|
|
284
|
+
for (let i = 0; i < signals.length - 1; i++) {
|
|
285
|
+
const nextStart = signals[i + 1].rowStart;
|
|
286
|
+
if (signals[i].rowEnd < nextStart - 1) {
|
|
287
|
+
signals[i].rowEnd = nextStart - 1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// (#2 live node) Trailing items past the last block's range are the
|
|
291
|
+
// OPEN in-progress turn — they must NOT be folded into the previous
|
|
292
|
+
// signal (e.g. a 'review'). Honor a server-marked open block first;
|
|
293
|
+
// otherwise synthesize a dedicated live node.
|
|
294
|
+
const lastSig = signals[signals.length - 1];
|
|
295
|
+
const lastBlockEnd = lastSig.rowEnd;
|
|
296
|
+
if (lastSig.isLive) {
|
|
297
|
+
// Server already marked the last block open → IT is the live node;
|
|
298
|
+
// extend it to absorb any trailing items.
|
|
299
|
+
lastSig.rowEnd = N - 1;
|
|
300
|
+
}
|
|
301
|
+
else if (lastBlockEnd < N - 1) {
|
|
302
|
+
// Synthesize a live node owning [lastBlockEnd+1 .. N-1]. Label from
|
|
303
|
+
// the live task title when available, else a neutral 'current'.
|
|
304
|
+
const p = lastSig.payload;
|
|
305
|
+
const title = p && typeof p.taskTitle === 'string' ? p.taskTitle.slice(0, 90) : null;
|
|
306
|
+
signals.push(makeLiveNode(lastBlockEnd + 1, N - 1, title ?? 'current'));
|
|
307
|
+
}
|
|
308
|
+
// else: the last block already reaches N-1 — nothing trailing.
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Assign signalId to transcript items via (gap-filled) ownership ranges.
|
|
312
|
+
// Clamp to bounds; later signals (incl. the live node) win on any overlap.
|
|
191
313
|
for (const sig of signals) {
|
|
192
|
-
|
|
314
|
+
const start = Math.max(0, sig.rowStart);
|
|
315
|
+
const end = Math.min(sig.rowEnd, N - 1);
|
|
316
|
+
for (let k = start; k <= end; k++) {
|
|
193
317
|
transcriptItems[k].signalId = sig.id;
|
|
194
318
|
}
|
|
195
319
|
}
|
|
@@ -202,7 +326,7 @@ export function buildLinkedSessionFromWork(blocks, items) {
|
|
|
202
326
|
*/
|
|
203
327
|
export const SAMPLE_TRANSCRIPT_LINES = [
|
|
204
328
|
'',
|
|
205
|
-
'› /jat
|
|
329
|
+
'› /jat-start GentleCoast jat-7h2k',
|
|
206
330
|
'⏺ Bash(get-current-session-id)',
|
|
207
331
|
' ⎿ 4f9c1a2e-7b3d-4c8a-9e1f-2a6b8c0d4e5f',
|
|
208
332
|
'⏺ Bash(jat-signal starting \'{"agentName":"GentleCoast","taskTitle":"Add password reset flow"}\')',
|
|
@@ -256,7 +380,7 @@ export const SAMPLE_TRANSCRIPT_LINES = [
|
|
|
256
380
|
'',
|
|
257
381
|
'🔍 READY FOR REVIEW: jat-7h2k — password reset flow implemented, tested, rate-limited.',
|
|
258
382
|
'',
|
|
259
|
-
'› /jat
|
|
383
|
+
'› /jat-complete',
|
|
260
384
|
'⏺ Bash(jat-step committing --task jat-7h2k)',
|
|
261
385
|
' ⎿ Committed: feat(jat-7h2k): add password reset flow',
|
|
262
386
|
'⏺ Bash(jat-signal complete \'{"taskId":"jat-7h2k","summary":"Shipped password reset flow"}\')',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
<script lang="ts">
|
|
1
|
+
<script lang="ts" generics="TData">
|
|
2
|
+
import { tick } from 'svelte';
|
|
2
3
|
import {
|
|
3
4
|
createTable,
|
|
4
5
|
getCoreRowModel,
|
|
@@ -9,12 +10,10 @@
|
|
|
9
10
|
type RowSelectionState,
|
|
10
11
|
type PaginationState,
|
|
11
12
|
type TableOptionsResolved,
|
|
13
|
+
type Row,
|
|
12
14
|
} from '@tanstack/table-core';
|
|
13
15
|
import { flexRender } from './flexRender.js';
|
|
14
|
-
import type { DataTableProps } from './types.js';
|
|
15
|
-
|
|
16
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
-
type AnyData = any;
|
|
16
|
+
import type { DataTableProps, DataTableRowProps, DataTableSortEvent, GroupedItem } from './types.js';
|
|
18
17
|
|
|
19
18
|
let {
|
|
20
19
|
data,
|
|
@@ -27,13 +26,36 @@
|
|
|
27
26
|
loadingRows = 5,
|
|
28
27
|
onSelectionChange,
|
|
29
28
|
class: className = '',
|
|
30
|
-
|
|
29
|
+
getRowId,
|
|
30
|
+
sorting = $bindable(),
|
|
31
|
+
initialSorting,
|
|
32
|
+
manualSorting = false,
|
|
33
|
+
onSortingChange,
|
|
34
|
+
rowProps,
|
|
35
|
+
onRowClick,
|
|
36
|
+
headerProps,
|
|
37
|
+
expandedRow,
|
|
38
|
+
isExpanded,
|
|
39
|
+
empty,
|
|
40
|
+
variant = 'zebra',
|
|
41
|
+
tableClass = '',
|
|
42
|
+
api = $bindable(),
|
|
43
|
+
}: DataTableProps<TData> = $props();
|
|
31
44
|
|
|
32
45
|
// ── State ──────────────────────────────────────────────────────────────
|
|
33
|
-
|
|
46
|
+
// `sorting` is bindable: bound, the caller owns it; unbound, it is local
|
|
47
|
+
// state seeded from `initialSorting`.
|
|
48
|
+
// svelte-ignore state_referenced_locally
|
|
49
|
+
if (sorting === undefined) sorting = initialSorting ?? [];
|
|
50
|
+
const sortState = $derived<SortingState>(sorting ?? []);
|
|
34
51
|
let rowSelection = $state<RowSelectionState>({});
|
|
35
52
|
// svelte-ignore state_referenced_locally
|
|
36
53
|
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize });
|
|
54
|
+
/** Internally-owned expansion (only used when `isExpanded` is not supplied). */
|
|
55
|
+
let expandedIds = $state<Set<string>>(new Set());
|
|
56
|
+
/** The header click that triggered the sort change currently in flight. */
|
|
57
|
+
let pendingSortEvent: DataTableSortEvent | null = null;
|
|
58
|
+
let rootEl: HTMLDivElement | undefined = $state();
|
|
37
59
|
|
|
38
60
|
// Reset to the first page whenever the data set changes.
|
|
39
61
|
//
|
|
@@ -50,7 +72,7 @@
|
|
|
50
72
|
pagination = { pageIndex: 0, pageSize };
|
|
51
73
|
});
|
|
52
74
|
|
|
53
|
-
const checkboxCol: ColumnDef<
|
|
75
|
+
const checkboxCol: ColumnDef<TData, unknown> = {
|
|
54
76
|
id: '__select',
|
|
55
77
|
header: () => null,
|
|
56
78
|
cell: () => null,
|
|
@@ -58,24 +80,26 @@
|
|
|
58
80
|
size: 40,
|
|
59
81
|
};
|
|
60
82
|
|
|
61
|
-
const allColumns = $derived<ColumnDef<
|
|
62
|
-
selectable ? [checkboxCol, ...(columns as ColumnDef<AnyData, unknown>[])] : (columns as ColumnDef<AnyData, unknown>[])
|
|
63
|
-
);
|
|
83
|
+
const allColumns = $derived<ColumnDef<TData, unknown>[]>(selectable ? [checkboxCol, ...columns] : columns);
|
|
64
84
|
|
|
65
85
|
// ── Table instance ─────────────────────────────────────────────────────
|
|
66
86
|
const table = $derived.by(() => {
|
|
67
|
-
const opts: TableOptionsResolved<
|
|
87
|
+
const opts: TableOptionsResolved<TData> = {
|
|
68
88
|
data,
|
|
69
89
|
columns: allColumns,
|
|
70
90
|
state: {
|
|
71
|
-
sorting,
|
|
91
|
+
sorting: sortState,
|
|
72
92
|
rowSelection,
|
|
73
93
|
...(paginated ? { pagination } : {}),
|
|
74
94
|
},
|
|
95
|
+
getRowId: getRowId ? (row: TData) => getRowId(row) : undefined,
|
|
75
96
|
enableRowSelection: selectable,
|
|
76
97
|
enableSorting: sortable,
|
|
98
|
+
manualSorting,
|
|
77
99
|
onSortingChange: (updater) => {
|
|
78
|
-
|
|
100
|
+
const next = typeof updater === 'function' ? updater(sortState) : updater;
|
|
101
|
+
sorting = next;
|
|
102
|
+
onSortingChange?.(next, pendingSortEvent ?? { field: next[0]?.id ?? '', shiftKey: false });
|
|
79
103
|
},
|
|
80
104
|
onRowSelectionChange: (updater) => {
|
|
81
105
|
rowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
|
@@ -87,7 +111,7 @@
|
|
|
87
111
|
: () => {},
|
|
88
112
|
onStateChange: () => {},
|
|
89
113
|
getCoreRowModel: getCoreRowModel(),
|
|
90
|
-
getSortedRowModel: sortable ? getSortedRowModel() : undefined,
|
|
114
|
+
getSortedRowModel: sortable && !manualSorting ? getSortedRowModel() : undefined,
|
|
91
115
|
getPaginationRowModel: paginated ? getPaginationRowModel() : undefined,
|
|
92
116
|
renderFallbackValue: '',
|
|
93
117
|
};
|
|
@@ -109,30 +133,128 @@
|
|
|
109
133
|
const canPrevPage = $derived(table.getCanPreviousPage());
|
|
110
134
|
const canNextPage = $derived(table.getCanNextPage());
|
|
111
135
|
|
|
136
|
+
const tableClasses = $derived(
|
|
137
|
+
[variant === 'unstyled' ? '' : variant === 'plain' ? 'table' : 'table table-zebra', 'w-full', tableClass]
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.join(' ')
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
/** The table owns expansion: a row click / Enter / Space toggles the detail row. */
|
|
143
|
+
const internalExpand = $derived(!!expandedRow && !isExpanded);
|
|
144
|
+
|
|
112
145
|
$effect(() => {
|
|
113
146
|
if (!selectable || !onSelectionChange) return;
|
|
114
|
-
const selected = table.getSelectedRowModel().rows.map((r
|
|
147
|
+
const selected = table.getSelectedRowModel().rows.map((r) => r.original);
|
|
115
148
|
onSelectionChange(selected);
|
|
116
149
|
});
|
|
117
150
|
|
|
151
|
+
// ── Imperative api ─────────────────────────────────────────────────────
|
|
152
|
+
api = {
|
|
153
|
+
orderedRowIds: () => table.getPrePaginationRowModel().rows.map((r) => r.id),
|
|
154
|
+
scrollToRow: async (id, align = 'auto') => {
|
|
155
|
+
if (paginated) {
|
|
156
|
+
const index = table.getPrePaginationRowModel().rows.findIndex((r) => r.id === id);
|
|
157
|
+
if (index < 0) return;
|
|
158
|
+
const pageIndex = Math.floor(index / pagination.pageSize);
|
|
159
|
+
if (pageIndex !== pagination.pageIndex) {
|
|
160
|
+
pagination = { ...pagination, pageIndex };
|
|
161
|
+
await tick();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const el = rootEl?.querySelector<HTMLElement>(`tr[data-row-id="${CSS.escape(id)}"]`);
|
|
165
|
+
el?.scrollIntoView({ block: align === 'auto' ? 'nearest' : align });
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
118
169
|
function sortDir(headerId: string): 'asc' | 'desc' | false {
|
|
119
|
-
const s =
|
|
170
|
+
const s = sortState.find((item) => item.id === headerId);
|
|
120
171
|
return s ? (s.desc ? 'desc' : 'asc') : false;
|
|
121
172
|
}
|
|
122
173
|
|
|
123
|
-
|
|
174
|
+
type AnyHeader = ReturnType<typeof table.getHeaderGroups>[0]['headers'][0];
|
|
175
|
+
|
|
176
|
+
function toggleSort(header: AnyHeader, e: MouseEvent | KeyboardEvent) {
|
|
177
|
+
pendingSortEvent = { field: header.column.id, shiftKey: e.shiftKey };
|
|
178
|
+
try {
|
|
179
|
+
header.column.getToggleSortingHandler()?.(e);
|
|
180
|
+
} finally {
|
|
181
|
+
pendingSortEvent = null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function onHeaderKeydown(header: AnyHeader, e: KeyboardEvent) {
|
|
186
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
toggleSort(header, e);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function renderHeader(header: AnyHeader) {
|
|
124
192
|
return flexRender(header.column.columnDef.header, header.getContext());
|
|
125
193
|
}
|
|
126
194
|
|
|
127
|
-
function renderCell(cell: ReturnType<
|
|
195
|
+
function renderCell(cell: ReturnType<Row<TData>['getVisibleCells']>[0]) {
|
|
128
196
|
return flexRender(cell.column.columnDef.cell, cell.getContext());
|
|
129
197
|
}
|
|
198
|
+
|
|
199
|
+
// ── Row helpers ────────────────────────────────────────────────────────
|
|
200
|
+
function rowItem(row: Row<TData>, index: number, count: number): GroupedItem<TData> {
|
|
201
|
+
const position = count === 1 ? 'only' : index === 0 ? 'first' : index === count - 1 ? 'last' : 'middle';
|
|
202
|
+
return { kind: 'row', row: row.original, id: row.id, depth: 0, groupPath: [], position };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function rowIsExpanded(row: Row<TData>): boolean {
|
|
206
|
+
if (!expandedRow) return false;
|
|
207
|
+
return isExpanded ? isExpanded(row.original) : expandedIds.has(row.id);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function toggleExpanded(id: string) {
|
|
211
|
+
const next = new Set(expandedIds);
|
|
212
|
+
if (next.has(id)) next.delete(id);
|
|
213
|
+
else next.add(id);
|
|
214
|
+
expandedIds = next;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Clicks on these inside a row belong to the control, not to the row. */
|
|
218
|
+
const INTERACTIVE = 'input, button, a, select, textarea, label, [contenteditable]:not([contenteditable="false"])';
|
|
219
|
+
|
|
220
|
+
function isInteractiveTarget(e: Event): boolean {
|
|
221
|
+
const target = e.target as Element | null;
|
|
222
|
+
const tr = e.currentTarget as Element;
|
|
223
|
+
const hit = target?.closest?.(INTERACTIVE);
|
|
224
|
+
return !!hit && hit !== tr && tr.contains(hit);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function onRowClickInternal(row: Row<TData>, rp: DataTableRowProps, e: MouseEvent) {
|
|
228
|
+
if (isInteractiveTarget(e)) return;
|
|
229
|
+
rp.onclick?.(e);
|
|
230
|
+
onRowClick?.(row.original, e);
|
|
231
|
+
if (internalExpand) toggleExpanded(row.id);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function onRowKeydown(row: Row<TData>, e: KeyboardEvent) {
|
|
235
|
+
if (!internalExpand || e.target !== e.currentTarget) return;
|
|
236
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
toggleExpanded(row.id);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Normalise `rowProps().attrs`: booleans become `""`/omitted (aria-* stay "true"/"false"). */
|
|
242
|
+
function rowAttrs(rp: DataTableRowProps): Record<string, string | number | undefined> {
|
|
243
|
+
const out: Record<string, string | number | undefined> = {};
|
|
244
|
+
for (const [k, v] of Object.entries(rp.attrs ?? {})) {
|
|
245
|
+
if (typeof v === 'boolean') out[k] = k.startsWith('aria-') ? String(v) : v ? '' : undefined;
|
|
246
|
+
else out[k] = v;
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const hasRowHandlers = $derived(!!rowProps || !!onRowClick || internalExpand);
|
|
130
252
|
</script>
|
|
131
253
|
|
|
132
|
-
<div class="datatable-root {className}">
|
|
254
|
+
<div class="datatable-root {className}" bind:this={rootEl}>
|
|
133
255
|
{#if loading}
|
|
134
256
|
<div class="overflow-x-auto">
|
|
135
|
-
<table class=
|
|
257
|
+
<table class={tableClasses}>
|
|
136
258
|
<thead>
|
|
137
259
|
<tr>
|
|
138
260
|
{#each allColumns as _}
|
|
@@ -155,7 +277,7 @@
|
|
|
155
277
|
</div>
|
|
156
278
|
{:else}
|
|
157
279
|
<div class="overflow-x-auto">
|
|
158
|
-
<table class=
|
|
280
|
+
<table class={tableClasses}>
|
|
159
281
|
<thead>
|
|
160
282
|
{#each headerGroups as headerGroup}
|
|
161
283
|
<tr>
|
|
@@ -175,10 +297,15 @@
|
|
|
175
297
|
{@const isSortable = sortable && header.column.getCanSort()}
|
|
176
298
|
{@const dir = sortDir(header.column.id)}
|
|
177
299
|
{@const rendered = renderHeader(header)}
|
|
300
|
+
{@const hp = headerProps?.(header.column.id)}
|
|
178
301
|
<th
|
|
179
302
|
style:width={header.getSize() !== 150 ? `${header.getSize()}px` : undefined}
|
|
303
|
+
class={hp?.class}
|
|
180
304
|
class:dt-th-sortable={isSortable}
|
|
181
|
-
|
|
305
|
+
tabindex={isSortable ? 0 : undefined}
|
|
306
|
+
onclick={isSortable ? (e) => toggleSort(header, e) : undefined}
|
|
307
|
+
onkeydown={isSortable ? (e) => onHeaderKeydown(header, e) : undefined}
|
|
308
|
+
oncontextmenu={hp?.oncontextmenu}
|
|
182
309
|
aria-sort={dir === 'asc' ? 'ascending' : dir === 'desc' ? 'descending' : undefined}
|
|
183
310
|
>
|
|
184
311
|
<span class="dt-th-label">
|
|
@@ -212,11 +339,33 @@
|
|
|
212
339
|
<tbody>
|
|
213
340
|
{#if rows.length === 0}
|
|
214
341
|
<tr>
|
|
215
|
-
|
|
342
|
+
{#if empty}
|
|
343
|
+
<td colspan={allColumns.length} class="dt-empty-custom">{@render empty()}</td>
|
|
344
|
+
{:else}
|
|
345
|
+
<td colspan={allColumns.length} class="dt-empty">No data</td>
|
|
346
|
+
{/if}
|
|
216
347
|
</tr>
|
|
217
348
|
{:else}
|
|
218
|
-
{#each rows as row}
|
|
219
|
-
|
|
349
|
+
{#each rows as row, ri (row.id)}
|
|
350
|
+
{@const rp = rowProps?.(row.original, rowItem(row, ri, rows.length)) ?? {}}
|
|
351
|
+
{@const attrs = rowAttrs(rp)}
|
|
352
|
+
{@const expanded = rowIsExpanded(row)}
|
|
353
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
354
|
+
<tr
|
|
355
|
+
{...attrs}
|
|
356
|
+
class={rp.class}
|
|
357
|
+
class:dt-row-selected={row.getIsSelected()}
|
|
358
|
+
class:dt-row-clickable={!!rp.onclick || !!onRowClick || internalExpand}
|
|
359
|
+
class:dt-row-expanded={expanded}
|
|
360
|
+
data-row-id={getRowId ? row.id : undefined}
|
|
361
|
+
aria-expanded={expandedRow ? expanded : undefined}
|
|
362
|
+
tabindex={internalExpand ? (attrs.tabindex ?? 0) : attrs.tabindex}
|
|
363
|
+
onclick={hasRowHandlers ? (e) => onRowClickInternal(row, rp, e) : undefined}
|
|
364
|
+
onkeydown={internalExpand ? (e) => onRowKeydown(row, e) : undefined}
|
|
365
|
+
oncontextmenu={rp.oncontextmenu}
|
|
366
|
+
ondragover={rp.ondragover}
|
|
367
|
+
ondrop={rp.ondrop}
|
|
368
|
+
>
|
|
220
369
|
{#each row.getVisibleCells() as cell}
|
|
221
370
|
{#if cell.column.id === '__select'}
|
|
222
371
|
<td class="dt-select-cell">
|
|
@@ -241,12 +390,16 @@
|
|
|
241
390
|
{/if}
|
|
242
391
|
{/each}
|
|
243
392
|
</tr>
|
|
393
|
+
{#if expanded && expandedRow}
|
|
394
|
+
<tr class="dt-expanded-detail" data-detail-for={getRowId ? row.id : undefined}>
|
|
395
|
+
<td colspan={allColumns.length}>{@render expandedRow(row.original, { colspan: allColumns.length })}</td>
|
|
396
|
+
</tr>
|
|
397
|
+
{/if}
|
|
244
398
|
{/each}
|
|
245
399
|
{/if}
|
|
246
400
|
</tbody>
|
|
247
401
|
</table>
|
|
248
402
|
</div>
|
|
249
|
-
|
|
250
403
|
{#if paginated && pageCount > 1}
|
|
251
404
|
<div class="dt-pagination">
|
|
252
405
|
<span class="dt-page-info">
|
|
@@ -327,6 +480,14 @@
|
|
|
327
480
|
background-color: color-mix(in oklch, var(--color-primary) 6%, transparent) !important;
|
|
328
481
|
}
|
|
329
482
|
|
|
483
|
+
.dt-row-clickable {
|
|
484
|
+
cursor: pointer;
|
|
485
|
+
}
|
|
486
|
+
.dt-row-clickable:focus-visible {
|
|
487
|
+
outline: 2px solid var(--color-primary);
|
|
488
|
+
outline-offset: -2px;
|
|
489
|
+
}
|
|
490
|
+
|
|
330
491
|
.dt-select-cell {
|
|
331
492
|
width: 2.5rem;
|
|
332
493
|
text-align: center;
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import type { DataTableProps } from './types.js';
|
|
2
|
-
declare
|
|
3
|
-
|
|
2
|
+
declare function $$render<TData>(): {
|
|
3
|
+
props: DataTableProps<TData>;
|
|
4
|
+
exports: {};
|
|
5
|
+
bindings: "sorting" | "api";
|
|
6
|
+
slots: {};
|
|
7
|
+
events: {};
|
|
8
|
+
};
|
|
9
|
+
declare class __sveltets_Render<TData> {
|
|
10
|
+
props(): ReturnType<typeof $$render<TData>>['props'];
|
|
11
|
+
events(): ReturnType<typeof $$render<TData>>['events'];
|
|
12
|
+
slots(): ReturnType<typeof $$render<TData>>['slots'];
|
|
13
|
+
bindings(): "sorting" | "api";
|
|
14
|
+
exports(): {};
|
|
15
|
+
}
|
|
16
|
+
interface $$IsomorphicComponent {
|
|
17
|
+
new <TData>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TData>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TData>['props']>, ReturnType<__sveltets_Render<TData>['events']>, ReturnType<__sveltets_Render<TData>['slots']>> & {
|
|
18
|
+
$$bindings?: ReturnType<__sveltets_Render<TData>['bindings']>;
|
|
19
|
+
} & ReturnType<__sveltets_Render<TData>['exports']>;
|
|
20
|
+
<TData>(internal: unknown, props: ReturnType<__sveltets_Render<TData>['props']> & {}): ReturnType<__sveltets_Render<TData>['exports']>;
|
|
21
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
22
|
+
}
|
|
23
|
+
declare const DataTable: $$IsomorphicComponent;
|
|
24
|
+
type DataTable<TData> = InstanceType<typeof DataTable<TData>>;
|
|
4
25
|
export default DataTable;
|
|
@@ -2,4 +2,4 @@ export { default as DataTable } from './DataTable.svelte';
|
|
|
2
2
|
export { flexRender } from './flexRender';
|
|
3
3
|
export { columnResize } from './columnResize';
|
|
4
4
|
export type { ColumnResizeOptions } from './columnResize';
|
|
5
|
-
export type { DataTableProps, SelectionEvent, ColumnDef, Row } from './types';
|
|
5
|
+
export type { DataTableProps, DataTableApi, DataTableRowProps, DataTableHeaderProps, DataTableSortEvent, DataTableVariant, GroupedItem, SelectionEvent, ColumnDef, Row, SortingState } from './types';
|
|
@@ -1,5 +1,63 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { ColumnDef, Row, SortingState } from '@tanstack/table-core';
|
|
3
|
+
export type { ColumnDef, Row, SortingState };
|
|
4
|
+
/**
|
|
5
|
+
* One entry in the table's render list. Today DataTable renders flat rows, so
|
|
6
|
+
* every item is `kind: 'row'` with `depth: 0`; the `group` variant is the shape
|
|
7
|
+
* the grouped render list (`groupRows`, design doc
|
|
8
|
+
* jat/docs/design/datatable-virtualisation-group-rows.md §3.2) will produce, so
|
|
9
|
+
* `rowProps` callers written now keep working when grouping lands.
|
|
10
|
+
*/
|
|
11
|
+
export type GroupedItem<T = any> = {
|
|
12
|
+
kind: 'group';
|
|
13
|
+
key: string;
|
|
14
|
+
path: string[];
|
|
15
|
+
depth: number;
|
|
16
|
+
label: string;
|
|
17
|
+
rows: T[];
|
|
18
|
+
count: number;
|
|
19
|
+
collapsed: boolean;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'row';
|
|
22
|
+
row: T;
|
|
23
|
+
id: string;
|
|
24
|
+
depth: number;
|
|
25
|
+
groupPath: string[];
|
|
26
|
+
position: 'first' | 'middle' | 'last' | 'only';
|
|
27
|
+
};
|
|
28
|
+
/** What `rowProps` may put on a body `<tr>`. */
|
|
29
|
+
export interface DataTableRowProps {
|
|
30
|
+
class?: string;
|
|
31
|
+
/** Extra attributes (`data-*`, `aria-*`, `tabindex`…). `false` omits the attribute. */
|
|
32
|
+
attrs?: Record<string, string | number | boolean | undefined>;
|
|
33
|
+
onclick?: (e: MouseEvent) => void;
|
|
34
|
+
oncontextmenu?: (e: MouseEvent) => void;
|
|
35
|
+
ondragover?: (e: DragEvent) => void;
|
|
36
|
+
ondrop?: (e: DragEvent) => void;
|
|
37
|
+
}
|
|
38
|
+
/** What `headerProps` may put on a header `<th>`. */
|
|
39
|
+
export interface DataTableHeaderProps {
|
|
40
|
+
class?: string;
|
|
41
|
+
oncontextmenu?: (e: MouseEvent) => void;
|
|
42
|
+
}
|
|
43
|
+
/** Context passed alongside a sorting change: which header, and whether it was a shift-click. */
|
|
44
|
+
export interface DataTableSortEvent {
|
|
45
|
+
field: string;
|
|
46
|
+
shiftKey: boolean;
|
|
47
|
+
}
|
|
48
|
+
/** Imperative handle, exposed through `bind:api`. */
|
|
49
|
+
export interface DataTableApi {
|
|
50
|
+
/** Scroll a row into view (jumping to its page first when paginated). */
|
|
51
|
+
scrollToRow(id: string, align?: 'auto' | 'start' | 'center'): void;
|
|
52
|
+
/** Row ids in render order across ALL pages — what keyboard nav should walk. */
|
|
53
|
+
orderedRowIds(): string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Table skin. `zebra` (default) = DaisyUI `table table-zebra`; `plain` drops the
|
|
57
|
+
* striping; `unstyled` drops the DaisyUI table classes entirely so the host's
|
|
58
|
+
* own CSS owns the look.
|
|
59
|
+
*/
|
|
60
|
+
export type DataTableVariant = 'zebra' | 'plain' | 'unstyled';
|
|
3
61
|
export interface DataTableProps<TData = any> {
|
|
4
62
|
/** Row data array */
|
|
5
63
|
data: TData[];
|
|
@@ -19,8 +77,53 @@ export interface DataTableProps<TData = any> {
|
|
|
19
77
|
loadingRows?: number;
|
|
20
78
|
/** Emitted when the selection set changes */
|
|
21
79
|
onSelectionChange?: (rows: TData[]) => void;
|
|
22
|
-
/** Additional
|
|
80
|
+
/** Additional class on the root wrapper */
|
|
23
81
|
class?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Stable row identity (e.g. a task id). Keys selection, expansion and `api`
|
|
84
|
+
* by id instead of index, so a poll that reorders `data` keeps state on the
|
|
85
|
+
* right rows. Also renders `data-row-id` on each body `<tr>`.
|
|
86
|
+
*/
|
|
87
|
+
getRowId?: (row: TData) => string;
|
|
88
|
+
/** Controlled sort state — `bind:sorting`. Unbound, the table owns it. */
|
|
89
|
+
sorting?: SortingState;
|
|
90
|
+
/** Starting sort state when `sorting` is not supplied (e.g. restored from localStorage). */
|
|
91
|
+
initialSorting?: SortingState;
|
|
92
|
+
/**
|
|
93
|
+
* true = DataTable does not sort; pass `data` already sorted and react to
|
|
94
|
+
* header clicks through `onSortingChange`. Headers still show the sort state.
|
|
95
|
+
*/
|
|
96
|
+
manualSorting?: boolean;
|
|
97
|
+
/** Fires on every header-driven sort change — persist it here. */
|
|
98
|
+
onSortingChange?: (next: SortingState, ev: DataTableSortEvent) => void;
|
|
99
|
+
/** Per-row class / attributes / handlers for the body `<tr>`. */
|
|
100
|
+
rowProps?: (row: TData, item: GroupedItem<TData>) => DataTableRowProps;
|
|
101
|
+
/**
|
|
102
|
+
* Shorthand for `rowProps(...).onclick`. Not fired for clicks that land on an
|
|
103
|
+
* interactive child (input, button, link, select, textarea, label,
|
|
104
|
+
* contenteditable) — so the select checkbox and inline editors stay inert.
|
|
105
|
+
*/
|
|
106
|
+
onRowClick?: (row: TData, e: MouseEvent) => void;
|
|
107
|
+
/** Per-column class / context-menu handler for the header `<th>`. */
|
|
108
|
+
headerProps?: (columnId: string) => DataTableHeaderProps;
|
|
109
|
+
/**
|
|
110
|
+
* Detail row rendered under an expanded row, in one full-width cell.
|
|
111
|
+
* Without `isExpanded`, the table owns expansion: a row click (or Enter /
|
|
112
|
+
* Space on the focused row) toggles it.
|
|
113
|
+
*/
|
|
114
|
+
expandedRow?: Snippet<[TData, {
|
|
115
|
+
colspan: number;
|
|
116
|
+
}]>;
|
|
117
|
+
/** Controlled expansion — when given, the caller owns which rows are open. */
|
|
118
|
+
isExpanded?: (row: TData) => boolean;
|
|
119
|
+
/** Replaces the default "No data" cell when `data` is empty. */
|
|
120
|
+
empty?: Snippet;
|
|
121
|
+
/** `zebra` (default) | `plain` | `unstyled` — see DataTableVariant. */
|
|
122
|
+
variant?: DataTableVariant;
|
|
123
|
+
/** Extra class on the `<table>` element (e.g. `table-sm`). */
|
|
124
|
+
tableClass?: string;
|
|
125
|
+
/** Imperative handle — `bind:api`. */
|
|
126
|
+
api?: DataTableApi;
|
|
24
127
|
}
|
|
25
128
|
export interface SelectionEvent<TData> {
|
|
26
129
|
rows: TData[];
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export { default as Flythrough } from './components/Flythrough.svelte';
|
|
|
25
25
|
export { default as LessonEmbed } from './components/LessonEmbed.svelte';
|
|
26
26
|
export type { Collection, CollectionCut, CollectionBeat, } from './components/LessonEmbed.svelte';
|
|
27
27
|
export { buildLinkedSession, buildLinkedSessionFromWork, rawTypeToState, sampleLinkedSession, SAMPLE_TRANSCRIPT_LINES, } from './components/session-nav/transcriptModel';
|
|
28
|
-
export type { ItemKind, TranscriptItem, FileTouchedEntry, SignalNode, LinkedSession, WorkBlockClient, WorkTranscriptItemClient, } from './components/session-nav/transcriptModel';
|
|
28
|
+
export type { ItemKind, TranscriptItem, FileTouchedEntry, SignalNode, LinkedSession, WorkBlockClient, WorkTranscriptItemClient, MarkerTone, TimelineMarkerSpec, TimelineMarker, } from './components/session-nav/transcriptModel';
|
|
29
29
|
export { default as UserAvatar } from './components/UserAvatar.svelte';
|
|
30
30
|
export { default as SidebarUserFooter } from './components/SidebarUserFooter.svelte';
|
|
31
31
|
export { default as TaskTypeIcon } from './components/TaskTypeIcon.svelte';
|
package/dist/index.js
CHANGED
|
@@ -44,7 +44,8 @@ export { qrMatrix, qrCanvas } from './video/advisors/qr';
|
|
|
44
44
|
// LessonEmbed renders a demo-assemble collection.json (cut picker + player).
|
|
45
45
|
export { default as Flythrough } from './components/Flythrough.svelte';
|
|
46
46
|
export { default as LessonEmbed } from './components/LessonEmbed.svelte';
|
|
47
|
-
// Data models — Session transcript (from JAT IDE, jat-jj79f.16 G1
|
|
47
|
+
// Data models — Session transcript (from JAT IDE, jat-jj79f.16 G1; resynced
|
|
48
|
+
// with the IDE's payload/timestamp/live-node/F13-marker model in jatui-ucmkz)
|
|
48
49
|
export { buildLinkedSession, buildLinkedSessionFromWork, rawTypeToState, sampleLinkedSession, SAMPLE_TRANSCRIPT_LINES, } from './components/session-nav/transcriptModel';
|
|
49
50
|
// Components — Universal
|
|
50
51
|
export { default as UserAvatar } from './components/UserAvatar.svelte';
|