@bimetal/tree-svelte-components 0.35.0 → 0.36.0
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/Tree.svelte +49 -10
- package/dist/Tree.svelte.d.ts +2 -1
- package/dist/Tree.svelte.d.ts.map +1 -1
- package/dist/styles/index.css.d.ts +3 -0
- package/package.json +13 -10
package/dist/Tree.svelte
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { onDestroy, untrack, type Snippet } from 'svelte';
|
|
3
3
|
import { createTreeControllerBinding } from '@bimetal/svelte';
|
|
4
4
|
import type { TreeStore } from '@bimetal/tree-data';
|
|
5
|
-
import type { TreeConfigInput, TreeController, TreeControllerSnapshot, TreeRowView } from '@bimetal/tree-headless';
|
|
6
|
-
import { treeRowIndent, treeCssVars } from '@bimetal/tree-headless';
|
|
5
|
+
import type { TreeConfigInput, TreeController, TreeControllerSnapshot, TreeRowView , TreeControllerOptions } from '@bimetal/tree-headless';
|
|
6
|
+
import { treeRowIndent, treeCssVars, reportUnroutedTreeError } from '@bimetal/tree-headless';
|
|
7
7
|
import TreeDialog from './TreeDialog.svelte';
|
|
8
8
|
import { isEditableTarget } from '@bimetal/a11y-dom';
|
|
9
9
|
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
onError = undefined,
|
|
30
30
|
onSelectionChange = undefined,
|
|
31
31
|
onCheckedChange = undefined,
|
|
32
|
+
loadChildren = undefined,
|
|
32
33
|
renderNode = undefined,
|
|
33
34
|
}: {
|
|
34
35
|
/**
|
|
@@ -63,19 +64,51 @@
|
|
|
63
64
|
/** OBSERVE hook (standalone only): the SECOND independent axis — the checkbox
|
|
64
65
|
* axis, not a dedup of selection. Fires when the checked set changes. */
|
|
65
66
|
onCheckedChange?: (checkedIds: readonly string[]) => void;
|
|
67
|
+
loadChildren?: TreeControllerOptions['loadChildren'];
|
|
66
68
|
/** Replace ONLY a row's content cell (icon/label/badge). */
|
|
67
69
|
renderNode?: Snippet<[TreeRowView]>;
|
|
68
70
|
} = $props();
|
|
69
71
|
|
|
70
72
|
// Dual-API: bound path uses the given controller (binding = null → never
|
|
71
73
|
// created/destroyed/setConfig here); standalone creates one ONCE under `untrack`.
|
|
72
|
-
|
|
74
|
+
// bimetal-181: der GETTER ist die Aussage — er liest die `$props()`-Bindungen bei
|
|
75
|
+
// jedem Callback-Aufruf erneut, statt den Stand des `untrack`-Aufbaus festzunageln.
|
|
76
|
+
const binding = untrack(() => (controller ? null : createTreeControllerBinding(() => ({ store: store!, treeId: treeId!, config, onError: (err) => (onError ?? reportUnroutedTreeError)(err), onSelectionChange, onCheckedChange, loadChildren }))));
|
|
73
77
|
const ctrl = controller ?? binding!.ctrl;
|
|
74
78
|
// Snapshot store — sourced from `ctrl` so it works for both API halves (both are
|
|
75
79
|
// a `SnapshotSource`); the bound path never touches the owned controller's lifecycle.
|
|
76
80
|
const snapshotStore = { subscribe: (run: (s: TreeControllerSnapshot) => void) => { run(ctrl.getSnapshot()); return ctrl.subscribe(() => run(ctrl.getSnapshot())); } };
|
|
77
81
|
|
|
78
82
|
let listEl: HTMLDivElement | null = $state(null);
|
|
83
|
+
|
|
84
|
+
// Virtualization: REPORT the viewport; the controller owns the window math
|
|
85
|
+
// (mcp-60, list parity).
|
|
86
|
+
$effect(() => {
|
|
87
|
+
const el = listEl;
|
|
88
|
+
if (!el) return;
|
|
89
|
+
const report = () => ctrl.setViewport(el.scrollTop, el.clientHeight);
|
|
90
|
+
report();
|
|
91
|
+
el.addEventListener('scroll', report, { passive: true });
|
|
92
|
+
// M1 (mcp-60 review): a windowed-out FOCUSED row drops DOM focus to
|
|
93
|
+
// <body> mid-interaction; relatedTarget-null focusout is ambiguous at
|
|
94
|
+
// dispatch time — a microtask later the unmounted row is disconnected.
|
|
95
|
+
// Fall back onto the container (tabindex -1, the keydown host).
|
|
96
|
+
let focusInside = false;
|
|
97
|
+
const onFocusIn = () => { focusInside = true; };
|
|
98
|
+
const onFocusOut = (e: FocusEvent) => {
|
|
99
|
+
if (e.relatedTarget instanceof Node) { focusInside = el.contains(e.relatedTarget); return; }
|
|
100
|
+
const target = e.target as HTMLElement;
|
|
101
|
+
queueMicrotask(() => {
|
|
102
|
+
if (target.isConnected) { focusInside = false; return; }
|
|
103
|
+
if (focusInside && document.activeElement === document.body) el.focus({ preventScroll: true });
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
el.addEventListener('focusin', onFocusIn);
|
|
107
|
+
el.addEventListener('focusout', onFocusOut);
|
|
108
|
+
const ro = new ResizeObserver(report);
|
|
109
|
+
ro.observe(el);
|
|
110
|
+
return () => { el.removeEventListener('scroll', report); el.removeEventListener('focusin', onFocusIn); el.removeEventListener('focusout', onFocusOut); ro.disconnect(); };
|
|
111
|
+
});
|
|
79
112
|
let suppressClick = false;
|
|
80
113
|
|
|
81
114
|
const onKey = (e: KeyboardEvent) =>
|
|
@@ -138,10 +171,12 @@
|
|
|
138
171
|
{#if $snapshotStore}
|
|
139
172
|
{@const snap = $snapshotStore}
|
|
140
173
|
{@const domFocus = snap.navAnchor}
|
|
141
|
-
<div class=
|
|
174
|
+
<div class={'bm-tree' + (snap.virtual ? ' bm-tree--virtual' : '')} data-theme={darkMode ? 'dark' : undefined} dir={dir} style={cssVars(snap)}>
|
|
142
175
|
<div class="bm-tree__toolbar">
|
|
143
176
|
{#if title}<h1 class="bm-tree__title">{title}</h1>{/if}
|
|
144
177
|
{#if tag}<span class="bm-tree__tag">{tag}</span>{/if}
|
|
178
|
+
<!-- mcp-64: Mutations-Affordanzen fallen im View-Only-Modus (kein toter Knopf). -->
|
|
179
|
+
{#if !snap.config.readOnly}
|
|
145
180
|
<button class="bm-tree__btn bm-tree__btn--primary" type="button" disabled={snap.focusId === null} onclick={() => ctrl.voidAddChild(snap.focusId)}>+ {snap.config.locale.addChild}</button>
|
|
146
181
|
<button class="bm-tree__btn" type="button" disabled={snap.focusId === null} onclick={() => snap.focusId && ctrl.voidAddSibling(snap.focusId)}>+ {snap.config.locale.addSibling}</button>
|
|
147
182
|
<button class="bm-tree__btn" type="button" disabled={snap.focusId === null} onclick={(e) => snap.focusId && ctrl.openEdit(snap.focusId, rect(e))}>{snap.config.locale.rename}</button>
|
|
@@ -151,15 +186,17 @@
|
|
|
151
186
|
<button class="bm-tree__btn bm-tree__btn--undo" type="button" aria-label={snap.config.locale.undo} title={snap.config.locale.undo} disabled={!snap.canUndo} onclick={() => ctrl.voidUndo()}>↶</button>
|
|
152
187
|
<button class="bm-tree__btn bm-tree__btn--redo" type="button" aria-label={snap.config.locale.redo} title={snap.config.locale.redo} disabled={!snap.canRedo} onclick={() => ctrl.voidRedo()}>↷</button>
|
|
153
188
|
{/if}
|
|
189
|
+
{/if}
|
|
154
190
|
<button class="bm-tree__btn" type="button" onclick={() => ctrl.expandAll()}>{snap.config.locale.expandAll}</button>
|
|
155
191
|
<button class="bm-tree__btn" type="button" onclick={() => ctrl.collapseAll()}>{snap.config.locale.collapseAll}</button>
|
|
156
|
-
<input class="bm-tree__search" type="search" placeholder={snap.config.locale.search} value={snap.filterQuery}
|
|
192
|
+
<input class="bm-tree__search" type="search" placeholder={(snap.config.locale.searchPlaceholder ?? snap.config.locale.search)} value={snap.filterQuery}
|
|
157
193
|
oninput={(e) => ctrl.setFilter((e.target as HTMLInputElement).value)} />
|
|
158
194
|
</div>
|
|
159
195
|
|
|
160
196
|
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
|
161
|
-
<div class="bm-tree__list" role="tree" aria-label={ariaLabel ?? title ?? snap.config.locale.label} bind:this={listEl} onkeydown={onKey}>
|
|
162
|
-
{#
|
|
197
|
+
<div class="bm-tree__list" tabindex="-1" role="tree" aria-label={ariaLabel ?? title ?? snap.config.locale.label} bind:this={listEl} onkeydown={onKey}>
|
|
198
|
+
{#if snap.virtual && snap.virtual.offsetTop > 0}<div class="bm-tree__spacer" aria-hidden="true" style="height: {snap.virtual.offsetTop}px"></div>{/if}
|
|
199
|
+
{#each (snap.virtual ? snap.virtual.rows : snap.rows) as r (r.node.id)}
|
|
163
200
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
164
201
|
<div class={rowClass(snap, r)}
|
|
165
202
|
data-node-id={r.node.id}
|
|
@@ -168,13 +205,14 @@
|
|
|
168
205
|
aria-posinset={r.posinset}
|
|
169
206
|
aria-setsize={r.setsize}
|
|
170
207
|
aria-selected={r.selected}
|
|
208
|
+
aria-busy={r.loading || undefined}
|
|
171
209
|
aria-expanded={r.hasChildren ? r.expanded : undefined}
|
|
172
210
|
aria-disabled={r.meta.disabled || undefined}
|
|
173
211
|
tabindex={r.node.id === domFocus ? 0 : -1}
|
|
174
|
-
style="padding-
|
|
212
|
+
style="padding-inline-start:{treeRowIndent(r.depth, snap) + 8}px"
|
|
175
213
|
onpointerdown={(e) => onRowPointerDown(e, r.node.id)}
|
|
176
214
|
onclick={(e) => onRowClick(e, r.node.id)}>
|
|
177
|
-
<span class={'bm-tree__chevron' + (r.hasChildren ? (r.expanded ? ' bm-tree__chevron--expanded' : '') : ' bm-tree__chevron--leaf')}>▶</span>
|
|
215
|
+
<span class={'bm-tree__chevron' + (r.hasChildren ? (r.expanded ? ' bm-tree__chevron--expanded' : '') : ' bm-tree__chevron--leaf') + (r.loading ? ' bm-tree__chevron--loading' : '')}>▶</span>
|
|
178
216
|
{#if snap.config.checkboxes}
|
|
179
217
|
<span class={checkboxClass(r.checkState)} role="checkbox" aria-checked={r.checkState === 'indeterminate' ? 'mixed' : r.checkState === 'checked'} aria-label={snap.config.locale.check}></span>
|
|
180
218
|
{/if}
|
|
@@ -185,6 +223,7 @@
|
|
|
185
223
|
{/if}
|
|
186
224
|
</div>
|
|
187
225
|
{/each}
|
|
226
|
+
{#if snap.virtual && snap.virtual.offsetBottom > 0}<div class="bm-tree__spacer" aria-hidden="true" style="height: {snap.virtual.offsetBottom}px"></div>{/if}
|
|
188
227
|
</div>
|
|
189
228
|
|
|
190
229
|
{#if snap.dialog}
|
|
@@ -195,7 +234,7 @@
|
|
|
195
234
|
{/if}
|
|
196
235
|
|
|
197
236
|
{#if snap.lastAction}
|
|
198
|
-
<div class="bm-tree__toast" role=
|
|
237
|
+
<div class="bm-tree__toast{snap.lastAction.kind === 'error' ? ' bm-tree__toast--error' : ''}" role={snap.lastAction.kind === 'error' ? 'alert' : 'status'}>
|
|
199
238
|
<span class="bm-tree__toast-msg">{snap.lastAction.description}</span>
|
|
200
239
|
<button class="bm-tree__toast-dismiss" type="button" onclick={() => ctrl.dismissUndo()}>×</button>
|
|
201
240
|
</div>
|
package/dist/Tree.svelte.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Snippet } from 'svelte';
|
|
2
2
|
import type { TreeStore } from '@bimetal/tree-data';
|
|
3
|
-
import type { TreeConfigInput, TreeController, TreeRowView } from '@bimetal/tree-headless';
|
|
3
|
+
import type { TreeConfigInput, TreeController, TreeRowView, TreeControllerOptions } from '@bimetal/tree-headless';
|
|
4
4
|
type $$ComponentProps = {
|
|
5
5
|
/**
|
|
6
6
|
* BOUND (composition contract, F1): an already-owned controller. The component
|
|
@@ -34,6 +34,7 @@ type $$ComponentProps = {
|
|
|
34
34
|
/** OBSERVE hook (standalone only): the SECOND independent axis — the checkbox
|
|
35
35
|
* axis, not a dedup of selection. Fires when the checked set changes. */
|
|
36
36
|
onCheckedChange?: (checkedIds: readonly string[]) => void;
|
|
37
|
+
loadChildren?: TreeControllerOptions['loadChildren'];
|
|
37
38
|
/** Replace ONLY a row's content cell (icon/label/badge). */
|
|
38
39
|
renderNode?: Snippet<[TreeRowView]>;
|
|
39
40
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tree.svelte.d.ts","sourceRoot":"","sources":["../src/Tree.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAE1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAA0B,WAAW,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"Tree.svelte.d.ts","sourceRoot":"","sources":["../src/Tree.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAE1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAA0B,WAAW,EAAG,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAO1I,KAAK,gBAAgB,GAAI;IACtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,8EAA8E;IAC9E,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gGAAgG;IAChG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;mFAC+E;IAC/E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iFAAiF;IACjF,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACjC,oGAAoG;IACpG,iBAAiB,CAAC,EAAE,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;IAC7D;8EAC0E;IAC1E,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;IAC1D,YAAY,CAAC,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAC;IACrD,4DAA4D;IAC5D,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;CACrC,CAAC;AAmMJ,QAAA,MAAM,IAAI,sDAAwC,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bimetal/tree-svelte-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "Default Svelte 5 tree / treeview — multi-select, expand/collapse, tri-state checkboxes, drag-reorder, rename, undo, theming. A thin binding over the canonical tree controller.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"svelte": "./dist/index.js",
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
"svelte": "./dist/index.js",
|
|
13
13
|
"import": "./dist/index.js"
|
|
14
14
|
},
|
|
15
|
-
"./styles":
|
|
15
|
+
"./styles": {
|
|
16
|
+
"types": "./dist/styles/index.css.d.ts",
|
|
17
|
+
"default": "./dist/styles/index.css"
|
|
18
|
+
}
|
|
16
19
|
},
|
|
17
20
|
"files": [
|
|
18
21
|
"dist",
|
|
@@ -20,25 +23,25 @@
|
|
|
20
23
|
"LICENSE"
|
|
21
24
|
],
|
|
22
25
|
"scripts": {
|
|
23
|
-
"build": "svelte-package -i src -o dist && node scripts/copy-css.mjs",
|
|
26
|
+
"build": "svelte-package -i src -o dist && node scripts/copy-css.mjs && node ../../../scripts/generate-css-stubs.mjs",
|
|
24
27
|
"dev": "svelte-package -i src -o dist --watch",
|
|
25
|
-
"typecheck": "svelte-check --tsconfig ./tsconfig.json",
|
|
28
|
+
"typecheck": "svelte-check --tsconfig ./tsconfig.test.json",
|
|
26
29
|
"test": "vitest run",
|
|
27
30
|
"prepublishOnly": "npm run build"
|
|
28
31
|
},
|
|
29
32
|
"dependencies": {
|
|
30
|
-
"@bimetal/svelte": "^0.
|
|
31
|
-
"@bimetal/tree-data": "^0.
|
|
32
|
-
"@bimetal/tree-headless": "^0.
|
|
33
|
-
"@bimetal/tree-themes": "^0.
|
|
34
|
-
"@bimetal/a11y-dom": "^0.
|
|
33
|
+
"@bimetal/svelte": "^0.36.0",
|
|
34
|
+
"@bimetal/tree-data": "^0.36.0",
|
|
35
|
+
"@bimetal/tree-headless": "^0.36.0",
|
|
36
|
+
"@bimetal/tree-themes": "^0.36.0",
|
|
37
|
+
"@bimetal/a11y-dom": "^0.36.0"
|
|
35
38
|
},
|
|
36
39
|
"peerDependencies": {
|
|
37
40
|
"svelte": "^5.46.4"
|
|
38
41
|
},
|
|
39
42
|
"devDependencies": {
|
|
40
43
|
"@sveltejs/package": "^2.5.0",
|
|
41
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
44
|
+
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
42
45
|
"@testing-library/svelte": "^5.2.7",
|
|
43
46
|
"happy-dom": "^18.0.1",
|
|
44
47
|
"svelte": "^5.46.4",
|