@joewinke/jatui 0.39.0 → 0.41.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 +52 -5
- package/THIRD_PARTY_NOTICES.md +31 -0
- package/dist/actions/scrollEdge.d.ts +55 -0
- package/dist/actions/scrollEdge.js +144 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +1 -1
- package/dist/artifact-blocks/blocks/diagram/schema.d.ts +1 -1
- package/dist/components/AnimatedNumber.svelte +42 -1
- package/dist/components/AnimatedNumber.svelte.d.ts +7 -0
- package/dist/components/BottomSheet.svelte +7 -6
- package/dist/components/BottomSheet.svelte.d.ts +2 -0
- package/dist/components/CloseButton.svelte +75 -0
- package/dist/components/CloseButton.svelte.d.ts +29 -0
- package/dist/components/Drawer.svelte +10 -11
- package/dist/components/Drawer.svelte.d.ts +2 -8
- package/dist/components/IconButton.svelte +200 -0
- package/dist/components/IconButton.svelte.d.ts +46 -0
- package/dist/components/LinkButton.svelte +117 -0
- package/dist/components/LinkButton.svelte.d.ts +32 -0
- package/dist/components/MediaLightbox.svelte +2 -7
- package/dist/components/Modal.svelte +2 -10
- package/dist/components/PinInput.svelte +94 -31
- package/dist/components/PinInput.svelte.d.ts +4 -0
- package/dist/components/SegmentedControl.svelte +263 -0
- package/dist/components/SegmentedControl.svelte.d.ts +48 -0
- package/dist/components/TextArea.svelte +67 -5
- package/dist/components/TextArea.svelte.d.ts +4 -6
- package/dist/components/ThemeSelector.svelte +29 -9
- package/dist/components/ThemeSelector.svelte.d.ts +6 -0
- package/dist/components/ThemeToggle.svelte +28 -3
- package/dist/components/ThemeToggle.svelte.d.ts +9 -0
- package/dist/components/basic/Breadcrumb.svelte +226 -11
- package/dist/components/basic/Breadcrumb.svelte.d.ts +27 -1
- package/dist/components/basic/breadcrumbCollapse.d.ts +19 -0
- package/dist/components/basic/breadcrumbCollapse.js +12 -0
- package/dist/components/graph/GraphCanvas.svelte.d.ts +2 -2
- package/dist/components/settings/SettingsRow.svelte +177 -0
- package/dist/components/settings/SettingsRow.svelte.d.ts +32 -0
- package/dist/components/settings/SettingsSection.svelte +139 -0
- package/dist/components/settings/SettingsSection.svelte.d.ts +21 -0
- package/dist/components/stats/ActivityHeatmap.svelte +412 -0
- package/dist/components/stats/ActivityHeatmap.svelte.d.ts +72 -0
- package/dist/components/stats/BarList.svelte +262 -0
- package/dist/components/stats/BarList.svelte.d.ts +57 -0
- package/dist/components/stats/ChartCard.svelte +230 -0
- package/dist/components/stats/ChartCard.svelte.d.ts +63 -0
- package/dist/components/stats/StatCard.svelte +387 -0
- package/dist/components/stats/StatCard.svelte.d.ts +37 -0
- package/dist/components/stats/StatGroup.svelte +67 -0
- package/dist/components/stats/StatGroup.svelte.d.ts +14 -0
- package/dist/components/stats/heatmap.d.ts +76 -0
- package/dist/components/stats/heatmap.js +151 -0
- package/dist/components/tree/TreeView.svelte +680 -0
- package/dist/components/tree/TreeView.svelte.d.ts +123 -0
- package/dist/components/tree/flattenTree.d.ts +57 -0
- package/dist/components/tree/flattenTree.js +91 -0
- package/dist/components/tree/treeKeys.d.ts +46 -0
- package/dist/components/tree/treeKeys.js +80 -0
- package/dist/data/grid/cells.d.ts +40 -0
- package/dist/data/grid/cells.js +89 -0
- package/dist/data/grid/clipboard.d.ts +46 -0
- package/dist/data/grid/clipboard.js +131 -0
- package/dist/data/grid/edits.d.ts +148 -0
- package/dist/data/grid/edits.js +222 -0
- package/dist/data/grid/index.d.ts +12 -0
- package/dist/data/grid/index.js +12 -0
- package/dist/index.d.ts +36 -1
- package/dist/index.js +31 -1
- package/dist/styles/tokens.css +80 -1
- package/dist/utils/pinInput.d.ts +33 -0
- package/dist/utils/pinInput.js +63 -0
- package/dist/utils/textareaRows.d.ts +23 -0
- package/dist/utils/textareaRows.js +13 -0
- package/dist/utils/themeWipe.d.ts +36 -17
- package/dist/utils/themeWipe.js +145 -35
- 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
|
@@ -75,6 +75,10 @@ export { default as InfiniteSearchGrid } from './components/InfiniteSearchGrid.s
|
|
|
75
75
|
export { createInfiniteLoader } from './components/infiniteGridLoader';
|
|
76
76
|
export type { InfinitePage, InfiniteLoadContext, InfiniteLoadFn, InfiniteLoader, InfiniteLoaderState, InfiniteStatus } from './components/infiniteGridLoader';
|
|
77
77
|
export { default as Button } from './components/Button.svelte';
|
|
78
|
+
export { default as IconButton } from './components/IconButton.svelte';
|
|
79
|
+
export type { IconButtonVariant, IconButtonSize } from './components/IconButton.svelte';
|
|
80
|
+
export { default as CloseButton } from './components/CloseButton.svelte';
|
|
81
|
+
export { default as LinkButton } from './components/LinkButton.svelte';
|
|
78
82
|
export { default as Badge } from './components/Badge.svelte';
|
|
79
83
|
export { default as Card } from './components/Card.svelte';
|
|
80
84
|
export { default as Drawer } from './components/Drawer.svelte';
|
|
@@ -83,6 +87,8 @@ export { default as LoadingSpinner } from './components/LoadingSpinner.svelte';
|
|
|
83
87
|
export { default as StatusBadge } from './components/StatusBadge.svelte';
|
|
84
88
|
export { default as TextInput } from './components/TextInput.svelte';
|
|
85
89
|
export { default as TextArea } from './components/TextArea.svelte';
|
|
90
|
+
export { textareaRowsHeight } from './utils/textareaRows';
|
|
91
|
+
export type { TextareaRowsInput } from './utils/textareaRows';
|
|
86
92
|
export { default as SelectInput } from './components/SelectInput.svelte';
|
|
87
93
|
export { default as PhoneInput } from './components/PhoneInput.svelte';
|
|
88
94
|
export { default as AudioWaveform } from './components/AudioWaveform.svelte';
|
|
@@ -202,6 +208,8 @@ export type { MapJob, MapAgent } from './types/maps';
|
|
|
202
208
|
export { loadGoogleMapsAPI, isGoogleMapsLoaded, isPlacesLoaded, isGeometryLoaded, } from './utils/googleMapsLoader';
|
|
203
209
|
export { tooltip } from './actions/tooltip';
|
|
204
210
|
export type { TooltipOptions } from './actions/tooltip';
|
|
211
|
+
export { scrollEdge, edgeOpacity, edgeDistances } from './actions/scrollEdge';
|
|
212
|
+
export type { ScrollEdgeOptions, ScrollEdgeSide } from './actions/scrollEdge';
|
|
205
213
|
export { staggerEntrance } from './actions/staggerEntrance';
|
|
206
214
|
export type { StaggerEntranceOptions } from './actions/staggerEntrance';
|
|
207
215
|
export { attention } from './actions/attention';
|
|
@@ -216,7 +224,8 @@ export { floating, makeVirtualAnchor } from './actions/floating';
|
|
|
216
224
|
export type { FloatingOptions, Placement } from './actions/floating';
|
|
217
225
|
export { glide } from './transitions/glide';
|
|
218
226
|
export type { GlideOrigin, GlideParams } from './transitions/glide';
|
|
219
|
-
export { themeWipe } from './utils/themeWipe';
|
|
227
|
+
export { themeWipe, wipeOrigin, coverRadius } from './utils/themeWipe';
|
|
228
|
+
export type { ThemeWipeOptions } from './utils/themeWipe';
|
|
220
229
|
export { viewTransition } from './utils/viewTransition';
|
|
221
230
|
export { MOTION_LEVELS, DEFAULT_MOTION_LEVEL, MOTION_LEVEL_ATTR, MOTION_DURATIONS, MOTION_LEVEL_SCALE, MOTION_LEVEL_TRAVEL, MOTION_EASING_POINTS, isMotionLevel, parseMotionLevel, prefersReducedMotion, resolveMotionLevel, motionContextFor, getMotionContext, motionDuration, effectiveEasing, setMotionLevel, cubicBezier, motionEasing, motionFade, motionScale, motionSlide, resolveMotionTiming, createMotionLevelStore } from './motion';
|
|
222
231
|
export type { MotionLevel, MotionDurationToken, MotionEasingToken, MotionContext, EasingFn, MotionTransitionParams, MotionTransitionConfig, MotionFadeParams, MotionScaleParams, MotionSlideParams, ResolvedMotionTiming, MotionState, MotionLevelStore, MotionLevelStoreOptions } from './motion';
|
|
@@ -240,6 +249,7 @@ export { default as AnimatedNumber } from './components/AnimatedNumber.svelte';
|
|
|
240
249
|
export { default as CopyButton } from './components/CopyButton.svelte';
|
|
241
250
|
export { default as HoldToConfirm } from './components/HoldToConfirm.svelte';
|
|
242
251
|
export { default as PinInput } from './components/PinInput.svelte';
|
|
252
|
+
export { applyCellInput as applyPinCellInput, distribute as distributePin, cellsFromValue as pinCellsFromValue, isPinComplete, sanitizePin } from './utils/pinInput';
|
|
243
253
|
export { default as PasswordMeter } from './components/PasswordMeter.svelte';
|
|
244
254
|
export { default as QuantityStepper } from './components/QuantityStepper.svelte';
|
|
245
255
|
export { default as TypingIndicator } from './components/messaging/TypingIndicator.svelte';
|
|
@@ -261,6 +271,9 @@ export type { StickState, StickToBottomOptions, StickController } from './action
|
|
|
261
271
|
export { ansiToHtml, ansiToHtmlStateful, ansiToHtmlWithLinks, ansiToHtmlWithHierarchy, linkifyUrls, stripAnsi, classifyLine, parseOutputLines, createDefaultAnsiState } from './utils/ansiToHtml';
|
|
262
272
|
export type { AnsiState, ParsedLine } from './utils/ansiToHtml';
|
|
263
273
|
export { default as Breadcrumb } from './components/basic/Breadcrumb.svelte';
|
|
274
|
+
export type { BreadcrumbItem } from './components/basic/Breadcrumb.svelte';
|
|
275
|
+
export { collapseBreadcrumb } from './components/basic/breadcrumbCollapse';
|
|
276
|
+
export type { BreadcrumbSlot } from './components/basic/breadcrumbCollapse';
|
|
264
277
|
export { default as Alert } from './components/basic/Alert.svelte';
|
|
265
278
|
export { default as ButtonGroup } from './components/basic/ButtonGroup.svelte';
|
|
266
279
|
export { default as Switch } from './components/basic/Switch.svelte';
|
|
@@ -338,3 +351,25 @@ export { stepsSummary, stepsRemaining, isStepsFinished, clampStepProgress } from
|
|
|
338
351
|
export { default as NotificationCenter } from './components/NotificationCenter.svelte';
|
|
339
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';
|
|
340
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';
|