@marktiderman/genesis-ui 2.5.0 → 2.7.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.
@@ -1,14 +1,14 @@
1
- export { AppShell, PageHeader } from '../../chunk-DWM7ELFI.js';
1
+ export { AppShell, PageHeader } from '../../chunk-ZPPLUGLA.js';
2
2
  import '../../chunk-46TZGMFW.js';
3
3
  export { Container, Stack, containerVariants, stackVariants } from '../../chunk-4CQHPWJD.js';
4
4
  export { FormPage } from '../../chunk-PLIYF6KI.js';
5
5
  import '../../chunk-OZZTPE7F.js';
6
6
  export { Section, SettingsPage, sectionHeadingVariants } from '../../chunk-P64MFQXZ.js';
7
- import '../../chunk-QJ6P2DW6.js';
8
7
  import '../../chunk-B2GIBUQD.js';
9
8
  import '../../chunk-NPNADZHM.js';
10
9
  import '../../chunk-FK42ZGGV.js';
11
10
  import '../../chunk-22XB3Q6H.js';
11
+ import '../../chunk-QJ6P2DW6.js';
12
12
  import '../../chunk-27ELYTGE.js';
13
13
  import '../../chunk-NWSCYLPT.js';
14
14
  import '../../chunk-F47U5KNW.js';
@@ -2,6 +2,7 @@ export { EmptyState, EmptyStateProps, NoFeaturesFound, NoFeedbackFound, NoSearch
2
2
  export { FormDescription, FormError, FormField, FormLabel } from './form-field.js';
3
3
  export { PageLoading, PageLoadingProps, pageLoadingVariants } from './page-loading.js';
4
4
  export { SettingsRow, SettingsRowProps } from './settings-row.js';
5
+ export { SortableList, SortableListItemContext, SortableListProps } from './sortable-list.js';
5
6
  export { StatusBadge, StatusBadgeProps, StatusBadgeStatus, statusBadgeVariants } from './status-badge.js';
6
7
  export { ToggleRow, ToggleRowProps } from './toggle-row.js';
7
8
  export { UserAvatar, UserAvatarProps, userAvatarVariants } from './user-avatar.js';
@@ -1,4 +1,5 @@
1
1
  export { ViewSettings, ViewToggle } from '../../chunk-46TZGMFW.js';
2
+ export { SortableList } from '../../chunk-TXOZMKNW.js';
2
3
  export { StatusBadge, statusBadgeVariants } from '../../chunk-XMK3T4TA.js';
3
4
  export { ToggleRow } from '../../chunk-6M4NHHKG.js';
4
5
  export { UserAvatar, userAvatarVariants } from '../../chunk-XBX6QMR5.js';
@@ -6,6 +7,7 @@ import '../../chunk-LNFF2RBD.js';
6
7
  export { SettingsRow } from '../../chunk-A7AHVDG7.js';
7
8
  import '../../chunk-FK42ZGGV.js';
8
9
  import '../../chunk-22XB3Q6H.js';
10
+ import '../../chunk-QJ6P2DW6.js';
9
11
  import '../../chunk-27ELYTGE.js';
10
12
  export { EmptyState, NoFeaturesFound, NoFeedbackFound, NoSearchResults } from '../../chunk-65SPT7EA.js';
11
13
  import '../../chunk-NWSCYLPT.js';
@@ -0,0 +1,78 @@
1
+ import * as React from 'react';
2
+
3
+ interface SortableListItemContext {
4
+ /** The item's position as shown, from 0. */
5
+ index: number;
6
+ /**
7
+ * The drag handle: drag it with a pointer, or focus it and press Space or
8
+ * Enter to pick the item up, the arrow keys to move it and Space or Enter
9
+ * again to drop it. Render it somewhere in the row.
10
+ */
11
+ handle: React.ReactNode;
12
+ /** True while this item is being dragged with a pointer. */
13
+ isDragging: boolean;
14
+ /** True while this item is picked up from the keyboard. */
15
+ isGrabbed: boolean;
16
+ /** Move this item without dragging, for a row menu of your own. */
17
+ moveUp: () => void;
18
+ moveDown: () => void;
19
+ moveTo: (index: number) => void;
20
+ }
21
+ interface SortableListReorder {
22
+ /** Key of the item that moved. */
23
+ key: string;
24
+ }
25
+ interface SortableListProps<T> {
26
+ items: readonly T[];
27
+ /** A stable key per item. Required: dragging finds the item again by key. */
28
+ getKey: (item: T) => string;
29
+ /** A name for an item, for the handle's label and announcements ("Week 1"). */
30
+ getItemLabel: (item: T) => string;
31
+ renderItem: (item: T, context: SortableListItemContext) => React.ReactNode;
32
+ /**
33
+ * Move the item at `from` to `to` (both from 0, `to` counted after the item
34
+ * is taken out). Each finished move calls this once: a keyboard pick-up
35
+ * that wanders and comes back calls nothing.
36
+ *
37
+ * The list shows the new order straight away and keeps showing it until the
38
+ * returned promise settles; by then `items` must be in the new order. A
39
+ * synchronous handler must update `items` in the same tick. A rejected
40
+ * promise puts the old order back and announces that the move failed. Other
41
+ * moves wait until this one settles.
42
+ */
43
+ onReorder: (from: number, to: number, move: SortableListReorder) => void | Promise<void>;
44
+ /** Accessible name of the list ("Course steps"). */
45
+ label: string;
46
+ /** Turns off dragging and every kind of move. */
47
+ disabled?: boolean | undefined;
48
+ /**
49
+ * A "Move" menu on each row (up, down, to top, to bottom), on by default so
50
+ * touch and screen-reader users always have a way to move an item. Turn it
51
+ * off only when the row offers `moveUp` / `moveDown` / `moveTo` itself.
52
+ */
53
+ moveMenu?: boolean | undefined;
54
+ className?: string | undefined;
55
+ itemClassName?: string | undefined;
56
+ }
57
+ /**
58
+ * A vertical list the user can rearrange: course steps, lesson blocks, form
59
+ * fields.
60
+ *
61
+ * Every way to move an item is equivalent, so dragging is never the only one
62
+ * (the same stance as KanbanBoard's "Move to…" menu):
63
+ * - drag the handle with a pointer;
64
+ * - focus the handle, Space or Enter to pick up, ↑/↓ and Home/End to move,
65
+ * Space or Enter to drop, Escape to put it back (tabbing away also puts it
66
+ * back); Alt+↑/↓ moves one step without picking up;
67
+ * - the row's "Move" menu, or `moveUp` / `moveDown` / `moveTo` from the render
68
+ * context.
69
+ *
70
+ * Moves are announced in a polite live region once they succeed. Native drag
71
+ * and drop needs no dependency and Radix has none to offer (docs/FRAMEWORK.md
72
+ * rule 3).
73
+ *
74
+ * @stability Beta
75
+ */
76
+ declare function SortableList<T>({ items, getKey, getItemLabel, renderItem, onReorder, label, disabled, moveMenu, className, itemClassName, }: SortableListProps<T>): React.JSX.Element;
77
+
78
+ export { SortableList, type SortableListItemContext, type SortableListProps, type SortableListReorder };
@@ -0,0 +1,5 @@
1
+ export { SortableList } from '../../chunk-TXOZMKNW.js';
2
+ import '../../chunk-QJ6P2DW6.js';
3
+ import '../../chunk-NZ5WBA4R.js';
4
+ //# sourceMappingURL=sortable-list.js.map
5
+ //# sourceMappingURL=sortable-list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"sortable-list.js"}
@@ -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