@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,434 +1,352 @@
1
- <script lang="ts">
2
- import { onMount } from 'svelte';
3
- import VTreeSearch from './VTreeSearch.svelte';
4
- import VTreeVirtualViewport from './VTreeVirtualViewport.svelte';
5
- import VTreeContextMenu from './VTreeContextMenu.svelte';
6
- import type { VTreeFlatRow } from './VTree.models.js';
7
- import type { VTreeContextMenuItem, VTreeProps, VTreeRecord } from './types.js';
8
-
9
- export type Props<T extends VTreeRecord = VTreeRecord> = VTreeProps<T>;
10
-
11
- const {
12
- data = [],
13
- adapter,
14
- idField = 'id',
15
- labelField = 'label',
16
- descriptionField = 'description',
17
- childrenField = 'children',
18
- hasChildrenField = 'hasChildren',
19
- height = 420,
20
- rowHeight = 36,
21
- indent = 18,
22
- overscan = 8,
23
- searchable = true,
24
- searchPlaceholder = 'Search tree...',
25
- searchValue = '',
26
- serverSideSearch = false,
27
- autoExpandSearchResults = true,
28
- loadOnMount = true,
29
- emptyMessage = 'Nothing here...',
30
- loadingMessage = 'Loading...',
31
- leftSection,
32
- rightSection,
33
- contextMenuItems,
34
- onselect,
35
- onclick,
36
- ondoubleclick,
37
- oncontext,
38
- ontoggle,
39
- onsearch,
40
- onloaderror
41
- }: Props = $props();
42
-
43
- let viewportRef = $state<{ scrollToIndex: (index: number) => void } | undefined>(undefined);
44
- let focusedIndex = $state(0);
45
- let selectedId = $state<string | null>(null);
46
- let searchQuery = $state('');
47
- let loadingRoot = $state(false);
48
-
49
- let rootNodes = $state<VTreeRecord[]>([]);
50
- let searchNodes = $state<VTreeRecord[]>([]);
51
- let childrenByParent = $state<Record<string, VTreeRecord[]>>({});
52
- let expandedById = $state<Record<string, boolean>>({});
53
- let loadingById = $state<Record<string, boolean>>({});
54
- let loadedById = $state<Record<string, boolean>>({});
55
- let errorsById = $state<Record<string, string>>({});
56
-
57
- let lastSearchHandle: ReturnType<typeof setTimeout> | undefined;
58
- let searchRunId = 0;
59
- let contextOpen = $state(false);
60
- let contextX = $state(0);
61
- let contextY = $state(0);
62
- let contextRow = $state<VTreeRecord | null>(null);
63
- let contextItems = $state<VTreeContextMenuItem[]>([]);
64
-
65
- function getNodeId(node: VTreeRecord): string {
66
- const value = node?.[idField];
67
- return String(value ?? '');
68
- }
69
-
70
- function getNodeLabel(node: VTreeRecord): string {
71
- return String(node?.[labelField] ?? '');
72
- }
73
-
74
- function getNodeDescription(node: VTreeRecord): string {
75
- const value = node?.[descriptionField];
76
- return value === undefined || value === null ? '' : String(value);
77
- }
78
-
79
- function getNodeChildren(node: VTreeRecord): VTreeRecord[] {
80
- const nodeId = getNodeId(node);
81
- const remoteChildren = childrenByParent[nodeId];
82
- if (remoteChildren) return remoteChildren;
83
-
84
- const localChildren = node?.[childrenField];
85
- return Array.isArray(localChildren) ? (localChildren as VTreeRecord[]) : [];
86
- }
87
-
88
- function nodeHasChildren(node: VTreeRecord): boolean {
89
- const children = getNodeChildren(node);
90
- if (children.length > 0) return true;
91
- return typeof node?.[hasChildrenField] === 'boolean'
92
- ? Boolean(node[hasChildrenField])
93
- : false;
94
- }
95
-
96
- function normalizeRows(rows: VTreeRecord[]): VTreeRecord[] {
97
- return Array.isArray(rows) ? rows : [];
98
- }
99
-
100
- async function loadRoot() {
101
- if (!adapter) {
102
- rootNodes = normalizeRows(data ?? []);
103
- return;
104
- }
105
-
106
- loadingRoot = true;
107
- try {
108
- const rows = await adapter.readRoot();
109
- rootNodes = normalizeRows(rows);
110
- searchNodes = [];
111
- errorsById = {};
112
- } catch (error) {
113
- onloaderror?.(error);
114
- } finally {
115
- loadingRoot = false;
116
- }
117
- }
118
-
119
- async function loadChildren(node: VTreeRecord) {
120
- if (!adapter) return;
121
-
122
- const nodeId = getNodeId(node);
123
- if (!nodeId || loadingById[nodeId] || loadedById[nodeId]) return;
124
-
125
- loadingById = { ...loadingById, [nodeId]: true };
126
- errorsById = { ...errorsById, [nodeId]: '' };
127
-
128
- try {
129
- const rows = await adapter.readChildren(node);
130
- childrenByParent = { ...childrenByParent, [nodeId]: normalizeRows(rows) };
131
- loadedById = { ...loadedById, [nodeId]: true };
132
- } catch (error) {
133
- const message = error instanceof Error ? error.message : 'Unable to load node';
134
- errorsById = { ...errorsById, [nodeId]: message };
135
- onloaderror?.(error, node);
136
- } finally {
137
- loadingById = { ...loadingById, [nodeId]: false };
138
- }
139
- }
140
-
141
- function matchesLocalSearch(node: VTreeRecord, query: string): boolean {
142
- const q = query.toLowerCase();
143
- return (
144
- getNodeLabel(node).toLowerCase().includes(q) ||
145
- getNodeDescription(node).toLowerCase().includes(q)
146
- );
147
- }
148
-
149
- function toggleNode(node: VTreeRecord) {
150
- const nodeId = getNodeId(node);
151
- if (!nodeId) return;
152
-
153
- const currentlyExpanded = Boolean(expandedById[nodeId]);
154
- expandedById = { ...expandedById, [nodeId]: !currentlyExpanded };
155
- ontoggle?.(node, !currentlyExpanded);
156
-
157
- if (!currentlyExpanded && adapter && nodeHasChildren(node)) {
158
- void loadChildren(node);
159
- }
160
- }
161
-
162
- const displayRoots = $derived.by(() => {
163
- if (adapter && serverSideSearch && searchQuery.trim().length > 0) return searchNodes;
164
- return rootNodes;
165
- });
166
-
167
- const flattenedRows = $derived.by(() => {
168
- const rows: VTreeFlatRow[] = [];
169
- const activeSearch = searchQuery.trim().toLowerCase();
170
- const useLocalSearch = activeSearch.length > 0 && !(adapter && serverSideSearch);
171
-
172
- const walk = (
173
- nodes: VTreeRecord[],
174
- level: number,
175
- parentId: string | null
176
- ): { rows: VTreeFlatRow[]; matches: boolean } => {
177
- const walkRows: VTreeFlatRow[] = [];
178
- let hasAnyMatch = false;
179
-
180
- for (const node of nodes) {
181
- const id = getNodeId(node);
182
- const children = getNodeChildren(node);
183
- const childResult = walk(children, level + 1, id);
184
- const selfMatch = useLocalSearch ? matchesLocalSearch(node, activeSearch) : false;
185
- const childHasMatch = childResult.matches;
186
- const includeNode = !useLocalSearch || selfMatch || childHasMatch;
187
-
188
- if (!includeNode) continue;
189
-
190
- hasAnyMatch = true;
191
- const expanded = Boolean(expandedById[id]);
192
- const forceExpanded = useLocalSearch && autoExpandSearchResults && childHasMatch;
193
- walkRows.push({
194
- id,
195
- node,
196
- level,
197
- parentId,
198
- expanded,
199
- hasChildren: nodeHasChildren(node),
200
- loading: Boolean(loadingById[id]),
201
- error: errorsById[id] || undefined,
202
- matched: selfMatch
203
- });
204
-
205
- if ((expanded || forceExpanded) && childResult.rows.length > 0) {
206
- walkRows.push(...childResult.rows);
207
- }
208
- }
209
-
210
- return { rows: walkRows, matches: hasAnyMatch };
211
- };
212
-
213
- for (const root of displayRoots) {
214
- rows.push(...walk([root], 1, null).rows);
215
- }
216
- return rows;
217
- });
218
-
219
- const treeDataSnapshot = $derived.by(() => {
220
- const buildTree = (nodes: VTreeRecord[]): VTreeRecord[] =>
221
- nodes.map((node) => {
222
- const children = getNodeChildren(node);
223
- if (children.length === 0) {
224
- return node;
225
- }
226
- return {
227
- ...node,
228
- [childrenField]: buildTree(children)
229
- };
230
- });
231
-
232
- return buildTree(displayRoots);
233
- });
234
-
235
- function selectRow(index: number) {
236
- const row = flattenedRows[index];
237
- if (!row) return;
238
-
239
- focusedIndex = index;
240
- selectedId = row.id;
241
- onselect?.(row.node);
242
- viewportRef?.scrollToIndex(index);
243
- }
244
-
245
- function moveFocus(delta: number) {
246
- if (flattenedRows.length === 0) return;
247
- const next = Math.min(flattenedRows.length - 1, Math.max(0, focusedIndex + delta));
248
- selectRow(next);
249
- }
250
-
251
- function handleTreeKeydown(event: KeyboardEvent) {
252
- if (flattenedRows.length === 0) return;
253
-
254
- const current = flattenedRows[focusedIndex];
255
- if (!current) {
256
- selectRow(0);
257
- return;
258
- }
259
-
260
- if (event.key === 'ArrowDown') {
261
- event.preventDefault();
262
- moveFocus(1);
263
- return;
264
- }
265
- if (event.key === 'ArrowUp') {
266
- event.preventDefault();
267
- moveFocus(-1);
268
- return;
269
- }
270
- if (event.key === 'PageDown') {
271
- event.preventDefault();
272
- moveFocus(10);
273
- return;
274
- }
275
- if (event.key === 'PageUp') {
276
- event.preventDefault();
277
- moveFocus(-10);
278
- return;
279
- }
280
- if (event.key === 'Home') {
281
- event.preventDefault();
282
- selectRow(0);
283
- return;
284
- }
285
- if (event.key === 'End') {
286
- event.preventDefault();
287
- selectRow(flattenedRows.length - 1);
288
- return;
289
- }
290
- if (event.key === 'ArrowRight') {
291
- event.preventDefault();
292
- if (current.hasChildren && !current.expanded) {
293
- toggleNode(current.node);
294
- } else {
295
- moveFocus(1);
296
- }
297
- return;
298
- }
299
- if (event.key === 'ArrowLeft') {
300
- event.preventDefault();
301
- if (current.expanded) {
302
- toggleNode(current.node);
303
- } else if (current.parentId) {
304
- const parentIndex = flattenedRows.findIndex((row) => row.id === current.parentId);
305
- if (parentIndex >= 0) selectRow(parentIndex);
306
- }
307
- return;
308
- }
309
- if (event.key === ' ' || event.key === 'Enter') {
310
- event.preventDefault();
311
- selectRow(focusedIndex);
312
- if (current.hasChildren) {
313
- toggleNode(current.node);
314
- }
315
- }
316
- }
317
-
318
- async function runServerSearch(query: string) {
319
- if (!adapter || !serverSideSearch) return;
320
- const runId = ++searchRunId;
321
-
322
- if (!query.trim()) {
323
- searchNodes = [];
324
- return;
325
- }
326
-
327
- try {
328
- const rows = await adapter.search(query.trim());
329
- if (runId !== searchRunId) return;
330
- searchNodes = normalizeRows(rows);
331
- focusedIndex = 0;
332
- } catch (error) {
333
- if (runId !== searchRunId) return;
334
- onloaderror?.(error);
335
- }
336
- }
337
-
338
- function handleSearchInput(value: string) {
339
- searchQuery = value;
340
- onsearch?.(value);
341
- }
342
-
343
- function handleRowClick(index: number, event: MouseEvent) {
344
- const row = flattenedRows[index];
345
- if (!row) return;
346
- selectRow(index);
347
- onclick?.(row.node, treeDataSnapshot, event);
348
- }
349
-
350
- function handleRowDoubleClick(index: number, event: MouseEvent) {
351
- const row = flattenedRows[index];
352
- if (!row) return;
353
- selectRow(index);
354
- ondoubleclick?.(row.node, treeDataSnapshot, event);
355
- }
356
-
357
- function handleRowContext(index: number, event: MouseEvent) {
358
- const row = flattenedRows[index];
359
- if (!row) return;
360
- event.preventDefault();
361
- selectRow(index);
362
- oncontext?.(row.node, treeDataSnapshot, event);
363
-
364
- if (!contextMenuItems) return;
365
- const items =
366
- typeof contextMenuItems === 'function'
367
- ? contextMenuItems(row.node, treeDataSnapshot)
368
- : contextMenuItems;
369
- contextItems = items ?? [];
370
- contextRow = row.node;
371
- contextX = event.clientX;
372
- contextY = event.clientY;
373
- contextOpen = contextItems.length > 0;
374
- }
375
-
376
- function handleViewportContext(event: MouseEvent) {
377
- event.preventDefault();
378
- oncontext?.(null, treeDataSnapshot, event);
379
-
380
- if (!contextMenuItems) return;
381
- const items =
382
- typeof contextMenuItems === 'function'
383
- ? contextMenuItems(null, treeDataSnapshot)
384
- : contextMenuItems;
385
- contextItems = items ?? [];
386
- contextRow = null;
387
- contextX = event.clientX;
388
- contextY = event.clientY;
389
- contextOpen = contextItems.length > 0;
390
- }
391
-
392
- function handleContextMenuItem(item: VTreeContextMenuItem) {
393
- if (item.id !== '__close__' && contextRow) {
394
- item.onselect?.(contextRow, treeDataSnapshot);
395
- }
396
- contextOpen = false;
397
- }
398
-
399
- $effect(() => {
400
- if (!adapter) rootNodes = normalizeRows(data ?? []);
401
- });
402
-
403
- $effect(() => {
404
- if (!adapter || !serverSideSearch) return;
405
- clearTimeout(lastSearchHandle);
406
- lastSearchHandle = setTimeout(() => {
407
- void runServerSearch(searchQuery);
408
- }, 250);
409
- return () => clearTimeout(lastSearchHandle);
410
- });
411
-
412
- $effect(() => {
413
- if (focusedIndex > flattenedRows.length - 1) {
414
- focusedIndex = Math.max(flattenedRows.length - 1, 0);
415
- }
416
- if (!selectedId && flattenedRows.length > 0) {
417
- selectedId = flattenedRows[0].id;
418
- }
419
- });
420
-
421
- onMount(() => {
422
- searchQuery = searchValue ?? '';
423
-
424
- if (adapter && loadOnMount) {
425
- void loadRoot();
426
- } else {
427
- rootNodes = normalizeRows(data ?? []);
428
- }
429
-
430
- return () => clearTimeout(lastSearchHandle);
431
- });
1
+ <script lang="ts">import { onMount } from "svelte";
2
+ import VTreeSearch from "./VTreeSearch.svelte";
3
+ import VTreeVirtualViewport from "./VTreeVirtualViewport.svelte";
4
+ import VTreeContextMenu from "./VTreeContextMenu.svelte";
5
+ const { data = [], adapter, idField = "id", labelField = "label", descriptionField = "description", childrenField = "children", hasChildrenField = "hasChildren", height = 420, rowHeight = 36, indent = 18, overscan = 8, searchable = true, searchPlaceholder = "Search tree...", searchValue = "", serverSideSearch = false, autoExpandSearchResults = true, loadOnMount = true, emptyMessage = "Nothing here...", loadingMessage = "Loading...", leftSection, rightSection, contextMenuItems, onselect, onclick, ondoubleclick, oncontext, ontoggle, onsearch, onloaderror } = $props();
6
+ let viewportRef = $state(undefined);
7
+ let focusedIndex = $state(0);
8
+ let selectedId = $state(null);
9
+ let searchQuery = $state("");
10
+ let loadingRoot = $state(false);
11
+ let rootNodes = $state([]);
12
+ let searchNodes = $state([]);
13
+ let childrenByParent = $state({});
14
+ let expandedById = $state({});
15
+ let loadingById = $state({});
16
+ let loadedById = $state({});
17
+ let errorsById = $state({});
18
+ let lastSearchHandle;
19
+ let searchRunId = 0;
20
+ let contextOpen = $state(false);
21
+ let contextX = $state(0);
22
+ let contextY = $state(0);
23
+ let contextRow = $state(null);
24
+ let contextItems = $state([]);
25
+ function getNodeId(node) {
26
+ const value = node?.[idField];
27
+ return String(value ?? "");
28
+ }
29
+ function getNodeLabel(node) {
30
+ return String(node?.[labelField] ?? "");
31
+ }
32
+ function getNodeDescription(node) {
33
+ const value = node?.[descriptionField];
34
+ return value === undefined || value === null ? "" : String(value);
35
+ }
36
+ function getNodeChildren(node) {
37
+ const nodeId = getNodeId(node);
38
+ const remoteChildren = childrenByParent[nodeId];
39
+ if (remoteChildren) return remoteChildren;
40
+ const localChildren = node?.[childrenField];
41
+ return Array.isArray(localChildren) ? localChildren : [];
42
+ }
43
+ function nodeHasChildren(node) {
44
+ const children = getNodeChildren(node);
45
+ if (children.length > 0) return true;
46
+ return typeof node?.[hasChildrenField] === "boolean" ? Boolean(node[hasChildrenField]) : false;
47
+ }
48
+ function normalizeRows(rows) {
49
+ return Array.isArray(rows) ? rows : [];
50
+ }
51
+ async function loadRoot() {
52
+ if (!adapter) {
53
+ rootNodes = normalizeRows(data ?? []);
54
+ return;
55
+ }
56
+ loadingRoot = true;
57
+ try {
58
+ const rows = await adapter.readRoot();
59
+ rootNodes = normalizeRows(rows);
60
+ searchNodes = [];
61
+ errorsById = {};
62
+ } catch (error) {
63
+ onloaderror?.(error);
64
+ } finally {
65
+ loadingRoot = false;
66
+ }
67
+ }
68
+ async function loadChildren(node) {
69
+ if (!adapter) return;
70
+ const nodeId = getNodeId(node);
71
+ if (!nodeId || loadingById[nodeId] || loadedById[nodeId]) return;
72
+ loadingById = {
73
+ ...loadingById,
74
+ [nodeId]: true
75
+ };
76
+ errorsById = {
77
+ ...errorsById,
78
+ [nodeId]: ""
79
+ };
80
+ try {
81
+ const rows = await adapter.readChildren(node);
82
+ childrenByParent = {
83
+ ...childrenByParent,
84
+ [nodeId]: normalizeRows(rows)
85
+ };
86
+ loadedById = {
87
+ ...loadedById,
88
+ [nodeId]: true
89
+ };
90
+ } catch (error) {
91
+ const message = error instanceof Error ? error.message : "Unable to load node";
92
+ errorsById = {
93
+ ...errorsById,
94
+ [nodeId]: message
95
+ };
96
+ onloaderror?.(error, node);
97
+ } finally {
98
+ loadingById = {
99
+ ...loadingById,
100
+ [nodeId]: false
101
+ };
102
+ }
103
+ }
104
+ function matchesLocalSearch(node, query) {
105
+ const q = query.toLowerCase();
106
+ return getNodeLabel(node).toLowerCase().includes(q) || getNodeDescription(node).toLowerCase().includes(q);
107
+ }
108
+ function toggleNode(node) {
109
+ const nodeId = getNodeId(node);
110
+ if (!nodeId) return;
111
+ const currentlyExpanded = Boolean(expandedById[nodeId]);
112
+ expandedById = {
113
+ ...expandedById,
114
+ [nodeId]: !currentlyExpanded
115
+ };
116
+ ontoggle?.(node, !currentlyExpanded);
117
+ if (!currentlyExpanded && adapter && nodeHasChildren(node)) {
118
+ void loadChildren(node);
119
+ }
120
+ }
121
+ const displayRoots = $derived.by(() => {
122
+ if (adapter && serverSideSearch && searchQuery.trim().length > 0) return searchNodes;
123
+ return rootNodes;
124
+ });
125
+ const flattenedRows = $derived.by(() => {
126
+ const rows = [];
127
+ const activeSearch = searchQuery.trim().toLowerCase();
128
+ const useLocalSearch = activeSearch.length > 0 && !(adapter && serverSideSearch);
129
+ const walk = (nodes, level, parentId) => {
130
+ const walkRows = [];
131
+ let hasAnyMatch = false;
132
+ for (const node of nodes) {
133
+ const id = getNodeId(node);
134
+ const children = getNodeChildren(node);
135
+ const childResult = walk(children, level + 1, id);
136
+ const selfMatch = useLocalSearch ? matchesLocalSearch(node, activeSearch) : false;
137
+ const childHasMatch = childResult.matches;
138
+ const includeNode = !useLocalSearch || selfMatch || childHasMatch;
139
+ if (!includeNode) continue;
140
+ hasAnyMatch = true;
141
+ const expanded = Boolean(expandedById[id]);
142
+ const forceExpanded = useLocalSearch && autoExpandSearchResults && childHasMatch;
143
+ walkRows.push({
144
+ id,
145
+ node,
146
+ level,
147
+ parentId,
148
+ expanded,
149
+ hasChildren: nodeHasChildren(node),
150
+ loading: Boolean(loadingById[id]),
151
+ error: errorsById[id] || undefined,
152
+ matched: selfMatch
153
+ });
154
+ if ((expanded || forceExpanded) && childResult.rows.length > 0) {
155
+ walkRows.push(...childResult.rows);
156
+ }
157
+ }
158
+ return {
159
+ rows: walkRows,
160
+ matches: hasAnyMatch
161
+ };
162
+ };
163
+ for (const root of displayRoots) {
164
+ rows.push(...walk([root], 1, null).rows);
165
+ }
166
+ return rows;
167
+ });
168
+ const treeDataSnapshot = $derived.by(() => {
169
+ const buildTree = (nodes) => nodes.map((node) => {
170
+ const children = getNodeChildren(node);
171
+ if (children.length === 0) {
172
+ return node;
173
+ }
174
+ return {
175
+ ...node,
176
+ [childrenField]: buildTree(children)
177
+ };
178
+ });
179
+ return buildTree(displayRoots);
180
+ });
181
+ function selectRow(index) {
182
+ const row = flattenedRows[index];
183
+ if (!row) return;
184
+ focusedIndex = index;
185
+ selectedId = row.id;
186
+ onselect?.(row.node);
187
+ viewportRef?.scrollToIndex(index);
188
+ }
189
+ function moveFocus(delta) {
190
+ if (flattenedRows.length === 0) return;
191
+ const next = Math.min(flattenedRows.length - 1, Math.max(0, focusedIndex + delta));
192
+ selectRow(next);
193
+ }
194
+ function handleTreeKeydown(event) {
195
+ if (flattenedRows.length === 0) return;
196
+ const current = flattenedRows[focusedIndex];
197
+ if (!current) {
198
+ selectRow(0);
199
+ return;
200
+ }
201
+ if (event.key === "ArrowDown") {
202
+ event.preventDefault();
203
+ moveFocus(1);
204
+ return;
205
+ }
206
+ if (event.key === "ArrowUp") {
207
+ event.preventDefault();
208
+ moveFocus(-1);
209
+ return;
210
+ }
211
+ if (event.key === "PageDown") {
212
+ event.preventDefault();
213
+ moveFocus(10);
214
+ return;
215
+ }
216
+ if (event.key === "PageUp") {
217
+ event.preventDefault();
218
+ moveFocus(-10);
219
+ return;
220
+ }
221
+ if (event.key === "Home") {
222
+ event.preventDefault();
223
+ selectRow(0);
224
+ return;
225
+ }
226
+ if (event.key === "End") {
227
+ event.preventDefault();
228
+ selectRow(flattenedRows.length - 1);
229
+ return;
230
+ }
231
+ if (event.key === "ArrowRight") {
232
+ event.preventDefault();
233
+ if (current.hasChildren && !current.expanded) {
234
+ toggleNode(current.node);
235
+ } else {
236
+ moveFocus(1);
237
+ }
238
+ return;
239
+ }
240
+ if (event.key === "ArrowLeft") {
241
+ event.preventDefault();
242
+ if (current.expanded) {
243
+ toggleNode(current.node);
244
+ } else if (current.parentId) {
245
+ const parentIndex = flattenedRows.findIndex((row) => row.id === current.parentId);
246
+ if (parentIndex >= 0) selectRow(parentIndex);
247
+ }
248
+ return;
249
+ }
250
+ if (event.key === " " || event.key === "Enter") {
251
+ event.preventDefault();
252
+ selectRow(focusedIndex);
253
+ if (current.hasChildren) {
254
+ toggleNode(current.node);
255
+ }
256
+ }
257
+ }
258
+ async function runServerSearch(query) {
259
+ if (!adapter || !serverSideSearch) return;
260
+ const runId = ++searchRunId;
261
+ if (!query.trim()) {
262
+ searchNodes = [];
263
+ return;
264
+ }
265
+ try {
266
+ const rows = await adapter.search(query.trim());
267
+ if (runId !== searchRunId) return;
268
+ searchNodes = normalizeRows(rows);
269
+ focusedIndex = 0;
270
+ } catch (error) {
271
+ if (runId !== searchRunId) return;
272
+ onloaderror?.(error);
273
+ }
274
+ }
275
+ function handleSearchInput(value) {
276
+ searchQuery = value;
277
+ onsearch?.(value);
278
+ }
279
+ function handleRowClick(index, event) {
280
+ const row = flattenedRows[index];
281
+ if (!row) return;
282
+ selectRow(index);
283
+ onclick?.(row.node, treeDataSnapshot, event);
284
+ }
285
+ function handleRowDoubleClick(index, event) {
286
+ const row = flattenedRows[index];
287
+ if (!row) return;
288
+ selectRow(index);
289
+ ondoubleclick?.(row.node, treeDataSnapshot, event);
290
+ }
291
+ function handleRowContext(index, event) {
292
+ const row = flattenedRows[index];
293
+ if (!row) return;
294
+ event.preventDefault();
295
+ selectRow(index);
296
+ oncontext?.(row.node, treeDataSnapshot, event);
297
+ if (!contextMenuItems) return;
298
+ const items = typeof contextMenuItems === "function" ? contextMenuItems(row.node, treeDataSnapshot) : contextMenuItems;
299
+ contextItems = items ?? [];
300
+ contextRow = row.node;
301
+ contextX = event.clientX;
302
+ contextY = event.clientY;
303
+ contextOpen = contextItems.length > 0;
304
+ }
305
+ function handleViewportContext(event) {
306
+ event.preventDefault();
307
+ oncontext?.(null, treeDataSnapshot, event);
308
+ if (!contextMenuItems) return;
309
+ const items = typeof contextMenuItems === "function" ? contextMenuItems(null, treeDataSnapshot) : contextMenuItems;
310
+ contextItems = items ?? [];
311
+ contextRow = null;
312
+ contextX = event.clientX;
313
+ contextY = event.clientY;
314
+ contextOpen = contextItems.length > 0;
315
+ }
316
+ function handleContextMenuItem(item) {
317
+ if (item.id !== "__close__" && contextRow) {
318
+ item.onselect?.(contextRow, treeDataSnapshot);
319
+ }
320
+ contextOpen = false;
321
+ }
322
+ $effect(() => {
323
+ if (!adapter) rootNodes = normalizeRows(data ?? []);
324
+ });
325
+ $effect(() => {
326
+ if (!adapter || !serverSideSearch) return;
327
+ clearTimeout(lastSearchHandle);
328
+ lastSearchHandle = setTimeout(() => {
329
+ void runServerSearch(searchQuery);
330
+ }, 250);
331
+ return () => clearTimeout(lastSearchHandle);
332
+ });
333
+ $effect(() => {
334
+ if (focusedIndex > flattenedRows.length - 1) {
335
+ focusedIndex = Math.max(flattenedRows.length - 1, 0);
336
+ }
337
+ if (!selectedId && flattenedRows.length > 0) {
338
+ selectedId = flattenedRows[0].id;
339
+ }
340
+ });
341
+ onMount(() => {
342
+ searchQuery = searchValue ?? "";
343
+ if (adapter && loadOnMount) {
344
+ void loadRoot();
345
+ } else {
346
+ rootNodes = normalizeRows(data ?? []);
347
+ }
348
+ return () => clearTimeout(lastSearchHandle);
349
+ });
432
350
  </script>
433
351
 
434
352
  <div class="card space-y-2 border border-surface-300-700 bg-surface-100-900 p-2">