@warkypublic/svelix 0.1.44 → 0.1.47

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 (109) hide show
  1. package/README.md +93 -33
  2. package/dist/components/BetterMenu/BetterMenu.svelte +21 -37
  3. package/dist/components/BetterMenu/BetterMenuAsyncButton.svelte +21 -31
  4. package/dist/components/BetterMenu/BetterMenuPreview.svelte +27 -29
  5. package/dist/components/BetterMenu/MenuRenderer.svelte +40 -51
  6. package/dist/components/Boxer/Boxer.svelte +488 -602
  7. package/dist/components/Boxer/BoxerTarget.svelte +6 -46
  8. package/dist/components/Boxer/BoxerTarget.svelte.d.ts +1 -0
  9. package/dist/components/Boxer/types.d.ts +0 -1
  10. package/dist/components/Button.svelte +19 -43
  11. package/dist/components/ButtonPreview.svelte +2 -11
  12. package/dist/components/CardGrid/CardGrid.svelte +1141 -0
  13. package/dist/components/CardGrid/CardGrid.svelte.d.ts +61 -0
  14. package/dist/components/CardGrid/CardGridFilterPanel.svelte +320 -0
  15. package/dist/components/CardGrid/CardGridFilterPanel.svelte.d.ts +11 -0
  16. package/dist/components/CardGrid/DefaultCard.svelte +287 -0
  17. package/dist/components/CardGrid/DefaultCard.svelte.d.ts +10 -0
  18. package/dist/components/CardGrid/ImageCardStory.svelte +308 -0
  19. package/dist/components/CardGrid/ImageCardStory.svelte.d.ts +18 -0
  20. package/dist/components/CardGrid/cardGrid.d.ts +33 -0
  21. package/dist/components/CardGrid/cardGrid.js +21 -0
  22. package/dist/components/CardGrid/index.d.ts +4 -0
  23. package/dist/components/CardGrid/index.js +4 -0
  24. package/dist/components/ContentEditor/CollaboraInsertTextDemo.svelte +5 -10
  25. package/dist/components/ContentEditor/ContentEditor.svelte +24 -50
  26. package/dist/components/ContentEditor/subcomponents/AudioPlayer.svelte +17 -17
  27. package/dist/components/ContentEditor/subcomponents/CollaboraEditor.svelte +244 -309
  28. package/dist/components/ContentEditor/subcomponents/EmailViewer.svelte +2 -3
  29. package/dist/components/ContentEditor/subcomponents/ImageViewer.svelte +14 -15
  30. package/dist/components/ContentEditor/subcomponents/MarkdownViewer.svelte +74 -90
  31. package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +75 -103
  32. package/dist/components/ContentEditor/subcomponents/Office365Editor.svelte +234 -297
  33. package/dist/components/ContentEditor/subcomponents/PdfViewer.svelte +14 -20
  34. package/dist/components/ContentEditor/subcomponents/TextEditor.svelte +122 -150
  35. package/dist/components/ContentEditor/subcomponents/UnknownFile.svelte +105 -126
  36. package/dist/components/ContentEditor/subcomponents/VideoPlayer.svelte +17 -17
  37. package/dist/components/ContentEditor/subcomponents/ZipViewer.svelte +2 -3
  38. package/dist/components/ErrorBoundary/ErrorBoundary.svelte +41 -58
  39. package/dist/components/ErrorBoundary/ErrorBoundaryPreview.svelte +8 -16
  40. package/dist/components/Former/Former.svelte +306 -424
  41. package/dist/components/Former/FormerButtonArea.svelte +17 -68
  42. package/dist/components/Former/FormerDrawer.svelte +26 -78
  43. package/dist/components/Former/FormerDrawerPreview.svelte +99 -89
  44. package/dist/components/Former/FormerModal.svelte +24 -70
  45. package/dist/components/Former/FormerModalPreview.svelte +99 -89
  46. package/dist/components/Former/FormerPreview.svelte +76 -78
  47. package/dist/components/Former/FormerRestApiPreview.svelte +55 -48
  48. package/dist/components/FormerControllers/ButtonCtrl.svelte +13 -44
  49. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +280 -445
  50. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlCalendar.svelte +95 -136
  51. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlPickerPanel.svelte +3 -62
  52. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlTimeFields.svelte +143 -217
  53. package/dist/components/FormerControllers/IconButtonCtrl.svelte +13 -42
  54. package/dist/components/FormerControllers/InlineWrapper.svelte +12 -41
  55. package/dist/components/FormerControllers/InlineWrapperPreview.svelte +4 -26
  56. package/dist/components/FormerControllers/NativeSelectCtrl.svelte +3 -29
  57. package/dist/components/FormerControllers/NumberInputCtrl.svelte +9 -35
  58. package/dist/components/FormerControllers/PasswordInputCtrl.svelte +4 -23
  59. package/dist/components/FormerControllers/SwitchCtrl.svelte +3 -20
  60. package/dist/components/FormerControllers/TextAreaCtrl.svelte +3 -24
  61. package/dist/components/FormerControllers/TextInputCtrl.svelte +3 -24
  62. package/dist/components/GlobalStateStore/GlobalStateStoreProvider.svelte +73 -120
  63. package/dist/components/Gridler/components/Gridler.svelte +338 -442
  64. package/dist/components/Gridler/components/GridlerCanvas.svelte +720 -1115
  65. package/dist/components/Gridler/components/GridlerEditor.svelte +2 -15
  66. package/dist/components/Gridler/components/GridlerFull.svelte +520 -841
  67. package/dist/components/Gridler/components/GridlerFull.svelte.d.ts +1 -1
  68. package/dist/components/Gridler/components/GridlerFullWithFormerPreview.svelte +194 -147
  69. package/dist/components/Gridler/components/GridlerSearch.svelte +7 -16
  70. package/dist/components/Gridler/components/GridlerSearchToggle.svelte +7 -19
  71. package/dist/components/Gridler/types.d.ts +1 -1
  72. package/dist/components/Icons/IconAdd.svelte +1 -3
  73. package/dist/components/Icons/IconAlertCircle.svelte +1 -3
  74. package/dist/components/Icons/IconAlertTriangle.svelte +1 -3
  75. package/dist/components/Icons/IconCamera.svelte +1 -3
  76. package/dist/components/Icons/IconClose.svelte +1 -3
  77. package/dist/components/Icons/IconEdit.svelte +1 -3
  78. package/dist/components/Icons/IconFilter.svelte +1 -3
  79. package/dist/components/Icons/IconSearch.svelte +1 -3
  80. package/dist/components/Icons/IconSort.svelte +1 -3
  81. package/dist/components/Icons/IconTrash.svelte +1 -3
  82. package/dist/components/Portal/Portal.svelte +14 -38
  83. package/dist/components/Screenshot/Screenshot.svelte +7 -13
  84. package/dist/components/Svark/Svark.svelte +680 -852
  85. package/dist/components/Svark/SvarkContextMenu.svelte +44 -63
  86. package/dist/components/Svark/SvarkInfiniteLoader.svelte +2 -6
  87. package/dist/components/Svark/SvarkPager.svelte +2 -28
  88. package/dist/components/Svark/SvarkSelectionDemo.svelte +4 -8
  89. package/dist/components/Svark/SvarkTopBar.svelte +9 -74
  90. package/dist/components/SvarkGrid/SvarkGrid.svelte +877 -1123
  91. package/dist/components/SvarkGrid/components/SvarkColumnFilterForm.svelte +45 -58
  92. package/dist/components/SvarkGrid/components/SvarkContextMenu.svelte +44 -63
  93. package/dist/components/SvarkGrid/components/SvarkHeaderFilterCell.svelte +41 -75
  94. package/dist/components/SvarkGrid/components/SvarkInfiniteLoader.svelte +2 -6
  95. package/dist/components/SvarkGrid/components/SvarkPager.svelte +2 -28
  96. package/dist/components/SvarkGrid/components/SvarkSearchPopover.svelte +8 -16
  97. package/dist/components/SvarkGrid/components/SvarkTopBar.svelte +6 -62
  98. package/dist/components/SvarkGrid/internal/SvarkGridView.svelte +345 -507
  99. package/dist/components/VTree/VTree.svelte +349 -431
  100. package/dist/components/VTree/VTreeContextMenu.svelte +21 -33
  101. package/dist/components/VTree/VTreeEventsDemo.svelte +48 -58
  102. package/dist/components/VTree/VTreeRow.svelte +13 -65
  103. package/dist/components/VTree/VTreeSearch.svelte +6 -13
  104. package/dist/components/VTree/VTreeVirtualViewport.svelte +37 -106
  105. package/dist/components/index.d.ts +1 -0
  106. package/dist/components/index.js +1 -0
  107. package/llm/COMPONENT_GUIDE.md +240 -0
  108. package/llm/plans/card_grid.plan.md +305 -0
  109. package/package.json +48 -46
@@ -1,445 +1,341 @@
1
- <script lang="ts">
2
- // @ts-nocheck
3
- import type { Snippet } from "svelte";
4
- import type {
5
- GridlerProps,
6
- GridlerColumn,
7
- GridlerCell,
8
- GridlerTheme,
9
- Item,
10
- Selection,
11
- VisibleRange,
12
- } from "../types";
13
- import { DEFAULT_THEME, DEFAULT_THEME_DARK } from "../types";
14
- import GridlerCanvas from "./GridlerCanvas.svelte";
15
- import { detectDarkMode, resolveSkeletonGridTheme } from "../utils/helpers";
16
-
17
- export interface Props extends Partial<GridlerProps> {
18
- columns: GridlerColumn[];
19
- rows: number;
20
- getCellContent: (item: Item) => GridlerCell;
21
- onSearchToggle?: () => void;
22
- onSearchClose?: () => void;
23
- onHeaderContextMenu?: (colIndex: number, x: number, y: number) => void;
24
- onGridMenuOpen?: (coords: {
25
- x: number;
26
- y: number;
27
- item?: Record<string, unknown>;
28
- column?: GridlerColumn;
29
- }) => void;
30
- onRowClick?: (row: number) => void;
31
- onRowContextMenu?: (row: number, x: number, y: number) => void;
32
- /** Resolve raw row data by row index for populating onCellEvent. */
33
- getRowData?: (row: number) => Record<string, unknown> | undefined;
34
- children?: Snippet;
35
- }
36
-
37
- const {
38
- columns,
39
- rows,
40
- getCellContent,
41
- onCellEdited,
42
- onCellDblClick,
43
- onRowAppended,
44
- onColumnMoved,
45
- onDelete,
46
- onSelectionCleared,
47
- width = "100%",
48
- height = "400px",
49
- rowHeight = 34,
50
- headerHeight = 36,
51
- fixedColumns = 0,
52
- resizableColumns,
53
- rowMarkers,
54
- showSearch,
55
- readonly,
56
- searchValue = "",
57
- onSearchValueChange,
58
- onSearchToggle,
59
- onSearchClose,
60
- onHeaderContextMenu,
61
- onMenuClick,
62
- onGridMenuOpen,
63
- onRowClick,
64
- onRowContextMenu,
65
- theme = {},
66
- selection = { type: "none" } as Selection,
67
- onSelectionChange,
68
- onVisibleRangeChange,
69
- // GridCommonProps additions
70
- onCellEvent,
71
- onGridEvent,
72
- onSortOrderChange,
73
- sortOrder,
74
- onFilterChange: _onFilterChange,
75
- filters: _filters,
76
- selectedItems: _selectedItems,
77
- onSelectedItemsChange: _onSelectedItemsChange,
78
- getRowData,
79
- settings,
80
- children,
81
- }: Props = $props();
82
-
83
- // ── Settings resolution (flat props win over settings object) ────────────
84
-
85
- const resolvedReadonly = $derived(readonly ?? settings?.readonly ?? false);
86
- const resolvedResizableColumns = $derived(resizableColumns ?? settings?.resizableColumns ?? true);
87
- const resolvedShowSearch = $derived(showSearch ?? settings?.showSearch ?? false);
88
- const resolvedRowMarkers = $derived<"checkbox" | "number" | "none">(
89
- rowMarkers ??
90
- (settings?.showRowMarkers
91
- ? (settings?.rowMarkerType ?? "checkbox")
92
- : (settings?.rowMarkerType ?? "none")),
93
- );
94
-
95
- // ── Theme ─────────────────────────────────────────────────────────────────────
96
-
97
- let isDarkMode = $state(false);
98
- let resolvedSkeletonTheme = $state<GridlerTheme>(DEFAULT_THEME);
99
-
100
- $effect(() => {
101
- isDarkMode = detectDarkMode();
102
- resolvedSkeletonTheme =
103
- resolveSkeletonGridTheme(isDarkMode) ??
104
- (isDarkMode ? DEFAULT_THEME_DARK : DEFAULT_THEME);
105
- const mo = new MutationObserver(() => {
106
- isDarkMode = detectDarkMode();
107
- resolvedSkeletonTheme =
108
- resolveSkeletonGridTheme(isDarkMode) ??
109
- (isDarkMode ? DEFAULT_THEME_DARK : DEFAULT_THEME);
110
- });
111
- const opts: MutationObserverInit = {
112
- attributes: true,
113
- attributeFilter: ["class", "data-theme"],
114
- };
115
- mo.observe(document.documentElement, opts);
116
- mo.observe(document.body, opts);
117
- return () => mo.disconnect();
118
- });
119
-
120
- const baseTheme: GridlerTheme = $derived(resolvedSkeletonTheme);
121
- const mergedTheme: GridlerTheme = $derived({ ...baseTheme, ...theme });
122
-
123
- // ── Selection / editing state ────────────────────────────────────────────────
124
-
125
- let currentSelection = $state<Selection>({ type: "none" });
126
- let focusedCell = $state<Item | null>(null);
127
-
128
- $effect(() => {
129
- if (!selection || selection.type === "none") return;
130
- currentSelection = selection;
131
- });
132
- let isEditing = $state(false);
133
- let editingCell = $state<Item | null>(null);
134
-
135
- let canvasComponent: ReturnType<typeof GridlerCanvas> | undefined = $state();
136
-
137
- // ── Editing helpers ───────────────────────────────────────────────────────────
138
-
139
- function beginEdit(cell: Item) {
140
- const cellContent = getCellContent(cell);
141
- if (cellContent.readonly) return;
142
- editingCell = cell;
143
- isEditing = true;
144
- }
145
-
146
- function cancelEdit() {
147
- isEditing = false;
148
- editingCell = null;
149
- }
150
-
151
- function handleEditComplete(value: string) {
152
- if (editingCell) {
153
- const cell = getCellContent(editingCell);
154
- const newCell: GridlerCell = { ...cell, displayData: value, data: value };
155
- onCellEdited?.(editingCell, newCell);
156
- }
157
- cancelEdit();
158
- }
159
-
160
- function getSelectionRowData(sel: Selection): Record<string, unknown>[] | undefined {
161
- if (!getRowData) return undefined;
162
- if (sel.type === "cell") {
163
- const d = getRowData(sel.item[1]);
164
- return d ? [d] : undefined;
165
- }
166
- if (sel.type === "row") {
167
- const items = sel.rows.map(getRowData).filter((r): r is Record<string, unknown> => r !== undefined);
168
- return items.length > 0 ? items : undefined;
169
- }
170
- if (sel.type === "range") {
171
- const minRow = Math.min(sel.start[1], sel.end[1]);
172
- const maxRow = Math.max(sel.start[1], sel.end[1]);
173
- const items: Record<string, unknown>[] = [];
174
- for (let i = minRow; i <= maxRow; i++) {
175
- const d = getRowData(i);
176
- if (d) items.push(d);
177
- }
178
- return items.length > 0 ? items : undefined;
179
- }
180
- return undefined;
181
- }
182
-
183
- // ── Row toggle (checkbox / number markers) ────────────────────────────────────
184
-
185
- function handleRowToggle(row: number) {
186
- const sel = canvasComponent?.getCurrentSelection() ?? currentSelection;
187
- let newSel: Selection;
188
- if (sel.type === "row" && sel.rows.includes(row)) {
189
- const newRows = sel.rows.filter((r) => r !== row);
190
- newSel =
191
- newRows.length > 0 ? { type: "row", rows: newRows } : { type: "none" };
192
- } else if (sel.type === "row") {
193
- newSel = { type: "row", rows: [...sel.rows, row] };
194
- } else {
195
- newSel = { type: "row", rows: [row] };
196
- }
197
- currentSelection = newSel;
198
- canvasComponent?.setCurrentSelection(newSel);
199
- onSelectionChange?.(newSel, getSelectionRowData(newSel));
200
- }
201
-
202
- // ── Clipboard ────────────────────────────────────────────────────────────────
203
-
204
- async function copySelection(): Promise<void> {
205
- const lines: string[] = [];
206
- const sel = canvasComponent?.getCurrentSelection() ?? currentSelection;
207
-
208
- if (sel.type === "cell") {
209
- lines.push(getCellContent(sel.item).displayData);
210
- } else if (sel.type === "range") {
211
- const [startCol, startRow] = sel.start;
212
- const [endCol, endRow] = sel.end;
213
- const minCol = Math.min(startCol, endCol);
214
- const maxCol = Math.max(startCol, endCol);
215
- const minRow = Math.min(startRow, endRow);
216
- const maxRow = Math.max(startRow, endRow);
217
- for (let row = minRow; row <= maxRow; row++) {
218
- const rowData: string[] = [];
219
- for (let col = minCol; col <= maxCol; col++) {
220
- rowData.push(getCellContent([col, row]).displayData);
221
- }
222
- lines.push(rowData.join("\t"));
223
- }
224
- } else if (sel.type === "row") {
225
- for (const row of sel.rows) {
226
- const rowData = columns.map(
227
- (_, col) => getCellContent([col, row]).displayData,
228
- );
229
- lines.push(rowData.join("\t"));
230
- }
231
- } else if (sel.type === "column") {
232
- for (let row = 0; row < rows; row++) {
233
- const rowData = sel.columns.map(
234
- (col) => getCellContent([col, row]).displayData,
235
- );
236
- lines.push(rowData.join("\t"));
237
- }
238
- }
239
-
240
- if (lines.length > 0) {
241
- try {
242
- await navigator.clipboard.writeText(lines.join("\n"));
243
- canvasComponent?.setAnnouncement(
244
- `Copied ${lines.length} row${lines.length > 1 ? "s" : ""} to clipboard`,
245
- );
246
- } catch {
247
- canvasComponent?.setAnnouncement("Clipboard access denied");
248
- }
249
- }
250
- }
251
-
252
- async function pasteFromClipboard(): Promise<void> {
253
- if (resolvedReadonly) return;
254
- const fc = canvasComponent?.getFocusedCell() ?? focusedCell;
255
- if (!fc) return;
256
- let text: string;
257
- try {
258
- text = await navigator.clipboard.readText();
259
- } catch {
260
- canvasComponent?.setAnnouncement("Clipboard access denied");
261
- return;
262
- }
263
- const pastedRows = text.split("\n").map((line) => line.split("\t"));
264
- const [startCol, startRow] = fc;
265
-
266
- for (let r = 0; r < pastedRows.length; r++) {
267
- for (let c = 0; c < pastedRows[r].length; c++) {
268
- const col = startCol + c;
269
- const row = startRow + r;
270
- if (col >= columns.length || row >= rows) continue;
271
- const item: Item = [col, row];
272
- const cell = getCellContent(item);
273
- if (!cell.readonly && cell.allowEditing !== false) {
274
- const newCell: GridlerCell = {
275
- ...cell,
276
- displayData: pastedRows[r][c],
277
- data: pastedRows[r][c],
278
- };
279
- onCellEdited?.(item, newCell);
280
- }
281
- }
282
- }
283
- canvasComponent?.setAnnouncement(
284
- `Pasted ${pastedRows.length} row${pastedRows.length > 1 ? "s" : ""}`,
285
- );
286
- }
287
-
288
- // ── Keyboard handler ──────────────────────────────────────────────────────────
289
-
290
- function handleKeyDown(e: KeyboardEvent) {
291
- if (!canvasComponent?.getHasFocus()) return;
292
- onGridEvent?.("keydown", undefined, undefined, { x: 0, y: 0, code: e.key });
293
-
294
- // Search toggle
295
- if ((e.ctrlKey || e.metaKey) && e.key === "f") {
296
- onSearchToggle?.();
297
- e.preventDefault();
298
- return;
299
- }
300
-
301
- // Copy/paste shortcuts
302
- if ((e.ctrlKey || e.metaKey) && e.key === "c") {
303
- copySelection();
304
- e.preventDefault();
305
- return;
306
- }
307
- if ((e.ctrlKey || e.metaKey) && e.key === "v") {
308
- pasteFromClipboard();
309
- e.preventDefault();
310
- return;
311
- }
312
- if ((e.ctrlKey || e.metaKey) && e.key === "a") {
313
- if (rows > 0 && columns.length > 0) {
314
- const newSel: Selection = {
315
- type: "range",
316
- start: [0, 0],
317
- end: [columns.length - 1, rows - 1],
318
- };
319
- currentSelection = newSel;
320
- canvasComponent?.setCurrentSelection(newSel);
321
- onSelectionChange?.(newSel, getSelectionRowData(newSel));
322
- canvasComponent?.setAnnouncement("All cells selected");
323
- }
324
- e.preventDefault();
325
- return;
326
- }
327
-
328
- focusedCell = canvasComponent?.getFocusedCell() ?? focusedCell;
329
- if (!focusedCell) return;
330
-
331
- const [col, row] = focusedCell;
332
- let newCol = col;
333
- let newRow = row;
334
-
335
- switch (e.key) {
336
- case "ArrowUp":
337
- newRow = Math.max(0, row - 1);
338
- e.preventDefault();
339
- break;
340
- case "ArrowDown":
341
- newRow = Math.min(rows - 1, row + 1);
342
- e.preventDefault();
343
- break;
344
- case "ArrowLeft":
345
- newCol = Math.max(0, col - 1);
346
- e.preventDefault();
347
- break;
348
- case "ArrowRight":
349
- newCol = Math.min(columns.length - 1, col + 1);
350
- e.preventDefault();
351
- break;
352
- case "Enter":
353
- if (!resolvedReadonly) {
354
- beginEdit(focusedCell);
355
- }
356
- onCellEvent?.("enter_key", getRowData?.(row) ?? {}, columns[col] ?? { id: "", title: "" });
357
- e.preventDefault();
358
- return;
359
- case "Escape": {
360
- cancelEdit();
361
- const sel = canvasComponent?.getCurrentSelection() ?? currentSelection;
362
- if (sel.type !== "none") {
363
- onSelectionCleared?.();
364
- }
365
- return;
366
- }
367
- case "Tab":
368
- if (e.shiftKey) {
369
- newCol = col - 1;
370
- if (newCol < 0) {
371
- newCol = columns.length - 1;
372
- newRow = Math.max(0, row - 1);
373
- }
374
- } else {
375
- newCol = col + 1;
376
- if (newCol >= columns.length) {
377
- newCol = 0;
378
- newRow = Math.min(rows - 1, row + 1);
379
- onRowAppended?.();
380
- }
381
- }
382
- onCellEvent?.("tab_key", getRowData?.(row) ?? {}, columns[col] ?? { id: "", title: "" });
383
- e.preventDefault();
384
- break;
385
- case "Delete":
386
- case "Backspace": {
387
- if (!resolvedReadonly) {
388
- const delSel =
389
- canvasComponent?.getCurrentSelection() ?? currentSelection;
390
- if (delSel.type !== "none") {
391
- onDelete?.(delSel);
392
- }
393
- }
394
- onCellEvent?.("delete_key", getRowData?.(row) ?? {}, columns[col] ?? { id: "", title: "" });
395
- return;
396
- }
397
- default:
398
- if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !resolvedReadonly) {
399
- beginEdit(focusedCell);
400
- }
401
- return;
402
- }
403
-
404
- if (newCol !== col || newRow !== row) {
405
- const newFocusedCell: Item = [newCol, newRow];
406
- focusedCell = newFocusedCell;
407
- canvasComponent?.setFocusedCell(newFocusedCell);
408
-
409
- const newSel: Selection = { type: "cell", item: newFocusedCell };
410
- currentSelection = newSel;
411
- canvasComponent?.setCurrentSelection(newSel);
412
- onSelectionChange?.(newSel, getSelectionRowData(newSel));
413
- canvasComponent?.setAnnouncement(
414
- `Row ${newRow + 1}, column ${columns[newCol]?.title ?? newCol + 1}`,
415
- );
416
- canvasComponent?.scrollCellIntoView(newFocusedCell);
417
- }
418
- }
419
-
420
- // ── Visible range / grid events ───────────────────────────────────────────────
421
-
422
- function handleVisibleRangeChange(range: VisibleRange) {
423
- onVisibleRangeChange?.(range);
424
- onGridEvent?.("scroll", undefined, undefined, undefined, { row: range.firstRow, column: range.firstCol });
425
- }
426
-
427
- async function handleMenuOpen(d: {
428
- x: number;
429
- y: number;
430
- item?: Record<string, unknown>;
431
- column?: GridlerColumn;
432
- }) {
433
- if (onMenuClick) {
434
- await onMenuClick(
435
- d.item,
436
- d.column,
437
- { x: d.x, y: d.y },
438
- );
439
- return;
440
- }
441
- onGridMenuOpen?.(d);
442
- }
1
+ <script lang="ts">import { DEFAULT_THEME, DEFAULT_THEME_DARK } from "../types";
2
+ import GridlerCanvas from "./GridlerCanvas.svelte";
3
+ import { detectDarkMode, resolveSkeletonGridTheme } from "../utils/helpers";
4
+ const { columns, rows, getCellContent, onCellEdited, onCellDblClick, onRowAppended, onColumnMoved, onDelete, onSelectionCleared, width = "100%", height = "400px", rowHeight = 34, headerHeight = 36, fixedColumns = 0, resizableColumns, rowMarkers, showSearch, readonly, searchValue = "", onSearchValueChange, onSearchToggle, onSearchClose, onHeaderContextMenu, onMenuClick, onGridMenuOpen, onRowClick, onRowContextMenu, theme = {}, selection = { type: "none" }, onSelectionChange, onVisibleRangeChange, onCellEvent, onGridEvent, onSortOrderChange, sortOrder, onFilterChange: _onFilterChange, filters: _filters, selectedItems: _selectedItems, onSelectedItemsChange: _onSelectedItemsChange, getRowData, settings, children } = $props();
5
+ // ── Settings resolution (flat props win over settings object) ────────────
6
+ const resolvedReadonly = $derived(readonly ?? settings?.readonly ?? false);
7
+ const resolvedResizableColumns = $derived(resizableColumns ?? settings?.resizableColumns ?? true);
8
+ const resolvedShowSearch = $derived(showSearch ?? settings?.showSearch ?? false);
9
+ const resolvedRowMarkers = $derived(rowMarkers ?? (settings?.showRowMarkers ? settings?.rowMarkerType ?? "checkbox" : settings?.rowMarkerType ?? "none"));
10
+ // ── Theme ─────────────────────────────────────────────────────────────────────
11
+ let isDarkMode = $state(false);
12
+ let resolvedSkeletonTheme = $state(DEFAULT_THEME);
13
+ $effect(() => {
14
+ isDarkMode = detectDarkMode();
15
+ resolvedSkeletonTheme = resolveSkeletonGridTheme(isDarkMode) ?? (isDarkMode ? DEFAULT_THEME_DARK : DEFAULT_THEME);
16
+ const mo = new MutationObserver(() => {
17
+ isDarkMode = detectDarkMode();
18
+ resolvedSkeletonTheme = resolveSkeletonGridTheme(isDarkMode) ?? (isDarkMode ? DEFAULT_THEME_DARK : DEFAULT_THEME);
19
+ });
20
+ const opts = {
21
+ attributes: true,
22
+ attributeFilter: ["class", "data-theme"]
23
+ };
24
+ mo.observe(document.documentElement, opts);
25
+ mo.observe(document.body, opts);
26
+ return () => mo.disconnect();
27
+ });
28
+ const baseTheme = $derived(resolvedSkeletonTheme);
29
+ const mergedTheme = $derived({
30
+ ...baseTheme,
31
+ ...theme
32
+ });
33
+ // ── Selection / editing state ────────────────────────────────────────────────
34
+ let currentSelection = $state({ type: "none" });
35
+ let focusedCell = $state(null);
36
+ $effect(() => {
37
+ if (!selection || selection.type === "none") return;
38
+ currentSelection = selection;
39
+ });
40
+ let isEditing = $state(false);
41
+ let editingCell = $state(null);
42
+ let canvasComponent = $state();
43
+ // ── Editing helpers ───────────────────────────────────────────────────────────
44
+ function beginEdit(cell) {
45
+ const cellContent = getCellContent(cell);
46
+ if (cellContent.readonly) return;
47
+ editingCell = cell;
48
+ isEditing = true;
49
+ }
50
+ function cancelEdit() {
51
+ isEditing = false;
52
+ editingCell = null;
53
+ }
54
+ function handleEditComplete(value) {
55
+ if (editingCell) {
56
+ const cell = getCellContent(editingCell);
57
+ const newCell = {
58
+ ...cell,
59
+ displayData: value,
60
+ data: value
61
+ };
62
+ onCellEdited?.(editingCell, newCell);
63
+ }
64
+ cancelEdit();
65
+ }
66
+ function getSelectionRowData(sel) {
67
+ if (!getRowData) return undefined;
68
+ if (sel.type === "cell") {
69
+ const d = getRowData(sel.item[1]);
70
+ return d ? [d] : undefined;
71
+ }
72
+ if (sel.type === "row") {
73
+ const items = sel.rows.map(getRowData).filter((r) => r !== undefined);
74
+ return items.length > 0 ? items : undefined;
75
+ }
76
+ if (sel.type === "range") {
77
+ const minRow = Math.min(sel.start[1], sel.end[1]);
78
+ const maxRow = Math.max(sel.start[1], sel.end[1]);
79
+ const items = [];
80
+ for (let i = minRow; i <= maxRow; i++) {
81
+ const d = getRowData(i);
82
+ if (d) items.push(d);
83
+ }
84
+ return items.length > 0 ? items : undefined;
85
+ }
86
+ return undefined;
87
+ }
88
+ // ── Row toggle (checkbox / number markers) ────────────────────────────────────
89
+ function handleRowToggle(row) {
90
+ const sel = canvasComponent?.getCurrentSelection() ?? currentSelection;
91
+ let newSel;
92
+ if (sel.type === "row" && sel.rows.includes(row)) {
93
+ const newRows = sel.rows.filter((r) => r !== row);
94
+ newSel = newRows.length > 0 ? {
95
+ type: "row",
96
+ rows: newRows
97
+ } : { type: "none" };
98
+ } else if (sel.type === "row") {
99
+ newSel = {
100
+ type: "row",
101
+ rows: [...sel.rows, row]
102
+ };
103
+ } else {
104
+ newSel = {
105
+ type: "row",
106
+ rows: [row]
107
+ };
108
+ }
109
+ currentSelection = newSel;
110
+ canvasComponent?.setCurrentSelection(newSel);
111
+ onSelectionChange?.(newSel, getSelectionRowData(newSel));
112
+ }
113
+ // ── Clipboard ────────────────────────────────────────────────────────────────
114
+ async function copySelection() {
115
+ const lines = [];
116
+ const sel = canvasComponent?.getCurrentSelection() ?? currentSelection;
117
+ if (sel.type === "cell") {
118
+ lines.push(getCellContent(sel.item).displayData);
119
+ } else if (sel.type === "range") {
120
+ const [startCol, startRow] = sel.start;
121
+ const [endCol, endRow] = sel.end;
122
+ const minCol = Math.min(startCol, endCol);
123
+ const maxCol = Math.max(startCol, endCol);
124
+ const minRow = Math.min(startRow, endRow);
125
+ const maxRow = Math.max(startRow, endRow);
126
+ for (let row = minRow; row <= maxRow; row++) {
127
+ const rowData = [];
128
+ for (let col = minCol; col <= maxCol; col++) {
129
+ rowData.push(getCellContent([col, row]).displayData);
130
+ }
131
+ lines.push(rowData.join(" "));
132
+ }
133
+ } else if (sel.type === "row") {
134
+ for (const row of sel.rows) {
135
+ const rowData = columns.map((_, col) => getCellContent([col, row]).displayData);
136
+ lines.push(rowData.join(" "));
137
+ }
138
+ } else if (sel.type === "column") {
139
+ for (let row = 0; row < rows; row++) {
140
+ const rowData = sel.columns.map((col) => getCellContent([col, row]).displayData);
141
+ lines.push(rowData.join(" "));
142
+ }
143
+ }
144
+ if (lines.length > 0) {
145
+ try {
146
+ await navigator.clipboard.writeText(lines.join("\n"));
147
+ canvasComponent?.setAnnouncement(`Copied ${lines.length} row${lines.length > 1 ? "s" : ""} to clipboard`);
148
+ } catch {
149
+ canvasComponent?.setAnnouncement("Clipboard access denied");
150
+ }
151
+ }
152
+ }
153
+ async function pasteFromClipboard() {
154
+ if (resolvedReadonly) return;
155
+ const fc = canvasComponent?.getFocusedCell() ?? focusedCell;
156
+ if (!fc) return;
157
+ let text;
158
+ try {
159
+ text = await navigator.clipboard.readText();
160
+ } catch {
161
+ canvasComponent?.setAnnouncement("Clipboard access denied");
162
+ return;
163
+ }
164
+ const pastedRows = text.split("\n").map((line) => line.split(" "));
165
+ const [startCol, startRow] = fc;
166
+ for (let r = 0; r < pastedRows.length; r++) {
167
+ for (let c = 0; c < pastedRows[r].length; c++) {
168
+ const col = startCol + c;
169
+ const row = startRow + r;
170
+ if (col >= columns.length || row >= rows) continue;
171
+ const item = [col, row];
172
+ const cell = getCellContent(item);
173
+ if (!cell.readonly && cell.allowEditing !== false) {
174
+ const newCell = {
175
+ ...cell,
176
+ displayData: pastedRows[r][c],
177
+ data: pastedRows[r][c]
178
+ };
179
+ onCellEdited?.(item, newCell);
180
+ }
181
+ }
182
+ }
183
+ canvasComponent?.setAnnouncement(`Pasted ${pastedRows.length} row${pastedRows.length > 1 ? "s" : ""}`);
184
+ }
185
+ // ── Keyboard handler ──────────────────────────────────────────────────────────
186
+ function handleKeyDown(e) {
187
+ if (!canvasComponent?.getHasFocus()) return;
188
+ onGridEvent?.("keydown", undefined, undefined, {
189
+ x: 0,
190
+ y: 0,
191
+ code: e.key
192
+ });
193
+ // Search toggle
194
+ if ((e.ctrlKey || e.metaKey) && e.key === "f") {
195
+ onSearchToggle?.();
196
+ e.preventDefault();
197
+ return;
198
+ }
199
+ // Copy/paste shortcuts
200
+ if ((e.ctrlKey || e.metaKey) && e.key === "c") {
201
+ copySelection();
202
+ e.preventDefault();
203
+ return;
204
+ }
205
+ if ((e.ctrlKey || e.metaKey) && e.key === "v") {
206
+ pasteFromClipboard();
207
+ e.preventDefault();
208
+ return;
209
+ }
210
+ if ((e.ctrlKey || e.metaKey) && e.key === "a") {
211
+ if (rows > 0 && columns.length > 0) {
212
+ const newSel = {
213
+ type: "range",
214
+ start: [0, 0],
215
+ end: [columns.length - 1, rows - 1]
216
+ };
217
+ currentSelection = newSel;
218
+ canvasComponent?.setCurrentSelection(newSel);
219
+ onSelectionChange?.(newSel, getSelectionRowData(newSel));
220
+ canvasComponent?.setAnnouncement("All cells selected");
221
+ }
222
+ e.preventDefault();
223
+ return;
224
+ }
225
+ focusedCell = canvasComponent?.getFocusedCell() ?? focusedCell;
226
+ if (!focusedCell) return;
227
+ const [col, row] = focusedCell;
228
+ let newCol = col;
229
+ let newRow = row;
230
+ switch (e.key) {
231
+ case "ArrowUp":
232
+ newRow = Math.max(0, row - 1);
233
+ e.preventDefault();
234
+ break;
235
+ case "ArrowDown":
236
+ newRow = Math.min(rows - 1, row + 1);
237
+ e.preventDefault();
238
+ break;
239
+ case "ArrowLeft":
240
+ newCol = Math.max(0, col - 1);
241
+ e.preventDefault();
242
+ break;
243
+ case "ArrowRight":
244
+ newCol = Math.min(columns.length - 1, col + 1);
245
+ e.preventDefault();
246
+ break;
247
+ case "Enter":
248
+ if (!resolvedReadonly) {
249
+ beginEdit(focusedCell);
250
+ }
251
+ onCellEvent?.("enter_key", getRowData?.(row) ?? {}, columns[col] ?? {
252
+ id: "",
253
+ title: ""
254
+ });
255
+ e.preventDefault();
256
+ return;
257
+ case "Escape": {
258
+ cancelEdit();
259
+ const sel = canvasComponent?.getCurrentSelection() ?? currentSelection;
260
+ if (sel.type !== "none") {
261
+ onSelectionCleared?.();
262
+ }
263
+ return;
264
+ }
265
+ case "Tab":
266
+ if (e.shiftKey) {
267
+ newCol = col - 1;
268
+ if (newCol < 0) {
269
+ newCol = columns.length - 1;
270
+ newRow = Math.max(0, row - 1);
271
+ }
272
+ } else {
273
+ newCol = col + 1;
274
+ if (newCol >= columns.length) {
275
+ newCol = 0;
276
+ newRow = Math.min(rows - 1, row + 1);
277
+ onRowAppended?.();
278
+ }
279
+ }
280
+ onCellEvent?.("tab_key", getRowData?.(row) ?? {}, columns[col] ?? {
281
+ id: "",
282
+ title: ""
283
+ });
284
+ e.preventDefault();
285
+ break;
286
+ case "Delete":
287
+ case "Backspace": {
288
+ if (!resolvedReadonly) {
289
+ const delSel = canvasComponent?.getCurrentSelection() ?? currentSelection;
290
+ if (delSel.type !== "none") {
291
+ onDelete?.(delSel);
292
+ }
293
+ }
294
+ onCellEvent?.("delete_key", getRowData?.(row) ?? {}, columns[col] ?? {
295
+ id: "",
296
+ title: ""
297
+ });
298
+ return;
299
+ }
300
+ default:
301
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !resolvedReadonly) {
302
+ beginEdit(focusedCell);
303
+ }
304
+ return;
305
+ }
306
+ if (newCol !== col || newRow !== row) {
307
+ const newFocusedCell = [newCol, newRow];
308
+ focusedCell = newFocusedCell;
309
+ canvasComponent?.setFocusedCell(newFocusedCell);
310
+ const newSel = {
311
+ type: "cell",
312
+ item: newFocusedCell
313
+ };
314
+ currentSelection = newSel;
315
+ canvasComponent?.setCurrentSelection(newSel);
316
+ onSelectionChange?.(newSel, getSelectionRowData(newSel));
317
+ canvasComponent?.setAnnouncement(`Row ${newRow + 1}, column ${columns[newCol]?.title ?? newCol + 1}`);
318
+ canvasComponent?.scrollCellIntoView(newFocusedCell);
319
+ }
320
+ }
321
+ // ── Visible range / grid events ───────────────────────────────────────────────
322
+ function handleVisibleRangeChange(range) {
323
+ onVisibleRangeChange?.(range);
324
+ onGridEvent?.("scroll", undefined, undefined, undefined, {
325
+ row: range.firstRow,
326
+ column: range.firstCol
327
+ });
328
+ }
329
+ async function handleMenuOpen(d) {
330
+ if (onMenuClick) {
331
+ await onMenuClick(d.item, d.column, {
332
+ x: d.x,
333
+ y: d.y
334
+ });
335
+ return;
336
+ }
337
+ onGridMenuOpen?.(d);
338
+ }
443
339
  </script>
444
340
 
445
341
  <GridlerCanvas