@lapis-notes/csv 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE.md +4 -0
  3. package/README.md +19 -2
  4. package/dist/app.css +210 -0
  5. package/dist/csv-settings.d.ts +13 -0
  6. package/dist/csv-settings.js +28 -0
  7. package/dist/index.d.ts +7 -0
  8. package/dist/index.js +7 -0
  9. package/dist/lib/CsvSheet.svelte +58 -0
  10. package/dist/lib/CsvSheet.svelte.d.ts +11 -0
  11. package/dist/lib/CsvTable.svelte +58 -0
  12. package/dist/lib/CsvTable.svelte.d.ts +11 -0
  13. package/dist/lib/codemirror/csv-language-extensions.d.ts +3 -0
  14. package/dist/lib/codemirror/csv-language-extensions.js +11 -0
  15. package/dist/lib/codemirror/csv-language.d.ts +3 -0
  16. package/dist/lib/codemirror/csv-language.js +24 -0
  17. package/dist/lib/codemirror/csv-rainbow-theme.d.ts +3 -0
  18. package/dist/lib/codemirror/csv-rainbow-theme.js +15 -0
  19. package/dist/lib/codemirror/csv-source-extensions.d.ts +7 -0
  20. package/dist/lib/codemirror/csv-source-extensions.js +9 -0
  21. package/dist/lib/codemirror/csv.parser.d.ts +2 -0
  22. package/dist/lib/codemirror/csv.parser.js +16 -0
  23. package/dist/lib/codemirror/csv.parser.terms.d.ts +1 -0
  24. package/dist/lib/codemirror/csv.parser.terms.js +2 -0
  25. package/dist/lib/codemirror/syntax.grammar +36 -0
  26. package/dist/lib/components/csv-cell.svelte +137 -0
  27. package/dist/lib/components/csv-cell.svelte.d.ts +11 -0
  28. package/dist/lib/components/csv-data-table-column-head.svelte +76 -0
  29. package/dist/lib/components/csv-data-table-column-head.svelte.d.ts +17 -0
  30. package/dist/lib/components/csv-data-table-column-header.svelte +159 -0
  31. package/dist/lib/components/csv-data-table-column-header.svelte.d.ts +23 -0
  32. package/dist/lib/components/csv-data-table-data-cell.svelte +72 -0
  33. package/dist/lib/components/csv-data-table-data-cell.svelte.d.ts +18 -0
  34. package/dist/lib/components/csv-data-table-drag-overlay.svelte +119 -0
  35. package/dist/lib/components/csv-data-table-drag-overlay.svelte.d.ts +13 -0
  36. package/dist/lib/components/csv-data-table-row-actions.svelte +47 -0
  37. package/dist/lib/components/csv-data-table-row-actions.svelte.d.ts +8 -0
  38. package/dist/lib/components/csv-data-table-row-grip-cell.svelte +102 -0
  39. package/dist/lib/components/csv-data-table-row-grip-cell.svelte.d.ts +19 -0
  40. package/dist/lib/components/csv-data-table-row.svelte +38 -0
  41. package/dist/lib/components/csv-data-table-row.svelte.d.ts +11 -0
  42. package/dist/lib/components/csv-data-table.svelte +903 -0
  43. package/dist/lib/components/csv-data-table.svelte.d.ts +21 -0
  44. package/dist/lib/components/csv-find.svelte +382 -0
  45. package/dist/lib/components/csv-find.svelte.d.ts +24 -0
  46. package/dist/lib/components/csv-row-virtualizer.svelte.d.ts +9 -0
  47. package/dist/lib/components/csv-row-virtualizer.svelte.js +59 -0
  48. package/dist/lib/components/csv-source-view.svelte +127 -0
  49. package/dist/lib/components/csv-source-view.svelte.d.ts +10 -0
  50. package/dist/lib/components/csv-status-bar.svelte +101 -0
  51. package/dist/lib/components/csv-status-bar.svelte.d.ts +14 -0
  52. package/dist/lib/components/csv-table-dnd.d.ts +9 -0
  53. package/dist/lib/components/csv-table-dnd.js +12 -0
  54. package/dist/lib/components/csv-table-drag-measure.d.ts +7 -0
  55. package/dist/lib/components/csv-table-drag-measure.js +38 -0
  56. package/dist/lib/components/csv-table-selection.d.ts +28 -0
  57. package/dist/lib/components/csv-table-selection.js +178 -0
  58. package/dist/lib/components/csv-table-view.svelte +265 -0
  59. package/dist/lib/components/csv-table-view.svelte.d.ts +16 -0
  60. package/dist/lib/csv-plugin.d.ts +9 -0
  61. package/dist/lib/csv-plugin.js +107 -0
  62. package/dist/lib/csv-view-settings.d.ts +43 -0
  63. package/dist/lib/csv-view-settings.js +86 -0
  64. package/dist/lib/ids.d.ts +8 -0
  65. package/dist/lib/ids.js +19 -0
  66. package/dist/lib/model/csv-column-sort.d.ts +16 -0
  67. package/dist/lib/model/csv-column-sort.js +81 -0
  68. package/dist/lib/model/csv-grid.d.ts +31 -0
  69. package/dist/lib/model/csv-grid.js +161 -0
  70. package/dist/lib/model/csv-search.d.ts +38 -0
  71. package/dist/lib/model/csv-search.js +101 -0
  72. package/dist/lib/model/csv-table-model.d.ts +10 -0
  73. package/dist/lib/model/csv-table-model.js +18 -0
  74. package/dist/lib/model/parse-csv.d.ts +10 -0
  75. package/dist/lib/model/parse-csv.js +13 -0
  76. package/dist/lib/model/serialize-csv.d.ts +5 -0
  77. package/dist/lib/model/serialize-csv.js +10 -0
  78. package/dist/lib/sheet/csv-sheet-toolbar.svelte +244 -0
  79. package/dist/lib/sheet/csv-sheet-toolbar.svelte.d.ts +20 -0
  80. package/dist/lib/sheet/csv-sheet-view.svelte +444 -0
  81. package/dist/lib/sheet/csv-sheet-view.svelte.d.ts +18 -0
  82. package/dist/lib/sheet/csv-sheet.svelte +1121 -0
  83. package/dist/lib/sheet/csv-sheet.svelte.d.ts +37 -0
  84. package/dist/lib/sheet/sheet-glyph.svelte +57 -0
  85. package/dist/lib/sheet/sheet-glyph.svelte.d.ts +7 -0
  86. package/dist/lib/sheet/sheet-history.d.ts +14 -0
  87. package/dist/lib/sheet/sheet-history.js +33 -0
  88. package/dist/lib/sheet/sheet-layout.d.ts +121 -0
  89. package/dist/lib/sheet/sheet-layout.js +336 -0
  90. package/dist/lib/sheet/sheet-paint.d.ts +45 -0
  91. package/dist/lib/sheet/sheet-paint.js +274 -0
  92. package/dist/lib/sheet/sheet-theme.d.ts +2 -0
  93. package/dist/lib/sheet/sheet-theme.js +26 -0
  94. package/dist/lib/views/csv-view-mode.d.ts +3 -0
  95. package/dist/lib/views/csv-view-mode.js +6 -0
  96. package/dist/lib/views/csv-view.d.ts +24 -0
  97. package/dist/lib/views/csv-view.js +125 -0
  98. package/dist/styles/csv-find.css +313 -0
  99. package/dist/styles/csv-table.css +882 -0
  100. package/dist/styles.css +37 -0
  101. package/manifest.json +45 -0
  102. package/package.json +86 -3
  103. package/registry-content/overview.md +3 -0
  104. package/registry.json +322 -0
  105. package/styles.css +1 -0
@@ -0,0 +1,1121 @@
1
+ <script lang="ts">
2
+ import { Button } from "@lapismd/design-core/shadcn/button";
3
+ import ArrowUpDownIcon from "@lucide/svelte/icons/arrow-up-down";
4
+ import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
5
+ import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
6
+ import type { CsvColumnSortDirection } from "../model/csv-column-sort";
7
+ import type { CsvGrid } from "../model/csv-grid";
8
+ import CsvSheetToolbar from "./csv-sheet-toolbar.svelte";
9
+ import { paintSheet, type SheetPaintTheme } from "./sheet-paint";
10
+ import { watchSheetTheme } from "./sheet-theme";
11
+ import SheetGlyph from "./sheet-glyph.svelte";
12
+ import {
13
+ SHEET_CELL_PADDING,
14
+ SHEET_HEADER_HEIGHT,
15
+ SHEET_LETTER_HEIGHT,
16
+ SHEET_RESIZE_SLOP,
17
+ SHEET_ROW_HEIGHT,
18
+ SHEET_SORT_HIT,
19
+ SHEET_SORT_PAD,
20
+ bodyOffsets,
21
+ cellDocumentRect,
22
+ chromeHeight,
23
+ clampColumnWidth,
24
+ columnEdges,
25
+ columnLetter,
26
+ fittedColumnWidth,
27
+ gridRowFromAddress,
28
+ hitTestSheet,
29
+ parseSheetAddress,
30
+ sheetRowNumber,
31
+ sheetCommands,
32
+ sheetContentSize,
33
+ sheetMenuTarget,
34
+ visibleColumnRange,
35
+ wrapLines,
36
+ wrappedRowHeight,
37
+ type SheetCommandId,
38
+ type SheetHit,
39
+ } from "./sheet-layout";
40
+
41
+ type Cell = { row: number; col: number };
42
+
43
+ let {
44
+ grid,
45
+ headerRow,
46
+ bodyRows,
47
+ searchCell = null,
48
+ sort = null,
49
+ widths,
50
+ onWidths,
51
+ onCellChange,
52
+ onSort,
53
+ onDeleteRow,
54
+ onDeleteColumn,
55
+ onMoveColumn,
56
+ onMoveRow,
57
+ onClearColumn,
58
+ onClearRow,
59
+ onClearCell,
60
+ onClearSheet,
61
+ onDeleteSheet,
62
+ canUndo,
63
+ canRedo,
64
+ onUndo,
65
+ onRedo,
66
+ onHeaderRow,
67
+ }: {
68
+ grid: CsvGrid;
69
+ headerRow: boolean;
70
+ bodyRows: readonly number[];
71
+ searchCell?: Cell | null;
72
+ sort?: { index: number; direction: CsvColumnSortDirection } | null;
73
+ widths: number[];
74
+ onWidths: (widths: number[]) => void;
75
+ onCellChange: (row: number, col: number, value: string) => void;
76
+ onSort: (col: number) => void;
77
+ onDeleteRow: (index: number) => void;
78
+ onDeleteColumn: (index: number) => void;
79
+ onMoveColumn: (from: number, to: number) => void;
80
+ onMoveRow: (from: number, to: number) => void;
81
+ onClearColumn: (col: number) => void;
82
+ onClearRow: (row: number) => void;
83
+ onClearCell: (row: number, col: number) => void;
84
+ onClearSheet: () => void;
85
+ onDeleteSheet: () => void;
86
+ canUndo: boolean;
87
+ canRedo: boolean;
88
+ onUndo: () => void;
89
+ onRedo: () => void;
90
+ onHeaderRow: (headerRow: boolean) => void;
91
+ } = $props();
92
+
93
+ let scroller = $state<HTMLDivElement | null>(null);
94
+ let canvas = $state<HTMLCanvasElement | null>(null);
95
+ let scrollLeft = $state(0);
96
+ let scrollTop = $state(0);
97
+ let viewportWidth = $state(0);
98
+ let viewportHeight = $state(0);
99
+ let active = $state<Cell | null>(null);
100
+ let editing = $state(false);
101
+ let editorValue = $state("");
102
+ let selection = $state<{
103
+ startIndex: number;
104
+ endIndex: number;
105
+ startCol: number;
106
+ endCol: number;
107
+ } | null>(null);
108
+ let menu = $state<{
109
+ x: number;
110
+ y: number;
111
+ target: "column" | "row" | "cell" | "sheet";
112
+ row: number;
113
+ col: number;
114
+ } | null>(null);
115
+ let drag: {
116
+ kind: "select" | "resize";
117
+ axis: "cell" | "row" | "column";
118
+ col: number;
119
+ index: number;
120
+ startWidth: number;
121
+ startX: number;
122
+ moved: boolean;
123
+ } | null = null;
124
+ let selectOnEdit = false;
125
+ let wrap = $state(false);
126
+ let colours = $state(false);
127
+ let themeEpoch = $state(0);
128
+ let measureCanvas: HTMLCanvasElement | null = null;
129
+
130
+ function measureText(text: string): number {
131
+ if (typeof document === "undefined") return text.length * 7.8;
132
+ measureCanvas ??= document.createElement("canvas");
133
+ const context = measureCanvas.getContext("2d");
134
+ if (!context) return text.length * 7.8;
135
+ const family = canvas ? getComputedStyle(canvas).fontFamily : "";
136
+ context.font = `13px ${family || "ui-monospace, monospace"}`;
137
+ return context.measureText(text).width;
138
+ }
139
+
140
+ const rowOffsets = $derived.by(() => {
141
+ if (!wrap) return null;
142
+ const rows = grid.getRows();
143
+ const heights = bodyRows.map((row) => {
144
+ const cells = rows[row] ?? [];
145
+ let lines = 1;
146
+ for (let col = 0; col < widths.length; col += 1) {
147
+ const inner = Math.max(1, (widths[col] ?? 0) - SHEET_CELL_PADDING);
148
+ lines = Math.max(
149
+ lines,
150
+ wrapLines(cells[col] ?? "", inner, measureText).length,
151
+ );
152
+ }
153
+ return wrappedRowHeight(lines);
154
+ });
155
+ return bodyOffsets(heights);
156
+ });
157
+ const size = $derived(
158
+ sheetContentSize(
159
+ widths,
160
+ bodyRows.length,
161
+ headerRow,
162
+ rowOffsets ? rowOffsets[rowOffsets.length - 1] : undefined,
163
+ ),
164
+ );
165
+ const theme = $derived(readTheme(canvas, themeEpoch));
166
+ const paintModel = $derived({
167
+ widths,
168
+ headerRow,
169
+ headerLabels: grid.getRows()[0] ?? [],
170
+ rowLabels: bodyRows.map((row) => String(sheetRowNumber(row) ?? "")),
171
+ body: bodyRows.map((row) => grid.getRows()[row] ?? []),
172
+ scrollLeft,
173
+ scrollTop,
174
+ viewportWidth,
175
+ viewportHeight,
176
+ active: activeCellPaint(),
177
+ selection,
178
+ search: searchPaint(),
179
+ theme,
180
+ wrap,
181
+ colours,
182
+ rowOffsets,
183
+ });
184
+
185
+ function sortFill(col: number): string {
186
+ const picked =
187
+ selection != null &&
188
+ bodyRows.length > 0 &&
189
+ selection.startIndex === 0 &&
190
+ selection.endIndex >= bodyRows.length - 1 &&
191
+ col >= selection.startCol &&
192
+ col <= selection.endCol;
193
+ return picked ? theme.selection : "transparent";
194
+ }
195
+
196
+ function readTheme(node: HTMLCanvasElement | null, _epoch = 0): SheetPaintTheme {
197
+ void _epoch;
198
+ const style = node ? getComputedStyle(node) : null;
199
+ const probe = node ? node.ownerDocument.createElement("span") : null;
200
+ if (probe && node) node.append(probe);
201
+ const value = (name: string, fallback: string) => {
202
+ const raw = style?.getPropertyValue(name).trim() || fallback;
203
+ if (!probe) return raw;
204
+ probe.style.backgroundColor = raw;
205
+ const resolved = getComputedStyle(probe).backgroundColor;
206
+ return resolved && resolved !== "rgba(0, 0, 0, 0)" ? resolved : raw;
207
+ };
208
+ const header = value("--csv-header-background", "#f4f4f5");
209
+ const columns = [1, 2, 3, 4, 5].map((slot, index) =>
210
+ value(
211
+ `--csv-col-${slot}`,
212
+ ["#c2410c", "#15803d", "#1d4ed8", "#a21caf", "#a16207"][index] ?? "#111",
213
+ ),
214
+ );
215
+ const headerTints = columns.map((column) => {
216
+ if (!probe) return header;
217
+ probe.style.backgroundColor = `color-mix(in oklab, ${column} 8%, ${header})`;
218
+ const resolved = getComputedStyle(probe).backgroundColor;
219
+ return resolved && resolved !== "rgba(0, 0, 0, 0)" ? resolved : header;
220
+ });
221
+ const theme = {
222
+ background: value("--csv-shell-background", "#fff"),
223
+ header,
224
+ gutter: value("--csv-index-background", "#f4f4f5"),
225
+ text: value("--csv-text-color", "#111"),
226
+ muted: value("--csv-muted-color", "#666"),
227
+ grid: value("--csv-surface-border", "#ddd"),
228
+ selection: value("--table-selection", "rgba(37, 99, 235, 0.16)"),
229
+ accent: value("--table-selection-border-color", "#2563eb"),
230
+ search: value("--csv-col-5", "#b45309"),
231
+ columns,
232
+ headerTints,
233
+ };
234
+ probe?.remove();
235
+ return theme;
236
+ }
237
+
238
+ function holdTheme(node: HTMLElement) {
239
+ return watchSheetTheme(node, () => {
240
+ themeEpoch += 1;
241
+ });
242
+ }
243
+
244
+ function bodyIndex(row: number): number {
245
+ return bodyRows.indexOf(row);
246
+ }
247
+
248
+ function gridRow(index: number): number {
249
+ return bodyRows[index] ?? -1;
250
+ }
251
+
252
+ function holdCanvas(node: HTMLCanvasElement) {
253
+ canvas = node;
254
+ return () => {
255
+ if (canvas === node) canvas = null;
256
+ };
257
+ }
258
+
259
+ const paintAttachment = $derived.by(() => {
260
+ const model = paintModel;
261
+ return (node: HTMLCanvasElement) => {
262
+ paintSheet(node, model);
263
+ };
264
+ });
265
+
266
+ function holdEditor(node: HTMLInputElement) {
267
+ node.focus();
268
+ if (selectOnEdit) node.select();
269
+ }
270
+
271
+ function activeCellPaint() {
272
+ if (!active || commandTarget === "column" || commandTarget === "row" || commandTarget === "sheet") {
273
+ return null;
274
+ }
275
+ if (headerRow && active.row === 0) {
276
+ return { index: 0, col: active.col, header: true };
277
+ }
278
+ const index = bodyIndex(active.row);
279
+ if (index < 0) return null;
280
+ return { index, col: active.col, header: false };
281
+ }
282
+
283
+ function searchPaint() {
284
+ if (!searchCell) return null;
285
+ if (headerRow && searchCell.row === 0) {
286
+ return { index: 0, col: searchCell.col, header: true };
287
+ }
288
+ const index = bodyIndex(searchCell.row);
289
+ if (index < 0) return null;
290
+ return { index, col: searchCell.col, header: false };
291
+ }
292
+
293
+ function holdScroller(node: HTMLDivElement) {
294
+ scroller = node;
295
+ const read = () => {
296
+ scrollLeft = node.scrollLeft;
297
+ scrollTop = node.scrollTop;
298
+ viewportWidth = node.clientWidth;
299
+ viewportHeight = node.clientHeight;
300
+ };
301
+ read();
302
+ node.addEventListener("scroll", read, { passive: true });
303
+ const observer = new ResizeObserver(read);
304
+ observer.observe(node);
305
+ return () => {
306
+ node.removeEventListener("scroll", read);
307
+ observer.disconnect();
308
+ if (scroller === node) scroller = null;
309
+ };
310
+ }
311
+
312
+ function pointerPoint(event: PointerEvent): { x: number; y: number } {
313
+ const node = scroller;
314
+ if (!node) return { x: 0, y: 0 };
315
+ const rect = node.getBoundingClientRect();
316
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
317
+ }
318
+
319
+ function hitAt(event: PointerEvent): SheetHit | null {
320
+ const point = pointerPoint(event);
321
+ return hitTestSheet({
322
+ x: point.x,
323
+ y: point.y,
324
+ scrollLeft,
325
+ scrollTop,
326
+ widths,
327
+ headerRow,
328
+ bodyCount: bodyRows.length,
329
+ offsets: rowOffsets,
330
+ });
331
+ }
332
+
333
+ const commandTarget = $derived.by((): "column" | "row" | "cell" | "sheet" | null => {
334
+ if (selection && widths.length > 0 && (bodyRows.length > 0 || selection.startIndex < 0)) {
335
+ const oneCol = selection.startCol === selection.endCol;
336
+ const oneRow = selection.startIndex === selection.endIndex;
337
+ const fullRows =
338
+ selection.startIndex === 0 && selection.endIndex >= bodyRows.length - 1;
339
+ const fullCols =
340
+ selection.startCol === 0 && selection.endCol >= widths.length - 1;
341
+ if (fullRows && fullCols) return "sheet";
342
+ if (fullRows && oneCol) return "column";
343
+ if (fullCols && oneRow) return "row";
344
+ }
345
+ if (active) return "cell";
346
+ return null;
347
+ });
348
+
349
+ const commandAt = $derived.by(() => {
350
+ if (commandTarget === "column" && selection) {
351
+ return { row: active?.row ?? 0, col: selection.startCol };
352
+ }
353
+ if (commandTarget === "row" && selection) {
354
+ const row = selection.startIndex < 0 ? 0 : gridRow(selection.startIndex);
355
+ return { row, col: active?.col ?? 0 };
356
+ }
357
+ return active;
358
+ });
359
+
360
+ const sortButtons = $derived.by(() => {
361
+ const edges = columnEdges(widths);
362
+ const range = visibleColumnRange({
363
+ scrollLeft,
364
+ viewportWidth: Math.max(viewportWidth, 1),
365
+ widths,
366
+ });
367
+ const bandTop = 0;
368
+ const bandHeight = SHEET_LETTER_HEIGHT;
369
+ const buttons = [];
370
+ for (let col = range.start; col < range.end; col += 1) {
371
+ const right = edges[col + 1];
372
+ if (right == null) continue;
373
+ const label = headerRow ? grid.getRows()[0]?.[col] : columnLetter(col);
374
+ buttons.push({
375
+ col,
376
+ left: right - SHEET_SORT_HIT + SHEET_SORT_PAD - scrollLeft,
377
+ top: bandTop + 1,
378
+ width: Math.max(0, SHEET_SORT_HIT - SHEET_RESIZE_SLOP - SHEET_SORT_PAD * 2),
379
+ height: Math.max(0, bandHeight - 2),
380
+ fill: sortFill(col),
381
+ direction: sort?.index === col ? sort.direction : null,
382
+ label: `Sort ${label || columnLetter(col)}`,
383
+ });
384
+ }
385
+ return buttons;
386
+ });
387
+
388
+ const toolbarCommands = $derived.by(() => {
389
+ const bounds = { rows: grid.getRowCount(), cols: grid.getColCount() };
390
+ if (commandTarget === "sheet") {
391
+ return sheetCommands("sheet", { row: 0, col: 0 }, bounds);
392
+ }
393
+ if (!commandTarget || !commandAt || commandAt.row < 0 || commandAt.col < 0) {
394
+ return [];
395
+ }
396
+ return sheetCommands(commandTarget, commandAt, bounds);
397
+ });
398
+
399
+ const reference = $derived.by(() => {
400
+ if (!commandTarget || !commandAt || commandAt.col < 0 || commandAt.row < 0) {
401
+ return "";
402
+ }
403
+ const letter = columnLetter(commandAt.col);
404
+ if (commandTarget === "column") return letter;
405
+ const rowNumber = sheetRowNumber(commandAt.row);
406
+ if (rowNumber == null) return letter;
407
+ if (commandTarget === "row") return String(rowNumber);
408
+ return `${letter}${rowNumber}`;
409
+ });
410
+
411
+ const menuItems = $derived.by(() => {
412
+ if (!menu) return [];
413
+ return sheetCommands(
414
+ menu.target,
415
+ { row: menu.row, col: menu.col },
416
+ { rows: grid.getRowCount(), cols: grid.getColCount() },
417
+ );
418
+ });
419
+
420
+ function applyCommand(
421
+ id: SheetCommandId,
422
+ row: number,
423
+ col: number,
424
+ target: "column" | "row" | "cell" | "sheet",
425
+ ) {
426
+ if (id === "move-left") onMoveColumn(col, col - 1);
427
+ else if (id === "move-right") onMoveColumn(col, col + 1);
428
+ else if (id === "move-up") onMoveRow(row, row - 1);
429
+ else if (id === "move-down") onMoveRow(row, row + 1);
430
+ else if (target === "sheet" && id === "delete") onDeleteSheet();
431
+ else if (target === "sheet") onClearSheet();
432
+ else if (id === "delete" && target === "column") onDeleteColumn(col);
433
+ else if (id === "delete" && target === "row") onDeleteRow(row);
434
+ else if (id === "clear" && target === "column") onClearColumn(col);
435
+ else if (id === "clear" && target === "row") onClearRow(row);
436
+ else onClearCell(row, col);
437
+ }
438
+
439
+ function goToAddress(value: string) {
440
+ const address = parseSheetAddress(value);
441
+ if (!address) return;
442
+ if (address.kind === "column") {
443
+ if (address.col >= grid.getColCount()) return;
444
+ selectColumns(address.col, address.col);
445
+ const row = headerRow ? 0 : Math.max(0, gridRow(0));
446
+ if (row >= 0) reveal(row, address.col);
447
+ return;
448
+ }
449
+ const row = gridRowFromAddress(address.row, grid.getRowCount());
450
+ if (row == null) return;
451
+ if (address.kind === "cell") {
452
+ if (address.col >= grid.getColCount()) return;
453
+ selectCell(row, address.col);
454
+ reveal(row, address.col);
455
+ return;
456
+ }
457
+ const headerIndex = headerRow && row === 0;
458
+ const index = headerIndex ? -1 : bodyIndex(row);
459
+ if (!headerIndex && index < 0) return;
460
+ selectRows(index, index);
461
+ reveal(row, 0);
462
+ }
463
+
464
+ function selectAll() {
465
+ const row = headerRow ? 0 : Math.max(0, gridRow(0));
466
+ active = { row, col: 0 };
467
+ selection = {
468
+ startIndex: 0,
469
+ endIndex: Math.max(0, bodyRows.length - 1),
470
+ startCol: 0,
471
+ endCol: Math.max(0, widths.length - 1),
472
+ };
473
+ editing = false;
474
+ }
475
+
476
+ function selectCell(row: number, col: number) {
477
+ active = { row, col };
478
+ selection = null;
479
+ editing = false;
480
+ }
481
+
482
+ function selectColumns(from: number, to: number) {
483
+ const startCol = Math.min(from, to);
484
+ const endCol = Math.max(from, to);
485
+ const body = gridRow(0);
486
+ active = { row: body >= 0 ? body : 0, col: startCol };
487
+ selection = {
488
+ startIndex: 0,
489
+ endIndex: Math.max(0, bodyRows.length - 1),
490
+ startCol,
491
+ endCol,
492
+ };
493
+ editing = false;
494
+ }
495
+
496
+ function selectRows(from: number, to: number) {
497
+ const startIndex = Math.min(from, to);
498
+ const endIndex = Math.max(from, to);
499
+ const row = startIndex < 0 ? 0 : gridRow(startIndex);
500
+ const lastCol = Math.max(0, widths.length - 1);
501
+ if (row >= 0) active = { row, col: 0 };
502
+ selection = {
503
+ startIndex,
504
+ endIndex,
505
+ startCol: 0,
506
+ endCol: lastCol,
507
+ };
508
+ editing = false;
509
+ }
510
+
511
+ function fitColumn(col: number) {
512
+ const surface = canvas;
513
+ const context = surface?.getContext("2d");
514
+ if (!surface || !context) return;
515
+ context.font = `13px ${getComputedStyle(surface).fontFamily || "monospace"}`;
516
+ const texts: string[] = [];
517
+ const rowCount = grid.getRowCount();
518
+ for (let row = 0; row < rowCount; row += 1) {
519
+ texts.push(grid.getCell(row, col));
520
+ }
521
+ const next = [...widths];
522
+ next[col] = fittedColumnWidth(
523
+ (text) => context.measureText(text).width,
524
+ texts,
525
+ );
526
+ onWidths(next);
527
+ }
528
+
529
+ function beginEdit(seed?: string) {
530
+ if (!active) return;
531
+ editorValue = seed ?? grid.getCell(active.row, active.col);
532
+ selectOnEdit = seed == null;
533
+ editing = true;
534
+ }
535
+
536
+ function commitEdit() {
537
+ if (!editing || !active) return;
538
+ const cell = active;
539
+ const value = editorValue;
540
+ editing = false;
541
+ onCellChange(cell.row, cell.col, value);
542
+ }
543
+
544
+ function cancelEdit() {
545
+ editing = false;
546
+ }
547
+
548
+ function moveActive(rowDelta: number, colDelta: number) {
549
+ const colCount = grid.getColCount();
550
+ if (!active) {
551
+ const row = headerRow ? 1 : 0;
552
+ selectCell(row, 0);
553
+ reveal(row, 0);
554
+ return;
555
+ }
556
+ const current = active;
557
+ const nextCol = Math.max(0, Math.min(colCount - 1, current.col + colDelta));
558
+ let nextRow = current.row + rowDelta;
559
+ if (headerRow) nextRow = Math.max(0, nextRow);
560
+ const maxRow = grid.getRowCount() - 1;
561
+ nextRow = Math.max(0, Math.min(maxRow, nextRow));
562
+ selectCell(nextRow, nextCol);
563
+ reveal(nextRow, nextCol);
564
+ }
565
+
566
+ function reveal(row: number, col: number) {
567
+ const node = scroller;
568
+ if (!node) return;
569
+ const edges = cellDocumentRect({
570
+ col,
571
+ top: 0,
572
+ widths,
573
+ height: SHEET_ROW_HEIGHT,
574
+ });
575
+ if (!edges) return;
576
+ if (edges.x < node.scrollLeft + 48) {
577
+ node.scrollLeft = Math.max(0, edges.x - 48);
578
+ } else if (edges.x + edges.width > node.scrollLeft + node.clientWidth) {
579
+ node.scrollLeft = edges.x + edges.width - node.clientWidth;
580
+ }
581
+ if (headerRow && row === 0) return;
582
+ const index = bodyIndex(row);
583
+ if (index < 0) return;
584
+ const top = rowOffsets ? (rowOffsets[index] ?? 0) : index * SHEET_ROW_HEIGHT;
585
+ const height = rowOffsets
586
+ ? (rowOffsets[index + 1] ?? top) - top
587
+ : SHEET_ROW_HEIGHT;
588
+ const chrome = chromeHeight(headerRow);
589
+ if (top < node.scrollTop) node.scrollTop = top;
590
+ else if (top + height > node.scrollTop + node.clientHeight - chrome) {
591
+ node.scrollTop = top + height - (node.clientHeight - chrome);
592
+ }
593
+ }
594
+
595
+ function editorStyle(): string {
596
+ if (!editing || !active) return "display: none";
597
+ const header = headerRow && active.row === 0;
598
+ const index = header ? 0 : bodyIndex(active.row);
599
+ if (!header && index < 0) return "display: none";
600
+ const rowTop = rowOffsets ? (rowOffsets[index] ?? 0) : index * SHEET_ROW_HEIGHT;
601
+ const rowHeight = rowOffsets
602
+ ? (rowOffsets[index + 1] ?? rowTop) - rowTop
603
+ : SHEET_ROW_HEIGHT;
604
+ const top = header
605
+ ? SHEET_LETTER_HEIGHT
606
+ : chromeHeight(headerRow) + rowTop - scrollTop;
607
+ const rect = cellDocumentRect({
608
+ col: active.col,
609
+ top,
610
+ widths,
611
+ height: header ? SHEET_HEADER_HEIGHT : rowHeight,
612
+ });
613
+ if (!rect) return "display: none";
614
+ return `left: ${rect.x - scrollLeft}px; top: ${rect.y}px; width: ${rect.width}px; height: ${rect.height}px;`;
615
+ }
616
+
617
+ function onPointerDown(event: PointerEvent) {
618
+ if (event.button === 2) return;
619
+ menu = null;
620
+ if (editing) commitEdit();
621
+ const hit = hitAt(event);
622
+ if (!hit) return;
623
+ scroller?.focus();
624
+ if (hit.kind === "resize") {
625
+ drag = {
626
+ kind: "resize",
627
+ axis: "cell",
628
+ col: hit.col,
629
+ index: 0,
630
+ startWidth: widths[hit.col] ?? 0,
631
+ startX: event.clientX,
632
+ moved: false,
633
+ };
634
+ capturePointer(event.pointerId);
635
+ return;
636
+ }
637
+ if (hit.kind === "sort") {
638
+ onSort(hit.col);
639
+ return;
640
+ }
641
+ if (hit.kind === "corner") {
642
+ selectAll();
643
+ return;
644
+ }
645
+ if (hit.kind === "header") {
646
+ selectCell(0, hit.col);
647
+ if (event.detail >= 2) beginEdit();
648
+ return;
649
+ }
650
+ if (hit.kind === "letter") {
651
+ selectColumns(hit.col, hit.col);
652
+ drag = {
653
+ kind: "select",
654
+ axis: "column",
655
+ col: hit.col,
656
+ index: 0,
657
+ startWidth: 0,
658
+ startX: event.clientX,
659
+ moved: true,
660
+ };
661
+ capturePointer(event.pointerId);
662
+ return;
663
+ }
664
+ if (hit.kind === "row" && hit.index < 0) {
665
+ selectRows(-1, -1);
666
+ drag = {
667
+ kind: "select",
668
+ axis: "row",
669
+ col: 0,
670
+ index: -1,
671
+ startWidth: 0,
672
+ startX: event.clientX,
673
+ moved: true,
674
+ };
675
+ capturePointer(event.pointerId);
676
+ return;
677
+ }
678
+ const row = gridRow(hit.index);
679
+ const col = hit.kind === "row" ? 0 : hit.col;
680
+ if (row < 0) return;
681
+ if (hit.kind === "row") {
682
+ selectRows(hit.index, hit.index);
683
+ drag = {
684
+ kind: "select",
685
+ axis: "row",
686
+ col: 0,
687
+ index: hit.index,
688
+ startWidth: 0,
689
+ startX: event.clientX,
690
+ moved: true,
691
+ };
692
+ capturePointer(event.pointerId);
693
+ return;
694
+ }
695
+ selectCell(row, col);
696
+ if (event.detail >= 2) {
697
+ beginEdit();
698
+ return;
699
+ }
700
+ drag = {
701
+ kind: "select",
702
+ axis: "cell",
703
+ col,
704
+ index: hit.index,
705
+ startWidth: 0,
706
+ startX: event.clientX,
707
+ moved: false,
708
+ };
709
+ selection = {
710
+ startIndex: drag.index,
711
+ endIndex: drag.index,
712
+ startCol: col,
713
+ endCol: col,
714
+ };
715
+ capturePointer(event.pointerId);
716
+ }
717
+
718
+ function capturePointer(pointerId: number) {
719
+ try {
720
+ scroller?.setPointerCapture(pointerId);
721
+ } catch {
722
+ return;
723
+ }
724
+ }
725
+
726
+ function onPointerMove(event: PointerEvent) {
727
+ const hit = hitAt(event);
728
+ if (scroller && !drag) {
729
+ scroller.style.cursor = hit?.kind === "resize" ? "col-resize" : "default";
730
+ }
731
+ if (!drag) return;
732
+ if (drag.kind === "resize") {
733
+ const next = [...widths];
734
+ next[drag.col] = clampColumnWidth(drag.startWidth + event.clientX - drag.startX);
735
+ onWidths(next);
736
+ return;
737
+ }
738
+ const current = hitAt(event);
739
+ if (!current) return;
740
+ if (drag.axis === "column" && current.kind !== "corner" && current.kind !== "row") {
741
+ selectColumns(drag.col, current.col);
742
+ return;
743
+ }
744
+ if (drag.axis === "row" && (current.kind === "body" || current.kind === "row")) {
745
+ selectRows(drag.index, current.index);
746
+ return;
747
+ }
748
+ if (current.kind !== "body" && current.kind !== "row" && current.kind !== "header") {
749
+ return;
750
+ }
751
+ const index = current.kind === "header" ? drag.index : current.index;
752
+ const col = current.kind === "row" ? drag.col : current.col;
753
+ if (index !== drag.index || col !== drag.col) drag.moved = true;
754
+ selection = {
755
+ startIndex: Math.min(drag.index, index),
756
+ endIndex: Math.max(drag.index, index),
757
+ startCol: Math.min(drag.col, col),
758
+ endCol: Math.max(drag.col, col),
759
+ };
760
+ }
761
+
762
+ function onPointerUp() {
763
+ if (drag?.kind === "select" && drag.axis === "cell" && !drag.moved) selection = null;
764
+ drag = null;
765
+ }
766
+
767
+ function onDoubleClick(event: MouseEvent) {
768
+ const hit = hitAt(event as unknown as PointerEvent);
769
+ if (hit?.kind === "resize") {
770
+ fitColumn(hit.col);
771
+ return;
772
+ }
773
+ if (hit?.kind === "header") {
774
+ selectCell(0, hit.col);
775
+ beginEdit();
776
+ return;
777
+ }
778
+ if (hit?.kind === "body" && active) beginEdit();
779
+ }
780
+
781
+ function onContextMenu(event: MouseEvent) {
782
+ event.preventDefault();
783
+ scroller?.focus();
784
+ if (editing) commitEdit();
785
+ const hit = hitAt(event as unknown as PointerEvent);
786
+ const point = scroller?.getBoundingClientRect();
787
+ const x = point ? event.clientX - point.left : 0;
788
+ const y = point ? event.clientY - point.top : 0;
789
+ if (hit?.kind === "corner" || (commandTarget === "sheet" && hit)) {
790
+ selectAll();
791
+ menu = {
792
+ x,
793
+ y,
794
+ target: "sheet",
795
+ row: headerRow ? 0 : Math.max(0, gridRow(0)),
796
+ col: 0,
797
+ };
798
+ return;
799
+ }
800
+ const target = hit ? sheetMenuTarget(hit) : null;
801
+ if (!hit || !target) {
802
+ menu = null;
803
+ return;
804
+ }
805
+ if (target === "column" && "col" in hit) {
806
+ selectColumns(hit.col, hit.col);
807
+ menu = { x, y, target, row: headerRow ? 0 : Math.max(0, gridRow(0)), col: hit.col };
808
+ return;
809
+ }
810
+ if (target === "row" && hit.kind === "row") {
811
+ selectRows(hit.index, hit.index);
812
+ menu = { x, y, target, row: hit.index < 0 ? 0 : gridRow(hit.index), col: 0 };
813
+ return;
814
+ }
815
+ if (hit.kind !== "body") {
816
+ menu = null;
817
+ return;
818
+ }
819
+ const row = gridRow(hit.index);
820
+ if (row < 0) {
821
+ menu = null;
822
+ return;
823
+ }
824
+ selectCell(row, hit.col);
825
+ menu = { x, y, target: "cell", row, col: hit.col };
826
+ }
827
+
828
+ function closeMenu(action: () => void, keepSelection = false) {
829
+ action();
830
+ menu = null;
831
+ if (!keepSelection) selection = null;
832
+ }
833
+
834
+ function onKeyDown(event: KeyboardEvent) {
835
+ if (editing) return;
836
+ if (event.key === "Enter") {
837
+ event.preventDefault();
838
+ if (commandTarget === "cell") beginEdit();
839
+ return;
840
+ }
841
+ if (event.key === "ArrowDown") {
842
+ event.preventDefault();
843
+ moveActive(1, 0);
844
+ } else if (event.key === "ArrowUp") {
845
+ event.preventDefault();
846
+ moveActive(-1, 0);
847
+ } else if (event.key === "ArrowRight") {
848
+ event.preventDefault();
849
+ moveActive(0, 1);
850
+ } else if (event.key === "ArrowLeft") {
851
+ event.preventDefault();
852
+ moveActive(0, -1);
853
+ } else if (event.key === "Tab") {
854
+ event.preventDefault();
855
+ moveActive(0, event.shiftKey ? -1 : 1);
856
+ } else if (event.key === "Backspace" || event.key === "Delete") {
857
+ event.preventDefault();
858
+ if (
859
+ (commandTarget === "column" || commandTarget === "row" || commandTarget === "sheet") &&
860
+ commandAt
861
+ ) {
862
+ applyCommand("delete", commandAt.row, commandAt.col, commandTarget);
863
+ selection = null;
864
+ return;
865
+ }
866
+ if (!active) return;
867
+ onCellChange(active.row, active.col, "");
868
+ } else if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {
869
+ if (commandTarget !== "cell") return;
870
+ event.preventDefault();
871
+ beginEdit(event.key);
872
+ }
873
+ }
874
+
875
+ function onEditorKeyDown(event: KeyboardEvent) {
876
+ if (event.key === "Enter") {
877
+ event.preventDefault();
878
+ commitEdit();
879
+ moveActive(1, 0);
880
+ } else if (event.key === "Escape") {
881
+ event.preventDefault();
882
+ cancelEdit();
883
+ scroller?.focus();
884
+ } else if (event.key === "Tab") {
885
+ event.preventDefault();
886
+ commitEdit();
887
+ moveActive(0, event.shiftKey ? -1 : 1);
888
+ }
889
+ }
890
+ </script>
891
+
892
+ <div class="csv-sheet-stage" {@attach holdTheme}>
893
+ <CsvSheetToolbar
894
+ {reference}
895
+ commands={toolbarCommands}
896
+ {wrap}
897
+ {colours}
898
+ {headerRow}
899
+ {canUndo}
900
+ {canRedo}
901
+ {onUndo}
902
+ {onRedo}
903
+ onAddress={goToAddress}
904
+ onCommand={(id) => {
905
+ const at = commandAt;
906
+ const target = commandTarget;
907
+ if (!at || !target) return;
908
+ const keep = id === "clear" || (target === "cell" && id === "delete");
909
+ closeMenu(() => applyCommand(id, at.row, at.col, target), keep);
910
+ }}
911
+ onWrap={(value) => {
912
+ wrap = value;
913
+ }}
914
+ onColours={(value) => {
915
+ colours = value;
916
+ }}
917
+ onHeaderRow={(value) => onHeaderRow(value)}
918
+ />
919
+ <div class="csv-sheet-body">
920
+ <div
921
+ class="csv-sheet"
922
+ data-testid="csv-sheet"
923
+ data-column-width-0={widths[0] ?? 0}
924
+ tabindex="0"
925
+ role="grid"
926
+ aria-rowcount={grid.getRowCount()}
927
+ aria-colcount={grid.getColCount()}
928
+ {@attach holdScroller}
929
+ onpointerdown={onPointerDown}
930
+ onpointermove={onPointerMove}
931
+ onpointerup={onPointerUp}
932
+ ondblclick={onDoubleClick}
933
+ oncontextmenu={onContextMenu}
934
+ onkeydown={onKeyDown}
935
+ >
936
+ <div class="csv-sheet__spacer" style:width="{size.width}px" style:height="{size.height}px"></div>
937
+ </div>
938
+ <canvas
939
+ class="csv-sheet__canvas"
940
+ aria-hidden="true"
941
+ {@attach holdCanvas}
942
+ {@attach paintAttachment}
943
+ ></canvas>
944
+ <div class="csv-sheet__sorts">
945
+ {#each sortButtons as button (button.col)}
946
+ <Button
947
+ class="csv-sheet__sort"
948
+ style="left: {button.left}px; top: {button.top}px; width: {button.width}px; height: {button.height}px; --csv-sort-fill: {button.fill};"
949
+ variant="ghost"
950
+ size="icon"
951
+ aria-label={button.label}
952
+ aria-pressed={button.direction != null}
953
+ onclick={(event) => {
954
+ event.stopPropagation();
955
+ onSort(button.col);
956
+ }}
957
+ oncontextmenu={onContextMenu}
958
+ >
959
+ {#if button.direction === "asc"}
960
+ <ChevronUpIcon class="csv-data-table__header-icon" />
961
+ {:else if button.direction === "desc"}
962
+ <ChevronDownIcon class="csv-data-table__header-icon" />
963
+ {:else}
964
+ <ArrowUpDownIcon class="csv-data-table__header-icon" />
965
+ {/if}
966
+ </Button>
967
+ {/each}
968
+ </div>
969
+ {#if editing}
970
+ <input
971
+ class="csv-sheet__editor"
972
+ data-testid="csv-sheet-editor"
973
+ style={editorStyle()}
974
+ bind:value={editorValue}
975
+ aria-label="Edit cell"
976
+ {@attach holdEditor}
977
+ onkeydown={onEditorKeyDown}
978
+ onblur={commitEdit}
979
+ />
980
+ {/if}
981
+ {#if menu}
982
+ <div
983
+ class="csv-sheet__menu"
984
+ style:left="{menu.x}px"
985
+ style:top="{menu.y}px"
986
+ role="menu"
987
+ data-sheet-menu={menu.target}
988
+ >
989
+ {#each menuItems as item (item.id)}
990
+ <button
991
+ type="button"
992
+ role="menuitem"
993
+ disabled={item.disabled}
994
+ onclick={() => {
995
+ const current = menu;
996
+ if (!current) return;
997
+ const keep =
998
+ item.id === "clear" || (current.target === "cell" && item.id === "delete");
999
+ closeMenu(() => applyCommand(item.id, current.row, current.col, current.target), keep);
1000
+ }}
1001
+ >
1002
+ <SheetGlyph command={item.id} />
1003
+ {item.label}
1004
+ </button>
1005
+ {/each}
1006
+ </div>
1007
+ {/if}
1008
+ </div>
1009
+ </div>
1010
+
1011
+ <style>
1012
+ .csv-sheet-stage {
1013
+ display: flex;
1014
+ flex-direction: column;
1015
+ position: relative;
1016
+ flex: 1 1 auto;
1017
+ min-width: 0;
1018
+ min-height: 0;
1019
+ }
1020
+
1021
+ .csv-sheet-body {
1022
+ position: relative;
1023
+ flex: 1 1 auto;
1024
+ min-width: 0;
1025
+ min-height: 0;
1026
+ }
1027
+
1028
+ .csv-sheet {
1029
+ position: absolute;
1030
+ inset: 0;
1031
+ overflow: auto;
1032
+ outline: none;
1033
+ }
1034
+
1035
+ .csv-sheet__canvas {
1036
+ position: absolute;
1037
+ inset: 0;
1038
+ width: 100%;
1039
+ height: 100%;
1040
+ pointer-events: none;
1041
+ }
1042
+
1043
+ .csv-sheet__sorts {
1044
+ position: absolute;
1045
+ inset: 0;
1046
+ z-index: 2;
1047
+ overflow: hidden;
1048
+ pointer-events: none;
1049
+ }
1050
+
1051
+ :global(.csv-sheet__sort) {
1052
+ position: absolute;
1053
+ box-sizing: border-box;
1054
+ min-width: 0;
1055
+ min-height: 0;
1056
+ padding: 1px;
1057
+ background: var(--csv-sort-fill, transparent);
1058
+ pointer-events: auto;
1059
+ }
1060
+
1061
+ :global(.csv-sheet__sort:hover) {
1062
+ background: color-mix(in srgb, var(--foreground) 10%, var(--csv-sort-fill, transparent));
1063
+ }
1064
+
1065
+ .csv-sheet__editor {
1066
+ position: absolute;
1067
+ z-index: 2;
1068
+ box-sizing: border-box;
1069
+ margin: 0;
1070
+ border: 2px solid var(--table-selection-border-color, #2563eb);
1071
+ padding: 0 6px;
1072
+ background: var(--csv-shell-background, #fff);
1073
+ color: var(--csv-text-color, inherit);
1074
+ font: inherit;
1075
+ font-family: var(--csv-font-mono);
1076
+ font-size: 13px;
1077
+ }
1078
+
1079
+ .csv-sheet__menu {
1080
+ position: absolute;
1081
+ z-index: 3;
1082
+ display: grid;
1083
+ min-width: 11rem;
1084
+ padding: 0.25rem;
1085
+ border: 1px solid var(--csv-surface-border, var(--border));
1086
+ border-radius: 0.375rem;
1087
+ background: var(--csv-shell-background, var(--background));
1088
+ box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
1089
+ color: var(--csv-text-color, var(--foreground));
1090
+ font-family: var(--font-interface, var(--font-sans, ui-sans-serif, system-ui, sans-serif));
1091
+ font-size: 13px;
1092
+ font-weight: 400;
1093
+ line-height: 1.3;
1094
+ }
1095
+
1096
+ .csv-sheet__menu button {
1097
+ display: flex;
1098
+ align-items: center;
1099
+ gap: 0.5rem;
1100
+ border: 0;
1101
+ background: transparent;
1102
+ color: inherit;
1103
+ padding: 0.35rem 0.5rem;
1104
+ text-align: left;
1105
+ font: inherit;
1106
+ cursor: pointer;
1107
+ }
1108
+
1109
+ .csv-sheet__menu button:hover {
1110
+ background: color-mix(in srgb, var(--foreground) 8%, transparent);
1111
+ }
1112
+
1113
+ .csv-sheet__menu button:disabled {
1114
+ opacity: 0.4;
1115
+ cursor: default;
1116
+ }
1117
+
1118
+ .csv-sheet__menu button:disabled:hover {
1119
+ background: transparent;
1120
+ }
1121
+ </style>