@keenmate/svelte-treeview 5.0.0-rc09 → 5.0.0-rc11

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 (53) hide show
  1. package/CHANGELOG.md +931 -866
  2. package/README.md +362 -1100
  3. package/ai/INDEX.txt +349 -349
  4. package/ai/advanced-patterns.txt +1 -1
  5. package/ai/drag-drop.txt +397 -397
  6. package/ai/performance.txt +349 -349
  7. package/ai/tree-editing.txt +1 -1
  8. package/component-variables.manifest.json +143 -142
  9. package/dist/components/ContextMenuDivider.svelte +2 -2
  10. package/dist/components/ContextMenuItem.svelte +58 -10
  11. package/dist/components/ContextMenuLevel.svelte +131 -0
  12. package/dist/components/ContextMenuLevel.svelte.d.ts +32 -0
  13. package/dist/components/Node.svelte +491 -486
  14. package/dist/components/Node.svelte.d.ts +1 -1
  15. package/dist/components/Tree.svelte +1323 -1276
  16. package/dist/components/Tree.svelte.d.ts +77 -69
  17. package/dist/components/TreeProvider.svelte +1 -0
  18. package/dist/constants.generated.d.ts +1 -1
  19. package/dist/constants.generated.js +1 -1
  20. package/dist/core/TreeController.svelte.d.ts +84 -53
  21. package/dist/core/TreeController.svelte.js +540 -126
  22. package/dist/index.d.ts +1 -1
  23. package/dist/ltree/ltree-node.svelte.js +2 -2
  24. package/dist/ltree/ltree.svelte.d.ts +1 -1
  25. package/dist/ltree/ltree.svelte.js +94 -9
  26. package/dist/ltree/types.d.ts +10 -0
  27. package/dist/styles/animations.css +17 -0
  28. package/dist/styles/{_base.css → base.css} +14 -11
  29. package/dist/styles/checkbox.css +96 -0
  30. package/dist/styles/context-menu.css +117 -0
  31. package/dist/styles/controls.css +4 -0
  32. package/dist/styles/dark-mode.css +44 -0
  33. package/dist/styles/debug.css +45 -0
  34. package/dist/styles/drag-drop.css +170 -0
  35. package/dist/styles/drop-zones.css +270 -0
  36. package/dist/styles/floating.css +5 -0
  37. package/dist/styles/loading.css +36 -0
  38. package/dist/styles/main.css +38 -31
  39. package/dist/styles/node.css +63 -0
  40. package/dist/styles/states.css +123 -0
  41. package/dist/styles/{_toggle-icons.css → toggle-icons.css} +29 -29
  42. package/dist/styles/variables.css +217 -0
  43. package/dist/styles.css +887 -869
  44. package/package.json +9 -3
  45. package/dist/styles/_checkbox.css +0 -83
  46. package/dist/styles/_context-menu.css +0 -134
  47. package/dist/styles/_debug.css +0 -45
  48. package/dist/styles/_drag-drop.css +0 -174
  49. package/dist/styles/_drop-zones.css +0 -270
  50. package/dist/styles/_loading.css +0 -40
  51. package/dist/styles/_node.css +0 -60
  52. package/dist/styles/_states.css +0 -92
  53. package/dist/styles/_variables.css +0 -189
package/README.md CHANGED
@@ -1,1100 +1,362 @@
1
- # @keenmate/svelte-treeview
2
-
3
- A high-performance, feature-rich hierarchical tree view component for Svelte 5 with drag & drop support, search functionality, and flexible data structures using LTree.
4
-
5
- ## Live Demo
6
-
7
- Browse interactive code examples and the full API reference at **[svelte-treeview.keenmate.dev](https://svelte-treeview.keenmate.dev)**
8
-
9
- ## What's New in v5.0.0-rc09
10
-
11
- - **Three-level selection model**: New `selectionMode: 'single' | 'multi'` prop (default `'single'`) cleanly separates the **focused** node (cursor), the **highlighted** set (Ctrl/Shift+click range), and **selected** checkboxes. When `showCheckboxes` is off, highlight mirrors into `selectedPaths` automatically — so consumers always have a single "what's selected" set to read regardless of UI style. Programmatic `highlightNode` / `highlightNodes` / `clearHighlight` respect `{ silent: true }`.
12
- - **Multi-drag the whole highlight set**: With `selectionMode='multi'`, Ctrl/Shift+click several rows and drag any one of them — the entire top-level-selected subset moves together. Descendants of highlighted ancestors ride along inside their subtree (not extracted). The set lands as siblings after/before/under `dropNode` in source order.
13
- - **OS-explorer drag-start sync**: Grabbing a non-highlighted node now replaces the highlight with just that node before the drag begins, matching Windows Explorer / macOS Finder. Previously the prior highlight stayed visually stuck while the drag silently carried only the grabbed node.
14
- - **Keyboard convention reshuffle**: **Enter** now toggles highlight on the focused node (in `multi` mode); **Space** is the universal expand/collapse toggle (when no checkbox). Ctrl/Shift+click no longer auto-toggles expand/collapse — modifier clicks are reserved for highlight management so multi-selecting folders doesn't open them.
15
- - **`clickTogglesCheckbox` prop**: Opt-in flag that makes a plain row click toggle the checkbox instead of focusing/highlighting, for checkbox-first UIs. Modified clicks still build the multi-highlight.
16
- - **Full CSS-variable theming surface**: ~50 new `--ltree-*` variables (typography, layout, toggle/checkbox/selection/drag-zone/context-menu state) on a `--base-*` token chain shared with other `@keenmate/*` web components. Set `--ltree-primary` once and the hover tint, drag-zone background, focus ring, and danger menu hover all derive from it via `color-mix()`. New `--ltree-rem` scales the whole component proportionally. Lucide SVG toggle icons replace the UTF-8 glyphs.
17
- - **SCSS pure CSS**: `main.scss` (1095 lines) split into eleven `_*.css` partials bundled by `lightningcss-cli` into `dist/styles.css`. `sass` removed from devDependencies. `./styles.scss` package export removed — switch to `./styles.css`.
18
-
19
- > **Breaking changes** in this release include the `selectionMode='single'` default (Ctrl/Shift+click no longer auto-multi-selects without opt-in), `selectedPaths` becoming populated in no-checkbox trees (mirrored from highlight), removal of `lastHighlightedPath` / `isHighlightAnchor` / `--ltree-*-rgb` / SCSS variable overrides, and `--ltree-node-indent-per-level` now scaling with `--ltree-rem` instead of document `rem`. See CHANGELOG for migration notes.
20
-
21
- ## What's New in v5.0.0-rc08
22
-
23
- - **`{ silent: true }` on highlight/selection methods**: Update tree state from URL params or other external sources without firing `onNodeClick` / `onHighlightChange` / `onSelectionChange` — perfect for deep links where you don't want the change callback to re-trigger your form loader. Applies to `highlightNode`, `highlightNodes`, `clearHighlight`, and `deselectAll`.
24
- - **Array variants for expand/collapse**: `expandNodes`, `collapseNodes`, `expandAll`, and `collapseAll` now take `string | string[]`. Open or close several places in one call.
25
- - **Exclusive focus mode**: Pass `{ exclusive: true }` to `expandNodes` / `expandAll` to open the target path and collapse everything else in a single pass — no two-step flicker compared to `collapseAll() + expandNodes(path)`.
26
- - **`noEmit` option for batching**: Suppress the change emit on individual expand/collapse calls when chaining several operations; emit once at the end.
27
-
28
- ## v5.0: Core/Renderer Split + Virtual Scroll
29
-
30
- > [!IMPORTANT]
31
- > **In version 5, the tree core (data structure, expand/collapse, search, drag & drop logic) has been completely separated from the renderer.** The architecture is open for you to build your own custom renderers on top of the same core via `TreeProvider` and `TreeController`.
32
-
33
- **Key changes in v5:**
34
- - **Core/Renderer split**: Use the built-in HTML `Tree` renderer, or create custom visualizations (Canvas, WebGL, SVG) via `TreeProvider` + `TreeController`
35
- - **Virtual scroll**: Render 50,000+ node trees smoothly with `virtualScroll={true}` — only ~50 DOM nodes at any time
36
- - **Canvas companion**: For canvas rendering, install [`@keenmate/svelte-treeview-canvas`](https://github.com/keenmate/svelte-treeview-canvas)
37
- - **Drop position naming**: `'above'`/`'below'` renamed to `'before'`/`'after'` (CSS classes and events updated accordingly)
38
-
39
- ### Rendering Modes
40
-
41
- | Mode | Props | DOM Nodes | Best For |
42
- |------|-------|-----------|----------|
43
- | Recursive | `useFlatRendering={false}` | All | Small trees (<100 nodes) |
44
- | Flat (default) | `useFlatRendering={true}` | All | Medium trees (100–10K) |
45
- | Virtual | `virtualScroll={true}` | ~50 | Large trees (10K+) |
46
-
47
- ```svelte
48
- <!-- Virtual scroll for large trees -->
49
- <Tree {data} virtualScroll={true} virtualContainerHeight="500px" />
50
-
51
- <!-- Flat mode (default) with progressive batching -->
52
- <Tree {data} progressiveRender={true} initialBatchSize={20} maxBatchSize={500} />
53
- ```
54
-
55
- ## Features
56
-
57
- - **Svelte 5 Native**: Built specifically for Svelte 5 with full support for runes and modern Svelte patterns
58
- - **High Performance**: Flat rendering with progressive loading, virtual scroll for 50,000+ nodes
59
- - **Drag & Drop**: Built-in drag and drop with position control (before/after/child), touch support, and async validation
60
- - **Tree Editing**: Built-in methods for add, move, remove operations with automatic path management
61
- - **Search & Filter**: Integrated FlexSearch for fast, full-text search capabilities
62
- - **Flexible Data Sources**: Works with any hierarchical data structure
63
- - **Multi-Select**: Ctrl+click toggle, Shift+click range select (visual or logical mode), `selectedPaths` bindable, selection-aware context menus
64
- - **Context Menus**: Dynamic right-click menus with shortcuts, submenus, named dividers, and two API approaches (callback or Svelte components)
65
- - **Visual Customization**: Extensive styling options and icon customization
66
- - **TypeScript Support**: Full TypeScript support with comprehensive type definitions
67
- - **Accessibility**: Built with accessibility in mind
68
-
69
- ## Installation
70
-
71
- ```bash
72
- npm install @keenmate/svelte-treeview
73
- ```
74
-
75
- ### Importing Styles
76
-
77
- The component requires CSS to display correctly. Import the styles in your app:
78
-
79
- **JavaScript import** (in your main.js/main.ts or Vite/Webpack entry):
80
- ```javascript
81
- import '@keenmate/svelte-treeview/styles.css';
82
- ```
83
-
84
- **Svelte component import:**
85
- ```svelte
86
- <style>
87
- @import '@keenmate/svelte-treeview/styles.css';
88
- </style>
89
- ```
90
-
91
- ## Quick Start
92
-
93
- ```svelte
94
- <script lang="ts">
95
- import { Tree } from '@keenmate/svelte-treeview';
96
-
97
- const data = [
98
- { path: '1', name: 'Documents', type: 'folder' },
99
- { path: '1.1', name: 'Projects', type: 'folder' },
100
- { path: '1.1.1', name: 'Project A', type: 'folder' },
101
- { path: '1.1.2', name: 'Project B', type: 'folder' },
102
- { path: '2', name: 'Pictures', type: 'folder' },
103
- { path: '2.1', name: 'Vacation', type: 'folder' }
104
- ];
105
- </script>
106
-
107
- <Tree
108
- {data}
109
- idMember="path"
110
- pathMember="path"
111
- displayValueMember="name"
112
- />
113
- ```
114
-
115
- > [!TIP]
116
- > **Performance tip:** When passing large arrays (1000+ items) to the Tree component, use `$state.raw()` instead of `$state()` to avoid severe performance issues. Svelte 5's `$state()` creates deep proxies — with thousands of items this causes up to 5,000x slowdown. The array itself remains reactive; only individual items lose deep reactivity (which Tree doesn't need).
117
- > ```typescript
118
- > // BAD - Each item becomes a Proxy
119
- > let treeData = $state<TreeNode[]>([])
120
- >
121
- > // GOOD - Items remain plain objects
122
- > let treeData = $state.raw<TreeNode[]>([])
123
- > ```
124
-
125
- ## Advanced Usage
126
-
127
- ### With Custom Node Templates
128
-
129
- ```svelte
130
- <script lang="ts">
131
- import { Tree } from '@keenmate/svelte-treeview';
132
-
133
- const fileData = [
134
- { path: '1', name: 'Documents', type: 'folder', icon: '📁' },
135
- { path: '1.1', name: 'report.pdf', type: 'file', icon: '📄', size: '2.3 MB' },
136
- { path: '2', name: 'Images', type: 'folder', icon: '🖼️' },
137
- { path: '2.1', name: 'photo.jpg', type: 'file', icon: '🖼️', size: '1.8 MB' }
138
- ];
139
- </script>
140
-
141
- <Tree
142
- data={fileData}
143
- idMember="path"
144
- pathMember="path"
145
- selectedNodeClass="ltree-selected-bold"
146
- onNodeClicked={(node) => console.log('Clicked:', node.data?.name)}
147
- >
148
- {#snippet nodeTemplate(node)}
149
- <div class="d-flex align-items-center">
150
- <span class="me-2">{node.data?.icon}</span>
151
- <strong>{node.data?.name}</strong>
152
- {#if node.data?.size}
153
- <small class="text-muted ms-2">({node.data?.size})</small>
154
- {/if}
155
- </div>
156
- {/snippet}
157
- </Tree>
158
- ```
159
-
160
- ### With Search and Filtering
161
-
162
- ```svelte
163
- <script lang="ts">
164
- import { Tree } from '@keenmate/svelte-treeview';
165
-
166
- let searchText = $state('');
167
- const data = [/* your data */];
168
- </script>
169
-
170
- <input
171
- type="text"
172
- placeholder="Search..."
173
- bind:value={searchText}
174
- />
175
-
176
- <Tree
177
- {data}
178
- idMember="path"
179
- pathMember="path"
180
- shouldUseInternalSearchIndex={true}
181
- searchValueMember="name"
182
- bind:searchText
183
- />
184
- ```
185
-
186
- ### With Advanced Search Options
187
-
188
- ```svelte
189
- <script lang="ts">
190
- import { Tree } from '@keenmate/svelte-treeview';
191
- import type { SearchOptions } from 'flexsearch';
192
-
193
- let treeRef;
194
- const data = [/* your data */];
195
-
196
- // Programmatic search with FlexSearch options
197
- function performAdvancedSearch(searchTerm: string) {
198
- const searchOptions: SearchOptions = {
199
- suggest: true, // Enable suggestions for typos
200
- limit: 10, // Limit results to 10 items
201
- bool: "and" // Use AND logic for multiple terms
202
- };
203
-
204
- const results = treeRef.searchNodes(searchTerm, searchOptions);
205
- console.log('Advanced search results:', results);
206
- }
207
-
208
- // Programmatic filtering with options
209
- function filterWithOptions(searchTerm: string) {
210
- const searchOptions: SearchOptions = {
211
- threshold: 0.8, // Similarity threshold
212
- depth: 2 // Search depth
213
- };
214
-
215
- treeRef.filterNodes(searchTerm, searchOptions);
216
- }
217
- </script>
218
-
219
- <Tree
220
- bind:this={treeRef}
221
- {data}
222
- idMember="path"
223
- pathMember="path"
224
- shouldUseInternalSearchIndex={true}
225
- searchValueMember="name"
226
- />
227
-
228
- <button onclick={() => performAdvancedSearch('document')}>
229
- Advanced Search
230
- </button>
231
- <button onclick={() => filterWithOptions('project')}>
232
- Filter with Options
233
- </button>
234
- ```
235
-
236
- #### FlexSearch Options Reference
237
-
238
- The `searchOptions` parameter accepts any options supported by FlexSearch. Common options include:
239
-
240
- | Option | Type | Description | Example |
241
- |--------|------|-------------|---------|
242
- | `suggest` | `boolean` | Enable suggestions for typos | `{ suggest: true }` |
243
- | `limit` | `number` | Maximum number of results | `{ limit: 10 }` |
244
- | `threshold` | `number` | Similarity threshold (0-1) | `{ threshold: 0.8 }` |
245
- | `depth` | `number` | Search depth for nested content | `{ depth: 2 }` |
246
- | `bool` | `string` | Boolean logic: "and", "or" | `{ bool: "and" }` |
247
- | `where` | `object` | Filter by field values | `{ where: { type: "folder" } }` |
248
-
249
- For complete FlexSearch documentation, visit: [FlexSearch Options](https://github.com/nextapps-de/flexsearch#options)
250
-
251
- ### With Drag & Drop
252
-
253
- **Note:** Drag and drop is disabled by default. Set `dragDropMode` to enable it.
254
-
255
- ```svelte
256
- <script lang="ts">
257
- import { Tree } from '@keenmate/svelte-treeview';
258
-
259
- let treeRef: Tree<MyNode>;
260
-
261
- const data = [
262
- { path: '1', name: 'Folder 1' },
263
- { path: '1.1', name: 'Item 1' },
264
- { path: '2', name: 'Folder 2' }
265
- ];
266
-
267
- function onDragStart(node, event) {
268
- console.log('Dragging:', node.data?.name);
269
- }
270
-
271
- // Same-tree moves are auto-handled - this callback is for notification/custom logic
272
- function onDrop(dropNode, draggedNode, position, event, operation) {
273
- console.log(`Dropped ${draggedNode.data?.name} ${position} ${dropNode?.data?.name}`);
274
- // position is 'before', 'after', or 'child'
275
- // operation is 'move' or 'copy' (Ctrl+drag)
276
- }
277
- </script>
278
-
279
- <Tree
280
- bind:this={treeRef}
281
- {data}
282
- idMember="path"
283
- pathMember="path"
284
- dragDropMode="both"
285
- orderMember="sortOrder"
286
- dragOverNodeClass="ltree-dragover-highlight"
287
- onNodeDragStart={onDragStart}
288
- onNodeDrop={onDrop}
289
- />
290
- ```
291
-
292
- #### Drop Position Control
293
-
294
- When using `dropZoneMode="floating"`, users can choose where to drop:
295
- - **Before**: Insert as sibling before the target node
296
- - **After**: Insert as sibling after the target node
297
- - **Child**: Insert as child of the target node
298
-
299
- #### Per-Node Drop Position Restrictions
300
-
301
- You can restrict which drop positions are allowed per node. This is useful for:
302
- - **Trash/Recycle Bin**: Only allow dropping INTO (child), not before/after
303
- - **Files**: Only allow before/after (can't drop INTO a file)
304
- - **Folders**: Allow all positions (default)
305
-
306
- ```svelte
307
- <script lang="ts">
308
- import { Tree, type DropPosition, type LTreeNode } from '@keenmate/svelte-treeview';
309
-
310
- // Dynamic callback approach
311
- function getAllowedDropPositions(node: LTreeNode<MyItem>): DropPosition[] | null {
312
- if (node.data?.type === 'file') return ['before', 'after'];
313
- if (node.data?.type === 'trash') return ['child'];
314
- return undefined; // all positions allowed
315
- }
316
- </script>
317
-
318
- <Tree
319
- {data}
320
- getAllowedDropPositionsCallback={getAllowedDropPositions}
321
- />
322
- ```
323
-
324
- Or use the member approach for server-side data:
325
- ```svelte
326
- <Tree
327
- {data}
328
- allowedDropPositionsMember="allowedDropPositions"
329
- />
330
-
331
- <!-- Where data items have: { allowedDropPositions: ['child'] } -->
332
- ```
333
-
334
- When restrictions are applied:
335
- - **Glow mode**: Snaps to the nearest allowed position
336
- - **Floating mode**: Only renders buttons for allowed positions
337
-
338
- #### Async Drop Validation
339
-
340
- Use `beforeDropCallback` to validate or modify drops, including async operations like confirmation dialogs:
341
-
342
- ```svelte
343
- <script lang="ts">
344
- async function beforeDrop(dropNode, draggedNode, position, event, operation) {
345
- // Cancel specific drops
346
- if (draggedNode.data.locked) {
347
- return false; // Cancel the drop
348
- }
349
-
350
- // Show confirmation dialog (async)
351
- if (position === 'child' && !dropNode.data.isFolder) {
352
- const confirmed = await showConfirmDialog('Drop as sibling instead?');
353
- if (!confirmed) return false;
354
- return { position: 'after' }; // Override position
355
- }
356
-
357
- // Proceed normally
358
- return true;
359
- }
360
- </script>
361
-
362
- <Tree
363
- {data}
364
- beforeDropCallback={beforeDrop}
365
- onNodeDrop={onDrop}
366
- />
367
- ```
368
-
369
- ### Tree Editing
370
-
371
- The tree provides built-in methods for programmatic editing:
372
-
373
- ```svelte
374
- <script lang="ts">
375
- import { Tree } from '@keenmate/svelte-treeview';
376
-
377
- let treeRef: Tree<MyNode>;
378
-
379
- // Add a new node
380
- function addChild() {
381
- const result = treeRef.addNode(
382
- selectedNode?.path || '', // parent path (empty = root)
383
- { id: Date.now(), path: '', name: 'New Item', sortOrder: 100 }
384
- );
385
- if (result.success) {
386
- console.log('Added:', result.node);
387
- }
388
- }
389
-
390
- // Move a node
391
- function moveUp() {
392
- const siblings = treeRef.getSiblings(selectedNode.path);
393
- const index = siblings.findIndex(s => s.path === selectedNode.path);
394
- if (index > 0) {
395
- treeRef.moveNode(selectedNode.path, siblings[index - 1].path, 'before');
396
- }
397
- }
398
-
399
- // Remove a node
400
- function remove() {
401
- treeRef.removeNode(selectedNode.path);
402
- }
403
- </script>
404
-
405
- <Tree
406
- bind:this={treeRef}
407
- {data}
408
- idMember="id"
409
- pathMember="path"
410
- orderMember="sortOrder"
411
- />
412
- ```
413
-
414
- **Note**: When using `orderMember`, the tree automatically calculates sort order values when moving nodes with 'before' or 'after' positions.
415
-
416
- ### With Context Menus
417
-
418
- The tree supports context menus with two approaches: **callback-based** (imperative, shared with web components) and **snippet + component** (declarative, Svelte-only).
419
-
420
- #### Callback-Based Context Menus
421
-
422
- ```svelte
423
- <script lang="ts">
424
- import { Tree } from '@keenmate/svelte-treeview';
425
- import type { ContextMenuEntry } from '@keenmate/svelte-treeview';
426
-
427
- const data = [
428
- { path: '1', name: 'Documents', type: 'folder', canEdit: true, canDelete: true },
429
- { path: '1.1', name: 'report.pdf', type: 'file', canEdit: true, canDelete: false },
430
- { path: '2', name: 'Images', type: 'folder', canEdit: false, canDelete: true }
431
- ];
432
-
433
- function createContextMenu(node, close: () => void): ContextMenuEntry[] {
434
- return [
435
- { label: 'Open', icon: '📂', shortcut: 'O',
436
- onclick: () => { alert(`Opening ${node.data?.name}`); close(); } },
437
- { label: 'Edit', icon: '✏️', shortcut: 'E', isVisible: node.data?.canEdit,
438
- onclick: () => { alert(`Editing ${node.data?.name}`); close(); } },
439
- { label: 'Export As...', icon: '📤', children: [
440
- { label: 'JSON', shortcut: 'J', onclick: () => { exportAs(node, 'json'); close(); } },
441
- { label: 'XML', shortcut: 'X', onclick: () => { exportAs(node, 'xml'); close(); } },
442
- ]},
443
- { divider: true, label: 'Danger zone' },
444
- { label: 'Delete', icon: '🗑️', className: 'danger',
445
- isDisabled: !node.data?.canDelete,
446
- onclick: () => { confirm(`Delete?`) && alert('Deleted!'); close(); } },
447
- ];
448
- }
449
- </script>
450
-
451
- <Tree
452
- {data}
453
- idMember="path"
454
- pathMember="path"
455
- contextMenuCallback={createContextMenu}
456
- />
457
- ```
458
-
459
- #### Snippet + Component Context Menus
460
-
461
- ```svelte
462
- <script lang="ts">
463
- import { Tree, ContextMenuItemC, ContextMenuDividerC } from '@keenmate/svelte-treeview';
464
- </script>
465
-
466
- <Tree {data} idMember="path" pathMember="path">
467
- {#snippet contextMenu(node, close)}
468
- <ContextMenuItemC label="Copy" icon="📋" shortcut="C"
469
- onclick={() => { copy(node); close(); }} />
470
- {#if node.data?.type === 'folder'}
471
- <ContextMenuItemC label="Export As..." icon="📤">
472
- <ContextMenuItemC label="JSON" shortcut="J"
473
- onclick={() => { exportAs(node, 'json'); close(); }} />
474
- <ContextMenuItemC label="XML" shortcut="X"
475
- onclick={() => { exportAs(node, 'xml'); close(); }} />
476
- </ContextMenuItemC>
477
- {/if}
478
- <ContextMenuDividerC label="Danger zone" />
479
- <ContextMenuItemC label="Delete" icon="🗑️" className="danger"
480
- isDisabled={!!node.data?.readonly}
481
- onclick={() => { del(node); close(); }} />
482
- {/snippet}
483
- </Tree>
484
- ```
485
-
486
- #### Context Menu Features
487
-
488
- - **Unified types**: `ContextMenuItem`, `ContextMenuDivider`, `ContextMenuEntry` shared across svelte-treeview and canvas-tree
489
- - **Keyboard shortcuts**: `shortcut` field renders a hint and activates on keypress when menu is open (supports `Ctrl+`, `Shift+`, `Alt+` modifiers)
490
- - **Submenus**: `children` array opens nested menus on hover
491
- - **Named dividers**: `{ divider: true, label: 'Section' }` renders as `---- Section ----`
492
- - **Visibility control**: `isVisible: false` hides items (callback approach); snippet approach uses `{#if}`
493
- - **Flexible styling**: `className="danger"` or any custom CSS class
494
- - **Dynamic menus**: Generate items based on node properties
495
- - **Icons and disabled states**: Visual organization and context-sensitive availability
496
- - **Position offset**: `contextMenuXOffset`/`contextMenuYOffset` for cursor clearance
497
- - **Auto-close**: Closes on scroll, click outside, Escape key, or programmatically
498
-
499
- ## Styling and Customization
500
-
501
- The component comes with default styles that provide a clean, modern look. You can customize it extensively:
502
-
503
- ### CSS Variables
504
-
505
- The component uses CSS custom properties for easy theming:
506
-
507
- ```css
508
- :root {
509
- --ltree-rem: 10px; /* Base sizing unit — scale all dimensions */
510
- --ltree-node-indent-per-level: calc(0.8 * var(--ltree-rem)); /* Indent per nesting level */
511
- --ltree-primary: #0d6efd; /* Tints (multi-select, dragover) derived
512
- automatically via color-mix() */
513
- --ltree-success: #198754;
514
- --ltree-danger: #dc3545;
515
- --ltree-light: #f8f9fa;
516
- --ltree-border: #dee2e6;
517
- --ltree-body-color: #212529;
518
- }
519
- ```
520
-
521
- **Note**: All dimensions are `calc(N × var(--ltree-rem))`. Set `--ltree-rem` once (default `10px`) to scale every size proportionally, or set it to `1rem` to make the component follow document font-size.
522
-
523
- ### Scaling and Theme Integration
524
-
525
- ```css
526
- /* Scale everything 20% larger */
527
- .my-bigger-tree {
528
- --ltree-rem: 12px;
529
- }
530
-
531
- /* Shared theme tokens — picked up by every @keenmate/* component */
532
- :root {
533
- --base-accent-color: #6366f1;
534
- --base-font-family: 'Inter', system-ui, sans-serif;
535
- --base-border-radius-sm: 0.4; /* unitless multiplier (× rem) */
536
- }
537
- ```
538
-
539
- See `/examples/theming` for the full CSS variable reference and a live demo.
540
-
541
- ### CSS Classes
542
-
543
- - `.ltree-tree` - Main tree container
544
- - `.ltree-node` - Individual node container
545
- - `.ltree-node-content` - Node content area
546
- - `.ltree-toggle-icon` - Expand/collapse icons
547
- - `.ltree-selected-*` - Selected node styles
548
- - `.ltree-dragover-*` - Drag-over node styles
549
- - `.ltree-draggable` - Draggable nodes
550
- - `.ltree-context-menu` - Context menu styling
551
- - `.ltree-drag-over` - Applied during drag operations
552
- - `.ltree-drop-valid` / `.ltree-drop-invalid` - Drop target validation
553
-
554
- ### Pre-built Selected Node Styles
555
-
556
- The component includes several pre-built classes for styling selected nodes:
557
-
558
- ```svelte
559
- <Tree
560
- {data}
561
- idMember="path"
562
- pathMember="path"
563
- selectedNodeClass="ltree-selected-bold"
564
- />
565
- ```
566
-
567
- **Available Selected Node Classes:**
568
-
569
- | Class | Description | Visual Effect |
570
- |-------|-------------|---------------|
571
- | `ltree-selected-bold` | Bold text with primary color | **Bold text** in theme primary color |
572
- | `ltree-selected-border` | Border and background highlight | Solid border with light background |
573
- | `ltree-selected-brackets` | Decorative brackets around text | > **Node Text** < |
574
-
575
- **Available Drag-over Node Classes:**
576
-
577
- | Class | Description | Visual Effect |
578
- |-------|-------------|---------------|
579
- | `ltree-dragover-highlight` | Dashed border with success color background | Green dashed border with subtle background |
580
- | `ltree-dragover-glow` | Blue glow effect | Glowing shadow effect with primary color theme |
581
-
582
- ### Custom Icon Classes
583
-
584
- ```svelte
585
- <Tree
586
- {data}
587
- idMember="path"
588
- pathMember="path"
589
- expandIconClass="custom-expand-icon"
590
- collapseIconClass="custom-collapse-icon"
591
- leafIconClass="custom-leaf-icon"
592
- />
593
- ```
594
-
595
- ## API Reference
596
-
597
- ### Tree Component Props
598
-
599
- #### Core Properties
600
- | Prop | Type | Default | Description |
601
- |------|------|---------|-------------|
602
- | `data` | `T[]` | **required** | Array of data objects |
603
- | `idMember` | `string` | **required** | Property name for unique identifiers |
604
- | `pathMember` | `string` | **required** | Property name for hierarchical paths |
605
- | `sortCallback` | `(items: LTreeNode<T>[]) => LTreeNode<T>[]` | `undefined` | Function to sort tree nodes |
606
-
607
- #### Data Mapping Properties
608
- | Prop | Type | Default | Description |
609
- |------|------|---------|-------------|
610
- | `parentPathMember` | `string \| null` | `null` | Property name for parent path references |
611
- | `levelMember` | `string \| null` | `null` | Property name for node level |
612
- | `isExpandedMember` | `string \| null` | `null` | Property name for expanded state |
613
- | `isSelectedMember` | `string \| null` | `null` | Property name for selected state |
614
- | `isDraggableMember` | `string \| null` | `null` | Property name for draggable state |
615
- | `isDropAllowedMember` | `string \| null` | `null` | Property name for drop allowed state |
616
- | `allowedDropPositionsMember` | `string \| null` | `null` | Property name for allowed drop positions array |
617
- | `isCollapsibleMember` | `string \| null` | `null` | Property name for collapsible state |
618
- | `getIsCollapsibleCallback` | `(node) => boolean` | `undefined` | Callback to determine if a node is collapsible |
619
- | `getIsDraggableCallback` | `(node) => boolean` | `undefined` | Callback to determine if a node is draggable |
620
- | `hasChildrenMember` | `string \| null` | `null` | Property name for children existence |
621
- | `isSorted` | `boolean \| null` | `null` | Whether items should be sorted |
622
-
623
- #### Display & Search Properties
624
- | Prop | Type | Default | Description |
625
- |------|------|---------|-------------|
626
- | `displayValueMember` | `string \| null` | `null` | Property name for display text |
627
- | `getDisplayValueCallback` | `(node) => string` | `undefined` | Function to get display value |
628
- | `searchValueMember` | `string \| null` | `null` | Property name for search indexing |
629
- | `getSearchValueCallback` | `(node) => string` | `undefined` | Function to get search value |
630
- | `shouldUseInternalSearchIndex` | `boolean` | `true` | Enable built-in search functionality |
631
- | `initializeIndexCallback` | `() => Index` | `undefined` | Function to initialize search index |
632
- | `searchText` | `string` (bindable) | `undefined` | Current search text |
633
-
634
- **Note**: When `shouldUseInternalSearchIndex` is enabled, node indexing is performed asynchronously using `requestIdleCallback` (with fallback to `setTimeout`). This ensures the tree renders immediately while search indexing happens during browser idle time, providing better performance for large datasets.
635
-
636
- **Important**: For internal search indexing to work, you must:
637
- 1. Set `shouldUseInternalSearchIndex={true}`
638
- 2. Provide either `searchValueMember` (property name) or `getSearchValueCallback` (function)
639
-
640
- Without both requirements, no search indexing will occur.
641
-
642
- **Performance Tuning**:
643
- - `indexerBatchSize` controls how many nodes are processed per idle callback. Lower values (10-25) provide smoother UI performance but slower indexing, while higher values (50-100) index faster but may cause brief UI pauses. Default: 25.
644
- - `indexerTimeout` sets the maximum wait time before forcing indexing when the browser is busy. Lower values (25-50ms) ensure more responsive indexing, while higher values (100-200ms) give more time for genuine idle periods. Default: 50ms.
645
-
646
- #### Tree Configuration
647
- | Prop | Type | Default | Description |
648
- |------|------|---------|-------------|
649
- | `treeId` | `string \| null` | auto-generated | Unique identifier for the tree |
650
- | `treePathSeparator` | `string \| null` | `"."` | Separator character for hierarchical paths (e.g., "." for "1.2.3" or "/" for "1/2/3") |
651
- | `selectedNode` | `LTreeNode<T>` (bindable) | `undefined` | Currently selected node |
652
- | `insertResult` | `InsertArrayResult<T>` (bindable) | `undefined` | Result of the last data insertion including failed nodes |
653
-
654
- #### Behavior Properties
655
- | Prop | Type | Default | Description |
656
- |------|------|---------|-------------|
657
- | `expandLevel` | `number \| null` | `2` | Automatically expand nodes up to this level |
658
- | `clickBehavior` | `ClickBehavior` | `'expand-and-focus'` | Node click behavior: `'select'` (click selects, dblclick expands), `'expand'` (click expands only), `'expand-and-focus'` (click selects + expands) |
659
- | `showCheckboxes` | `boolean` | `false` | Show selection checkboxes before each node. Clicking a checkbox toggles the node's selection (same as Ctrl+click). |
660
- | `orderMember` | `string \| null` | `null` | Property name for sort order (enables before/after positioning in drag-drop) |
661
- | `indexerBatchSize` | `number \| null` | `25` | Number of nodes to process per batch during search indexing |
662
- | `indexerTimeout` | `number \| null` | `50` | Maximum time (ms) to wait for idle callback before forcing indexing |
663
- | `isLoading` | `boolean` | `false` | Show loading placeholder instead of tree content |
664
- | `shouldDisplayDebugInformation` | `boolean` | `false` | Show debug information panel with tree statistics and enable console debug logging |
665
- | `shouldDisplayContextMenuInDebugMode` | `boolean` | `false` | Display persistent context menu at fixed position for styling development |
666
-
667
- #### Rendering Properties
668
- | Prop | Type | Default | Description |
669
- |------|------|---------|-------------|
670
- | `useFlatRendering` | `boolean` | `true` | Use flat rendering mode (faster for large trees) |
671
- | `progressiveRender` | `boolean` | `true` | Progressively render nodes in batches |
672
- | `initialBatchSize` | `number` | `20` | First batch size for progressive rendering |
673
- | `maxBatchSize` | `number` | `500` | Maximum batch size cap |
674
- | `virtualScroll` | `boolean` | `false` | Enable virtual scrolling (flat mode only, renders visible + overscan rows) |
675
- | `virtualRowHeight` | `number` | auto | Explicit row height in px (auto-measured from first row if not set) |
676
- | `virtualOverscan` | `number` | `5` | Extra rows rendered above/below viewport |
677
- | `virtualContainerHeight` | `string` | auto/`'400px'` | CSS height for scroll container (auto-detected from parent if not set) |
678
- | `isRendering` | `boolean` (bindable) | `false` | Whether the tree is currently rendering (useful for progress indicators) |
679
- | `onRenderStart` | `() => void` | `undefined` | Called when progressive rendering begins |
680
- | `onRenderProgress` | `(rendered: number, total: number) => void` | `undefined` | Called after each batch with progress info |
681
- | `onRenderComplete` | `() => void` | `undefined` | Called when progressive rendering finishes |
682
-
683
- #### Drag & Drop Properties
684
- | Prop | Type | Default | Description |
685
- |------|------|---------|-------------|
686
- | `dragDropMode` | `DragDropMode` | `'none'` | Controls allowed drag operations: `'none'`, `'self'`, `'cross'`, `'both'` |
687
- | `dropZoneMode` | `string` | `'glow'` | Drop indicator style: `'floating'` or `'glow'` |
688
- | `dropZoneLayout` | `string` | `'around'` | Zone arrangement: `'around'`, `'above'`, `'below'`, `'wave'`, `'wave2'` |
689
- | `dropZoneStart` | `number \| string` | `33` | Where zones start horizontally (number=%, string=CSS value) |
690
- | `dropZoneMaxWidth` | `number` | `120` | Max width in pixels for wave layouts |
691
- | `allowCopy` | `boolean` | `false` | Enable Ctrl+drag to copy instead of move |
692
- | `autoHandleCopy` | `boolean` | `true` | Auto-handle same-tree copies (false for external DB/API) |
693
- | `allowedDropPositionsMember` | `string \| null` | `null` | Property name for allowed drop positions array |
694
- | `getAllowedDropPositionsCallback` | `(node) => DropPosition[] \| null` | `undefined` | Callback returning allowed drop positions per node |
695
- | `beforeDropCallback` | `(dropNode, draggedNode, position, event, operation) => ...` | `undefined` | Async-capable callback to validate/modify drops |
696
-
697
- #### Event Handler Properties
698
- | Prop | Type | Default | Description |
699
- |------|------|---------|-------------|
700
- | `onNodeClicked` | `(node) => void` | `undefined` | Node click event handler |
701
- | `onNodeDragStart` | `(node, event) => void` | `undefined` | Drag start event handler |
702
- | `onNodeDragOver` | `(node, event) => void` | `undefined` | Drag over event handler |
703
- | `onNodeDrop` | `(dropNode, draggedNode, position, event, operation) => void` | `undefined` | Drop event handler. `dropNode` can be `null` (e.g., drop on empty tree). Position is `'before'`, `'after'`, or `'child'`. Operation is `'move'` or `'copy'` |
704
-
705
- #### Visual Styling Properties
706
- | Prop | Type | Default | Description |
707
- |------|------|---------|-------------|
708
- | `bodyClass` | `string \| null` | `undefined` | CSS class for tree body |
709
- | `selectedNodeClass` | `string \| null` | `undefined` | CSS class for selected nodes |
710
- | `dragOverNodeClass` | `string \| null` | `undefined` | CSS class for nodes being dragged over |
711
- | `expandIconClass` | `string \| null` | `"ltree-icon-expand"` | CSS class for expand icons |
712
- | `collapseIconClass` | `string \| null` | `"ltree-icon-collapse"` | CSS class for collapse icons |
713
- | `leafIconClass` | `string \| null` | `"ltree-icon-leaf"` | CSS class for leaf node icons |
714
- | `scrollHighlightTimeout` | `number \| null` | `4000` | Duration (ms) for scroll highlight animation |
715
- | `scrollHighlightClass` | `string \| null` | `'ltree-scroll-highlight'` | CSS class to apply for scroll highlight effect |
716
-
717
- #### Snippets
718
- | Snippet | Parameters | Description |
719
- |---------|------------|-------------|
720
- | `nodeTemplate` | `(node)` | Custom node template |
721
- | `treeHeader` | | Tree header content |
722
- | `treeFooter` | | Tree footer content |
723
- | `noDataFound` | | Content shown when tree has no data |
724
- | `dropPlaceholder` | | Content shown in empty drop target tree |
725
- | `loadingPlaceholder` | | Content shown while `isLoading` is true |
726
- | `contextMenu` | `(node, closeMenu)` | Context menu template |
727
-
728
- #### Public Methods
729
- | Method | Parameters | Description |
730
- |--------|------------|-------------|
731
- | `expandNodes` | `nodePath: string` | Expand nodes at specified path |
732
- | `collapseNodes` | `nodePath: string` | Collapse nodes at specified path |
733
- | `expandAll` | `nodePath?: string` | Expand all nodes or nodes under path |
734
- | `collapseAll` | `nodePath?: string` | Collapse all nodes or nodes under path |
735
- | `filterNodes` | `searchText: string, searchOptions?: SearchOptions` | Filter the tree display using internal search index with optional FlexSearch options |
736
- | `searchNodes` | `searchText: string \| null \| undefined, searchOptions?: SearchOptions` | Search nodes using internal search index and return matching nodes with optional FlexSearch options |
737
- | `scrollToPath` | `path: string, options?: ScrollToPathOptions` | Scroll to and highlight a specific node |
738
- | `update` | `updates: Partial<Props>` | Programmatically update component props from external JavaScript |
739
- | `addNode` | `parentPath: string, data: T, pathSegment?: string` | Add a new node under the specified parent |
740
- | `moveNode` | `sourcePath: string, targetPath: string, position: 'before' \| 'after' \| 'child'` | Move a node to a new location |
741
- | `removeNode` | `path: string, includeDescendants?: boolean` | Remove a node (and optionally its descendants) |
742
- | `getNodeByPath` | `path: string` | Get a node by its path |
743
- | `getChildren` | `parentPath: string` | Get direct children of a node |
744
- | `getSiblings` | `path: string` | Get siblings of a node (including itself) |
745
- | `updateNode` | `path: string, data: Partial<T>` | Update a node's data properties |
746
- | `copyNodeWithDescendants` | `sourcePath: string, targetPath: string, position: DropPosition` | Copy a node and its subtree to a new location |
747
- | `refreshNode` | `path: string` | Force re-render of a specific node |
748
- | `refreshSiblings` | `path: string` | Force re-render of a node's siblings |
749
- | `getExpandedPaths` | | Get array of all currently expanded node paths |
750
- | `setExpandedPaths` | `paths: string[]` | Restore expanded state from saved paths |
751
- | `getAllData` | | Get all tree data as a flat array |
752
- | `applyChanges` | | Apply pending changes and refresh the tree |
753
- | `closeContextMenu` | | Programmatically close the context menu |
754
-
755
- #### ScrollToPath Options
756
-
757
- | Option | Type | Default | Description |
758
- |--------|------|---------|-------------|
759
- | `expand` | `boolean` | `true` | Automatically expand parent nodes to make target visible |
760
- | `expandTarget` | `boolean` | `false` | Also expand the target node itself (not just its ancestors) |
761
- | `highlight` | `boolean` | `true` | Apply temporary highlight animation to the target node |
762
- | `scrollOptions` | `ScrollIntoViewOptions` | `{ behavior: 'smooth', block: 'center' }` | Native browser scroll options |
763
- | `containerScroll` | `boolean` | `false` | Scroll only within nearest scrollable ancestor (prevents page scroll) |
764
- | `containerElement` | `HTMLElement` | `undefined` | Explicit scrollable container element to use for scrolling |
765
-
766
- **Usage Example:**
767
- ```typescript
768
- // Basic usage - scroll to path with default options
769
- await tree.scrollToPath('1.2.3');
770
-
771
- // Advanced usage - custom options
772
- await tree.scrollToPath('1.2.3', {
773
- expand: false, // Don't auto-expand parent nodes
774
- highlight: false, // Skip highlight animation
775
- scrollOptions: { // Custom scroll behavior
776
- behavior: 'instant',
777
- block: 'start'
778
- }
779
- });
780
-
781
- // Scroll within a scrollable container (prevents page scroll)
782
- await tree.scrollToPath('1.2.3', { containerScroll: true });
783
- ```
784
-
785
- **Highlight Classes Example:**
786
- ```svelte
787
- <!-- Default background highlight -->
788
- <Tree
789
- {data}
790
- idMember="path"
791
- pathMember="path"
792
- scrollHighlightClass="ltree-scroll-highlight"
793
- scrollHighlightTimeout={5000}
794
- />
795
-
796
- <!-- Red arrow highlight -->
797
- <Tree
798
- {data}
799
- idMember="path"
800
- pathMember="path"
801
- scrollHighlightClass="ltree-scroll-highlight-arrow"
802
- scrollHighlightTimeout={3000}
803
- />
804
-
805
- <!-- Custom highlight class -->
806
- <Tree
807
- {data}
808
- idMember="path"
809
- pathMember="path"
810
- scrollHighlightClass="my-custom-highlight"
811
- scrollHighlightTimeout={2000}
812
- />
813
- ```
814
-
815
- **Available Built-in Highlight Classes:**
816
- - `ltree-scroll-highlight` - Background glow with blue color (default)
817
- - `ltree-scroll-highlight-arrow` - Red left arrow indicator
818
-
819
- #### Statistics
820
- The tree provides real-time statistics about the loaded data:
821
-
822
- | Property | Type | Description |
823
- |----------|------|-------------|
824
- | `statistics` | `{ nodeCount: number; maxLevel: number; filteredNodeCount: number; isIndexing: boolean; pendingIndexCount: number }` | Returns current node count, maximum depth level, filtered nodes count, indexing status, and pending index count |
825
-
826
- ```typescript
827
- const { nodeCount, maxLevel, filteredNodeCount, isIndexing, pendingIndexCount } = tree.statistics;
828
- console.log(`Tree has ${nodeCount} nodes with maximum depth of ${maxLevel} levels`);
829
- if (filteredNodeCount > 0) {
830
- console.log(`Currently showing ${filteredNodeCount} filtered nodes`);
831
- }
832
- if (isIndexing) {
833
- console.log(`Search indexing in progress: ${pendingIndexCount} nodes pending`);
834
- }
835
- ```
836
-
837
- #### External Updates (Vanilla JavaScript)
838
-
839
- The `update()` method allows you to programmatically update component props from external JavaScript code (outside of Svelte's reactivity system). This is particularly useful for HTML/JavaScript integration or dynamic configuration from non-Svelte code.
840
-
841
- ```javascript
842
- // Get reference to the tree component
843
- const treeElement = document.querySelector('#my-tree');
844
-
845
- // Update multiple props at once
846
- treeElement.update({
847
- searchText: 'Production',
848
- expandLevel: 3,
849
- shouldDisplayDebugInformation: true,
850
- data: newDataArray,
851
- contextMenuXOffset: 10
852
- });
853
-
854
- // Update single prop
855
- treeElement.update({ searchText: 'new search' });
856
-
857
- // Update data and configuration
858
- treeElement.update({
859
- data: fetchedData,
860
- expandLevel: 5,
861
- selectedNodeClass: 'custom-selected'
862
- });
863
- ```
864
-
865
- **Updatable Properties:**
866
- All Tree props can be updated except snippets/templates, including:
867
- - Data and state: `data`, `searchText`, `selectedNode`, `expandLevel`
868
- - Members: `idMember`, `pathMember`, `displayValueMember`, `searchValueMember`
869
- - Callbacks: `sortCallback`, `getDisplayValueCallback`, `onNodeClicked`, etc.
870
- - Visual: `bodyClass`, `selectedNodeClass`, `expandIconClass`, etc.
871
- - Context menu: `contextMenuCallback`, `contextMenuXOffset`, `contextMenuYOffset`
872
- - Behavior: `clickBehavior`, `shouldUseInternalSearchIndex`, etc.
873
-
874
- ### Debug Information
875
-
876
- Enable debug information to see real-time tree statistics and console logging:
877
-
878
- ```svelte
879
- <Tree
880
- {data}
881
- idMember="path"
882
- pathMember="path"
883
- shouldDisplayDebugInformation={true}
884
- />
885
- ```
886
-
887
- #### Debug Panel
888
- The visual debug panel shows:
889
- - Tree ID
890
- - Data array length
891
- - Expand level setting
892
- - Node count
893
- - Maximum depth levels
894
- - Filtered node count (when filtering is active)
895
- - Search indexing progress (when indexing is active)
896
- - Currently dragged node
897
-
898
- #### Console Debug Logging
899
- When enabled, the component will log detailed information to the browser console including:
900
-
901
- **Tree Operations:**
902
- - Data mapping and sorting performance metrics
903
- - Node filtering and search operations
904
- - Tree structure changes
905
-
906
- **Async Search Indexing:**
907
- - Indexer initialization with batch size
908
- - Queue management (items added, queue size)
909
- - Batch processing details (timeout status, items processed, timing)
910
- - Indexing completion and progress updates
911
-
912
- This provides valuable insights for performance optimization and troubleshooting, especially when working with large datasets or complex search operations.
913
-
914
- ## Data Structure
915
-
916
- The component expects hierarchical data with path-based organization:
917
-
918
- ```typescript
919
- interface NodeData {
920
- path: string; // e.g., "1.2.3" for hierarchical positioning
921
- // ... your custom properties
922
- }
923
- ```
924
-
925
- ### Path Examples
926
-
927
- - Root level: `"1"`, `"2"`, `"3"`
928
- - Second level: `"1.1"`, `"1.2"`, `"2.1"`
929
- - Third level: `"1.1.1"`, `"1.2.1"`, `"2.1.1"`
930
-
931
- ### Sorting Requirements
932
-
933
- **Important:** For proper tree construction, your `sortCallback` must sort by **level first** to ensure parent nodes are inserted before their children:
934
-
935
- ```typescript
936
- const sortCallback = (items: LTreeNode<T>[]) => {
937
- return items.sort((a, b) => {
938
- // First, sort by level (shallower levels first)
939
- const aLevel = a.path.split('.').length;
940
- const bLevel = b.path.split('.').length;
941
- if (aLevel !== bLevel) {
942
- return aLevel - bLevel;
943
- }
944
-
945
- // Then sort by your custom criteria
946
- return (a.data?.name ?? '').localeCompare(b.data?.name ?? '');
947
- });
948
- };
949
- ```
950
-
951
- **Why this matters:** If deeper level nodes are processed before their parents, you'll get "Could not find parent node" errors during tree construction. Level-first sorting ensures hierarchical integrity and enables progressive rendering for large datasets.
952
-
953
- ### Insert Result Information
954
-
955
- The tree provides detailed information about data insertion through the `insertResult` bindable property:
956
-
957
- ```typescript
958
- interface InsertArrayResult<T> {
959
- successful: number; // Number of nodes successfully inserted
960
- failed: Array<{ // Nodes that failed to insert
961
- node: LTreeNode<T>; // The processed tree node
962
- originalData: T; // The original data object
963
- error: string; // Error message (usually "Could not find parent...")
964
- }>;
965
- total: number; // Total number of nodes processed
966
- }
967
- ```
968
-
969
- #### Usage Example
970
-
971
- ```svelte
972
- <script lang="ts">
973
- import { Tree } from '@keenmate/svelte-treeview';
974
-
975
- let insertResult = $state();
976
-
977
- const data = [
978
- { id: '1', path: '1', name: 'Root' },
979
- { id: '1.2', path: '1.2', name: 'Child' }, // Missing parent "1.1"
980
- { id: '1.1.1', path: '1.1.1', name: 'Deep' } // Missing parent "1.1"
981
- ];
982
-
983
- // Check results after tree processes data
984
- $effect(() => {
985
- if (insertResult) {
986
- console.log(`${insertResult.successful} nodes inserted successfully`);
987
- console.log(`${insertResult.failed.length} nodes failed to insert`);
988
-
989
- insertResult.failed.forEach(failure => {
990
- console.log(`Failed: ${failure.originalData.name} - ${failure.error}`);
991
- });
992
- }
993
- });
994
- </script>
995
-
996
- <Tree
997
- {data}
998
- idMember="id"
999
- pathMember="path"
1000
- displayValueMember="name"
1001
- bind:insertResult
1002
- />
1003
- ```
1004
-
1005
- #### Benefits
1006
-
1007
- - **Data Validation**: Identify missing parent nodes in hierarchical data
1008
- - **Debugging**: Clear error messages with node paths like "Node: 1.1.1 - Could not find parent node: 1.1"
1009
- - **Data Integrity**: Handle incomplete datasets gracefully
1010
- - **Search Accuracy**: Failed nodes are excluded from search index, ensuring search results match visible tree
1011
- - **User Feedback**: Inform users about data issues with detailed failure information
1012
-
1013
- ## Performance
1014
-
1015
- The component is optimized for large datasets:
1016
-
1017
- - **Virtual Scroll**: Renders only visible rows (~50 DOM nodes) for trees with 50,000+ nodes
1018
- - **Flat Rendering Mode**: Single `{#each}` loop instead of recursive components (default, ~12x faster initial render)
1019
- - **Progressive Rendering**: Batched rendering prevents UI freeze during initial load
1020
- - **Async Search Indexing**: Uses `requestIdleCallback` for non-blocking search index building
1021
- - **LTree**: Efficient hierarchical data structure with FlexSearch integration
1022
-
1023
- ### Performance Benchmarks (5500 nodes)
1024
-
1025
- | Operation | Time |
1026
- |-----------|------|
1027
- | Initial render (flat) | ~25ms |
1028
- | Initial render (virtual) | ~5ms |
1029
- | Expand/collapse | ~100-150ms |
1030
- | Search filtering | <50ms |
1031
- | insertArray | <100ms |
1032
-
1033
- ### Virtual Scroll
1034
-
1035
- For trees with 10,000+ nodes, enable virtual scroll to keep DOM size constant:
1036
-
1037
- ```svelte
1038
- <Tree
1039
- {data}
1040
- virtualScroll={true}
1041
- virtualContainerHeight="500px"
1042
- virtualOverscan={5}
1043
- />
1044
- ```
1045
-
1046
- Virtual scroll auto-measures row height from the first rendered node. Override with `virtualRowHeight={32}` if needed. Requires flat rendering mode (the default).
1047
-
1048
- ### Performance Logging
1049
-
1050
- Built-in performance measurement for debugging:
1051
- ```typescript
1052
- import { enablePerfLogging } from '@keenmate/svelte-treeview';
1053
- enablePerfLogging();
1054
-
1055
- // Or from browser console:
1056
- window.components['svelte-treeview'].perf.enable()
1057
- ```
1058
-
1059
- **Important**: See the [$state.raw() tip](#quick-start) above - using `$state()` instead of `$state.raw()` for tree data can cause 5,000x slowdown!
1060
-
1061
- ## CanvasTree (Canvas-Based Rendering)
1062
-
1063
- Canvas rendering is available as a separate companion package: [`@keenmate/svelte-treeview-canvas`](https://github.com/keenmate/svelte-treeview-canvas)
1064
-
1065
- It renders trees on HTML5 Canvas for high-performance visualization with multiple layout modes (tree, balanced, fishbone, radial, box), keyboard navigation, drag & drop, and custom node rendering. Install it separately:
1066
-
1067
- ```bash
1068
- npm install @keenmate/svelte-treeview-canvas
1069
- ```
1070
-
1071
- ## Development Setup & Contributing
1072
-
1073
- For developers working on the project, you can use either standard npm commands or the provided Makefile:
1074
-
1075
- ```bash
1076
- # Using Makefile (recommended for consistency)
1077
- make setup # or make install
1078
- make dev
1079
-
1080
- # Or using standard npm commands
1081
- npm install
1082
- npm run dev
1083
- ```
1084
-
1085
- We welcome contributions! Please see our contributing guidelines for details.
1086
-
1087
- > **For AI Agents / LLMs**: Comprehensive documentation is available in the `ai/` folder with topic-specific files (basic-setup.txt, drag-drop.txt, performance.txt, etc.). Start with `ai/INDEX.txt` for navigation.
1088
-
1089
- ## License
1090
-
1091
- MIT License - see LICENSE file for details.
1092
-
1093
- ## Support
1094
-
1095
- - **GitHub Issues**: [Report bugs or request features](https://github.com/keenmate/svelte-treeview/issues)
1096
- - **Live demo & docs**: [svelte-treeview.keenmate.dev](https://svelte-treeview.keenmate.dev)
1097
-
1098
- ---
1099
-
1100
- Built with ❤️ by [KeenMate](https://github.com/keenmate)
1
+ # @keenmate/svelte-treeview
2
+
3
+ A high-performance, feature-rich hierarchical tree view component for Svelte 5 with drag & drop support, search functionality, and flexible data structures using LTree.
4
+
5
+ ## What is it
6
+
7
+ `@keenmate/svelte-treeview` is a hierarchical tree-view component for Svelte 5 apps. It renders flat, path-keyed data (`"1"`, `"1.2"`, `"1.2.3"`) into an expandable tree with built-in drag & drop, three-level selection (focus / multi-highlight / checkboxes), context menus, integrated FlexSearch filtering, and virtual scrolling for 50,000+ nodes.
8
+
9
+ It's aimed at Svelte 5 developers building file browsers, org charts, navigation trees, settings dialogs, or any UI that displays hierarchical data. The core (data structure, expand/collapse, search, drag-and-drop logic) is decoupled from the renderer via `TreeProvider` + `TreeController`, so you can plug in custom HTML, Canvas, or SVG renderers on the same engine.
10
+
11
+ The component ships standalone with sensible light/dark defaults and integrates cleanly with [Pure Admin](https://pureadmin.io/) and the wider `@keenmate/*` design-token suite via the `--base-*` CSS variable contract.
12
+
13
+ ### How it differs from `@keenmate/web-treeview`
14
+
15
+ There's a vanilla-TypeScript sibling [`@keenmate/web-treeview`](https://github.com/KeenMate/web-treeview) built on the same LTree path-based engine. Same logical tree, different DOM strategy. Neither is "more mature"; they target different priorities.
16
+
17
+ | | svelte-treeview | web-treeview |
18
+ |---|---|---|
19
+ | Framework | Svelte 5 | Vanilla TS web component |
20
+ | Rendering modes | Recursive (default) + flat | Flat only |
21
+ | Children DOM | `.stv__children` wrapper (recursive mode) | None — siblings under `.wtv__tree` |
22
+ | Indent math | `level × indent` | `(level − 1) × indent` (root at zero offset) |
23
+ | Virtual scroll | Flat mode only | Built-in (three-div spacer / `translateY`) |
24
+ | Label markup | `<span class="stv__node-label">` by default replace via `nodeTemplate` snippet | `<span class="wtv__node-label">` by default replace via `renderNodeCallback` |
25
+ | Checkbox | `<label>` + custom `.stv__checkbox-box` span | Bare native `<input type="checkbox">` |
26
+ | Update mechanism | Svelte 5 runes + per-node `_rev` keyed `{#each}` | Imperative reconciler diffing `data-rev` / `data-expanded` attributes |
27
+
28
+ svelte-treeview is broader (two rendering modes, easier vertical guide lines via `.stv__children`); web-treeview is purpose-built for virtual scrolling over large datasets with a flatter DOM.
29
+
30
+ ## Live Demo
31
+
32
+ Browse interactive code examples and the full API reference at **[svelte-treeview.keenmate.dev](https://svelte-treeview.keenmate.dev)**
33
+
34
+ ## What's New in v5.0.0-rc11
35
+
36
+ - **Selection model three symmetric imperative families (highlight / select / focus)** — The imperative API had drifted into aliases and asymmetries: `selectNode`/`selectNodes` secretly drove the *highlight* set, the highlight set cleared with `clearHighlight()` while checkboxes cleared with `deselectAll()`, and `highlightNodes()` silently replaced rather than added. The surface is now three concerns × one shape — **highlight** (`highlightNode`/`highlightNodes`/`setHighlightedPaths`/`highlightAll`/`clearHighlight`) for the UI multi-select set, **select** (`selectNode`/`selectNodes`/`setSelectedPaths`/`selectAll`/`deselectNode`/`clearSelection`) for the checkbox set, and **focus** (`focusNode`/`clearFocus`) for the single cursor. Two shared types, `HighlightMode` and `TreeMutationOptions { silent? }`, replace the inline literals. Breaking within the RC: `deselectAll` → `clearSelection`, `highlightNodes` is now additive, and `selectNode`/`selectNodes` are now real checkbox setters.
37
+ - **CSS BEM rename (`ltree` → `stv`) and `is*/should*` boolean props** — Every class and variable now follows the BlissFramework BEM rule with the registered `stv` prefix: `--ltree-*` → `--stv-*`, `.ltree-node` → `.stv__node`, state classes as modifiers (`.stv__node-content--drag-over`), and the highlight family folded in (`.stv__node-content--highlight-bold`). Ten ambiguous boolean props were renamed to read as predicates — `showCheckboxes` `shouldShowCheckboxes`, `virtualScroll` → `isVirtualScrollEnabled`, `allowCopy` → `isCopyAllowed`, and so on. No aliases — within the RC cycle consumers search-and-replace each prop and CSS selector.
38
+ - **Theming — dark mode rewritten to Strategy B (`color-scheme` + `light-dark()`)** — `dark-mode.css` collapsed from ~140 lines of per-token redeclaration to ~45 lines that flip `color-scheme` on the framework-theme and per-instance selectors, letting the `light-dark(<light>, <dark>)` fallbacks in `variables.css` resolve the dark branch automatically. Consumer `--base-*` overrides now survive the dark branch instead of being clobbered by hardcoded literals, and adding a new themeable variable no longer means touching every signal block.
39
+ - **Highlight & focus styling — marker is a fallback, not a fighter, plus a new glow flavor** — The always-on `.stv__node-content--highlighted` marker used to paint its default background *underneath* any configured `highlightedNodeClass`, so picking "Bold" showed bold text over the marker tint. It's now applied only when no highlight class is set, so a configured class is the sole source of styling — no `!important` needed. `.stv__node-content--focused` became a pure hook (no default outline, honoring the demo's "None" option), and a fifth built-in highlight flavor `--highlight-glow` (a soft accent ring) joined bold/border/brackets/fill.
40
+ - **Drag & drop — pinned nodes are respected, copied/added nodes behave** — Three correctness fixes: multi-drag now filters out locked (`isDraggable=false`) nodes that merely happen to be in the highlight set instead of dragging them along; `addNode` (and therefore `copyNodeWithDescendants`) now seeds per-node flags from the `getIs*Callback`/`*Member` props, so nodes dropped into a second tree are no longer frozen non-draggable and drop-rejecting; and desktop drag-drop now honors `node.isDropAllowed` (previously only the touch path did). Covered by new vitest + Playwright regression suites.
41
+ - **Context menus positioning ported to `@floating-ui/dom`** Root menu and submenus are now placed by `computePosition` + `autoUpdate` with `flip()`/`shift()` instead of raw inline `left/top` and CSS `:hover`, so menus flip above the cursor near the viewport bottom and submenus slide sideways instead of clipping. `contextMenuXOffset`/`contextMenuYOffset` keep their exact semantics.
42
+ - **Docs — README cut to ~350 lines with topical docs split out** — The README dropped from 1103 to 347 lines: Advanced Usage, Styling, and API Reference moved into `docs/usage.md`, `docs/theming.md`, `docs/examples.md`, and `docs/accessibility.md`, with a new intro and a Demos & docs link block up top.
43
+
44
+ ## What's New in v5.0.0-rc10
45
+
46
+ - **Built-in dark mode — all four canonical signals covered** — a `dark-mode.css` partial flips the tree's surface, text, border, and accent palette when any of the canonical signals is active: OS preference (`prefers-color-scheme: dark`), page `<html style="color-scheme: dark">` resolved via `light-dark()`, framework theme classes (`[data-theme="dark"]`, `[data-bs-theme="dark"]`, `.dark`), and the new per-instance `theme` prop. Symmetric `light` selectors let a single tree force light on an otherwise-dark page. Zero JavaScript — pure CSS resolution.
47
+ - **`theme` prop on `<Tree>` — per-instance dark/light override** — `'dark' | 'light' | null | undefined`. Forwarded to the root `.stv__container` as `data-theme`, where per-instance CSS selectors take over and beat ambient signals. Leave `undefined` to inherit from the page. (rc10 originally landed this on `.ltree-container`; the BEM rename to `.stv__container` shipped in rc12.)
48
+ - **CSS variables rescoped from `:root` to `.stv__container` — subtree theming actually works** — mirrors the `:host`-scoped pattern from sibling `@keenmate/*` components and is the only way `--base-*` theming flows at subtree scope. A wrapper around the tree that sets `--base-accent-color: red` now re-tints the tree (it previously had no effect because the substitution was frozen at `:root`). Multiple trees on the same page can be themed differently via wrapper-scoped `--base-*` overrides. Consumer note: setting `--stv-*` directly on a wrapper no longer cascades — use `--base-*` on the wrapper or target `.stv__container` explicitly.
49
+ - **`--stv-bg` the tree paints its own surface** — `.stv__container` gets a `background: var(--stv-bg)` so consumers don't need to wrap the tree in a colored container for a visible surface. Override to `transparent` to restore the pre-rc10 layered behavior. Companion `--stv-elevated-bg` reads through the `--base-elevated-bg` chain for floating chrome (context menu).
50
+ - **CSS file layout aligned with the Bliss web-component guidelines** — all `_xxx.css` partials renamed to `xxx.css`, `main.css` now declares `@layer variables, component, overrides;` and imports each partial into the matching layer. Consumers' unlayered overrides automatically beat every rule in the library — no `!important` needed. Note: if your app ships an unlayered universal CSS reset (`* { padding: 0 }` from normalize / Bootstrap / Tailwind preflight / etc.), wrap it in a low-priority `@layer reset` or the tree's defaults won't apply.
51
+ - **`getIsDropAllowedCallback` prop + `getIsDraggableCallback` seeded at insert-time** — callback variant for `isDropAllowedMember`, matching the pattern rc09 introduced for `getIsExpandedCallback` / `getIsSelectableCallback` / `getIsSelectedCallback`. Also, the existing `getIsDraggableCallback` prop is now actually applied during the seed walk — previously it was only consumed lazily in some paths.
52
+ - **Bug fixes — virtual scroll + `clickBehavior='select'` double-click** — virtual scroll no longer gets stuck at bottom after a filter shrinks the tree. Double-click to expand in `clickBehavior='select'` mode finally works (the browser's native `dblclick` event couldn't fire because the first click destroyed the row via the focus re-render; the controller now detects the double manually).
53
+
54
+ ## v5.0: Core/Renderer Split + Virtual Scroll
55
+
56
+ > [!IMPORTANT]
57
+ > **In version 5, the tree core (data structure, expand/collapse, search, drag & drop logic) has been completely separated from the renderer.** The architecture is open for you to build your own custom renderers on top of the same core via `TreeProvider` and `TreeController`.
58
+
59
+ **Key changes in v5:**
60
+ - **Core/Renderer split**: Use the built-in HTML `Tree` renderer, or create custom visualizations (Canvas, WebGL, SVG) via `TreeProvider` + `TreeController`
61
+ - **Virtual scroll**: Render 50,000+ node trees smoothly with `isVirtualScrollEnabled={true}` — only ~50 DOM nodes at any time
62
+ - **Canvas companion**: For canvas rendering, install [`@keenmate/svelte-treeview-canvas`](https://github.com/keenmate/svelte-treeview-canvas)
63
+ - **Drop position naming**: `'above'`/`'below'` renamed to `'before'`/`'after'` (CSS classes and events updated accordingly)
64
+
65
+ ### Rendering Modes
66
+
67
+ | Mode | Props | DOM Nodes | Best For |
68
+ |------|-------|-----------|----------|
69
+ | Recursive | `isFlatRenderingEnabled={false}` | All | Small trees (<100 nodes) |
70
+ | Flat (default) | `isFlatRenderingEnabled={true}` | All | Medium trees (100–10K) |
71
+ | Virtual | `isVirtualScrollEnabled={true}` | ~50 | Large trees (10K+) |
72
+
73
+ ```svelte
74
+ <!-- Virtual scroll for large trees -->
75
+ <Tree {data} isVirtualScrollEnabled={true} virtualContainerHeight="500px" />
76
+
77
+ <!-- Flat mode (default) with progressive batching -->
78
+ <Tree {data} isProgressiveRender={true} initialBatchSize={20} maxBatchSize={500} />
79
+ ```
80
+
81
+ ## Features
82
+
83
+ - **Svelte 5 Native**: Built specifically for Svelte 5 with full support for runes and modern Svelte patterns
84
+ - **High Performance**: Flat rendering with progressive loading, virtual scroll for 50,000+ nodes
85
+ - **Drag & Drop**: Built-in drag and drop with position control (before/after/child), touch support, and async validation
86
+ - **Tree Editing**: Built-in methods for add, move, remove operations with automatic path management
87
+ - **Search & Filter**: Integrated FlexSearch for fast, full-text search capabilities
88
+ - **Flexible Data Sources**: Works with any hierarchical data structure
89
+ - **Multi-Select**: Ctrl+click toggle, Shift+click range select (visual or logical mode), `selectedPaths` bindable, selection-aware context menus
90
+ - **Context Menus**: Dynamic right-click menus with shortcuts, submenus, named dividers, and two API approaches (callback or Svelte components)
91
+ - **Visual Customization**: Extensive styling options and icon customization
92
+ - **TypeScript Support**: Full TypeScript support with comprehensive type definitions
93
+ - **Accessibility**: Built with accessibility in mind
94
+
95
+ ## Installation
96
+
97
+ ```bash
98
+ npm install @keenmate/svelte-treeview
99
+ ```
100
+
101
+ ### Importing Styles
102
+
103
+ The component requires CSS to display correctly. Import the styles in your app:
104
+
105
+ **JavaScript import** (in your main.js/main.ts or Vite/Webpack entry):
106
+ ```javascript
107
+ import '@keenmate/svelte-treeview/styles.css';
108
+ ```
109
+
110
+ **Svelte component import:**
111
+ ```svelte
112
+ <style>
113
+ @import '@keenmate/svelte-treeview/styles.css';
114
+ </style>
115
+ ```
116
+
117
+ ## Quick Start
118
+
119
+ ```svelte
120
+ <script lang="ts">
121
+ import { Tree } from '@keenmate/svelte-treeview';
122
+
123
+ const data = [
124
+ { path: '1', name: 'Documents', type: 'folder' },
125
+ { path: '1.1', name: 'Projects', type: 'folder' },
126
+ { path: '1.1.1', name: 'Project A', type: 'folder' },
127
+ { path: '1.1.2', name: 'Project B', type: 'folder' },
128
+ { path: '2', name: 'Pictures', type: 'folder' },
129
+ { path: '2.1', name: 'Vacation', type: 'folder' }
130
+ ];
131
+ </script>
132
+
133
+ <Tree
134
+ {data}
135
+ idMember="path"
136
+ pathMember="path"
137
+ displayValueMember="name"
138
+ />
139
+ ```
140
+
141
+ > [!TIP]
142
+ > **Performance tip:** When passing large arrays (1000+ items) to the Tree component, use `$state.raw()` instead of `$state()` to avoid severe performance issues. Svelte 5's `$state()` creates deep proxies — with thousands of items this causes up to 5,000x slowdown. The array itself remains reactive; only individual items lose deep reactivity (which Tree doesn't need).
143
+ > ```typescript
144
+ > // BAD - Each item becomes a Proxy
145
+ > let treeData = $state<TreeNode[]>([])
146
+ >
147
+ > // GOOD - Items remain plain objects
148
+ > let treeData = $state.raw<TreeNode[]>([])
149
+ > ```
150
+
151
+ ## Demos & docs
152
+
153
+ - 🚀 [Live demo](https://svelte-treeview.keenmate.dev) — interactive examples and the full feature gallery
154
+ - 📘 [Usage / API reference](./docs/usage.md) — every Prop, Method, Event, and Snippet
155
+ - 🎨 [Theming contract](./docs/theming.md) — `--base-*` tokens, `--stv-*` variables, dark mode, cascade layers
156
+ - 📚 [Examples / cookbook](./docs/examples.md) — node templates, search, drag & drop, tree editing, context menus
157
+ - ♿ [Accessibility](./docs/accessibility.md) — keyboard navigation, focus management, selection model
158
+ - 📒 [Release history](./CHANGELOG.md)
159
+
160
+ ## Data Structure
161
+
162
+ The component expects hierarchical data with path-based organization:
163
+
164
+ ```typescript
165
+ interface NodeData {
166
+ path: string; // e.g., "1.2.3" for hierarchical positioning
167
+ // ... your custom properties
168
+ }
169
+ ```
170
+
171
+ ### Path Examples
172
+
173
+ - Root level: `"1"`, `"2"`, `"3"`
174
+ - Second level: `"1.1"`, `"1.2"`, `"2.1"`
175
+ - Third level: `"1.1.1"`, `"1.2.1"`, `"2.1.1"`
176
+
177
+ ### Sorting Requirements
178
+
179
+ **Important:** For proper tree construction, your `sortCallback` must sort by **level first** to ensure parent nodes are inserted before their children:
180
+
181
+ ```typescript
182
+ const sortCallback = (items: LTreeNode<T>[]) => {
183
+ return items.sort((a, b) => {
184
+ // First, sort by level (shallower levels first)
185
+ const aLevel = a.path.split('.').length;
186
+ const bLevel = b.path.split('.').length;
187
+ if (aLevel !== bLevel) {
188
+ return aLevel - bLevel;
189
+ }
190
+
191
+ // Then sort by your custom criteria
192
+ return (a.data?.name ?? '').localeCompare(b.data?.name ?? '');
193
+ });
194
+ };
195
+ ```
196
+
197
+ **Why this matters:** If deeper level nodes are processed before their parents, you'll get "Could not find parent node" errors during tree construction. Level-first sorting ensures hierarchical integrity and enables progressive rendering for large datasets.
198
+
199
+ ### Insert Result Information
200
+
201
+ The tree provides detailed information about data insertion through the `insertResult` bindable property:
202
+
203
+ ```typescript
204
+ interface InsertArrayResult<T> {
205
+ successful: number; // Number of nodes successfully inserted
206
+ failed: Array<{ // Nodes that failed to insert
207
+ node: LTreeNode<T>; // The processed tree node
208
+ originalData: T; // The original data object
209
+ error: string; // Error message (usually "Could not find parent...")
210
+ }>;
211
+ total: number; // Total number of nodes processed
212
+ }
213
+ ```
214
+
215
+ #### Usage Example
216
+
217
+ ```svelte
218
+ <script lang="ts">
219
+ import { Tree } from '@keenmate/svelte-treeview';
220
+
221
+ let insertResult = $state();
222
+
223
+ const data = [
224
+ { id: '1', path: '1', name: 'Root' },
225
+ { id: '1.2', path: '1.2', name: 'Child' }, // Missing parent "1.1"
226
+ { id: '1.1.1', path: '1.1.1', name: 'Deep' } // Missing parent "1.1"
227
+ ];
228
+
229
+ // Check results after tree processes data
230
+ $effect(() => {
231
+ if (insertResult) {
232
+ console.log(`${insertResult.successful} nodes inserted successfully`);
233
+ console.log(`${insertResult.failed.length} nodes failed to insert`);
234
+
235
+ insertResult.failed.forEach(failure => {
236
+ console.log(`Failed: ${failure.originalData.name} - ${failure.error}`);
237
+ });
238
+ }
239
+ });
240
+ </script>
241
+
242
+ <Tree
243
+ {data}
244
+ idMember="id"
245
+ pathMember="path"
246
+ displayValueMember="name"
247
+ bind:insertResult
248
+ />
249
+ ```
250
+
251
+ #### Benefits
252
+
253
+ - **Data Validation**: Identify missing parent nodes in hierarchical data
254
+ - **Debugging**: Clear error messages with node paths like "Node: 1.1.1 - Could not find parent node: 1.1"
255
+ - **Data Integrity**: Handle incomplete datasets gracefully
256
+ - **Search Accuracy**: Failed nodes are excluded from search index, ensuring search results match visible tree
257
+ - **User Feedback**: Inform users about data issues with detailed failure information
258
+
259
+ ## Performance
260
+
261
+ The component is optimized for large datasets:
262
+
263
+ - **Virtual Scroll**: Renders only visible rows (~50 DOM nodes) for trees with 50,000+ nodes
264
+ - **Flat Rendering Mode**: Single `{#each}` loop instead of recursive components (default, ~12x faster initial render)
265
+ - **Progressive Rendering**: Batched rendering prevents UI freeze during initial load
266
+ - **Async Search Indexing**: Uses `requestIdleCallback` for non-blocking search index building
267
+ - **LTree**: Efficient hierarchical data structure with FlexSearch integration
268
+
269
+ ### Performance Benchmarks (5500 nodes)
270
+
271
+ | Operation | Time |
272
+ |-----------|------|
273
+ | Initial render (flat) | ~25ms |
274
+ | Initial render (virtual) | ~5ms |
275
+ | Expand/collapse | ~100-150ms |
276
+ | Search filtering | <50ms |
277
+ | insertArray | <100ms |
278
+
279
+ ### Virtual Scroll
280
+
281
+ For trees with 10,000+ nodes, enable virtual scroll to keep DOM size constant:
282
+
283
+ ```svelte
284
+ <Tree
285
+ {data}
286
+ isVirtualScrollEnabled={true}
287
+ virtualContainerHeight="500px"
288
+ virtualOverscan={5}
289
+ />
290
+ ```
291
+
292
+ Virtual scroll auto-measures row height from the first rendered node. Override with `virtualRowHeight={32}` if needed. Requires flat rendering mode (the default).
293
+
294
+ ### Performance Logging
295
+
296
+ Built-in performance measurement for debugging:
297
+ ```typescript
298
+ import { enablePerfLogging } from '@keenmate/svelte-treeview';
299
+ enablePerfLogging();
300
+
301
+ // Or from browser console:
302
+ window.components['svelte-treeview'].perf.enable()
303
+ ```
304
+
305
+ **Important**: See the [$state.raw() tip](#quick-start) above - using `$state()` instead of `$state.raw()` for tree data can cause 5,000x slowdown!
306
+
307
+ ## CanvasTree (Canvas-Based Rendering)
308
+
309
+ Canvas rendering is available as a separate companion package: [`@keenmate/svelte-treeview-canvas`](https://github.com/keenmate/svelte-treeview-canvas)
310
+
311
+ It renders trees on HTML5 Canvas for high-performance visualization with multiple layout modes (tree, balanced, fishbone, radial, box), keyboard navigation, drag & drop, and custom node rendering. Install it separately:
312
+
313
+ ```bash
314
+ npm install @keenmate/svelte-treeview-canvas
315
+ ```
316
+
317
+ ## Development Setup & Contributing
318
+
319
+ For developers working on the project, you can use either standard npm commands or the provided Makefile:
320
+
321
+ ```bash
322
+ # Using Makefile (recommended for consistency)
323
+ make setup # or make install
324
+ make dev
325
+
326
+ # Or using standard npm commands
327
+ npm install
328
+ npm run dev
329
+ ```
330
+
331
+ We welcome contributions! Please see our contributing guidelines for details.
332
+
333
+ > **For AI Agents / LLMs**: Comprehensive documentation is available in the `ai/` folder with topic-specific files (basic-setup.txt, drag-drop.txt, performance.txt, etc.). Start with `ai/INDEX.txt` for navigation.
334
+
335
+ ## About
336
+
337
+ Authored and maintained by [KeenMate](https://keenmate.com/).
338
+ The component ships standalone with sensible light/dark defaults;
339
+ when mounted inside [Pure Admin](https://pureadmin.io/) — or any
340
+ host that publishes the `--base-*` taxonomy via
341
+ [`@keenmate/theme-designer`](https://www.npmjs.com/package/@keenmate/theme-designer) — it adopts the host's colors,
342
+ typography, and sizing automatically. There is no runtime
343
+ dependency on Pure Admin; the integration is opt-in via CSS
344
+ variables.
345
+
346
+ ## Built with BlissFramework
347
+
348
+ Follows the [BlissFramework component guidelines](https://blissframework.dev/)
349
+ for structure, theming, color-scheme, and accessibility.
350
+
351
+ ## License
352
+
353
+ MIT License - see LICENSE file for details.
354
+
355
+ ## Support
356
+
357
+ - **GitHub Issues**: [Report bugs or request features](https://github.com/keenmate/svelte-treeview/issues)
358
+ - **Live demo & docs**: [svelte-treeview.keenmate.dev](https://svelte-treeview.keenmate.dev)
359
+
360
+ ---
361
+
362
+ Built with ❤️ by [KeenMate](https://github.com/keenmate)