@joewinke/jatui 0.40.0 → 0.41.1

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 (40) hide show
  1. package/CLAUDE.md +29 -0
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/dist/components/AnimatedNumber.svelte +60 -1
  4. package/dist/components/AnimatedNumber.svelte.d.ts +7 -0
  5. package/dist/components/SegmentedControl.svelte +263 -0
  6. package/dist/components/SegmentedControl.svelte.d.ts +48 -0
  7. package/dist/components/settings/SettingsRow.svelte +177 -0
  8. package/dist/components/settings/SettingsRow.svelte.d.ts +32 -0
  9. package/dist/components/settings/SettingsSection.svelte +139 -0
  10. package/dist/components/settings/SettingsSection.svelte.d.ts +21 -0
  11. package/dist/components/stats/ActivityHeatmap.svelte +412 -0
  12. package/dist/components/stats/ActivityHeatmap.svelte.d.ts +72 -0
  13. package/dist/components/stats/BarList.svelte +262 -0
  14. package/dist/components/stats/BarList.svelte.d.ts +57 -0
  15. package/dist/components/stats/ChartCard.svelte +230 -0
  16. package/dist/components/stats/ChartCard.svelte.d.ts +63 -0
  17. package/dist/components/stats/StatCard.svelte +387 -0
  18. package/dist/components/stats/StatCard.svelte.d.ts +37 -0
  19. package/dist/components/stats/StatGroup.svelte +67 -0
  20. package/dist/components/stats/StatGroup.svelte.d.ts +14 -0
  21. package/dist/components/stats/heatmap.d.ts +76 -0
  22. package/dist/components/stats/heatmap.js +151 -0
  23. package/dist/components/tree/TreeView.svelte +680 -0
  24. package/dist/components/tree/TreeView.svelte.d.ts +123 -0
  25. package/dist/components/tree/flattenTree.d.ts +57 -0
  26. package/dist/components/tree/flattenTree.js +91 -0
  27. package/dist/components/tree/treeKeys.d.ts +46 -0
  28. package/dist/components/tree/treeKeys.js +80 -0
  29. package/dist/data/grid/cells.d.ts +40 -0
  30. package/dist/data/grid/cells.js +89 -0
  31. package/dist/data/grid/clipboard.d.ts +46 -0
  32. package/dist/data/grid/clipboard.js +131 -0
  33. package/dist/data/grid/edits.d.ts +148 -0
  34. package/dist/data/grid/edits.js +222 -0
  35. package/dist/data/grid/index.d.ts +12 -0
  36. package/dist/data/grid/index.js +12 -0
  37. package/dist/index.d.ts +22 -0
  38. package/dist/index.js +21 -0
  39. package/dist/showcase/Globe.svelte +1 -1
  40. package/package.json +5 -1
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Spreadsheet clipboard + CSV I/O (jatui/grid, jatui-6kitr).
3
+ *
4
+ * Behaviour follows BoardUI's data-grid utils (MIT, © 2026 Mertcan Dundar
5
+ * Esmergul): TSV that round-trips quoted tabs/newlines from Excel and Google
6
+ * Sheets, and CSV export with a BOM and formula-injection escaping. Clean
7
+ * re-implementation.
8
+ */
9
+ /**
10
+ * Parse clipboard text from Excel / Sheets / Numbers into a cell matrix.
11
+ *
12
+ * - Tab separates cells, newline (LF or CRLF) separates rows.
13
+ * - A cell that STARTS with `"` is quoted: tabs and newlines inside it are
14
+ * literal and `""` is an escaped quote. (Sheets quotes any cell holding a
15
+ * newline or a quote.) A `"` mid-cell is literal.
16
+ * - One trailing newline (every spreadsheet adds it) does not make an empty row.
17
+ * - Ragged rows are padded with '' to the widest row.
18
+ */
19
+ export function parseClipboardTsv(text) {
20
+ const rows = [];
21
+ let row = [];
22
+ let cell = '';
23
+ let i = 0;
24
+ const n = text.length;
25
+ let atCellStart = true;
26
+ while (i < n) {
27
+ const ch = text[i];
28
+ if (atCellStart && ch === '"') {
29
+ // Quoted cell: read to the closing quote.
30
+ i++;
31
+ let closed = false;
32
+ while (i < n) {
33
+ if (text[i] === '"') {
34
+ if (text[i + 1] === '"') {
35
+ cell += '"';
36
+ i += 2;
37
+ continue;
38
+ }
39
+ i++;
40
+ closed = true;
41
+ break;
42
+ }
43
+ cell += text[i++];
44
+ }
45
+ if (!closed) {
46
+ // Unbalanced: treat the whole thing as literal text.
47
+ cell = '"' + cell;
48
+ }
49
+ atCellStart = false;
50
+ continue;
51
+ }
52
+ atCellStart = false;
53
+ if (ch === '\t') {
54
+ row.push(cell);
55
+ cell = '';
56
+ atCellStart = true;
57
+ i++;
58
+ }
59
+ else if (ch === '\n' || ch === '\r') {
60
+ row.push(cell);
61
+ rows.push(row);
62
+ row = [];
63
+ cell = '';
64
+ atCellStart = true;
65
+ i += ch === '\r' && text[i + 1] === '\n' ? 2 : 1;
66
+ }
67
+ else {
68
+ cell += ch;
69
+ i++;
70
+ }
71
+ }
72
+ // Flush the last row unless the text ended exactly on a newline.
73
+ if (!atCellStart || row.length > 0) {
74
+ row.push(cell);
75
+ rows.push(row);
76
+ }
77
+ const width = rows.reduce((w, r) => Math.max(w, r.length), 0);
78
+ for (const r of rows)
79
+ while (r.length < width)
80
+ r.push('');
81
+ return rows;
82
+ }
83
+ /** Serialize a matrix as clipboard TSV, quoting cells that hold a tab, newline or quote. */
84
+ export function serializeTsv(rows) {
85
+ return rows
86
+ .map((r) => r
87
+ .map((v) => {
88
+ const s = cellText(v);
89
+ return /[\t\n\r"]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
90
+ })
91
+ .join('\t'))
92
+ .join('\n');
93
+ }
94
+ /** Characters that make a spreadsheet treat a cell as a formula (OWASP CSV injection). */
95
+ const FORMULA_START = /^[=+\-@\t\r]/;
96
+ /**
97
+ * Neutralise a cell a spreadsheet would execute (`=HYPERLINK(...)`,
98
+ * `+cmd|...`, `@SUM(...)`) by prefixing a single quote. Only STRINGS are
99
+ * escaped: a real number such as -5 is data, not a formula.
100
+ */
101
+ export function escapeCsvFormula(value) {
102
+ return typeof value === 'string' && FORMULA_START.test(value) ? `'${value}` : value;
103
+ }
104
+ function cellText(v) {
105
+ if (v == null)
106
+ return '';
107
+ if (v instanceof Date)
108
+ return Number.isNaN(v.getTime()) ? '' : v.toISOString();
109
+ if (typeof v === 'object')
110
+ return JSON.stringify(v);
111
+ return String(v);
112
+ }
113
+ /** RFC 4180 CSV from a matrix (first row = header if you include one). */
114
+ export function serializeCsv(rows, opts = {}) {
115
+ const { bom = true, escapeFormulas = true, delimiter = ',', newline = '\r\n' } = opts;
116
+ const needsQuote = new RegExp(`[${delimiter.replace(/[\\\]^-]/g, '\\$&')}"\\n\\r]`);
117
+ const body = rows
118
+ .map((r) => r
119
+ .map((v) => {
120
+ const s = cellText(escapeFormulas ? escapeCsvFormula(v) : v);
121
+ return needsQuote.test(s) || /^\s|\s$/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
122
+ })
123
+ .join(delimiter))
124
+ .join(newline);
125
+ return (bom ? '' : '') + body;
126
+ }
127
+ /** CSV from objects: one header row, then one row per object. */
128
+ export function serializeRowsToCsv(data, columns, opts) {
129
+ const get = (row, c) => typeof c.value === 'function' ? c.value(row) : row[c.value];
130
+ return serializeCsv([columns.map((c) => c.header), ...data.map((row) => columns.map((c) => get(row, c)))], opts);
131
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Diff-based edit transactions + undo/redo for editable grids (jatui/grid,
3
+ * jatui-6kitr).
4
+ *
5
+ * `history.ts` snapshots the whole state per step, which is heavy for a
6
+ * 10k-row table. This stores only what changed (cell before/after, deleted
7
+ * rows with their index) and refuses to undo a transaction whose cells no
8
+ * longer hold what it wrote: when the data changed underneath (a server
9
+ * refresh, a collaborator), the history is cleared instead of silently
10
+ * reverting someone else's change. That stale guard follows BoardUI's data
11
+ * grid (MIT, © 2026 Mertcan Dundar Esmergul); clean re-implementation.
12
+ *
13
+ * Everything is immutable: functions return new arrays and copy only the
14
+ * rows they touch.
15
+ */
16
+ export interface CellEdit {
17
+ rowId: string;
18
+ columnId: string;
19
+ before: unknown;
20
+ after: unknown;
21
+ }
22
+ export type GridTransaction<TRow> = {
23
+ kind: 'cells';
24
+ edits: CellEdit[];
25
+ label?: string;
26
+ } | {
27
+ kind: 'delete';
28
+ rows: {
29
+ row: TRow;
30
+ index: number;
31
+ }[];
32
+ label?: string;
33
+ } | {
34
+ kind: 'insert';
35
+ rows: {
36
+ row: TRow;
37
+ index: number;
38
+ }[];
39
+ label?: string;
40
+ };
41
+ export interface GridAccess<TRow> {
42
+ getRowId: (row: TRow) => string;
43
+ /** Default: `row[columnId]`. */
44
+ getCell?: (row: TRow, columnId: string) => unknown;
45
+ /** Default: `{ ...row, [columnId]: value }`. Must return a NEW row. */
46
+ setCell?: (row: TRow, columnId: string, value: unknown) => TRow;
47
+ /** Default: Object.is. */
48
+ equals?: (a: unknown, b: unknown) => boolean;
49
+ }
50
+ /**
51
+ * Apply cell edits (`direction: 'forward'` writes `after`, `'backward'`
52
+ * writes `before`). Edits for unknown rows are skipped.
53
+ */
54
+ export declare function applyEdits<TRow>(data: readonly TRow[], edits: readonly CellEdit[], access: GridAccess<TRow>, direction?: 'forward' | 'backward'): TRow[];
55
+ /** Build edits for writing `values` (rowId → columnId → value), capturing `before` from `data`. */
56
+ export declare function diffEdits<TRow>(data: readonly TRow[], values: ReadonlyArray<{
57
+ rowId: string;
58
+ columnId: string;
59
+ value: unknown;
60
+ }>, access: GridAccess<TRow>): CellEdit[];
61
+ /** Does `data` currently hold what `tx` would hold after `direction`? (the stale guard) */
62
+ export declare function transactionMatches<TRow>(data: readonly TRow[], tx: GridTransaction<TRow>, access: GridAccess<TRow>, state: 'applied' | 'reverted'): boolean;
63
+ /** Apply a whole transaction forward (redo) or backward (undo). */
64
+ export declare function applyTransaction<TRow>(data: readonly TRow[], tx: GridTransaction<TRow>, access: GridAccess<TRow>, direction: 'forward' | 'backward'): TRow[];
65
+ /** Delete rows by id, returning the new data and the transaction to record. */
66
+ export declare function deleteRows<TRow>(data: readonly TRow[], ids: Iterable<string>, access: Pick<GridAccess<TRow>, 'getRowId'>, label?: string): {
67
+ data: TRow[];
68
+ transaction: GridTransaction<TRow>;
69
+ };
70
+ export type HistoryStep<TRow> = {
71
+ ok: true;
72
+ data: TRow[];
73
+ transaction: GridTransaction<TRow>;
74
+ } | {
75
+ ok: false;
76
+ reason: 'empty' | 'stale';
77
+ };
78
+ export interface EditHistory<TRow> {
79
+ /** Record a transaction you have already applied. Clears the redo stack. */
80
+ record(tx: GridTransaction<TRow>): void;
81
+ /** Shorthand: apply cell edits to `data`, record them, return the new data. */
82
+ commit(data: readonly TRow[], edits: CellEdit[], label?: string): TRow[];
83
+ undo(data: readonly TRow[]): HistoryStep<TRow>;
84
+ redo(data: readonly TRow[]): HistoryStep<TRow>;
85
+ clear(): void;
86
+ readonly canUndo: boolean;
87
+ readonly canRedo: boolean;
88
+ readonly size: {
89
+ undo: number;
90
+ redo: number;
91
+ };
92
+ /** Fires after every change to the stacks (for reactive canUndo/canRedo). */
93
+ subscribe(fn: () => void): () => void;
94
+ }
95
+ export interface EditHistoryOptions<TRow> extends GridAccess<TRow> {
96
+ /** Max undo depth. Default 100. */
97
+ limit?: number;
98
+ }
99
+ export declare function createEditHistory<TRow>(opts: EditHistoryOptions<TRow>): EditHistory<TRow>;
100
+ export interface PasteTarget {
101
+ /** Visible row ids in display order. */
102
+ rowIds: readonly string[];
103
+ /** Visible column ids in display order. */
104
+ columnIds: readonly string[];
105
+ /** Top-left cell of the paste. */
106
+ start: {
107
+ row: number;
108
+ col: number;
109
+ };
110
+ /**
111
+ * Per-column parser (e.g. `parseCellValue` with that column's spec).
112
+ * Return `{ ok: false }` to reject a cell. Default: keep the text.
113
+ */
114
+ parse?: (raw: string, columnId: string) => {
115
+ ok: true;
116
+ value: unknown;
117
+ } | {
118
+ ok: false;
119
+ error: string;
120
+ };
121
+ /** Columns that cannot be written (read-only / computed). */
122
+ readOnly?: (columnId: string) => boolean;
123
+ /**
124
+ * A 1×1 clipboard fills the whole selection when given (Excel behaviour).
125
+ * Selection bounds inclusive, in display indices.
126
+ */
127
+ fillRange?: {
128
+ rowEnd: number;
129
+ colEnd: number;
130
+ };
131
+ }
132
+ export interface PastePlan {
133
+ values: {
134
+ rowId: string;
135
+ columnId: string;
136
+ value: unknown;
137
+ }[];
138
+ errors: {
139
+ rowId: string;
140
+ columnId: string;
141
+ raw: string;
142
+ error: string;
143
+ }[];
144
+ /** Cells that fell outside the grid. */
145
+ clipped: number;
146
+ }
147
+ /** Map a clipboard matrix onto grid cells. Feed `values` to `diffEdits`, then `history.commit`. */
148
+ export declare function planPaste(matrix: readonly (readonly string[])[], target: PasteTarget): PastePlan;
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Diff-based edit transactions + undo/redo for editable grids (jatui/grid,
3
+ * jatui-6kitr).
4
+ *
5
+ * `history.ts` snapshots the whole state per step, which is heavy for a
6
+ * 10k-row table. This stores only what changed (cell before/after, deleted
7
+ * rows with their index) and refuses to undo a transaction whose cells no
8
+ * longer hold what it wrote: when the data changed underneath (a server
9
+ * refresh, a collaborator), the history is cleared instead of silently
10
+ * reverting someone else's change. That stale guard follows BoardUI's data
11
+ * grid (MIT, © 2026 Mertcan Dundar Esmergul); clean re-implementation.
12
+ *
13
+ * Everything is immutable: functions return new arrays and copy only the
14
+ * rows they touch.
15
+ */
16
+ const defaultGet = (row, col) => row[col];
17
+ const defaultSet = (row, col, v) => ({ ...row, [col]: v });
18
+ function resolve(a) {
19
+ return {
20
+ getRowId: a.getRowId,
21
+ getCell: a.getCell ?? defaultGet,
22
+ setCell: a.setCell ?? defaultSet,
23
+ equals: a.equals ?? Object.is
24
+ };
25
+ }
26
+ /**
27
+ * Apply cell edits (`direction: 'forward'` writes `after`, `'backward'`
28
+ * writes `before`). Edits for unknown rows are skipped.
29
+ */
30
+ export function applyEdits(data, edits, access, direction = 'forward') {
31
+ const { getRowId, setCell } = resolve(access);
32
+ const byRow = new Map();
33
+ for (const e of edits) {
34
+ const list = byRow.get(e.rowId);
35
+ if (list)
36
+ list.push(e);
37
+ else
38
+ byRow.set(e.rowId, [e]);
39
+ }
40
+ if (!byRow.size)
41
+ return data.slice();
42
+ const ordered = direction === 'forward' ? (l) => l : (l) => l.slice().reverse();
43
+ return data.map((row) => {
44
+ const list = byRow.get(getRowId(row));
45
+ if (!list)
46
+ return row;
47
+ let next = row;
48
+ for (const e of ordered(list))
49
+ next = setCell(next, e.columnId, direction === 'forward' ? e.after : e.before);
50
+ return next;
51
+ });
52
+ }
53
+ /** Build edits for writing `values` (rowId → columnId → value), capturing `before` from `data`. */
54
+ export function diffEdits(data, values, access) {
55
+ const { getRowId, getCell, equals } = resolve(access);
56
+ const rows = new Map(data.map((r) => [getRowId(r), r]));
57
+ const out = [];
58
+ for (const v of values) {
59
+ const row = rows.get(v.rowId);
60
+ if (!row)
61
+ continue;
62
+ const before = getCell(row, v.columnId);
63
+ if (!equals(before, v.value))
64
+ out.push({ rowId: v.rowId, columnId: v.columnId, before, after: v.value });
65
+ }
66
+ return out;
67
+ }
68
+ /** Does `data` currently hold what `tx` would hold after `direction`? (the stale guard) */
69
+ export function transactionMatches(data, tx, access, state) {
70
+ const { getRowId, getCell, equals } = resolve(access);
71
+ const rows = new Map(data.map((r) => [getRowId(r), r]));
72
+ if (tx.kind === 'cells') {
73
+ // Last write per cell wins when a transaction touched a cell twice.
74
+ const final = new Map();
75
+ const first = new Map();
76
+ for (const e of tx.edits) {
77
+ const k = e.rowId + '\u0000' + e.columnId;
78
+ final.set(k, e);
79
+ if (!first.has(k))
80
+ first.set(k, e);
81
+ }
82
+ for (const [k, e] of final) {
83
+ const row = rows.get(e.rowId);
84
+ if (!row)
85
+ return false;
86
+ const expected = state === 'applied' ? e.after : first.get(k).before;
87
+ if (!equals(getCell(row, e.columnId), expected))
88
+ return false;
89
+ }
90
+ return true;
91
+ }
92
+ const present = tx.rows.every((r) => rows.has(getRowId(r.row)));
93
+ const absent = tx.rows.every((r) => !rows.has(getRowId(r.row)));
94
+ const rowsShouldExist = (tx.kind === 'insert') === (state === 'applied');
95
+ return rowsShouldExist ? present : absent;
96
+ }
97
+ function removeRows(data, ids, getRowId) {
98
+ return data.filter((r) => !ids.has(getRowId(r)));
99
+ }
100
+ function insertRows(data, rows) {
101
+ const out = data.slice();
102
+ for (const r of rows.slice().sort((a, b) => a.index - b.index)) {
103
+ out.splice(Math.min(Math.max(0, r.index), out.length), 0, r.row);
104
+ }
105
+ return out;
106
+ }
107
+ /** Apply a whole transaction forward (redo) or backward (undo). */
108
+ export function applyTransaction(data, tx, access, direction) {
109
+ if (tx.kind === 'cells')
110
+ return applyEdits(data, tx.edits, access, direction);
111
+ const removing = (tx.kind === 'delete') === (direction === 'forward');
112
+ if (removing) {
113
+ return removeRows(data, new Set(tx.rows.map((r) => access.getRowId(r.row))), access.getRowId);
114
+ }
115
+ return insertRows(data, tx.rows);
116
+ }
117
+ /** Delete rows by id, returning the new data and the transaction to record. */
118
+ export function deleteRows(data, ids, access, label) {
119
+ const set = new Set(ids);
120
+ const rows = [];
121
+ data.forEach((row, index) => {
122
+ if (set.has(access.getRowId(row)))
123
+ rows.push({ row, index });
124
+ });
125
+ return {
126
+ data: removeRows(data, set, access.getRowId),
127
+ transaction: { kind: 'delete', rows, label }
128
+ };
129
+ }
130
+ export function createEditHistory(opts) {
131
+ const limit = Math.max(1, opts.limit ?? 100);
132
+ let past = [];
133
+ let future = [];
134
+ const subs = new Set();
135
+ const emit = () => subs.forEach((f) => f());
136
+ function step(data, from) {
137
+ const stack = from === 'undo' ? past : future;
138
+ const tx = stack[stack.length - 1];
139
+ if (!tx)
140
+ return { ok: false, reason: 'empty' };
141
+ if (!transactionMatches(data, tx, opts, from === 'undo' ? 'applied' : 'reverted')) {
142
+ past = [];
143
+ future = [];
144
+ emit();
145
+ return { ok: false, reason: 'stale' };
146
+ }
147
+ stack.pop();
148
+ (from === 'undo' ? future : past).push(tx);
149
+ const next = applyTransaction(data, tx, opts, from === 'undo' ? 'backward' : 'forward');
150
+ emit();
151
+ return { ok: true, data: next, transaction: tx };
152
+ }
153
+ return {
154
+ record(tx) {
155
+ if (tx.kind === 'cells' ? !tx.edits.length : !tx.rows.length)
156
+ return;
157
+ past.push(tx);
158
+ if (past.length > limit)
159
+ past = past.slice(past.length - limit);
160
+ future = [];
161
+ emit();
162
+ },
163
+ commit(data, edits, label) {
164
+ if (!edits.length)
165
+ return data.slice();
166
+ const next = applyEdits(data, edits, opts, 'forward');
167
+ this.record({ kind: 'cells', edits, label });
168
+ return next;
169
+ },
170
+ undo: (data) => step(data, 'undo'),
171
+ redo: (data) => step(data, 'redo'),
172
+ clear() {
173
+ past = [];
174
+ future = [];
175
+ emit();
176
+ },
177
+ get canUndo() {
178
+ return past.length > 0;
179
+ },
180
+ get canRedo() {
181
+ return future.length > 0;
182
+ },
183
+ get size() {
184
+ return { undo: past.length, redo: future.length };
185
+ },
186
+ subscribe(fn) {
187
+ subs.add(fn);
188
+ return () => subs.delete(fn);
189
+ }
190
+ };
191
+ }
192
+ /** Map a clipboard matrix onto grid cells. Feed `values` to `diffEdits`, then `history.commit`. */
193
+ export function planPaste(matrix, target) {
194
+ const plan = { values: [], errors: [], clipped: 0 };
195
+ const { rowIds, columnIds, start } = target;
196
+ let src = matrix;
197
+ if (target.fillRange && matrix.length === 1 && matrix[0]?.length === 1) {
198
+ const h = target.fillRange.rowEnd - start.row + 1;
199
+ const w = target.fillRange.colEnd - start.col + 1;
200
+ src = Array.from({ length: Math.max(1, h) }, () => Array.from({ length: Math.max(1, w) }, () => matrix[0][0]));
201
+ }
202
+ src.forEach((line, r) => {
203
+ line.forEach((raw, c) => {
204
+ const ri = start.row + r;
205
+ const ci = start.col + c;
206
+ if (ri >= rowIds.length || ci >= columnIds.length) {
207
+ plan.clipped++;
208
+ return;
209
+ }
210
+ const rowId = rowIds[ri];
211
+ const columnId = columnIds[ci];
212
+ if (target.readOnly?.(columnId))
213
+ return;
214
+ const parsed = target.parse ? target.parse(raw, columnId) : { ok: true, value: raw };
215
+ if (parsed.ok)
216
+ plan.values.push({ rowId, columnId, value: parsed.value });
217
+ else
218
+ plan.errors.push({ rowId, columnId, raw, error: parsed.error });
219
+ });
220
+ });
221
+ return plan;
222
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * jatui/grid — pure helpers for spreadsheet-style grids (jatui-6kitr).
3
+ *
4
+ * TSV clipboard parse/serialize, injection-safe CSV export, A1 addressing,
5
+ * typed cell parsing, and diff-based edit transactions with a stale-data
6
+ * undo guard. No DOM, no dependencies: point an editable table (MOS tables,
7
+ * IDE tables, a future DataTable editable mode) at these instead of
8
+ * re-deriving them.
9
+ */
10
+ export { parseClipboardTsv, serializeTsv, escapeCsvFormula, serializeCsv, serializeRowsToCsv, type SerializeCsvOptions, type CsvColumn } from './clipboard';
11
+ export { columnLetter, columnIndex, parseCellRef, parseCellValue, type GridCellType, type GridCellSpec, type ParseCellResult } from './cells';
12
+ export { applyEdits, diffEdits, applyTransaction, transactionMatches, deleteRows, createEditHistory, planPaste, type CellEdit, type GridTransaction, type GridAccess, type EditHistory, type EditHistoryOptions, type HistoryStep, type PasteTarget, type PastePlan } from './edits';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * jatui/grid — pure helpers for spreadsheet-style grids (jatui-6kitr).
3
+ *
4
+ * TSV clipboard parse/serialize, injection-safe CSV export, A1 addressing,
5
+ * typed cell parsing, and diff-based edit transactions with a stale-data
6
+ * undo guard. No DOM, no dependencies: point an editable table (MOS tables,
7
+ * IDE tables, a future DataTable editable mode) at these instead of
8
+ * re-deriving them.
9
+ */
10
+ export { parseClipboardTsv, serializeTsv, escapeCsvFormula, serializeCsv, serializeRowsToCsv } from './clipboard';
11
+ export { columnLetter, columnIndex, parseCellRef, parseCellValue } from './cells';
12
+ export { applyEdits, diffEdits, applyTransaction, transactionMatches, deleteRows, createEditHistory, planPaste } from './edits';
package/dist/index.d.ts CHANGED
@@ -351,3 +351,25 @@ export { stepsSummary, stepsRemaining, isStepsFinished, clampStepProgress } from
351
351
  export { default as NotificationCenter } from './components/NotificationCenter.svelte';
352
352
  export { DEFAULT_NOTIFICATION_TABS, DATE_GROUP_LABELS as NOTIFICATION_DATE_GROUP_LABELS, dateGroupKey as notificationDateGroupKey, groupNotificationsByDate, toTimestampMs as notificationTimestampMs, isNotificationUnread, unreadIds as unreadNotificationIds, filterByTab as filterNotificationsByTab, tabCounts as notificationTabCounts, unreadSummary as notificationUnreadSummary } from './components/notificationCenter';
353
353
  export type { NotificationItem, NotificationAction, NotificationTab, NotificationStatus, NotificationGroup, DateGroupKey as NotificationDateGroupKey } from './components/notificationCenter';
354
+ export { default as TreeView } from './components/tree/TreeView.svelte';
355
+ export type { TreeRowContext, TreeViewVirtualOptions } from './components/tree/TreeView.svelte';
356
+ export { flattenTree, collectBranchIds, pathToNode } from './components/tree/flattenTree';
357
+ export type { FlatTreeNode, FlattenTreeOptions } from './components/tree/flattenTree';
358
+ export { resolveTreeKey, treeTypeahead } from './components/tree/treeKeys';
359
+ export type { TreeKeyResult } from './components/tree/treeKeys';
360
+ export { default as StatCard } from './components/stats/StatCard.svelte';
361
+ export type { StatTone } from './components/stats/StatCard.svelte';
362
+ export { default as StatGroup } from './components/stats/StatGroup.svelte';
363
+ export { default as ChartCard } from './components/stats/ChartCard.svelte';
364
+ export type { ChartCardPoint, ChartCardContext } from './components/stats/ChartCard.svelte';
365
+ export { default as BarList } from './components/stats/BarList.svelte';
366
+ export type { BarListItem } from './components/stats/BarList.svelte';
367
+ export { default as ActivityHeatmap } from './components/stats/ActivityHeatmap.svelte';
368
+ export { buildHeatmap, moveHeatFocus, isoDay as heatmapIsoDay, defaultLevelOf as heatmapLevelOf } from './components/stats/heatmap';
369
+ export type { HeatDay, HeatmapLayout, BuildHeatmapOptions } from './components/stats/heatmap';
370
+ export { default as SegmentedControl } from './components/SegmentedControl.svelte';
371
+ export type { SegmentedOption } from './components/SegmentedControl.svelte';
372
+ export { default as SettingsSection } from './components/settings/SettingsSection.svelte';
373
+ export { default as SettingsRow } from './components/settings/SettingsRow.svelte';
374
+ export type { SettingsRowContext } from './components/settings/SettingsRow.svelte';
375
+ export * from './data/grid/index';
package/dist/index.js CHANGED
@@ -393,3 +393,24 @@ export { splitStreamedText } from './components/agent/streamedText';
393
393
  export { stepsSummary, stepsRemaining, isStepsFinished, clampStepProgress } from './components/progress/stepsSummary';
394
394
  export { default as NotificationCenter } from './components/NotificationCenter.svelte';
395
395
  export { DEFAULT_NOTIFICATION_TABS, DATE_GROUP_LABELS as NOTIFICATION_DATE_GROUP_LABELS, dateGroupKey as notificationDateGroupKey, groupNotificationsByDate, toTimestampMs as notificationTimestampMs, isNotificationUnread, unreadIds as unreadNotificationIds, filterByTab as filterNotificationsByTab, tabCounts as notificationTabCounts, unreadSummary as notificationUnreadSummary } from './components/notificationCenter';
396
+ // BoardUI wave 3 (jatui-6kitr) — tree, data and dashboards.
397
+ // TreeView renders flattenTree's flat row list (role="tree", aria-level/
398
+ // posinset/setsize, connector guides, full APG keys + typeahead, optional
399
+ // jatui/virtual windowing). Stats: StatCard/StatGroup (AnimatedNumber's new
400
+ // mode="tween"), ChartCard (headline driven by chart hover, no chart lib),
401
+ // BarList, ActivityHeatmap. SegmentedControl = radio semantics (Tabs switch
402
+ // panels; this sets a value). SettingsSection/SettingsRow = settings pages.
403
+ // Pure grid helpers also live on the 'jatui/grid' subpath.
404
+ export { default as TreeView } from './components/tree/TreeView.svelte';
405
+ export { flattenTree, collectBranchIds, pathToNode } from './components/tree/flattenTree';
406
+ export { resolveTreeKey, treeTypeahead } from './components/tree/treeKeys';
407
+ export { default as StatCard } from './components/stats/StatCard.svelte';
408
+ export { default as StatGroup } from './components/stats/StatGroup.svelte';
409
+ export { default as ChartCard } from './components/stats/ChartCard.svelte';
410
+ export { default as BarList } from './components/stats/BarList.svelte';
411
+ export { default as ActivityHeatmap } from './components/stats/ActivityHeatmap.svelte';
412
+ export { buildHeatmap, moveHeatFocus, isoDay as heatmapIsoDay, defaultLevelOf as heatmapLevelOf } from './components/stats/heatmap';
413
+ export { default as SegmentedControl } from './components/SegmentedControl.svelte';
414
+ export { default as SettingsSection } from './components/settings/SettingsSection.svelte';
415
+ export { default as SettingsRow } from './components/settings/SettingsRow.svelte';
416
+ export * from './data/grid/index';
@@ -37,7 +37,7 @@
37
37
  let paused = false;
38
38
 
39
39
  const prefersReducedMotion =
40
- typeof window !== 'undefined'
40
+ typeof window !== 'undefined' && typeof window.matchMedia === 'function'
41
41
  ? window.matchMedia('(prefers-reduced-motion: reduce)').matches
42
42
  : false;
43
43
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.40.0",
3
+ "version": "0.41.1",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",
@@ -34,6 +34,10 @@
34
34
  "svelte": "./dist/components/table/index.js",
35
35
  "default": "./dist/components/table/index.js"
36
36
  },
37
+ "./grid": {
38
+ "types": "./dist/data/grid/index.d.ts",
39
+ "default": "./dist/data/grid/index.js"
40
+ },
37
41
  "./virtual": {
38
42
  "types": "./dist/virtual/index.d.ts",
39
43
  "svelte": "./dist/virtual/index.js",