@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.
- package/dist/components/Boxer/Boxer.svelte +5 -1
- package/dist/components/CardGrid/CardGrid.svelte +50 -4
- package/dist/components/CardGrid/CardGrid.svelte.d.ts +4 -0
- package/dist/components/CardGrid/DefaultCard.svelte +7 -2
- package/dist/components/CardGrid/DefaultCard.svelte.d.ts +1 -0
- package/dist/components/CardGrid/cardGrid.d.ts +24 -0
- package/dist/components/CardGrid/cardGrid.js +20 -0
- package/dist/components/Former/FormerDrawer.svelte +10 -22
- package/dist/components/Former/FormerModal.svelte +10 -22
- package/dist/components/Former/formerOverlayHelpers.svelte.d.ts +11 -0
- package/dist/components/Former/formerOverlayHelpers.svelte.js +38 -0
- package/dist/components/Former/index.d.ts +1 -0
- package/dist/components/Former/index.js +1 -0
- package/dist/components/FormerControllers/ButtonCtrl.svelte +11 -16
- package/dist/components/FormerControllers/CtrlFieldShell.svelte +15 -0
- package/dist/components/FormerControllers/CtrlFieldShell.svelte.d.ts +12 -0
- package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +6 -13
- package/dist/components/FormerControllers/IconButtonCtrl.svelte +11 -16
- package/dist/components/FormerControllers/NativeSelectCtrl.svelte +7 -13
- package/dist/components/FormerControllers/NumberInputCtrl.svelte +18 -14
- package/dist/components/FormerControllers/PasswordInputCtrl.svelte +7 -13
- package/dist/components/FormerControllers/SwitchCtrl.svelte +4 -3
- package/dist/components/FormerControllers/TextAreaCtrl.svelte +7 -13
- package/dist/components/FormerControllers/TextInputCtrl.svelte +7 -13
- package/dist/components/FormerControllers/ctrlLoading.svelte.d.ts +8 -0
- package/dist/components/FormerControllers/ctrlLoading.svelte.js +43 -0
- package/dist/components/FormerControllers/index.d.ts +2 -0
- package/dist/components/FormerControllers/index.js +2 -0
- package/dist/components/FormerControllers/utils.d.ts +1 -0
- package/dist/components/FormerControllers/utils.js +3 -0
- package/dist/components/Gridler/components/Gridler.svelte +10 -4
- package/dist/components/Gridler/components/GridlerFull.svelte +2 -1
- package/dist/components/Gridler/utils/serverPaging.d.ts +1 -0
- package/dist/components/Gridler/utils/serverPaging.js +7 -0
- package/dist/components/VTree/VTree.svelte +107 -2
- package/dist/components/VTree/VTree.utils.d.ts +23 -0
- package/dist/components/VTree/VTree.utils.js +73 -0
- package/dist/components/VTree/VTreeControlledSelectionDemo.svelte +20 -0
- package/dist/components/VTree/VTreeControlledSelectionDemo.svelte.d.ts +18 -0
- package/dist/components/VTree/VTreeCustomizationDemo.svelte +23 -0
- package/dist/components/VTree/VTreeCustomizationDemo.svelte.d.ts +18 -0
- package/dist/components/VTree/VTreeInteractionDemo.svelte +29 -0
- package/dist/components/VTree/VTreeInteractionDemo.svelte.d.ts +18 -0
- package/dist/components/VTree/VTreeMultiSelectDemo.svelte +27 -0
- package/dist/components/VTree/VTreeMultiSelectDemo.svelte.d.ts +18 -0
- package/dist/components/VTree/VTreeRow.svelte +71 -57
- package/dist/components/VTree/VTreeRow.svelte.d.ts +34 -18
- package/dist/components/VTree/VTreeVirtualViewport.svelte +20 -40
- package/dist/components/VTree/VTreeVirtualViewport.svelte.d.ts +35 -18
- package/dist/components/VTree/types.d.ts +39 -1
- package/llm/COMPONENT_GUIDE.md +25 -0
- 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">
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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) =>
|
|
25
|
-
|
|
26
|
-
|
|
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">
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
{
|
|
72
|
-
<span class="
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
}
|
package/llm/COMPONENT_GUIDE.md
CHANGED
|
@@ -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
|
|