@marktiderman/genesis-ui 2.5.0 → 2.6.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/dist/components/data/index.d.ts +38 -2
- package/dist/components/data/index.js +114 -22
- package/dist/components/data/index.js.map +1 -1
- package/dist/components/sheet-grid/sheet-grid.d.ts +108 -0
- package/dist/components/sheet-grid/sheet-grid.js +434 -0
- package/dist/components/sheet-grid/sheet-grid.js.map +1 -0
- package/dist/index.js +4 -4
- package/package.json +14 -3
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface SheetGridColumn<T> {
|
|
5
|
+
key: string;
|
|
6
|
+
header: string;
|
|
7
|
+
/** Width in px. Default 160. */
|
|
8
|
+
width?: number;
|
|
9
|
+
/** The cell's text: what is shown when no `render` is given, edited, and copied. Default `String(row[key])`. */
|
|
10
|
+
getText?: (row: T) => string;
|
|
11
|
+
/** Custom display. Editing and copying still use `getText`. */
|
|
12
|
+
render?: (row: T) => ReactNode;
|
|
13
|
+
/** Whether this column's cells can be edited. */
|
|
14
|
+
editable?: boolean;
|
|
15
|
+
/** Why the cells can't be edited, shown on hover. */
|
|
16
|
+
readOnlyReason?: string;
|
|
17
|
+
/** Fixed choices: the editor becomes a select. */
|
|
18
|
+
options?: string[];
|
|
19
|
+
/** Shown before the header text, e.g. an icon for the field's type. */
|
|
20
|
+
icon?: ReactNode;
|
|
21
|
+
/** `end` right-aligns the cells, as for numbers. */
|
|
22
|
+
align?: "start" | "end";
|
|
23
|
+
}
|
|
24
|
+
/** A column pinned to the left of every row, outside the cell grid (row numbers, a checkbox, an open button). */
|
|
25
|
+
interface SheetGridRowHeader<T> {
|
|
26
|
+
width: number;
|
|
27
|
+
render: (row: T, index: number) => ReactNode;
|
|
28
|
+
/** The header cell above it. */
|
|
29
|
+
header?: ReactNode;
|
|
30
|
+
}
|
|
31
|
+
/** One cell's new text. A paste or a clear produces several at once. */
|
|
32
|
+
interface SheetCellChange<T> {
|
|
33
|
+
row: T;
|
|
34
|
+
rowKey: string;
|
|
35
|
+
column: string;
|
|
36
|
+
value: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Reject `onCommit` with this to mark specific cells, rather than every cell
|
|
40
|
+
* in the commit, as failed.
|
|
41
|
+
*/
|
|
42
|
+
declare class SheetCommitError extends Error {
|
|
43
|
+
readonly cells: {
|
|
44
|
+
rowKey: string;
|
|
45
|
+
column: string;
|
|
46
|
+
message: string;
|
|
47
|
+
}[];
|
|
48
|
+
constructor(message: string, cells: {
|
|
49
|
+
rowKey: string;
|
|
50
|
+
column: string;
|
|
51
|
+
message: string;
|
|
52
|
+
}[]);
|
|
53
|
+
}
|
|
54
|
+
interface SheetGridProps<T> {
|
|
55
|
+
rows: T[];
|
|
56
|
+
columns: SheetGridColumn<T>[];
|
|
57
|
+
getKey: (row: T) => string;
|
|
58
|
+
/**
|
|
59
|
+
* Save one or more cells. Return a promise to show them as saving until it
|
|
60
|
+
* settles; a rejection marks them failed with the error's message (or the
|
|
61
|
+
* cells a `SheetCommitError` names) and keeps their old values.
|
|
62
|
+
*/
|
|
63
|
+
onCommit: (changes: SheetCellChange<T>[]) => void | Promise<void>;
|
|
64
|
+
/** Cmd/Ctrl+Z inside the grid. */
|
|
65
|
+
onUndo?: () => void;
|
|
66
|
+
/** Called when a paste or clear touched cells that can't be edited. */
|
|
67
|
+
onSkipped?: (count: number) => void;
|
|
68
|
+
/** Viewport height in px. Default 480. */
|
|
69
|
+
height?: number;
|
|
70
|
+
/** Row height in px. Default 32. */
|
|
71
|
+
rowHeight?: number;
|
|
72
|
+
/** Accessible name for the grid. */
|
|
73
|
+
label: string;
|
|
74
|
+
className?: string;
|
|
75
|
+
/** A pinned column at the left of every row. */
|
|
76
|
+
rowHeader?: SheetGridRowHeader<T>;
|
|
77
|
+
/** Rendered after the last row, inside the scroll area (e.g. "Add a record"). */
|
|
78
|
+
footer?: ReactNode;
|
|
79
|
+
/** Shown when there are no rows. */
|
|
80
|
+
empty?: ReactNode;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A spreadsheet grid: virtualized rows, a keyboard-driven active cell and
|
|
84
|
+
* range, in-place editing, and copy and paste through the system clipboard
|
|
85
|
+
* as tab-separated text, so it round-trips with Sheets and Excel.
|
|
86
|
+
*
|
|
87
|
+
* Keys: arrows, Tab and Shift+Tab move; Shift+arrows extend the range;
|
|
88
|
+
* Enter or F2 edits (Enter again saves and moves down); typing starts an
|
|
89
|
+
* edit; Escape cancels; Delete clears; Home/End and Ctrl+Home/End jump;
|
|
90
|
+
* PageUp/PageDown move a screen; Ctrl/Cmd+Z undoes.
|
|
91
|
+
*
|
|
92
|
+
* Why it isn't Radix (docs/FRAMEWORK.md rule 3): Radix has no data grid.
|
|
93
|
+
* The focus model follows the WAI-ARIA grid pattern instead: one tab stop
|
|
94
|
+
* (the grid), the active cell exposed through aria-activedescendant, and
|
|
95
|
+
* aria-selected for the range, so a screen reader announces each move.
|
|
96
|
+
* Virtualization comes from TanStack Virtual (decision 20's spike: stable,
|
|
97
|
+
* headless, actively released; react-data-grid's latest has been a beta for
|
|
98
|
+
* years and brings its own CSS).
|
|
99
|
+
*
|
|
100
|
+
* @stability Experimental
|
|
101
|
+
*/
|
|
102
|
+
declare function SheetGrid<T>({ rows, columns, getKey, onCommit, onUndo, onSkipped, height, rowHeight, label, className, rowHeader, footer, empty, }: SheetGridProps<T>): React.JSX.Element;
|
|
103
|
+
/** Parse spreadsheet clipboard text (tab-separated, quoted when needed). */
|
|
104
|
+
declare function parseTsv(text: string): string[][];
|
|
105
|
+
/** Write a block of cells as spreadsheet clipboard text (the inverse of `parseTsv`). */
|
|
106
|
+
declare function toTsv(grid: string[][]): string;
|
|
107
|
+
|
|
108
|
+
export { type SheetCellChange, SheetCommitError, SheetGrid, type SheetGridColumn, type SheetGridProps, type SheetGridRowHeader, parseTsv, toTsv };
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Spinner } from '../../chunk-OVU3Z2MT.js';
|
|
3
|
+
import { cn } from '../../chunk-NZ5WBA4R.js';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { useRef, useState, useCallback, useMemo, useEffect } from 'react';
|
|
6
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
7
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
8
|
+
|
|
9
|
+
var SheetCommitError = class extends Error {
|
|
10
|
+
constructor(message, cells) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.cells = cells;
|
|
13
|
+
this.name = "SheetCommitError";
|
|
14
|
+
}
|
|
15
|
+
cells;
|
|
16
|
+
};
|
|
17
|
+
function SheetGrid({
|
|
18
|
+
rows,
|
|
19
|
+
columns,
|
|
20
|
+
getKey,
|
|
21
|
+
onCommit,
|
|
22
|
+
onUndo,
|
|
23
|
+
onSkipped,
|
|
24
|
+
height = 480,
|
|
25
|
+
rowHeight = 32,
|
|
26
|
+
label,
|
|
27
|
+
className,
|
|
28
|
+
rowHeader,
|
|
29
|
+
footer,
|
|
30
|
+
empty
|
|
31
|
+
}) {
|
|
32
|
+
const scroller = useRef(null);
|
|
33
|
+
const [active, setActive] = useState({ r: 0, c: 0 });
|
|
34
|
+
const [anchor, setAnchor] = useState(null);
|
|
35
|
+
const [editing, setEditingState] = useState(null);
|
|
36
|
+
const editingRef = useRef(null);
|
|
37
|
+
const setEditing = (next) => {
|
|
38
|
+
editingRef.current = next;
|
|
39
|
+
setEditingState(next);
|
|
40
|
+
};
|
|
41
|
+
const [status, setStatus] = useState({});
|
|
42
|
+
const id = React.useId();
|
|
43
|
+
const virtualizer = useVirtualizer({
|
|
44
|
+
count: rows.length,
|
|
45
|
+
getScrollElement: () => scroller.current,
|
|
46
|
+
estimateSize: () => rowHeight,
|
|
47
|
+
overscan: 10,
|
|
48
|
+
// Render the first screen before layout is measured (server render, tests).
|
|
49
|
+
initialRect: { width: 1024, height }
|
|
50
|
+
});
|
|
51
|
+
const textOf = useCallback(
|
|
52
|
+
(row, column) => column.getText ? column.getText(row) : String(row[column.key] ?? ""),
|
|
53
|
+
[]
|
|
54
|
+
);
|
|
55
|
+
const cellKey = (rowKey, column) => `${rowKey}\0${column}`;
|
|
56
|
+
const lead = rowHeader?.width ?? 0;
|
|
57
|
+
const width = useMemo(() => lead + columns.reduce((sum, c) => sum + (c.width ?? 160), 0), [columns, lead]);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
setActive((a) => ({ r: Math.min(a.r, Math.max(0, rows.length - 1)), c: Math.min(a.c, Math.max(0, columns.length - 1)) }));
|
|
60
|
+
}, [rows.length, columns.length]);
|
|
61
|
+
const range = useMemo(() => {
|
|
62
|
+
const from = anchor ?? active;
|
|
63
|
+
return {
|
|
64
|
+
r0: Math.min(from.r, active.r),
|
|
65
|
+
r1: Math.max(from.r, active.r),
|
|
66
|
+
c0: Math.min(from.c, active.c),
|
|
67
|
+
c1: Math.max(from.c, active.c)
|
|
68
|
+
};
|
|
69
|
+
}, [anchor, active]);
|
|
70
|
+
const inRange = (r, c) => r >= range.r0 && r <= range.r1 && c >= range.c0 && c <= range.c1;
|
|
71
|
+
const moveTo = useCallback(
|
|
72
|
+
(cell, extend = false) => {
|
|
73
|
+
const next = { r: Math.max(0, Math.min(rows.length - 1, cell.r)), c: Math.max(0, Math.min(columns.length - 1, cell.c)) };
|
|
74
|
+
if (extend) setAnchor((a) => a ?? active);
|
|
75
|
+
else setAnchor(null);
|
|
76
|
+
setActive(next);
|
|
77
|
+
virtualizer.scrollToIndex(next.r, { align: "auto" });
|
|
78
|
+
},
|
|
79
|
+
[rows.length, columns.length, active, virtualizer]
|
|
80
|
+
);
|
|
81
|
+
const commit = useCallback(
|
|
82
|
+
async (changes) => {
|
|
83
|
+
if (!changes.length) return;
|
|
84
|
+
const keys = changes.map((ch) => cellKey(ch.rowKey, ch.column));
|
|
85
|
+
setStatus((s) => ({ ...s, ...Object.fromEntries(keys.map((k) => [k, { saving: true }])) }));
|
|
86
|
+
try {
|
|
87
|
+
await onCommit(changes);
|
|
88
|
+
setStatus((s) => {
|
|
89
|
+
const next = { ...s };
|
|
90
|
+
for (const k of keys) delete next[k];
|
|
91
|
+
return next;
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
95
|
+
setStatus((s) => {
|
|
96
|
+
const next = { ...s };
|
|
97
|
+
for (const k of keys) delete next[k];
|
|
98
|
+
if (error instanceof SheetCommitError && error.cells.length) {
|
|
99
|
+
for (const cell of error.cells) next[cellKey(cell.rowKey, cell.column)] = { error: cell.message };
|
|
100
|
+
} else {
|
|
101
|
+
for (const k of keys) next[k] = { error: message };
|
|
102
|
+
}
|
|
103
|
+
return next;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
[onCommit]
|
|
108
|
+
);
|
|
109
|
+
const startEdit = (cell, draft) => {
|
|
110
|
+
const column = columns[cell.c];
|
|
111
|
+
const row = rows[cell.r];
|
|
112
|
+
if (!column?.editable || !row) return;
|
|
113
|
+
setEditing({ cell, rowKey: getKey(row), draft: draft ?? textOf(row, column) });
|
|
114
|
+
};
|
|
115
|
+
const finishEdit = async (move) => {
|
|
116
|
+
const current = editingRef.current;
|
|
117
|
+
if (!current) return;
|
|
118
|
+
const { cell, rowKey, draft } = current;
|
|
119
|
+
const row = rows.find((r) => getKey(r) === rowKey);
|
|
120
|
+
const column = columns[cell.c];
|
|
121
|
+
setEditing(null);
|
|
122
|
+
scroller.current?.focus();
|
|
123
|
+
if (move) moveTo(move);
|
|
124
|
+
if (!row) return;
|
|
125
|
+
if (draft !== textOf(row, column)) {
|
|
126
|
+
await commit([{ row, rowKey, column: column.key, value: draft }]);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const blockChanges = (values, origin) => {
|
|
130
|
+
const changes = [];
|
|
131
|
+
let skipped = 0;
|
|
132
|
+
values.forEach((line, dr) => {
|
|
133
|
+
line.forEach((value, dc) => {
|
|
134
|
+
const row = rows[origin.r + dr];
|
|
135
|
+
const column = columns[origin.c + dc];
|
|
136
|
+
if (!row || !column) return;
|
|
137
|
+
if (!column.editable) {
|
|
138
|
+
skipped++;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (value !== textOf(row, column)) changes.push({ row, rowKey: getKey(row), column: column.key, value });
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
if (skipped) onSkipped?.(skipped);
|
|
145
|
+
return changes;
|
|
146
|
+
};
|
|
147
|
+
const onKeyDown = (e) => {
|
|
148
|
+
if (editing) return;
|
|
149
|
+
if (e.target !== e.currentTarget) return;
|
|
150
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
151
|
+
const page = Math.max(1, Math.floor(height / rowHeight) - 1);
|
|
152
|
+
const { r, c } = active;
|
|
153
|
+
const go = (cell) => {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
moveTo(cell, e.shiftKey && e.key !== "Tab");
|
|
156
|
+
};
|
|
157
|
+
switch (e.key) {
|
|
158
|
+
case "ArrowUp":
|
|
159
|
+
return go({ r: mod ? 0 : r - 1, c });
|
|
160
|
+
case "ArrowDown":
|
|
161
|
+
return go({ r: mod ? rows.length - 1 : r + 1, c });
|
|
162
|
+
case "ArrowLeft":
|
|
163
|
+
return go({ r, c: mod ? 0 : c - 1 });
|
|
164
|
+
case "ArrowRight":
|
|
165
|
+
return go({ r, c: mod ? columns.length - 1 : c + 1 });
|
|
166
|
+
case "Tab":
|
|
167
|
+
return go({ r, c: e.shiftKey ? c - 1 : c + 1 });
|
|
168
|
+
case "Home":
|
|
169
|
+
return go({ r: mod ? 0 : r, c: 0 });
|
|
170
|
+
case "End":
|
|
171
|
+
return go({ r: mod ? rows.length - 1 : r, c: columns.length - 1 });
|
|
172
|
+
case "PageUp":
|
|
173
|
+
return go({ r: r - page, c });
|
|
174
|
+
case "PageDown":
|
|
175
|
+
return go({ r: r + page, c });
|
|
176
|
+
case "Enter":
|
|
177
|
+
case "F2":
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
return startEdit(active);
|
|
180
|
+
case "Escape":
|
|
181
|
+
setAnchor(null);
|
|
182
|
+
return;
|
|
183
|
+
case "Delete":
|
|
184
|
+
case "Backspace": {
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
const blank = Array.from({ length: range.r1 - range.r0 + 1 }, () => Array.from({ length: range.c1 - range.c0 + 1 }, () => ""));
|
|
187
|
+
void commit(blockChanges(blank, { r: range.r0, c: range.c0 }));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
default:
|
|
191
|
+
if (mod && (e.key === "z" || e.key === "Z") && !e.shiftKey) {
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
onUndo?.();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (!mod && !e.altKey && e.key.length === 1) {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
startEdit(active, e.key);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const onCopy = (e) => {
|
|
203
|
+
if (editing || e.target !== e.currentTarget || rows.length === 0 || columns.length === 0) return;
|
|
204
|
+
const block = [];
|
|
205
|
+
for (let r = range.r0; r <= range.r1; r++) {
|
|
206
|
+
const line = [];
|
|
207
|
+
for (let c = range.c0; c <= range.c1; c++) line.push(textOf(rows[r], columns[c]));
|
|
208
|
+
block.push(line);
|
|
209
|
+
}
|
|
210
|
+
e.clipboardData.setData("text/plain", toTsv(block));
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
};
|
|
213
|
+
const onPaste = (e) => {
|
|
214
|
+
if (editing || e.target !== e.currentTarget) return;
|
|
215
|
+
const text = e.clipboardData.getData("text/plain");
|
|
216
|
+
if (!text) return;
|
|
217
|
+
e.preventDefault();
|
|
218
|
+
let block = parseTsv(text.replace(/\r?\n$/, ""));
|
|
219
|
+
if (block.length === 1 && block[0].length === 1 && (range.r1 > range.r0 || range.c1 > range.c0)) {
|
|
220
|
+
block = Array.from({ length: range.r1 - range.r0 + 1 }, () => Array.from({ length: range.c1 - range.c0 + 1 }, () => block[0][0]));
|
|
221
|
+
}
|
|
222
|
+
void commit(blockChanges(block, { r: range.r0, c: range.c0 }));
|
|
223
|
+
};
|
|
224
|
+
const items = virtualizer.getVirtualItems();
|
|
225
|
+
const activeCellId = `${id}-cell-${active.r}-${active.c}`;
|
|
226
|
+
return /* @__PURE__ */ jsx("div", { className: cn("overflow-hidden rounded-lg border bg-background", className), "data-testid": "sheet-grid", children: /* @__PURE__ */ jsxs(
|
|
227
|
+
"div",
|
|
228
|
+
{
|
|
229
|
+
ref: scroller,
|
|
230
|
+
role: "grid",
|
|
231
|
+
"aria-label": label,
|
|
232
|
+
"aria-rowcount": rows.length + 1,
|
|
233
|
+
"aria-colcount": columns.length,
|
|
234
|
+
"aria-multiselectable": "true",
|
|
235
|
+
"aria-activedescendant": rows.length ? activeCellId : void 0,
|
|
236
|
+
tabIndex: 0,
|
|
237
|
+
className: "group/grid relative overflow-auto text-sm outline-none",
|
|
238
|
+
style: { height },
|
|
239
|
+
onKeyDown,
|
|
240
|
+
onCopy,
|
|
241
|
+
onPaste,
|
|
242
|
+
children: [
|
|
243
|
+
/* @__PURE__ */ jsxs("div", { role: "row", "aria-rowindex": 1, className: "sticky top-0 z-20 flex border-b bg-muted", style: { width }, children: [
|
|
244
|
+
rowHeader && /* @__PURE__ */ jsx(
|
|
245
|
+
"div",
|
|
246
|
+
{
|
|
247
|
+
role: "presentation",
|
|
248
|
+
className: "sticky left-0 z-10 flex shrink-0 items-center border-r bg-muted px-2 text-xs text-muted-foreground",
|
|
249
|
+
style: { width: rowHeader.width, height: rowHeight },
|
|
250
|
+
children: rowHeader.header
|
|
251
|
+
}
|
|
252
|
+
),
|
|
253
|
+
columns.map((column, c) => /* @__PURE__ */ jsxs(
|
|
254
|
+
"div",
|
|
255
|
+
{
|
|
256
|
+
role: "columnheader",
|
|
257
|
+
"aria-colindex": c + 1,
|
|
258
|
+
className: "flex shrink-0 items-center gap-1.5 border-r px-2 py-1.5 text-xs font-medium text-muted-foreground",
|
|
259
|
+
style: { width: column.width ?? 160, height: rowHeight },
|
|
260
|
+
title: column.readOnlyReason,
|
|
261
|
+
children: [
|
|
262
|
+
column.icon && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", "aria-hidden": true, children: column.icon }),
|
|
263
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: column.header })
|
|
264
|
+
]
|
|
265
|
+
},
|
|
266
|
+
column.key
|
|
267
|
+
))
|
|
268
|
+
] }),
|
|
269
|
+
rows.length === 0 && empty && /* @__PURE__ */ jsx("div", { className: "sticky left-0 px-3 py-6 text-sm text-muted-foreground", style: { width: "100%" }, children: empty }),
|
|
270
|
+
/* @__PURE__ */ jsx("div", { style: { height: virtualizer.getTotalSize(), width, position: "relative" }, children: items.map((item) => {
|
|
271
|
+
const row = rows[item.index];
|
|
272
|
+
const rowKey = getKey(row);
|
|
273
|
+
return /* @__PURE__ */ jsxs(
|
|
274
|
+
"div",
|
|
275
|
+
{
|
|
276
|
+
role: "row",
|
|
277
|
+
"aria-rowindex": item.index + 2,
|
|
278
|
+
className: "group/row absolute left-0 flex border-b hover:bg-muted/40",
|
|
279
|
+
style: { top: item.start, height: rowHeight, width },
|
|
280
|
+
children: [
|
|
281
|
+
rowHeader && /* @__PURE__ */ jsx(
|
|
282
|
+
"div",
|
|
283
|
+
{
|
|
284
|
+
role: "presentation",
|
|
285
|
+
className: "sticky left-0 z-10 flex shrink-0 items-center border-r bg-background px-2 text-xs text-muted-foreground group-hover/row:bg-muted",
|
|
286
|
+
style: { width: rowHeader.width },
|
|
287
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
288
|
+
children: rowHeader.render(row, item.index)
|
|
289
|
+
}
|
|
290
|
+
),
|
|
291
|
+
columns.map((column, c) => {
|
|
292
|
+
const isActive = active.r === item.index && active.c === c;
|
|
293
|
+
const selected = inRange(item.index, c);
|
|
294
|
+
const state = status[cellKey(rowKey, column.key)];
|
|
295
|
+
const isEditing = editing && editing.rowKey === rowKey && editing.cell.c === c;
|
|
296
|
+
return /* @__PURE__ */ jsxs(
|
|
297
|
+
"div",
|
|
298
|
+
{
|
|
299
|
+
id: `${id}-cell-${item.index}-${c}`,
|
|
300
|
+
role: "gridcell",
|
|
301
|
+
"aria-colindex": c + 1,
|
|
302
|
+
"aria-selected": selected,
|
|
303
|
+
"aria-readonly": !column.editable || void 0,
|
|
304
|
+
"aria-invalid": state?.error ? true : void 0,
|
|
305
|
+
"aria-busy": state?.saving || void 0,
|
|
306
|
+
title: state?.error ?? (!column.editable ? column.readOnlyReason : void 0),
|
|
307
|
+
className: cn(
|
|
308
|
+
"relative flex shrink-0 items-center border-r px-2",
|
|
309
|
+
selected && (range.r1 > range.r0 || range.c1 > range.c0) && "bg-primary/5",
|
|
310
|
+
// The active cell shows while the grid has focus, as in a spreadsheet.
|
|
311
|
+
isActive && "group-focus-within/grid:outline group-focus-within/grid:outline-2 group-focus-within/grid:-outline-offset-2 group-focus-within/grid:outline-primary",
|
|
312
|
+
state?.error && "bg-destructive/10",
|
|
313
|
+
column.align === "end" && "justify-end tabular-nums"
|
|
314
|
+
),
|
|
315
|
+
style: { width: column.width ?? 160 },
|
|
316
|
+
onMouseDown: (e) => {
|
|
317
|
+
if (isEditing) return;
|
|
318
|
+
e.preventDefault();
|
|
319
|
+
scroller.current?.focus();
|
|
320
|
+
moveTo({ r: item.index, c }, e.shiftKey);
|
|
321
|
+
},
|
|
322
|
+
onDoubleClick: () => startEdit({ r: item.index, c }),
|
|
323
|
+
children: [
|
|
324
|
+
isEditing ? column.options ? /* @__PURE__ */ jsxs(
|
|
325
|
+
"select",
|
|
326
|
+
{
|
|
327
|
+
autoFocus: true,
|
|
328
|
+
"aria-label": `${column.header}, row ${item.index + 1}`,
|
|
329
|
+
className: "h-full w-full bg-background text-sm outline-none",
|
|
330
|
+
value: editing.draft,
|
|
331
|
+
onChange: (e) => setEditing({ ...editing, draft: e.target.value }),
|
|
332
|
+
onBlur: () => void finishEdit(),
|
|
333
|
+
onKeyDown: (e) => {
|
|
334
|
+
if (e.key === "Enter") void finishEdit({ r: item.index + 1, c });
|
|
335
|
+
if (e.key === "Escape") {
|
|
336
|
+
setEditing(null);
|
|
337
|
+
scroller.current?.focus();
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
children: [
|
|
341
|
+
!column.options.includes(editing.draft) && /* @__PURE__ */ jsx("option", { value: editing.draft, children: editing.draft || "\u2014" }),
|
|
342
|
+
column.options.map((o) => /* @__PURE__ */ jsx("option", { value: o, children: o }, o))
|
|
343
|
+
]
|
|
344
|
+
}
|
|
345
|
+
) : /* @__PURE__ */ jsx(
|
|
346
|
+
"input",
|
|
347
|
+
{
|
|
348
|
+
autoFocus: true,
|
|
349
|
+
"aria-label": `${column.header}, row ${item.index + 1}`,
|
|
350
|
+
className: "h-full w-full bg-background text-sm outline-none",
|
|
351
|
+
value: editing.draft,
|
|
352
|
+
onChange: (e) => setEditing({ ...editing, draft: e.target.value }),
|
|
353
|
+
onBlur: () => void finishEdit(),
|
|
354
|
+
onKeyDown: (e) => {
|
|
355
|
+
if (e.key === "Enter") {
|
|
356
|
+
e.preventDefault();
|
|
357
|
+
void finishEdit({ r: item.index + 1, c });
|
|
358
|
+
} else if (e.key === "Tab") {
|
|
359
|
+
e.preventDefault();
|
|
360
|
+
void finishEdit({ r: item.index, c: c + (e.shiftKey ? -1 : 1) });
|
|
361
|
+
} else if (e.key === "Escape") {
|
|
362
|
+
setEditing(null);
|
|
363
|
+
scroller.current?.focus();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
) : /* @__PURE__ */ jsx("span", { className: "truncate", children: column.render ? column.render(row) : textOf(row, column) || /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }),
|
|
368
|
+
state?.saving && /* @__PURE__ */ jsx(Spinner, { size: "sm", className: "absolute right-1 h-3 w-3", "aria-label": "Saving" })
|
|
369
|
+
]
|
|
370
|
+
},
|
|
371
|
+
column.key
|
|
372
|
+
);
|
|
373
|
+
})
|
|
374
|
+
]
|
|
375
|
+
},
|
|
376
|
+
rowKey
|
|
377
|
+
);
|
|
378
|
+
}) }),
|
|
379
|
+
footer && /* @__PURE__ */ jsx("div", { className: "sticky left-0 border-b", style: { width: Math.max(width, 0) }, children: footer })
|
|
380
|
+
]
|
|
381
|
+
}
|
|
382
|
+
) });
|
|
383
|
+
}
|
|
384
|
+
function parseTsv(text) {
|
|
385
|
+
const rows = [];
|
|
386
|
+
let row = [];
|
|
387
|
+
let field = "";
|
|
388
|
+
let quoted = false;
|
|
389
|
+
let wasQuoted = false;
|
|
390
|
+
for (let i = 0; i < text.length; i++) {
|
|
391
|
+
const ch = text[i];
|
|
392
|
+
if (quoted) {
|
|
393
|
+
if (ch === '"' && text[i + 1] === '"') {
|
|
394
|
+
field += '"';
|
|
395
|
+
i++;
|
|
396
|
+
} else if (ch === '"') {
|
|
397
|
+
quoted = false;
|
|
398
|
+
} else {
|
|
399
|
+
field += ch;
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (ch === '"' && field === "") {
|
|
404
|
+
quoted = true;
|
|
405
|
+
wasQuoted = true;
|
|
406
|
+
} else if (ch === " ") {
|
|
407
|
+
row.push(field);
|
|
408
|
+
field = "";
|
|
409
|
+
wasQuoted = false;
|
|
410
|
+
} else if (ch === "\n" || ch === "\r") {
|
|
411
|
+
if (ch === "\r" && text[i + 1] === "\n") i++;
|
|
412
|
+
row.push(field);
|
|
413
|
+
rows.push(row);
|
|
414
|
+
row = [];
|
|
415
|
+
field = "";
|
|
416
|
+
wasQuoted = false;
|
|
417
|
+
} else field += ch;
|
|
418
|
+
}
|
|
419
|
+
if (field !== "" || wasQuoted || row.length) {
|
|
420
|
+
row.push(field);
|
|
421
|
+
rows.push(row);
|
|
422
|
+
}
|
|
423
|
+
return rows;
|
|
424
|
+
}
|
|
425
|
+
function tsvField(text) {
|
|
426
|
+
return /[\t\n\r"]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
427
|
+
}
|
|
428
|
+
function toTsv(grid) {
|
|
429
|
+
return grid.map((row) => row.length === 1 && row[0] === "" ? '""' : row.map(tsvField).join(" ")).join("\n");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export { SheetCommitError, SheetGrid, parseTsv, toTsv };
|
|
433
|
+
//# sourceMappingURL=sheet-grid.js.map
|
|
434
|
+
//# sourceMappingURL=sheet-grid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/sheet-grid/sheet-grid.tsx"],"names":[],"mappings":"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","file":"sheet-grid.js","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { cn } from \"../../utils\";\nimport { Spinner } from \"../ui/spinner\";\n\n/* ── Types ── */\n\nexport interface SheetGridColumn<T> {\n key: string;\n header: string;\n /** Width in px. Default 160. */\n width?: number;\n /** The cell's text: what is shown when no `render` is given, edited, and copied. Default `String(row[key])`. */\n getText?: (row: T) => string;\n /** Custom display. Editing and copying still use `getText`. */\n render?: (row: T) => ReactNode;\n /** Whether this column's cells can be edited. */\n editable?: boolean;\n /** Why the cells can't be edited, shown on hover. */\n readOnlyReason?: string;\n /** Fixed choices: the editor becomes a select. */\n options?: string[];\n /** Shown before the header text, e.g. an icon for the field's type. */\n icon?: ReactNode;\n /** `end` right-aligns the cells, as for numbers. */\n align?: \"start\" | \"end\";\n}\n\n/** A column pinned to the left of every row, outside the cell grid (row numbers, a checkbox, an open button). */\nexport interface SheetGridRowHeader<T> {\n width: number;\n render: (row: T, index: number) => ReactNode;\n /** The header cell above it. */\n header?: ReactNode;\n}\n\n/** One cell's new text. A paste or a clear produces several at once. */\nexport interface SheetCellChange<T> {\n row: T;\n rowKey: string;\n column: string;\n value: string;\n}\n\n/**\n * Reject `onCommit` with this to mark specific cells, rather than every cell\n * in the commit, as failed.\n */\nexport class SheetCommitError extends Error {\n constructor(\n message: string,\n readonly cells: { rowKey: string; column: string; message: string }[],\n ) {\n super(message);\n this.name = \"SheetCommitError\";\n }\n}\n\nexport interface SheetGridProps<T> {\n rows: T[];\n columns: SheetGridColumn<T>[];\n getKey: (row: T) => string;\n /**\n * Save one or more cells. Return a promise to show them as saving until it\n * settles; a rejection marks them failed with the error's message (or the\n * cells a `SheetCommitError` names) and keeps their old values.\n */\n onCommit: (changes: SheetCellChange<T>[]) => void | Promise<void>;\n /** Cmd/Ctrl+Z inside the grid. */\n onUndo?: () => void;\n /** Called when a paste or clear touched cells that can't be edited. */\n onSkipped?: (count: number) => void;\n /** Viewport height in px. Default 480. */\n height?: number;\n /** Row height in px. Default 32. */\n rowHeight?: number;\n /** Accessible name for the grid. */\n label: string;\n className?: string;\n /** A pinned column at the left of every row. */\n rowHeader?: SheetGridRowHeader<T>;\n /** Rendered after the last row, inside the scroll area (e.g. \"Add a record\"). */\n footer?: ReactNode;\n /** Shown when there are no rows. */\n empty?: ReactNode;\n}\n\ninterface Cell {\n r: number;\n c: number;\n}\n\n/* ── Component ── */\n\n/**\n * A spreadsheet grid: virtualized rows, a keyboard-driven active cell and\n * range, in-place editing, and copy and paste through the system clipboard\n * as tab-separated text, so it round-trips with Sheets and Excel.\n *\n * Keys: arrows, Tab and Shift+Tab move; Shift+arrows extend the range;\n * Enter or F2 edits (Enter again saves and moves down); typing starts an\n * edit; Escape cancels; Delete clears; Home/End and Ctrl+Home/End jump;\n * PageUp/PageDown move a screen; Ctrl/Cmd+Z undoes.\n *\n * Why it isn't Radix (docs/FRAMEWORK.md rule 3): Radix has no data grid.\n * The focus model follows the WAI-ARIA grid pattern instead: one tab stop\n * (the grid), the active cell exposed through aria-activedescendant, and\n * aria-selected for the range, so a screen reader announces each move.\n * Virtualization comes from TanStack Virtual (decision 20's spike: stable,\n * headless, actively released; react-data-grid's latest has been a beta for\n * years and brings its own CSS).\n *\n * @stability Experimental\n */\nexport function SheetGrid<T>({\n rows,\n columns,\n getKey,\n onCommit,\n onUndo,\n onSkipped,\n height = 480,\n rowHeight = 32,\n label,\n className,\n rowHeader,\n footer,\n empty,\n}: SheetGridProps<T>) {\n const scroller = useRef<HTMLDivElement>(null);\n const [active, setActive] = useState<Cell>({ r: 0, c: 0 });\n const [anchor, setAnchor] = useState<Cell | null>(null);\n // The row being edited is remembered by its key, not its position: rows can\n // be refetched, added or removed while the editor is open.\n const [editing, setEditingState] = useState<{ cell: Cell; rowKey: string; draft: string } | null>(null);\n // Mirrors `editing` synchronously. Enter and the blur that follows it fire\n // in the same tick; only the first may save, or the cell is sent twice\n // with the same (by then stale) version.\n const editingRef = useRef<{ cell: Cell; rowKey: string; draft: string } | null>(null);\n const setEditing = (next: { cell: Cell; rowKey: string; draft: string } | null) => {\n editingRef.current = next;\n setEditingState(next);\n };\n const [status, setStatus] = useState<Record<string, { saving?: boolean; error?: string }>>({});\n const id = React.useId();\n\n const virtualizer = useVirtualizer({\n count: rows.length,\n getScrollElement: () => scroller.current,\n estimateSize: () => rowHeight,\n overscan: 10,\n // Render the first screen before layout is measured (server render, tests).\n initialRect: { width: 1024, height },\n });\n\n const textOf = useCallback(\n (row: T, column: SheetGridColumn<T>) =>\n column.getText ? column.getText(row) : String((row as Record<string, unknown>)[column.key] ?? \"\"),\n [],\n );\n const cellKey = (rowKey: string, column: string) => `${rowKey}\\u0000${column}`;\n const lead = rowHeader?.width ?? 0;\n const width = useMemo(() => lead + columns.reduce((sum, c) => sum + (c.width ?? 160), 0), [columns, lead]);\n\n // Keep the active cell inside the grid as rows come and go.\n useEffect(() => {\n setActive((a) => ({ r: Math.min(a.r, Math.max(0, rows.length - 1)), c: Math.min(a.c, Math.max(0, columns.length - 1)) }));\n }, [rows.length, columns.length]);\n\n const range = useMemo(() => {\n const from = anchor ?? active;\n return {\n r0: Math.min(from.r, active.r),\n r1: Math.max(from.r, active.r),\n c0: Math.min(from.c, active.c),\n c1: Math.max(from.c, active.c),\n };\n }, [anchor, active]);\n const inRange = (r: number, c: number) => r >= range.r0 && r <= range.r1 && c >= range.c0 && c <= range.c1;\n\n const moveTo = useCallback(\n (cell: Cell, extend = false) => {\n const next = { r: Math.max(0, Math.min(rows.length - 1, cell.r)), c: Math.max(0, Math.min(columns.length - 1, cell.c)) };\n if (extend) setAnchor((a) => a ?? active);\n else setAnchor(null);\n setActive(next);\n virtualizer.scrollToIndex(next.r, { align: \"auto\" });\n },\n [rows.length, columns.length, active, virtualizer],\n );\n\n const commit = useCallback(\n async (changes: SheetCellChange<T>[]) => {\n if (!changes.length) return;\n const keys = changes.map((ch) => cellKey(ch.rowKey, ch.column));\n setStatus((s) => ({ ...s, ...Object.fromEntries(keys.map((k) => [k, { saving: true }])) }));\n try {\n await onCommit(changes);\n setStatus((s) => {\n const next = { ...s };\n for (const k of keys) delete next[k];\n return next;\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n setStatus((s) => {\n const next = { ...s };\n for (const k of keys) delete next[k];\n if (error instanceof SheetCommitError && error.cells.length) {\n for (const cell of error.cells) next[cellKey(cell.rowKey, cell.column)] = { error: cell.message };\n } else {\n for (const k of keys) next[k] = { error: message };\n }\n return next;\n });\n }\n },\n [onCommit],\n );\n\n const startEdit = (cell: Cell, draft?: string) => {\n const column = columns[cell.c];\n const row = rows[cell.r];\n if (!column?.editable || !row) return;\n setEditing({ cell, rowKey: getKey(row), draft: draft ?? textOf(row, column) });\n };\n\n const finishEdit = async (move?: Cell) => {\n const current = editingRef.current;\n if (!current) return;\n const { cell, rowKey, draft } = current;\n const row = rows.find((r) => getKey(r) === rowKey);\n const column = columns[cell.c]!;\n setEditing(null);\n scroller.current?.focus();\n if (move) moveTo(move);\n // The row left the grid while it was being edited: drop the draft rather\n // than save it to whichever row now sits at that position.\n if (!row) return;\n if (draft !== textOf(row, column)) {\n await commit([{ row, rowKey, column: column.key, value: draft }]);\n }\n };\n\n /** Changes for writing `values` (a block) at the active cell, skipping read-only cells. */\n const blockChanges = (values: string[][], origin: Cell) => {\n const changes: SheetCellChange<T>[] = [];\n let skipped = 0;\n values.forEach((line, dr) => {\n line.forEach((value, dc) => {\n const row = rows[origin.r + dr];\n const column = columns[origin.c + dc];\n if (!row || !column) return;\n if (!column.editable) {\n skipped++;\n return;\n }\n if (value !== textOf(row, column)) changes.push({ row, rowKey: getKey(row), column: column.key, value });\n });\n });\n if (skipped) onSkipped?.(skipped);\n return changes;\n };\n\n const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n if (editing) return; // the editor handles its own keys\n // Keys pressed in a row header or the footer (a checkbox, a button) belong to that control.\n if (e.target !== e.currentTarget) return;\n const mod = e.metaKey || e.ctrlKey;\n const page = Math.max(1, Math.floor(height / rowHeight) - 1);\n const { r, c } = active;\n const go = (cell: Cell) => {\n e.preventDefault();\n moveTo(cell, e.shiftKey && e.key !== \"Tab\");\n };\n switch (e.key) {\n case \"ArrowUp\":\n return go({ r: mod ? 0 : r - 1, c });\n case \"ArrowDown\":\n return go({ r: mod ? rows.length - 1 : r + 1, c });\n case \"ArrowLeft\":\n return go({ r, c: mod ? 0 : c - 1 });\n case \"ArrowRight\":\n return go({ r, c: mod ? columns.length - 1 : c + 1 });\n case \"Tab\":\n return go({ r, c: e.shiftKey ? c - 1 : c + 1 });\n case \"Home\":\n return go({ r: mod ? 0 : r, c: 0 });\n case \"End\":\n return go({ r: mod ? rows.length - 1 : r, c: columns.length - 1 });\n case \"PageUp\":\n return go({ r: r - page, c });\n case \"PageDown\":\n return go({ r: r + page, c });\n case \"Enter\":\n case \"F2\":\n e.preventDefault();\n return startEdit(active);\n case \"Escape\":\n setAnchor(null);\n return;\n case \"Delete\":\n case \"Backspace\": {\n e.preventDefault();\n const blank = Array.from({ length: range.r1 - range.r0 + 1 }, () => Array.from({ length: range.c1 - range.c0 + 1 }, () => \"\"));\n void commit(blockChanges(blank, { r: range.r0, c: range.c0 }));\n return;\n }\n default:\n if (mod && (e.key === \"z\" || e.key === \"Z\") && !e.shiftKey) {\n e.preventDefault();\n onUndo?.();\n return;\n }\n if (!mod && !e.altKey && e.key.length === 1) {\n e.preventDefault();\n startEdit(active, e.key);\n }\n }\n };\n\n const onCopy = (e: React.ClipboardEvent<HTMLDivElement>) => {\n if (editing || e.target !== e.currentTarget || rows.length === 0 || columns.length === 0) return;\n const block: string[][] = [];\n for (let r = range.r0; r <= range.r1; r++) {\n const line: string[] = [];\n for (let c = range.c0; c <= range.c1; c++) line.push(textOf(rows[r]!, columns[c]!));\n block.push(line);\n }\n e.clipboardData.setData(\"text/plain\", toTsv(block));\n e.preventDefault();\n };\n\n const onPaste = (e: React.ClipboardEvent<HTMLDivElement>) => {\n if (editing || e.target !== e.currentTarget) return;\n const text = e.clipboardData.getData(\"text/plain\");\n if (!text) return;\n e.preventDefault();\n let block = parseTsv(text.replace(/\\r?\\n$/, \"\"));\n // One value pasted over a range fills the range, as spreadsheets do.\n if (block.length === 1 && block[0]!.length === 1 && (range.r1 > range.r0 || range.c1 > range.c0)) {\n block = Array.from({ length: range.r1 - range.r0 + 1 }, () => Array.from({ length: range.c1 - range.c0 + 1 }, () => block[0]![0]!));\n }\n void commit(blockChanges(block, { r: range.r0, c: range.c0 }));\n };\n\n const items = virtualizer.getVirtualItems();\n const activeCellId = `${id}-cell-${active.r}-${active.c}`;\n\n return (\n <div className={cn(\"overflow-hidden rounded-lg border bg-background\", className)} data-testid=\"sheet-grid\">\n <div\n ref={scroller}\n role=\"grid\"\n aria-label={label}\n aria-rowcount={rows.length + 1}\n aria-colcount={columns.length}\n aria-multiselectable=\"true\"\n aria-activedescendant={rows.length ? activeCellId : undefined}\n tabIndex={0}\n className=\"group/grid relative overflow-auto text-sm outline-none\"\n style={{ height }}\n onKeyDown={onKeyDown}\n onCopy={onCopy}\n onPaste={onPaste}\n >\n <div role=\"row\" aria-rowindex={1} className=\"sticky top-0 z-20 flex border-b bg-muted\" style={{ width }}>\n {rowHeader && (\n <div\n role=\"presentation\"\n className=\"sticky left-0 z-10 flex shrink-0 items-center border-r bg-muted px-2 text-xs text-muted-foreground\"\n style={{ width: rowHeader.width, height: rowHeight }}\n >\n {rowHeader.header}\n </div>\n )}\n {columns.map((column, c) => (\n <div\n key={column.key}\n role=\"columnheader\"\n aria-colindex={c + 1}\n className=\"flex shrink-0 items-center gap-1.5 border-r px-2 py-1.5 text-xs font-medium text-muted-foreground\"\n style={{ width: column.width ?? 160, height: rowHeight }}\n title={column.readOnlyReason}\n >\n {column.icon && <span className=\"flex shrink-0 items-center\" aria-hidden>{column.icon}</span>}\n <span className=\"truncate\">{column.header}</span>\n </div>\n ))}\n </div>\n {rows.length === 0 && empty && (\n <div className=\"sticky left-0 px-3 py-6 text-sm text-muted-foreground\" style={{ width: \"100%\" }}>\n {empty}\n </div>\n )}\n <div style={{ height: virtualizer.getTotalSize(), width, position: \"relative\" }}>\n {items.map((item) => {\n const row = rows[item.index]!;\n const rowKey = getKey(row);\n return (\n <div\n key={rowKey}\n role=\"row\"\n aria-rowindex={item.index + 2}\n className=\"group/row absolute left-0 flex border-b hover:bg-muted/40\"\n style={{ top: item.start, height: rowHeight, width }}\n >\n {rowHeader && (\n <div\n role=\"presentation\"\n className=\"sticky left-0 z-10 flex shrink-0 items-center border-r bg-background px-2 text-xs text-muted-foreground group-hover/row:bg-muted\"\n style={{ width: rowHeader.width }}\n onMouseDown={(e) => e.stopPropagation()}\n >\n {rowHeader.render(row, item.index)}\n </div>\n )}\n {columns.map((column, c) => {\n const isActive = active.r === item.index && active.c === c;\n const selected = inRange(item.index, c);\n const state = status[cellKey(rowKey, column.key)];\n const isEditing = editing && editing.rowKey === rowKey && editing.cell.c === c;\n return (\n <div\n key={column.key}\n id={`${id}-cell-${item.index}-${c}`}\n role=\"gridcell\"\n aria-colindex={c + 1}\n aria-selected={selected}\n aria-readonly={!column.editable || undefined}\n aria-invalid={state?.error ? true : undefined}\n aria-busy={state?.saving || undefined}\n title={state?.error ?? (!column.editable ? column.readOnlyReason : undefined)}\n className={cn(\n \"relative flex shrink-0 items-center border-r px-2\",\n selected && (range.r1 > range.r0 || range.c1 > range.c0) && \"bg-primary/5\",\n // The active cell shows while the grid has focus, as in a spreadsheet.\n isActive && \"group-focus-within/grid:outline group-focus-within/grid:outline-2 group-focus-within/grid:-outline-offset-2 group-focus-within/grid:outline-primary\",\n state?.error && \"bg-destructive/10\",\n column.align === \"end\" && \"justify-end tabular-nums\",\n )}\n style={{ width: column.width ?? 160 }}\n onMouseDown={(e) => {\n if (isEditing) return;\n e.preventDefault();\n scroller.current?.focus();\n moveTo({ r: item.index, c }, e.shiftKey);\n }}\n onDoubleClick={() => startEdit({ r: item.index, c })}\n >\n {isEditing ? (\n column.options ? (\n <select\n autoFocus\n aria-label={`${column.header}, row ${item.index + 1}`}\n className=\"h-full w-full bg-background text-sm outline-none\"\n value={editing.draft}\n onChange={(e) => setEditing({ ...editing, draft: e.target.value })}\n onBlur={() => void finishEdit()}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") void finishEdit({ r: item.index + 1, c });\n if (e.key === \"Escape\") {\n setEditing(null);\n scroller.current?.focus();\n }\n }}\n >\n {!column.options.includes(editing.draft) && <option value={editing.draft}>{editing.draft || \"—\"}</option>}\n {column.options.map((o) => (\n <option key={o} value={o}>\n {o}\n </option>\n ))}\n </select>\n ) : (\n <input\n autoFocus\n aria-label={`${column.header}, row ${item.index + 1}`}\n className=\"h-full w-full bg-background text-sm outline-none\"\n value={editing.draft}\n onChange={(e) => setEditing({ ...editing, draft: e.target.value })}\n onBlur={() => void finishEdit()}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n void finishEdit({ r: item.index + 1, c });\n } else if (e.key === \"Tab\") {\n e.preventDefault();\n void finishEdit({ r: item.index, c: c + (e.shiftKey ? -1 : 1) });\n } else if (e.key === \"Escape\") {\n setEditing(null);\n scroller.current?.focus();\n }\n }}\n />\n )\n ) : (\n <span className=\"truncate\">{column.render ? column.render(row) : textOf(row, column) || <span className=\"text-muted-foreground\">—</span>}</span>\n )}\n {state?.saving && <Spinner size=\"sm\" className=\"absolute right-1 h-3 w-3\" aria-label=\"Saving\" />}\n </div>\n );\n })}\n </div>\n );\n })}\n </div>\n {footer && (\n <div className=\"sticky left-0 border-b\" style={{ width: Math.max(width, 0) }}>\n {footer}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n/* ── TSV for copy and paste ── */\n\n/** Parse spreadsheet clipboard text (tab-separated, quoted when needed). */\nexport function parseTsv(text: string): string[][] {\n const rows: string[][] = [];\n let row: string[] = [];\n let field = \"\";\n let quoted = false;\n // A field that was quoted counts even when empty: `\"\"` is one blank cell.\n let wasQuoted = false;\n for (let i = 0; i < text.length; i++) {\n const ch = text[i]!;\n if (quoted) {\n if (ch === '\"' && text[i + 1] === '\"') {\n field += '\"';\n i++;\n } else if (ch === '\"') {\n quoted = false;\n } else {\n field += ch;\n }\n continue;\n }\n if (ch === '\"' && field === \"\") {\n quoted = true;\n wasQuoted = true;\n } else if (ch === \"\\t\") {\n row.push(field);\n field = \"\";\n wasQuoted = false;\n } else if (ch === \"\\n\" || ch === \"\\r\") {\n if (ch === \"\\r\" && text[i + 1] === \"\\n\") i++;\n row.push(field);\n rows.push(row);\n row = [];\n field = \"\";\n wasQuoted = false;\n } else field += ch;\n }\n if (field !== \"\" || wasQuoted || row.length) {\n row.push(field);\n rows.push(row);\n }\n return rows;\n}\n\nfunction tsvField(text: string): string {\n return /[\\t\\n\\r\"]/.test(text) ? `\"${text.replace(/\"/g, '\"\"')}\"` : text;\n}\n\n/** Write a block of cells as spreadsheet clipboard text (the inverse of `parseTsv`). */\nexport function toTsv(grid: string[][]): string {\n // A row that is one blank cell would otherwise be an empty line, which a\n // paste can't tell from no row at all: write it as `\"\"`.\n return grid.map((row) => (row.length === 1 && row[0] === \"\" ? '\"\"' : row.map(tsvField).join(\"\\t\"))).join(\"\\n\");\n}\n"]}
|