@warkypublic/svelix 0.1.47 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/components/Boxer/Boxer.svelte +5 -1
  2. package/dist/components/CardGrid/CardGrid.svelte +50 -4
  3. package/dist/components/CardGrid/CardGrid.svelte.d.ts +4 -0
  4. package/dist/components/CardGrid/DefaultCard.svelte +7 -2
  5. package/dist/components/CardGrid/DefaultCard.svelte.d.ts +1 -0
  6. package/dist/components/CardGrid/cardGrid.d.ts +24 -0
  7. package/dist/components/CardGrid/cardGrid.js +20 -0
  8. package/dist/components/Former/FormerDrawer.svelte +10 -22
  9. package/dist/components/Former/FormerModal.svelte +10 -22
  10. package/dist/components/Former/formerOverlayHelpers.svelte.d.ts +11 -0
  11. package/dist/components/Former/formerOverlayHelpers.svelte.js +38 -0
  12. package/dist/components/Former/index.d.ts +1 -0
  13. package/dist/components/Former/index.js +1 -0
  14. package/dist/components/FormerControllers/ButtonCtrl.svelte +11 -16
  15. package/dist/components/FormerControllers/CtrlFieldShell.svelte +15 -0
  16. package/dist/components/FormerControllers/CtrlFieldShell.svelte.d.ts +12 -0
  17. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +6 -13
  18. package/dist/components/FormerControllers/IconButtonCtrl.svelte +11 -16
  19. package/dist/components/FormerControllers/NativeSelectCtrl.svelte +7 -13
  20. package/dist/components/FormerControllers/NumberInputCtrl.svelte +18 -14
  21. package/dist/components/FormerControllers/PasswordInputCtrl.svelte +7 -13
  22. package/dist/components/FormerControllers/SwitchCtrl.svelte +4 -3
  23. package/dist/components/FormerControllers/TextAreaCtrl.svelte +7 -13
  24. package/dist/components/FormerControllers/TextInputCtrl.svelte +7 -13
  25. package/dist/components/FormerControllers/ctrlLoading.svelte.d.ts +8 -0
  26. package/dist/components/FormerControllers/ctrlLoading.svelte.js +43 -0
  27. package/dist/components/FormerControllers/index.d.ts +2 -0
  28. package/dist/components/FormerControllers/index.js +2 -0
  29. package/dist/components/FormerControllers/utils.d.ts +1 -0
  30. package/dist/components/FormerControllers/utils.js +3 -0
  31. package/dist/components/Gridler/components/Gridler.svelte +10 -4
  32. package/dist/components/Gridler/components/GridlerFull.svelte +2 -1
  33. package/dist/components/Gridler/utils/serverPaging.d.ts +1 -0
  34. package/dist/components/Gridler/utils/serverPaging.js +7 -0
  35. package/dist/components/VTree/VTree.svelte +107 -2
  36. package/dist/components/VTree/VTree.utils.d.ts +23 -0
  37. package/dist/components/VTree/VTree.utils.js +73 -0
  38. package/dist/components/VTree/VTreeControlledSelectionDemo.svelte +20 -0
  39. package/dist/components/VTree/VTreeControlledSelectionDemo.svelte.d.ts +18 -0
  40. package/dist/components/VTree/VTreeCustomizationDemo.svelte +23 -0
  41. package/dist/components/VTree/VTreeCustomizationDemo.svelte.d.ts +18 -0
  42. package/dist/components/VTree/VTreeInteractionDemo.svelte +29 -0
  43. package/dist/components/VTree/VTreeInteractionDemo.svelte.d.ts +18 -0
  44. package/dist/components/VTree/VTreeMultiSelectDemo.svelte +27 -0
  45. package/dist/components/VTree/VTreeMultiSelectDemo.svelte.d.ts +18 -0
  46. package/dist/components/VTree/VTreeRow.svelte +71 -57
  47. package/dist/components/VTree/VTreeRow.svelte.d.ts +34 -18
  48. package/dist/components/VTree/VTreeVirtualViewport.svelte +20 -40
  49. package/dist/components/VTree/VTreeVirtualViewport.svelte.d.ts +35 -18
  50. package/dist/components/VTree/types.d.ts +39 -1
  51. package/llm/COMPONENT_GUIDE.md +25 -0
  52. package/package.json +45 -45
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const VTreeInteractionDemo: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type VTreeInteractionDemo = InstanceType<typeof VTreeInteractionDemo>;
18
+ export default VTreeInteractionDemo;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">import VTree from "./VTree.svelte";
2
+ const data = [
3
+ {
4
+ id: "a",
5
+ label: "Alpha"
6
+ },
7
+ {
8
+ id: "b",
9
+ label: "Beta"
10
+ },
11
+ {
12
+ id: "c",
13
+ label: "Gamma"
14
+ }
15
+ ];
16
+ let selectedItems = [];
17
+ let selectionMode = "checkbox";
18
+ </script>
19
+
20
+ <div class="space-y-2">
21
+ <select bind:value={selectionMode}>
22
+ <option value="checkbox">checkbox</option>
23
+ <option value="modifier">modifier</option>
24
+ </select>
25
+ <VTree {data} multiple {selectionMode} {selectedItems} onSelectedItemsChange={(items) => selectedItems = items} searchable={false} />
26
+ <p class="text-xs">Selected: {selectedItems.map((item) => item.label).join(', ') || 'none'}</p>
27
+ </div>
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const VTreeMultiSelectDemo: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type VTreeMultiSelectDemo = InstanceType<typeof VTreeMultiSelectDemo>;
18
+ export default VTreeMultiSelectDemo;
@@ -1,16 +1,50 @@
1
- <script lang="ts">const { row, index, rowHeight, indent, isFocused, isSelected, getNodeLabel, getNodeDescription, treeDataSnapshot, leftSection, rightSection, onselectrow, onrowdoubleclick, onrowcontext, ontogglerow } = $props();
2
- const rowClasses = $derived([
3
- "grid grid-cols-[1fr_auto] items-center select-none border-b border-surface-200-700 hover:bg-surface-100-800",
4
- isFocused ? "bg-primary-100 dark:bg-primary-900/35" : "",
5
- isSelected ? "bg-primary-200 dark:bg-primary-900/60" : ""
6
- ].join(" "));
1
+ <script lang="ts">import { isImageLikeValue, mergeClasses } from "./VTree.utils.js";
2
+ const { row, index, rowHeight, autoMeasure = false, indent, isFocused, isSelected, multiple = false, selectionMode = "modifier", isChecked = false, iconValue, icon, showRowSeparator = true, hoverEffect = true, classes, getNodeLabel, getNodeDescription, treeDataSnapshot, leftSection, rightSection, onselectrow, oncheckboxselect, onrowdoubleclick, onrowcontext, ontogglerow, onrowlongpress, onrowhover, onrowhoverend, longPressDelay = 500, longPressMoveTolerance = 10 } = $props();
3
+ const DEFAULT_CLASSES = {
4
+ row: "grid grid-cols-[1fr_auto] items-center select-none",
5
+ rowSeparator: "border-b border-surface-200-700",
6
+ rowFocused: "bg-primary-100 dark:bg-primary-900/35",
7
+ rowSelected: "bg-primary-200 dark:bg-primary-900/60",
8
+ rowHover: "hover:bg-surface-100-800",
9
+ expandIcon: "inline-flex h-5 w-5 shrink-0 items-center justify-center rounded text-surface-700-200-token hover:bg-surface-200-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500",
10
+ indentGuide: "min-w-0 flex items-center gap-1.5 pr-2 text-surface-900-100-token",
11
+ label: "truncate text-sm",
12
+ description: "truncate text-xs text-surface-500-400-token",
13
+ iconWrapper: "inline-flex h-5 w-5 shrink-0 items-center justify-center text-sm"
14
+ };
15
+ let longPressTimer;
16
+ let startX = 0;
17
+ let startY = 0;
18
+ let suppressNextClick = false;
7
19
  const sectionArgs = $derived({
8
20
  node: row.node,
9
21
  level: row.level,
10
22
  rowIndex: index,
11
23
  treeData: treeDataSnapshot
12
24
  });
13
- export {};
25
+ const rowClasses = $derived([
26
+ mergeClasses(DEFAULT_CLASSES.row, classes?.row),
27
+ showRowSeparator ? mergeClasses(DEFAULT_CLASSES.rowSeparator, classes?.rowSeparator) : "",
28
+ hoverEffect ? mergeClasses(DEFAULT_CLASSES.rowHover, classes?.rowHover) : "",
29
+ isFocused ? mergeClasses(DEFAULT_CLASSES.rowFocused, classes?.rowFocused) : "",
30
+ isSelected ? mergeClasses(DEFAULT_CLASSES.rowSelected, classes?.rowSelected) : ""
31
+ ].filter(Boolean).join(" "));
32
+ function cancelLongPress() {
33
+ clearTimeout(longPressTimer);
34
+ longPressTimer = undefined;
35
+ }
36
+ function handlePointerDown(event) {
37
+ startX = event.clientX;
38
+ startY = event.clientY;
39
+ cancelLongPress();
40
+ longPressTimer = setTimeout(() => {
41
+ suppressNextClick = true;
42
+ onrowlongpress?.(index, event);
43
+ }, longPressDelay);
44
+ }
45
+ function handlePointerMove(event) {
46
+ if (Math.hypot(event.clientX - startX, event.clientY - startY) > longPressMoveTolerance) cancelLongPress();
47
+ }
14
48
  </script>
15
49
 
16
50
  <div
@@ -20,65 +54,45 @@ export {};
20
54
  aria-expanded={row.hasChildren ? row.expanded : undefined}
21
55
  aria-selected={isSelected}
22
56
  tabindex="-1"
23
- style={`height: ${rowHeight}px;`}
24
- onclick={(event) => onselectrow?.(index, event)}
25
- ondblclick={(event) => onrowdoubleclick?.(index, event)}
26
- oncontextmenu={(event) => {
27
- event.stopPropagation();
28
- onrowcontext?.(index, event);
29
- }}
30
- onkeydown={(event) => {
31
- if (event.key !== 'Enter' && event.key !== ' ') return;
32
- event.preventDefault();
33
- onselectrow?.(index, new MouseEvent('click'));
34
- if (row.hasChildren) {
35
- ontogglerow?.(row.node);
36
- }
57
+ style={autoMeasure ? `min-height: ${rowHeight}px;` : `height: ${rowHeight}px;`}
58
+ onclick={(event) => {
59
+ if (suppressNextClick) { suppressNextClick = false; return; }
60
+ onselectrow?.(index, event);
37
61
  }}
62
+ ondblclick={(event) => onrowdoubleclick?.(index, event)}
63
+ oncontextmenu={(event) => { event.stopPropagation(); onrowcontext?.(index, event); }}
64
+ onkeydown={() => undefined}
65
+ onpointerdown={handlePointerDown}
66
+ onpointermove={handlePointerMove}
67
+ onpointerup={cancelLongPress}
68
+ onpointercancel={cancelLongPress}
69
+ onpointerenter={(event) => onrowhover?.(index, event)}
70
+ onpointerleave={(event) => { cancelLongPress(); onrowhoverend?.(index, event); }}
38
71
  >
39
- <div
40
- class="min-w-0 flex items-center gap-1.5 pr-2 text-surface-900-100-token"
41
- style={`padding-left: ${(row.level - 1) * indent}px;`}
42
- >
72
+ <div class={mergeClasses(DEFAULT_CLASSES.indentGuide, classes?.indentGuide)} style={`padding-left: ${(row.level - 1) * indent}px;`}>
43
73
  {#if leftSection}
44
- <span class="inline-flex shrink-0 items-center">{@render leftSection(sectionArgs)}</span>
74
+ <span class="inline-flex shrink-0 items-center">
75
+ {@render leftSection(sectionArgs)}
76
+ </span>
45
77
  {/if}
46
-
47
78
  {#if row.hasChildren}
48
- <button
49
- class="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded text-surface-700-200-token hover:bg-surface-200-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
50
- type="button"
51
- aria-label={row.expanded ? 'Collapse node' : 'Expand node'}
52
- onclick={(event) => {
53
- event.stopPropagation();
54
- ontogglerow?.(row.node);
55
- }}
56
- >
57
- {#if row.loading}
58
- <span
59
- class="h-3 w-3 animate-spin rounded-full border-2 border-surface-400/30 border-t-surface-700 dark:border-surface-300/30 dark:border-t-surface-100"
60
- aria-hidden="true"
61
- ></span>
62
- {:else}
63
- {row.expanded ? '▾' : '▸'}
64
- {/if}
79
+ <button class={mergeClasses(DEFAULT_CLASSES.expandIcon, classes?.expandIcon)} type="button" aria-label={row.expanded ? 'Collapse node' : 'Expand node'} onclick={(event) => { event.stopPropagation(); ontogglerow?.(row.node); }}>
80
+ {#if row.loading}<span class="h-3 w-3 animate-spin rounded-full border-2 border-surface-400/30 border-t-surface-700 dark:border-surface-300/30 dark:border-t-surface-100" aria-hidden="true"></span>{:else}{row.expanded ? '▾' : '▸'}{/if}
65
81
  </button>
66
- {:else}
67
- <span class="inline-flex h-5 w-5 shrink-0" aria-hidden="true"></span>
82
+ {:else}<span class="inline-flex h-5 w-5 shrink-0" aria-hidden="true"></span>{/if}
83
+ {#if multiple && selectionMode === 'checkbox'}
84
+ <input class="h-4 w-4 shrink-0" type="checkbox" checked={isChecked} aria-label={`Select ${getNodeLabel(row.node)}`} onclick={(event) => { event.stopPropagation(); oncheckboxselect?.(index, event); }} />
68
85
  {/if}
69
-
70
- <span class="truncate text-sm" class:font-semibold={row.matched}>{getNodeLabel(row.node)}</span>
71
- {#if getNodeDescription(row.node)}
72
- <span class="truncate text-xs text-surface-500-400-token">{getNodeDescription(row.node)}</span>
86
+ {#if icon}
87
+ <span class={mergeClasses(DEFAULT_CLASSES.iconWrapper, classes?.iconWrapper)}>{@render icon(sectionArgs)}</span>
88
+ {:else if iconValue !== undefined && iconValue !== null && iconValue !== ''}
89
+ <span class={mergeClasses(DEFAULT_CLASSES.iconWrapper, classes?.iconWrapper)}>{#if isImageLikeValue(iconValue)}<img class="h-4 w-4 object-contain" src={String(iconValue)} alt="" />{:else}{String(iconValue)}{/if}</span>
73
90
  {/if}
91
+ <span class={mergeClasses(DEFAULT_CLASSES.label, classes?.label)} class:font-semibold={row.matched}>{getNodeLabel(row.node)}</span>
92
+ {#if getNodeDescription(row.node)}<span class={mergeClasses(DEFAULT_CLASSES.description, classes?.description)}>{getNodeDescription(row.node)}</span>{/if}
74
93
  </div>
75
-
76
94
  <div class="flex items-center gap-2 pr-2">
77
- {#if row.error}
78
- <span class="text-xs text-error-500">{row.error}</span>
79
- {/if}
80
- {#if rightSection}
81
- <span class="inline-flex items-center">{@render rightSection(sectionArgs)}</span>
82
- {/if}
95
+ {#if row.error}<span class="text-xs text-error-500">{row.error}</span>{/if}
96
+ {#if rightSection}<span class="inline-flex items-center">{@render rightSection(sectionArgs)}</span>{/if}
83
97
  </div>
84
98
  </div>
@@ -1,36 +1,52 @@
1
- import type { VTreeFlatRow } from './VTree.models.js';
2
- import type { VTreeRecord } from './types.js';
3
1
  import type { Snippet } from 'svelte';
2
+ import type { VTreeFlatRow } from './VTree.models.js';
3
+ import type { VTreeClassOverrides, VTreeRecord } from './types.js';
4
4
  export interface Props<T extends VTreeRecord = VTreeRecord> {
5
5
  row: VTreeFlatRow<T>;
6
6
  index: number;
7
7
  rowHeight: number;
8
+ autoMeasure?: boolean;
8
9
  indent: number;
9
10
  isFocused: boolean;
10
11
  isSelected: boolean;
12
+ multiple?: boolean;
13
+ selectionMode?: 'modifier' | 'checkbox';
14
+ isChecked?: boolean;
15
+ iconValue?: unknown;
16
+ icon?: Snippet<[{
17
+ node: T;
18
+ level: number;
19
+ rowIndex: number;
20
+ treeData: T[];
21
+ }]>;
22
+ showRowSeparator?: boolean;
23
+ hoverEffect?: boolean;
24
+ classes?: VTreeClassOverrides;
11
25
  getNodeLabel: (node: T) => string;
12
26
  getNodeDescription: (node: T) => string;
13
27
  treeDataSnapshot: T[];
14
- leftSection?: Snippet<[
15
- {
16
- node: T;
17
- level: number;
18
- rowIndex: number;
19
- treeData: T[];
20
- }
21
- ]>;
22
- rightSection?: Snippet<[
23
- {
24
- node: T;
25
- level: number;
26
- rowIndex: number;
27
- treeData: T[];
28
- }
29
- ]>;
28
+ leftSection?: Snippet<[{
29
+ node: T;
30
+ level: number;
31
+ rowIndex: number;
32
+ treeData: T[];
33
+ }]>;
34
+ rightSection?: Snippet<[{
35
+ node: T;
36
+ level: number;
37
+ rowIndex: number;
38
+ treeData: T[];
39
+ }]>;
30
40
  onselectrow?: (index: number, event: MouseEvent) => void;
41
+ oncheckboxselect?: (index: number, event: MouseEvent) => void;
31
42
  onrowdoubleclick?: (index: number, event: MouseEvent) => void;
32
43
  onrowcontext?: (index: number, event: MouseEvent) => void;
33
44
  ontogglerow?: (node: T) => void;
45
+ onrowlongpress?: (index: number, event: PointerEvent) => void;
46
+ onrowhover?: (index: number, event: PointerEvent) => void;
47
+ onrowhoverend?: (index: number, event: PointerEvent) => void;
48
+ longPressDelay?: number;
49
+ longPressMoveTolerance?: number;
34
50
  }
35
51
  declare const VTreeRow: import("svelte").Component<Props<VTreeRecord>, {}, "">;
36
52
  type VTreeRow = ReturnType<typeof VTreeRow>;
@@ -1,14 +1,22 @@
1
1
  <script lang="ts">import { createVirtualizer } from "@tanstack/svelte-virtual";
2
2
  import VTreeRow from "./VTreeRow.svelte";
3
- const { rows, rowHeight, height, indent, overscan, focusedIndex, selectedId, loadingRoot, loadingMessage, emptyMessage, getNodeLabel, getNodeDescription, treeDataSnapshot, leftSection, rightSection, onselectrow, onrowdoubleclick, onrowcontext, onviewportcontext, ontogglerow, ontreekeydown } = $props();
3
+ import { resolveRowHeight } from "./VTree.utils.js";
4
+ const { rows, rowHeight, autoMeasure = false, height, indent, overscan, focusedIndex, selectedId, selectedNodeId, selectedItemIds = [], loadingRoot, loadingMessage, emptyMessage, multiple = false, selectionMode = "modifier", iconField, icon, classes, showRowSeparator = true, hoverEffect = true, getNodeLabel, getNodeDescription, treeDataSnapshot, leftSection, rightSection, onselectrow, oncheckboxselect, onrowdoubleclick, onrowcontext, onviewportcontext, ontogglerow, onrowlongpress, onrowhover, onrowhoverend, longPressDelay, longPressMoveTolerance, ontreekeydown } = $props();
4
5
  let viewportRef = $state(null);
5
6
  let virtualizerInstance = $state(null);
6
7
  function toCssSize(value) {
7
8
  return typeof value === "number" ? `${value}px` : value;
8
9
  }
10
+ function estimate(index) {
11
+ const row = rows[index];
12
+ return resolveRowHeight(rowHeight, row?.node, row?.level ?? 1, index);
13
+ }
9
14
  export function scrollToIndex(index) {
10
15
  virtualizerInstance?.scrollToIndex(index, { align: "auto" });
11
16
  }
17
+ function measure(node) {
18
+ if (autoMeasure) virtualizerInstance?.measureElement(node);
19
+ }
12
20
  $effect(() => {
13
21
  if (!viewportRef) {
14
22
  virtualizerInstance = null;
@@ -16,7 +24,7 @@ $effect(() => {
16
24
  }
17
25
  const virtualizer = createVirtualizer({
18
26
  count: rows.length,
19
- estimateSize: () => rowHeight,
27
+ estimateSize: estimate,
20
28
  overscan,
21
29
  getScrollElement: () => viewportRef
22
30
  });
@@ -28,7 +36,7 @@ $effect(() => {
28
36
  if (!virtualizerInstance || !viewportRef) return;
29
37
  virtualizerInstance.setOptions({
30
38
  count: rows.length,
31
- estimateSize: () => rowHeight,
39
+ estimateSize: estimate,
32
40
  overscan,
33
41
  getScrollElement: () => viewportRef
34
42
  });
@@ -37,49 +45,21 @@ const virtualItems = $derived(virtualizerInstance?.getVirtualItems() ?? []);
37
45
  const totalSize = $derived(virtualizerInstance?.getTotalSize() ?? 0);
38
46
  </script>
39
47
 
40
- <div
41
- class="card overflow-auto border border-surface-300-700 bg-surface-50-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
42
- bind:this={viewportRef}
43
- style={`height: ${toCssSize(height)};`}
44
- onkeydown={(event) => ontreekeydown?.(event)}
45
- oncontextmenu={(event) => onviewportcontext?.(event)}
46
- tabindex="0"
47
- role="tree"
48
- aria-label="Virtual tree"
49
- >
48
+ <div class="card overflow-auto border border-surface-300-700 bg-surface-50-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500" bind:this={viewportRef} style={`height: ${toCssSize(height)};`} onkeydown={(event) => ontreekeydown?.(event)} oncontextmenu={(event) => onviewportcontext?.(event)} tabindex="0" role="tree" aria-label="Virtual tree">
50
49
  {#if loadingRoot}
51
50
  <div class="grid min-h-24 place-items-center px-3 text-sm text-surface-500-400-token">{loadingMessage}</div>
52
51
  {:else if rows.length === 0}
53
52
  <div class="grid min-h-24 place-items-center px-3 text-sm text-surface-500-400-token">{emptyMessage}</div>
54
53
  {:else}
55
54
  <div class="relative w-full" style={`height: ${totalSize}px;`}>
56
- <div
57
- class="absolute left-0 top-0 w-full"
58
- style={`transform: translateY(${virtualItems[0]?.start ?? 0}px);`}
59
- >
60
- {#each virtualItems as item (item.key)}
61
- {@const row = rows[item.index]}
62
- {#if row}
63
- <VTreeRow
64
- row={row}
65
- index={item.index}
66
- {rowHeight}
67
- {indent}
68
- isFocused={item.index === focusedIndex}
69
- isSelected={row.id === selectedId}
70
- {getNodeLabel}
71
- {getNodeDescription}
72
- {treeDataSnapshot}
73
- {leftSection}
74
- {rightSection}
75
- {onselectrow}
76
- {onrowdoubleclick}
77
- {onrowcontext}
78
- {ontogglerow}
79
- />
80
- {/if}
81
- {/each}
82
- </div>
55
+ {#each virtualItems as item (item.key)}
56
+ {@const row = rows[item.index]}
57
+ {#if row}
58
+ <div class="absolute left-0 top-0 w-full" style={`transform: translateY(${item.start}px);`} {@attach measure}>
59
+ <VTreeRow row={row} index={item.index} rowHeight={estimate(item.index)} {autoMeasure} {indent} isFocused={item.index === focusedIndex} isSelected={row.id === (selectedNodeId || selectedId)} {multiple} {selectionMode} isChecked={selectedItemIds.includes(row.id)} iconValue={iconField ? row.node[iconField] : undefined} {icon} {classes} {showRowSeparator} {hoverEffect} {getNodeLabel} {getNodeDescription} {treeDataSnapshot} {leftSection} {rightSection} {onselectrow} {oncheckboxselect} {onrowdoubleclick} {onrowcontext} {ontogglerow} {onrowlongpress} {onrowhover} {onrowhoverend} {longPressDelay} {longPressMoveTolerance} />
60
+ </div>
61
+ {/if}
62
+ {/each}
83
63
  </div>
84
64
  {/if}
85
65
  </div>
@@ -1,41 +1,58 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { VTreeFlatRow } from './VTree.models.js';
3
- import type { VTreeRecord } from './types.js';
3
+ import type { VTreeClassOverrides, VTreeRecord } from './types.js';
4
4
  export interface Props<T extends VTreeRecord = VTreeRecord> {
5
5
  rows: VTreeFlatRow<T>[];
6
- rowHeight: number;
6
+ rowHeight: number | ((node: T, level: number, index: number) => number);
7
+ autoMeasure?: boolean;
7
8
  height: string | number;
8
9
  indent: number;
9
10
  overscan: number;
10
11
  focusedIndex: number;
11
12
  selectedId: string | null;
13
+ selectedNodeId?: string;
14
+ selectedItemIds?: string[];
12
15
  loadingRoot: boolean;
13
16
  loadingMessage: string;
14
17
  emptyMessage: string;
18
+ multiple?: boolean;
19
+ selectionMode?: 'modifier' | 'checkbox';
20
+ iconField?: keyof T & string;
21
+ icon?: Snippet<[{
22
+ node: T;
23
+ level: number;
24
+ rowIndex: number;
25
+ treeData: T[];
26
+ }]>;
27
+ classes?: VTreeClassOverrides;
28
+ showRowSeparator?: boolean;
29
+ hoverEffect?: boolean;
15
30
  getNodeLabel: (node: T) => string;
16
31
  getNodeDescription: (node: T) => string;
17
32
  treeDataSnapshot: T[];
18
- leftSection?: Snippet<[
19
- {
20
- node: T;
21
- level: number;
22
- rowIndex: number;
23
- treeData: T[];
24
- }
25
- ]>;
26
- rightSection?: Snippet<[
27
- {
28
- node: T;
29
- level: number;
30
- rowIndex: number;
31
- treeData: T[];
32
- }
33
- ]>;
33
+ leftSection?: Snippet<[{
34
+ node: T;
35
+ level: number;
36
+ rowIndex: number;
37
+ treeData: T[];
38
+ }]>;
39
+ rightSection?: Snippet<[{
40
+ node: T;
41
+ level: number;
42
+ rowIndex: number;
43
+ treeData: T[];
44
+ }]>;
34
45
  onselectrow?: (index: number, event: MouseEvent) => void;
46
+ oncheckboxselect?: (index: number, event: MouseEvent) => void;
35
47
  onrowdoubleclick?: (index: number, event: MouseEvent) => void;
36
48
  onrowcontext?: (index: number, event: MouseEvent) => void;
37
49
  onviewportcontext?: (event: MouseEvent) => void;
38
50
  ontogglerow?: (node: T) => void;
51
+ onrowlongpress?: (index: number, event: PointerEvent) => void;
52
+ onrowhover?: (index: number, event: PointerEvent) => void;
53
+ onrowhoverend?: (index: number, event: PointerEvent) => void;
54
+ longPressDelay?: number;
55
+ longPressMoveTolerance?: number;
39
56
  ontreekeydown?: (event: KeyboardEvent) => void;
40
57
  }
41
58
  declare const VTreeVirtualViewport: import("svelte").Component<Props<VTreeRecord>, {
@@ -45,7 +45,8 @@ export interface VTreeProps<T extends VTreeRecord = VTreeRecord> {
45
45
  childrenField?: keyof T & string;
46
46
  hasChildrenField?: keyof T & string;
47
47
  height?: number | string;
48
- rowHeight?: number;
48
+ rowHeight?: number | ((node: T, level: number, index: number) => number);
49
+ autoMeasure?: boolean;
49
50
  indent?: number;
50
51
  overscan?: number;
51
52
  searchable?: boolean;
@@ -80,4 +81,41 @@ export interface VTreeProps<T extends VTreeRecord = VTreeRecord> {
80
81
  ontoggle?: (node: T, expanded: boolean) => void;
81
82
  onsearch?: (query: string) => void;
82
83
  onloaderror?: (error: unknown, node?: T) => void;
84
+ iconField?: keyof T & string;
85
+ icon?: Snippet<[{
86
+ node: T;
87
+ level: number;
88
+ rowIndex: number;
89
+ treeData: T[];
90
+ }]>;
91
+ classes?: VTreeClassOverrides;
92
+ showRowSeparator?: boolean;
93
+ hoverEffect?: boolean;
94
+ /** Controlled single-selection. Undefined means uncontrolled; null means explicitly empty. */
95
+ selected?: T | null;
96
+ /** Enables selectedItems/onSelectedItemsChange multi-selection. */
97
+ multiple?: boolean;
98
+ selectionMode?: 'modifier' | 'checkbox';
99
+ onSelectedChange?: (node: T | null) => void;
100
+ selectedItems?: T[];
101
+ onSelectedItemsChange?: (items: T[]) => void;
102
+ onsingleclick?: (node: T, treeData: T[], event: MouseEvent) => void;
103
+ singleClickDelay?: number;
104
+ onlongpress?: (node: T, treeData: T[], event: PointerEvent) => void;
105
+ longPressDelay?: number;
106
+ longPressMoveTolerance?: number;
107
+ onrowhover?: (node: T, treeData: T[], rowIndex: number, event: PointerEvent) => void;
108
+ onrowhoverend?: (node: T, treeData: T[], rowIndex: number, event: PointerEvent) => void;
109
+ }
110
+ export interface VTreeClassOverrides {
111
+ row?: string;
112
+ rowSeparator?: string;
113
+ rowFocused?: string;
114
+ rowSelected?: string;
115
+ rowHover?: string;
116
+ expandIcon?: string;
117
+ indentGuide?: string;
118
+ label?: string;
119
+ description?: string;
120
+ iconWrapper?: string;
83
121
  }
@@ -130,6 +130,30 @@ The project now contains a mix of simple components, folder-based component modu
130
130
  </script>
131
131
  ```
132
132
 
133
+ **⚠️ Guardrail — never read and write the same `$state` inside one effect**
134
+
135
+ ```svelte
136
+ <script lang="ts">
137
+ // ❌ Infinite loop: .filter() always returns a NEW array reference, even
138
+ // when nothing was removed. The effect tracks its own write target, sees
139
+ // a "changed" value on every run, and reruns forever — Svelte throws
140
+ // effect_update_depth_exceeded ("reads and writes the same piece of state").
141
+ $effect(() => {
142
+ selected = selected.filter((item) => validIds.has(item.id));
143
+ });
144
+
145
+ // ✅ Read the current value via untrack() so the effect doesn't depend on
146
+ // its own write target, and only write when something actually changed.
147
+ $effect(() => {
148
+ const current = untrack(() => selected);
149
+ const next = current.filter((item) => validIds.has(item.id));
150
+ if (next.length !== current.length) selected = next;
151
+ });
152
+ </script>
153
+ ```
154
+
155
+ Same hazard applies to any derived-and-reassigned value, not just `.filter()`: object/array spreads (`{...x}`, `[...x]`), `new Set(x)` / `new Map(x)`, `.map()`, `.sort()` copies — anything producing a new reference even when the content is unchanged. Real-world case: `CardGrid.svelte`'s "Reconcile selection" effect.
156
+
133
157
  #### Snippet - Children/Slots
134
158
  ```svelte
135
159
  <script lang="ts">
@@ -1211,6 +1235,7 @@ Before considering a component complete:
1211
1235
  - [ ] Focus management is correct
1212
1236
  - [ ] Documented in stories (autodocs)
1213
1237
  - [ ] Re-exported from `src/lib/components/index.ts` and therefore reachable from `src/lib/index.ts`
1238
+ - [ ] No `$effect` reads and writes the same `$state` without `untrack()` / a change-guard (see $effect Guardrail above)
1214
1239
 
1215
1240
  ## Advanced Patterns
1216
1241