@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 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
- const binding = untrack(() => (controller ? null : createTreeControllerBinding({ store: store!, treeId: treeId!, config, onError: (err) => onError?.(err), onSelectionChange, onCheckedChange })));
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="bm-tree" data-theme={darkMode ? 'dark' : undefined} dir={dir} style={cssVars(snap)}>
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
- {#each snap.rows as r (r.node.id)}
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-left:{treeRowIndent(r.depth, snap) + 8}px"
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="status">
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>
@@ -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;AAOlH,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,4DAA4D;IAC5D,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;CACrC,CAAC;AA8JJ,QAAA,MAAM,IAAI,sDAAwC,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,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"}
@@ -0,0 +1,3 @@
1
+ // Generated by scripts/generate-css-stubs.mjs — DO NOT EDIT.
2
+ // Type-stub for side-effect CSS import (TS moduleResolution: nodenext/bundler).
3
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bimetal/tree-svelte-components",
3
- "version": "0.35.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": "./dist/styles/index.css"
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.35.0",
31
- "@bimetal/tree-data": "^0.35.0",
32
- "@bimetal/tree-headless": "^0.35.0",
33
- "@bimetal/tree-themes": "^0.35.0",
34
- "@bimetal/a11y-dom": "^0.35.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": "^5.0.3",
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",