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

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.
@@ -0,0 +1,142 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/keenmate/schemas/main/component-variables.schema.json",
3
+ "component": "@keenmate/svelte-treeview",
4
+ "prefix": "ltree",
5
+ "baseVariables": [
6
+ { "name": "base-accent-color", "required": true, "usage": "Primary accent — selection, drop-zones, focus rings, debug panel. Tints derived via color-mix()." },
7
+ { "name": "base-success-color", "required": false, "usage": "Drop-valid border and dragover highlight" },
8
+ { "name": "base-danger-color", "required": false, "usage": "Drop-invalid border, danger context-menu items, scroll-highlight arrows" },
9
+ { "name": "base-main-bg", "required": false, "usage": "Used for --ltree-light (context-menu hover tint, debug stat chips)" },
10
+ { "name": "base-border-color", "required": true, "usage": "Default border color (checkbox, context-menu, spinner track)" },
11
+ { "name": "base-hover-bg", "required": false, "usage": "Node row hover background" },
12
+ { "name": "base-text-color-1", "required": true, "usage": "Primary body text color (node labels, context-menu items)" },
13
+ { "name": "base-text-color-3", "required": false, "usage": "Muted text (path, toggle icon, loading 'more', node-path)" },
14
+ { "name": "base-text-color-4", "required": false, "usage": "Subtle text (context-menu shortcut, submenu arrow, divider label)" },
15
+ { "name": "base-text-color-on-accent", "required": false, "usage": "Text on accent backgrounds — checkbox checkmark color" },
16
+ { "name": "base-input-bg", "required": false, "usage": "Checkbox background (unchecked)" },
17
+ { "name": "base-dropdown-bg", "required": false, "usage": "Context-menu and submenu background" },
18
+ { "name": "base-dropdown-box-shadow", "required": false, "usage": "Context-menu shadow" },
19
+ { "name": "base-font-family", "required": false, "usage": "All text in component" },
20
+ { "name": "base-font-size-xs", "required": false, "usage": "Small text — node path, context-menu shortcut, icon font-size (multiplier × --ltree-rem)" },
21
+ { "name": "base-font-size-sm", "required": false, "usage": "Default node text and context-menu items (multiplier × --ltree-rem)" },
22
+ { "name": "base-font-weight-medium", "required": false, "usage": "Node label weight" },
23
+ { "name": "base-border-radius-sm", "required": false, "usage": "Checkbox, context-menu, node-content border radius (multiplier × --ltree-rem)" }
24
+ ],
25
+ "componentVariables": [
26
+ { "name": "ltree-rem", "category": "sizing", "usage": "Base sizing unit for proportional scaling (default 10px). Set to 1rem to scale with document font-size." },
27
+
28
+ { "name": "ltree-primary", "category": "color", "usage": "Primary accent color (selection, focus, links). Tints derived via color-mix()." },
29
+ { "name": "ltree-success", "category": "color", "usage": "Success color (drop-valid border, dragover-highlight)" },
30
+ { "name": "ltree-danger", "category": "color", "usage": "Danger color (drop-invalid, scroll-highlight arrows, danger menu items)" },
31
+ { "name": "ltree-light", "category": "color", "usage": "Light surface (context-menu hover, debug stat chips)" },
32
+ { "name": "ltree-border", "category": "color", "usage": "Default border color" },
33
+ { "name": "ltree-body-color", "category": "color", "usage": "Default body text color" },
34
+
35
+ { "name": "ltree-font-family", "category": "typography", "usage": "Font family for all tree text" },
36
+ { "name": "ltree-node-font-size", "category": "typography", "usage": "Node row text size" },
37
+ { "name": "ltree-node-icon-font-size", "category": "typography", "usage": "Per-node icon font size (emoji, etc.)" },
38
+ { "name": "ltree-node-icon-margin-right", "category": "spacing", "usage": "Gap between node icon and label" },
39
+ { "name": "ltree-node-label-font-weight", "category": "typography", "usage": "Node label weight" },
40
+ { "name": "ltree-node-label-margin-right", "category": "spacing", "usage": "Gap after node label" },
41
+ { "name": "ltree-node-path-font-size", "category": "typography", "usage": "Path / debug muted text size" },
42
+ { "name": "ltree-node-path-color", "category": "color", "usage": "Path / debug muted text color" },
43
+
44
+ { "name": "ltree-node-indent-per-level", "category": "spacing", "usage": "Indentation added per nesting level" },
45
+ { "name": "ltree-node-content-padding", "category": "spacing", "usage": "Inner padding of a node row" },
46
+ { "name": "ltree-node-content-border-radius", "category": "border", "usage": "Node row corner rounding" },
47
+ { "name": "ltree-node-hover-bg", "category": "surface", "usage": "Node row hover background" },
48
+ { "name": "ltree-children-margin-top", "category": "spacing", "usage": "Top margin of nested child list" },
49
+
50
+ { "name": "ltree-toggle-icon-size", "category": "sizing", "usage": "Toggle icon (chevron / +/- / arrow / leaf) SVG size" },
51
+ { "name": "ltree-toggle-icon-width", "category": "sizing", "usage": "Width reserved for the toggle column" },
52
+ { "name": "ltree-toggle-icon-color", "category": "color", "usage": "Toggle icon color (via currentColor mask)" },
53
+ { "name": "ltree-toggle-icon-margin-right", "category": "spacing", "usage": "Gap between toggle and node content" },
54
+ { "name": "ltree-toggle-icon-transition", "category": "animation", "usage": "Rotation animation timing" },
55
+
56
+ { "name": "ltree-checkbox-size", "category": "sizing", "usage": "Checkbox square dimension" },
57
+ { "name": "ltree-checkbox-border-width", "category": "border", "usage": "Checkbox border thickness (hairline, does not scale)" },
58
+ { "name": "ltree-checkbox-border-color", "category": "color", "usage": "Unchecked checkbox border" },
59
+ { "name": "ltree-checkbox-border-radius", "category": "border", "usage": "Checkbox corner rounding" },
60
+ { "name": "ltree-checkbox-bg", "category": "surface", "usage": "Unchecked checkbox background" },
61
+ { "name": "ltree-checkbox-checked-bg", "category": "surface", "usage": "Checked / indeterminate checkbox background" },
62
+ { "name": "ltree-checkbox-checked-border-color", "category": "color", "usage": "Checked checkbox border" },
63
+ { "name": "ltree-checkbox-checkmark-color", "category": "color", "usage": "Tick / dash glyph color" },
64
+ { "name": "ltree-checkbox-focus-ring-width", "category": "border", "usage": "Focus-visible ring thickness" },
65
+ { "name": "ltree-checkbox-focus-ring-color", "category": "color", "usage": "Focus-visible ring color (defaults to a 25% tint of --ltree-primary)" },
66
+ { "name": "ltree-checkbox-focus-ring", "category": "border", "usage": "Composed focus-visible box-shadow (uses the two parts above; override directly for full control)" },
67
+
68
+ { "name": "ltree-highlight-bg", "category": "state", "usage": "Explorer-style highlighted node background" },
69
+ { "name": "ltree-highlight-color", "category": "state", "usage": "Highlighted node text color" },
70
+ { "name": "ltree-multi-selected-bg", "category": "state", "usage": "Multi-select tint background" },
71
+ { "name": "ltree-multi-selected-outline", "category": "state", "usage": "Multi-select outline" },
72
+
73
+ { "name": "ltree-dragover-bg", "category": "state", "usage": "Drag-over background tint" },
74
+ { "name": "ltree-dragover-shadow", "category": "state", "usage": "Drag-over glow shadow" },
75
+ { "name": "ltree-drop-placeholder-bg", "category": "state", "usage": "Empty-tree drop placeholder background" },
76
+ { "name": "ltree-drop-placeholder-color", "category": "state", "usage": "Placeholder text color" },
77
+ { "name": "ltree-drop-placeholder-border-radius", "category": "border", "usage": "Placeholder corner rounding" },
78
+ { "name": "ltree-drop-placeholder-min-height", "category": "sizing", "usage": "Placeholder min height" },
79
+
80
+ { "name": "ltree-drop-zone-border-radius", "category": "border", "usage": "Drop zone corner rounding" },
81
+ { "name": "ltree-drop-zone-before-bg", "category": "drop-zone", "usage": "Before zone resting background (sage)" },
82
+ { "name": "ltree-drop-zone-before-color", "category": "drop-zone", "usage": "Before zone resting text" },
83
+ { "name": "ltree-drop-zone-before-active-bg", "category": "drop-zone", "usage": "Before zone active background" },
84
+ { "name": "ltree-drop-zone-before-active-color", "category": "drop-zone", "usage": "Before zone active text" },
85
+ { "name": "ltree-drop-zone-before-active-shadow", "category": "drop-zone", "usage": "Before zone active shadow" },
86
+ { "name": "ltree-drop-zone-after-bg", "category": "drop-zone", "usage": "After zone resting background (coral)" },
87
+ { "name": "ltree-drop-zone-after-color", "category": "drop-zone", "usage": "After zone resting text" },
88
+ { "name": "ltree-drop-zone-after-active-bg", "category": "drop-zone", "usage": "After zone active background" },
89
+ { "name": "ltree-drop-zone-after-active-color", "category": "drop-zone", "usage": "After zone active text" },
90
+ { "name": "ltree-drop-zone-after-active-shadow", "category": "drop-zone", "usage": "After zone active shadow" },
91
+ { "name": "ltree-drop-zone-child-bg", "category": "drop-zone", "usage": "Child zone resting background (lavender)" },
92
+ { "name": "ltree-drop-zone-child-color", "category": "drop-zone", "usage": "Child zone resting text" },
93
+ { "name": "ltree-drop-zone-child-active-bg", "category": "drop-zone", "usage": "Child zone active background" },
94
+ { "name": "ltree-drop-zone-child-active-color", "category": "drop-zone", "usage": "Child zone active text" },
95
+ { "name": "ltree-drop-zone-child-active-shadow", "category": "drop-zone", "usage": "Child zone active shadow" },
96
+
97
+ { "name": "ltree-drop-glow-before-color", "category": "drop-zone", "usage": "Border glow color for 'before' position" },
98
+ { "name": "ltree-drop-glow-after-color", "category": "drop-zone", "usage": "Border glow color for 'after' position" },
99
+ { "name": "ltree-drop-glow-child-color", "category": "drop-zone", "usage": "Border glow color for 'child' position" },
100
+ { "name": "ltree-drop-glow-child-bg", "category": "drop-zone", "usage": "Child zone glow background tint" },
101
+ { "name": "ltree-drop-glow-size", "category": "border", "usage": "Glow border thickness (hairline, does not scale)" },
102
+ { "name": "ltree-drop-arrow-size", "category": "sizing", "usage": "Direction arrow size in glow mode" },
103
+ { "name": "ltree-drop-arrow-position", "category": "spacing", "usage": "Horizontal arrow position (% of node width)" },
104
+ { "name": "ltree-drop-arrow-before-rotation", "category": "animation", "usage": "Arrow rotation for 'before' position" },
105
+ { "name": "ltree-drop-arrow-after-rotation", "category": "animation", "usage": "Arrow rotation for 'after' position" },
106
+ { "name": "ltree-drop-arrow-child-rotation", "category": "animation", "usage": "Arrow rotation for 'child' position (45deg by default)" },
107
+ { "name": "ltree-drop-arrow-before-image", "category": "icon", "usage": "SVG data URL for the 'before' direction arrow" },
108
+ { "name": "ltree-drop-arrow-after-image", "category": "icon", "usage": "SVG data URL for the 'after' direction arrow" },
109
+ { "name": "ltree-drop-arrow-child-image", "category": "icon", "usage": "SVG data URL for the 'child' direction arrow" },
110
+
111
+ { "name": "tree-ghost-bg", "category": "state", "usage": "Touch drag ghost background (legacy prefix, kept for backward compat)" },
112
+ { "name": "tree-ghost-color", "category": "state", "usage": "Touch drag ghost text color (legacy prefix)" },
113
+
114
+ { "name": "ltree-context-menu-bg", "category": "context-menu", "usage": "Menu background" },
115
+ { "name": "ltree-context-menu-border-color", "category": "context-menu", "usage": "Menu border color" },
116
+ { "name": "ltree-context-menu-border-radius", "category": "border", "usage": "Menu corner rounding" },
117
+ { "name": "ltree-context-menu-shadow", "category": "context-menu", "usage": "Menu drop shadow" },
118
+ { "name": "ltree-context-menu-min-width", "category": "sizing", "usage": "Menu minimum width" },
119
+ { "name": "ltree-context-menu-padding", "category": "spacing", "usage": "Menu inner padding" },
120
+ { "name": "ltree-context-menu-item-padding", "category": "spacing", "usage": "Menu item padding" },
121
+ { "name": "ltree-context-menu-item-font-size", "category": "typography", "usage": "Menu item font size" },
122
+ { "name": "ltree-context-menu-item-color", "category": "color", "usage": "Menu item text color" },
123
+ { "name": "ltree-context-menu-item-hover-bg", "category": "surface", "usage": "Menu item hover background" },
124
+ { "name": "ltree-context-menu-icon-size", "category": "sizing", "usage": "Menu icon column width" },
125
+ { "name": "ltree-context-menu-icon-font-size", "category": "typography", "usage": "Menu icon font size" },
126
+ { "name": "ltree-context-menu-shortcut-color", "category": "color", "usage": "Keyboard shortcut hint color" },
127
+ { "name": "ltree-context-menu-shortcut-font-size", "category": "typography", "usage": "Keyboard shortcut hint font size" },
128
+ { "name": "ltree-context-menu-arrow-color", "category": "color", "usage": "Submenu arrow color" },
129
+ { "name": "ltree-context-menu-divider-color", "category": "color", "usage": "Divider line color" },
130
+ { "name": "ltree-context-menu-divider-label-color", "category": "color", "usage": "Named divider label color" },
131
+
132
+ { "name": "ltree-loading-bg", "category": "surface", "usage": "Loading overlay background" },
133
+ { "name": "ltree-loading-color", "category": "color", "usage": "'Loading more…' text color" },
134
+ { "name": "ltree-spinner-size", "category": "sizing", "usage": "Spinner diameter" },
135
+ { "name": "ltree-spinner-track", "category": "color", "usage": "Spinner background ring color" },
136
+ { "name": "ltree-spinner-color", "category": "color", "usage": "Spinner active color" },
137
+
138
+ { "name": "ltree-scroll-highlight-bg", "category": "state", "usage": "scrollToPath highlight background flash" },
139
+ { "name": "ltree-scroll-highlight-shadow", "category": "state", "usage": "scrollToPath highlight glow" },
140
+ { "name": "ltree-scroll-highlight-arrow-color", "category": "state", "usage": "scrollToPath direction arrow color" }
141
+ ]
142
+ }
@@ -54,21 +54,21 @@
54
54
  const callbacks = getContext<NodeCallbacks<T>>('NodeCallbacks');
55
55
  const config = getContext<NodeConfig>('NodeConfig');
56
56
 
57
- // Destructure config for convenience.
58
- // Works reactively because nodeConfig uses $state() (not .raw()) and is mutated
59
- // in-place via Object.assign, so the proxy reference stays the same.
60
- const {
61
- expandIconClass,
62
- collapseIconClass,
63
- leafIconClass,
64
- highlightedNodeClass,
65
- focusedNodeClass,
66
- dragOverNodeClass,
67
- allowCopy,
68
- } = config;
57
+ // Read every config field through $derived so updates from controller.update()
58
+ // or the controller's runtime $effect syncs propagate into this Node's render.
59
+ // Plain destructuring would snapshot primitives once and never react.
60
+ const expandIconClass = $derived(config.expandIconClass);
61
+ const collapseIconClass = $derived(config.collapseIconClass);
62
+ const leafIconClass = $derived(config.leafIconClass);
63
+ const highlightedNodeClass = $derived(config.highlightedNodeClass);
64
+ const focusedNodeClass = $derived(config.focusedNodeClass);
65
+ // dragOverNodeClass is applied to the DOM directly by the controller
66
+ // (see hoveredNodeForDrop $effect in TreeController) — no per-Node binding.
67
+ const allowCopy = $derived(config.allowCopy);
69
68
  const clickBehavior = $derived(config.clickBehavior);
70
69
  const showCheckboxes = $derived(config.showCheckboxes);
71
70
  const checkboxMode = $derived(config.checkboxMode);
71
+ const clickTogglesCheckbox = $derived(config.clickTogglesCheckbox);
72
72
 
73
73
  // Indeterminate state: driven by controller's _updateAncestorVisualStates
74
74
  const isIndeterminate = $derived(checkboxMode === 'cascade' && node.visualState === 'indeterminate');
@@ -81,15 +81,15 @@
81
81
  const accordionExpand = $derived(config.accordionExpand);
82
82
  const toggleIconMode = $derived(config.toggleIconMode);
83
83
 
84
- // Compute if THIS node is the one being hovered for drop
84
+ // Compute if THIS node is the one being hovered for drop.
85
+ // Single source of truth from the controller — guarantees exactly one highlighted
86
+ // row at a time, unlike the per-node local flag we used before (HTML5 dragleave
87
+ // is unreliable and leaked stale highlights when moving fast between rows).
85
88
  const isHoveredForDrop = $derived(hoveredNodeForDropPath === node.path);
86
89
 
87
90
  const tree = getContext<Ltree<T>>("Ltree")
88
91
  const renderCoordinator = getContext<RenderCoordinator | null>("RenderCoordinator")
89
92
 
90
- // Drag over state
91
- let isDraggedOver = $state(false);
92
-
93
93
  // Track glow position for glow mode
94
94
  let glowPosition = $state<'before' | 'after' | 'child' | null>(null);
95
95
 
@@ -177,8 +177,8 @@
177
177
  // — only applied before first-child nodes (where level > previous node's level).
178
178
  const indentStyle = $derived(
179
179
  flatMode
180
- ? `margin-left: calc(${node?.level || 1} * var(--tree-node-indent-per-level, 0.5rem))${flatGap ? '; margin-top: 2px' : ''}`
181
- : `margin-left: var(--tree-node-indent-per-level, 0.5rem)`,
180
+ ? `margin-left: calc(${node?.level || 1} * var(--ltree-node-indent-per-level, 0.5rem))${flatGap ? '; margin-top: 2px' : ''}`
181
+ : `margin-left: var(--ltree-node-indent-per-level, 0.5rem)`,
182
182
  )
183
183
 
184
184
  // Progressive rendering state - only used in recursive mode
@@ -296,14 +296,27 @@
296
296
  function _onNodeClicked(event?: MouseEvent) {
297
297
  uiLogger.debug(`Node clicked: ${node.path}`, { id: node.id, hasChildren: node.hasChildren })
298
298
  const modifiers = event ? { ctrl: event.ctrlKey || event.metaKey, shift: event.shiftKey } : undefined;
299
+ const hasModifiers = !!(modifiers?.ctrl || modifiers?.shift);
300
+
301
+ // Plain click on a selectable node with checkboxes shown → toggle the checkbox
302
+ // instead of focusing/highlighting. Expand still happens if clickBehavior asks for it.
303
+ // Modified clicks (Ctrl/Shift) fall through to the normal multi-highlight path.
304
+ if (clickTogglesCheckbox && showCheckboxes && node.isSelectable && !hasModifiers) {
305
+ callbacks.onCheckboxToggle(node, { skipFocus: true });
306
+ if (clickBehavior !== 'select') toggleExpanded();
307
+ return;
308
+ }
299
309
 
300
310
  if (clickBehavior === 'expand') {
301
- // Expand only — no selection callback
302
- toggleExpanded()
311
+ // Expand only — no selection callback. Ctrl/Shift have no meaning here
312
+ // (no selection to extend), so they shouldn't toggle expand either.
313
+ if (!hasModifiers) toggleExpanded()
303
314
  } else if (clickBehavior === 'expand-and-focus') {
304
- // Select + expand on single click
315
+ // Select + expand on single click. Modified clicks are reserved for
316
+ // highlight management — don't also toggle expand (matches OS file
317
+ // explorers where Ctrl/Shift+click manages selection without opening).
305
318
  callbacks.onNodeClicked(node, modifiers)
306
- toggleExpanded()
319
+ if (!hasModifiers) toggleExpanded()
307
320
  } else {
308
321
  // 'select' — single click selects only
309
322
  callbacks.onNodeClicked(node, modifiers)
@@ -365,7 +378,7 @@
365
378
  <!-- svelte-ignore a11y_click_events_have_key_events -->
366
379
  <!-- svelte-ignore a11y_no_static_element_interactions -->
367
380
  <div
368
- class="ltree-node-content {node.isHighlighted ? highlightedNodeClass : ''} {node.isFocused && focusedNodeClass ? focusedNodeClass : ''} {isDraggedOver && dragOverNodeClass ? dragOverNodeClass : ''}"
381
+ class="ltree-node-content {node.isHighlighted ? highlightedNodeClass : ''} {node.isFocused && focusedNodeClass ? focusedNodeClass : ''}"
369
382
  class:ltree-clickable={node.isSelectable}
370
383
  class:ltree-dragged={isDraggedNode}
371
384
  class:ltree-draggable={node?.isDraggable}
@@ -403,7 +416,6 @@
403
416
  if (e.dataTransfer) {
404
417
  e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move';
405
418
  }
406
- isDraggedOver = true;
407
419
  // In glow mode, calculate and update the glow position
408
420
  if (dropZoneMode === 'glow') {
409
421
  glowPosition = calculateGlowPosition(e, e.currentTarget as HTMLElement);
@@ -417,7 +429,6 @@
417
429
  const y = e.clientY;
418
430
 
419
431
  if (x < rect.left || x >= rect.right || y < rect.top || y >= rect.bottom) {
420
- isDraggedOver = false;
421
432
  glowPosition = null;
422
433
  callbacks.onNodeDragLeave(node, e);
423
434
  }
@@ -428,7 +439,6 @@
428
439
  if (e.dataTransfer) {
429
440
  e.dataTransfer.dropEffect = (allowCopy && e.ctrlKey) ? 'copy' : 'move';
430
441
  }
431
- isDraggedOver = false;
432
442
  // In glow mode, use the calculated glowPosition for the drop
433
443
  if (dropZoneMode === 'glow' && glowPosition) {
434
444
  callbacks.onZoneDrop(node, glowPosition, e);
@@ -12,6 +12,7 @@
12
12
  type DropOperation,
13
13
  type ClickBehavior,
14
14
  type CheckboxMode,
15
+ type SelectionMode,
15
16
  type TreeChange,
16
17
  type ApplyChangesResult
17
18
  } from '../ltree/types.js';
@@ -31,8 +32,11 @@
31
32
  parentPathMember?: string | null | undefined;
32
33
  levelMember?: string | null | undefined;
33
34
  isExpandedMember?: string | null | undefined;
35
+ getIsExpandedCallback?: (node: LTreeNode<T>) => boolean;
34
36
  isSelectableMember?: string | null | undefined;
37
+ getIsSelectableCallback?: (node: LTreeNode<T>) => boolean;
35
38
  isSelectedMember?: string | null | undefined;
39
+ getIsSelectedCallback?: (node: LTreeNode<T>) => boolean;
36
40
  isDraggableMember?: string | null | undefined;
37
41
  getIsDraggableCallback?: (node: LTreeNode<T>) => boolean;
38
42
  isDropAllowedMember?: string | null | undefined;
@@ -76,8 +80,16 @@
76
80
  // BEHAVIOUR
77
81
  expandLevel?: number | null | undefined;
78
82
  clickBehavior?: ClickBehavior | null | undefined;
83
+ /**
84
+ * `'single'` (default): plain click highlights one node; Ctrl/Shift+click
85
+ * degrade to plain click; Shift+Arrow / Enter are no-ops.
86
+ * `'multi'`: Ctrl-toggle, Shift-range, Shift+Arrow extend, Enter toggles
87
+ * highlight on the focused node.
88
+ */
89
+ selectionMode?: SelectionMode | null | undefined;
79
90
  showCheckboxes?: boolean | null | undefined;
80
91
  checkboxMode?: CheckboxMode | null | undefined;
92
+ clickTogglesCheckbox?: boolean | null | undefined;
81
93
  beforeCheckboxToggleCallback?: (node: LTreeNode<T>, checked: boolean, affectedPaths: string[]) => boolean | string[] | void;
82
94
  rangeSelectionMode?: 'visual' | 'logical';
83
95
  initializeIndexCallback?: () => Index;
@@ -185,8 +197,11 @@
185
197
  hasChildrenMember,
186
198
 
187
199
  isExpandedMember,
200
+ getIsExpandedCallback,
188
201
  isSelectableMember,
202
+ getIsSelectableCallback,
189
203
  isSelectedMember,
204
+ getIsSelectedCallback,
190
205
  isDraggableMember,
191
206
  getIsDraggableCallback,
192
207
  isDropAllowedMember,
@@ -223,8 +238,10 @@
223
238
  expandLevel = 2,
224
239
 
225
240
  clickBehavior = 'expand-and-focus',
241
+ selectionMode = 'single',
226
242
  showCheckboxes = false,
227
243
  checkboxMode = 'independent',
244
+ clickTogglesCheckbox = false,
228
245
  beforeCheckboxToggleCallback,
229
246
  rangeSelectionMode = 'visual',
230
247
  shouldUseInternalSearchIndex = true,
@@ -306,8 +323,11 @@
306
323
  levelMember,
307
324
  hasChildrenMember,
308
325
  isExpandedMember,
326
+ getIsExpandedCallback,
309
327
  isSelectableMember,
328
+ getIsSelectableCallback,
310
329
  isSelectedMember,
330
+ getIsSelectedCallback,
311
331
  isDraggableMember,
312
332
  getIsDraggableCallback,
313
333
  isDropAllowedMember,
@@ -330,8 +350,10 @@
330
350
  selectedPaths,
331
351
  expandLevel,
332
352
  clickBehavior,
353
+ selectionMode,
333
354
  showCheckboxes,
334
355
  checkboxMode,
356
+ clickTogglesCheckbox,
335
357
  beforeCheckboxToggleCallback,
336
358
  rangeSelectionMode,
337
359
  shouldUseInternalSearchIndex,
@@ -440,8 +462,10 @@
440
462
 
441
463
  // Visual config sync (drives nodeConfig update via controller's internal effect)
442
464
  $effect(() => { controller.clickBehavior = clickBehavior ?? 'expand-and-focus'; });
465
+ $effect(() => { controller.selectionMode = selectionMode ?? 'single'; });
443
466
  $effect(() => { controller.showCheckboxes = showCheckboxes ?? false; });
444
467
  $effect(() => { controller.checkboxMode = checkboxMode ?? 'independent'; });
468
+ $effect(() => { controller.clickTogglesCheckbox = clickTogglesCheckbox ?? false; });
445
469
  $effect(() => { controller.beforeCheckboxToggleHandler = beforeCheckboxToggleCallback; });
446
470
  $effect(() => { controller.rangeSelectionMode = rangeSelectionMode ?? 'visual'; });
447
471
  $effect(() => { controller.expandIconClass = expandIconClass ?? 'ltree-icon-expand'; });
@@ -683,8 +707,11 @@
683
707
  | "levelMember"
684
708
  | "hasChildrenMember"
685
709
  | "isExpandedMember"
710
+ | "getIsExpandedCallback"
686
711
  | "isSelectableMember"
712
+ | "getIsSelectableCallback"
687
713
  | "isSelectedMember"
714
+ | "getIsSelectedCallback"
688
715
  | "isDraggableMember"
689
716
  | "getIsDraggableCallback"
690
717
  | "isDropAllowedMember"
@@ -703,8 +730,10 @@
703
730
  | "selectedPaths"
704
731
  | "expandLevel"
705
732
  | "clickBehavior"
733
+ | "selectionMode"
706
734
  | "showCheckboxes"
707
735
  | "checkboxMode"
736
+ | "clickTogglesCheckbox"
708
737
  | "beforeCheckboxToggleCallback"
709
738
  | "rangeSelectionMode"
710
739
  | "shouldUseInternalSearchIndex"
@@ -756,8 +785,11 @@
756
785
  if (updates.levelMember !== undefined) levelMember = updates.levelMember;
757
786
  if (updates.hasChildrenMember !== undefined) hasChildrenMember = updates.hasChildrenMember;
758
787
  if (updates.isExpandedMember !== undefined) isExpandedMember = updates.isExpandedMember;
788
+ if (updates.getIsExpandedCallback !== undefined) getIsExpandedCallback = updates.getIsExpandedCallback;
759
789
  if (updates.isSelectableMember !== undefined) isSelectableMember = updates.isSelectableMember;
790
+ if (updates.getIsSelectableCallback !== undefined) getIsSelectableCallback = updates.getIsSelectableCallback;
760
791
  if (updates.isSelectedMember !== undefined) isSelectedMember = updates.isSelectedMember;
792
+ if (updates.getIsSelectedCallback !== undefined) getIsSelectedCallback = updates.getIsSelectedCallback;
761
793
  if (updates.isDraggableMember !== undefined) isDraggableMember = updates.isDraggableMember;
762
794
  if (updates.getIsDraggableCallback !== undefined) getIsDraggableCallback = updates.getIsDraggableCallback;
763
795
  if (updates.isDropAllowedMember !== undefined) isDropAllowedMember = updates.isDropAllowedMember;
@@ -776,8 +808,10 @@
776
808
  if (updates.selectedPaths !== undefined) selectedPaths = updates.selectedPaths;
777
809
  if (updates.expandLevel !== undefined) expandLevel = updates.expandLevel;
778
810
  if (updates.clickBehavior !== undefined) clickBehavior = updates.clickBehavior;
811
+ if (updates.selectionMode !== undefined) selectionMode = updates.selectionMode;
779
812
  if (updates.showCheckboxes !== undefined) showCheckboxes = updates.showCheckboxes;
780
813
  if (updates.checkboxMode !== undefined) checkboxMode = updates.checkboxMode;
814
+ if (updates.clickTogglesCheckbox !== undefined) clickTogglesCheckbox = updates.clickTogglesCheckbox;
781
815
  if (updates.beforeCheckboxToggleCallback !== undefined) beforeCheckboxToggleCallback = updates.beforeCheckboxToggleCallback;
782
816
  if (updates.rangeSelectionMode !== undefined) rangeSelectionMode = updates.rangeSelectionMode;
783
817
  if (updates.shouldUseInternalSearchIndex !== undefined) shouldUseInternalSearchIndex = updates.shouldUseInternalSearchIndex;
@@ -837,6 +871,8 @@
837
871
 
838
872
  let handled = true;
839
873
 
874
+ // Shift+nav extends the highlight range. In single mode the controller
875
+ // short-circuits these to no-ops (see _navHighlightTo).
840
876
  switch (event.key) {
841
877
  case 'ArrowDown': event.shiftKey ? controller.navHighlightNext() : controller.navNextSibling(); break;
842
878
  case 'ArrowUp': event.shiftKey ? controller.navHighlightPrev() : controller.navPrevSibling(); break;
@@ -848,7 +884,19 @@
848
884
  case 'PageDown': event.shiftKey ? controller.navHighlightPageDown() : controller.navPageDown(); break;
849
885
  case 'PageUp': event.shiftKey ? controller.navHighlightPageUp() : controller.navPageUp(); break;
850
886
  case 'Enter':
851
- case ' ': controller.navToggle(); break;
887
+ // single: no-op (per spec); multi: toggle highlight on focused node.
888
+ if (controller.selectionMode === 'multi') controller.toggleFocusedHighlight();
889
+ else handled = false;
890
+ break;
891
+ case ' ':
892
+ // With checkboxes: toggle the focused node's checkbox.
893
+ // Without: keep the legacy expand/collapse behaviour as a useful fallback.
894
+ if (controller.showCheckboxes && controller.focusedNode?.isSelectable) {
895
+ controller.nodeCallbacks.onCheckboxToggle(controller.focusedNode);
896
+ } else {
897
+ controller.navToggle();
898
+ }
899
+ break;
852
900
  default: handled = false;
853
901
  }
854
902
 
@@ -1,6 +1,6 @@
1
1
  import type { Index, SearchOptions } from 'flexsearch';
2
2
  import { type LTreeNode } from '../ltree/ltree-node.svelte.js';
3
- import { type InsertArrayResult, type InsertBranchResult, type DeleteBranchResult, type ContextMenuEntry, type DropPosition, type DragDropMode, type DropOperation, type ClickBehavior, type CheckboxMode, type TreeChange, type ApplyChangesResult } from '../ltree/types.js';
3
+ import { type InsertArrayResult, type InsertBranchResult, type DeleteBranchResult, type ContextMenuEntry, type DropPosition, type DragDropMode, type DropOperation, type ClickBehavior, type CheckboxMode, type SelectionMode, type TreeChange, type ApplyChangesResult } from '../ltree/types.js';
4
4
  import type { RenderStats } from './RenderCoordinator.svelte.js';
5
5
  import { TreeController } from '../core/TreeController.svelte.js';
6
6
  import type { TreeNavigationOverrides } from '../core/navigation.js';
@@ -11,8 +11,11 @@ declare function $$render<T>(): {
11
11
  parentPathMember?: string | null | undefined;
12
12
  levelMember?: string | null | undefined;
13
13
  isExpandedMember?: string | null | undefined;
14
+ getIsExpandedCallback?: (node: LTreeNode<T>) => boolean;
14
15
  isSelectableMember?: string | null | undefined;
16
+ getIsSelectableCallback?: (node: LTreeNode<T>) => boolean;
15
17
  isSelectedMember?: string | null | undefined;
18
+ getIsSelectedCallback?: (node: LTreeNode<T>) => boolean;
16
19
  isDraggableMember?: string | null | undefined;
17
20
  getIsDraggableCallback?: (node: LTreeNode<T>) => boolean;
18
21
  isDropAllowedMember?: string | null | undefined;
@@ -45,8 +48,16 @@ declare function $$render<T>(): {
45
48
  loadingPlaceholder?: any;
46
49
  expandLevel?: number | null | undefined;
47
50
  clickBehavior?: ClickBehavior | null | undefined;
51
+ /**
52
+ * `'single'` (default): plain click highlights one node; Ctrl/Shift+click
53
+ * degrade to plain click; Shift+Arrow / Enter are no-ops.
54
+ * `'multi'`: Ctrl-toggle, Shift-range, Shift+Arrow extend, Enter toggles
55
+ * highlight on the focused node.
56
+ */
57
+ selectionMode?: SelectionMode | null | undefined;
48
58
  showCheckboxes?: boolean | null | undefined;
49
59
  checkboxMode?: CheckboxMode | null | undefined;
60
+ clickTogglesCheckbox?: boolean | null | undefined;
50
61
  beforeCheckboxToggleCallback?: (node: LTreeNode<T>, checked: boolean, affectedPaths: string[]) => boolean | string[] | void;
51
62
  rangeSelectionMode?: "visual" | "logical";
52
63
  initializeIndexCallback?: () => Index;
@@ -221,8 +232,11 @@ declare function $$render<T>(): {
221
232
  parentPathMember?: string | null | undefined;
222
233
  levelMember?: string | null | undefined;
223
234
  isExpandedMember?: string | null | undefined;
235
+ getIsExpandedCallback?: (node: LTreeNode<T>) => boolean;
224
236
  isSelectableMember?: string | null | undefined;
237
+ getIsSelectableCallback?: (node: LTreeNode<T>) => boolean;
225
238
  isSelectedMember?: string | null | undefined;
239
+ getIsSelectedCallback?: (node: LTreeNode<T>) => boolean;
226
240
  isDraggableMember?: string | null | undefined;
227
241
  getIsDraggableCallback?: (node: LTreeNode<T>) => boolean;
228
242
  isDropAllowedMember?: string | null | undefined;
@@ -255,8 +269,16 @@ declare function $$render<T>(): {
255
269
  loadingPlaceholder?: any;
256
270
  expandLevel?: number | null | undefined;
257
271
  clickBehavior?: ClickBehavior | null | undefined;
272
+ /**
273
+ * `'single'` (default): plain click highlights one node; Ctrl/Shift+click
274
+ * degrade to plain click; Shift+Arrow / Enter are no-ops.
275
+ * `'multi'`: Ctrl-toggle, Shift-range, Shift+Arrow extend, Enter toggles
276
+ * highlight on the focused node.
277
+ */
278
+ selectionMode?: SelectionMode | null | undefined;
258
279
  showCheckboxes?: boolean | null | undefined;
259
280
  checkboxMode?: CheckboxMode | null | undefined;
281
+ clickTogglesCheckbox?: boolean | null | undefined;
260
282
  beforeCheckboxToggleCallback?: (node: LTreeNode<T>, checked: boolean, affectedPaths: string[]) => boolean | string[] | void;
261
283
  rangeSelectionMode?: "visual" | "logical";
262
284
  initializeIndexCallback?: () => Index;
@@ -342,7 +364,7 @@ declare function $$render<T>(): {
342
364
  onTreeKeydown?: (event: KeyboardEvent, controller: TreeController<T>) => boolean | void;
343
365
  /** Override individual navigation methods (e.g. for custom ArrowDown/Up behavior) */
344
366
  navigationOverrides?: TreeNavigationOverrides<T>;
345
- }, "treeId" | "treePathSeparator" | "idMember" | "pathMember" | "parentPathMember" | "levelMember" | "hasChildrenMember" | "isExpandedMember" | "isSelectableMember" | "isSelectedMember" | "isDraggableMember" | "getIsDraggableCallback" | "isDropAllowedMember" | "displayValueMember" | "getDisplayValueCallback" | "searchValueMember" | "getSearchValueCallback" | "isCollapsibleMember" | "getIsCollapsibleCallback" | "orderMember" | "isSorted" | "sortCallback" | "data" | "focusedNode" | "highlightedPaths" | "selectedPaths" | "expandLevel" | "clickBehavior" | "showCheckboxes" | "checkboxMode" | "beforeCheckboxToggleCallback" | "rangeSelectionMode" | "shouldUseInternalSearchIndex" | "initializeIndexCallback" | "searchText" | "indexerBatchSize" | "indexerTimeout" | "shouldDisplayDebugInformation" | "shouldDisplayContextMenuInDebugMode" | "onNodeClick" | "onHighlightChange" | "onSelectionChange" | "onNodeDragStart" | "onNodeDragOver" | "onNodeDrop" | "beforeDropCallback" | "beforeCopyCallback" | "beforeCutCallback" | "beforePasteCallback" | "getContextMenuItemsCallback" | "virtualScroll" | "virtualRowHeight" | "virtualOverscan" | "virtualContainerHeight" | "dragDropMode" | "dropZoneMode" | "bodyClass" | "expandIconClass" | "collapseIconClass" | "leafIconClass" | "toggleIconMode" | "highlightedNodeClass" | "focusedNodeClass" | "dragOverNodeClass" | "scrollHighlightTimeout" | "scrollHighlightClass" | "contextMenuXOffset" | "contextMenuYOffset" | "accordionExpand">>) => void;
367
+ }, "treeId" | "treePathSeparator" | "idMember" | "pathMember" | "parentPathMember" | "levelMember" | "hasChildrenMember" | "isExpandedMember" | "getIsExpandedCallback" | "isSelectableMember" | "getIsSelectableCallback" | "isSelectedMember" | "getIsSelectedCallback" | "isDraggableMember" | "getIsDraggableCallback" | "isDropAllowedMember" | "displayValueMember" | "getDisplayValueCallback" | "searchValueMember" | "getSearchValueCallback" | "isCollapsibleMember" | "getIsCollapsibleCallback" | "orderMember" | "isSorted" | "sortCallback" | "data" | "focusedNode" | "highlightedPaths" | "selectedPaths" | "expandLevel" | "clickBehavior" | "selectionMode" | "showCheckboxes" | "checkboxMode" | "clickTogglesCheckbox" | "beforeCheckboxToggleCallback" | "rangeSelectionMode" | "shouldUseInternalSearchIndex" | "initializeIndexCallback" | "searchText" | "indexerBatchSize" | "indexerTimeout" | "shouldDisplayDebugInformation" | "shouldDisplayContextMenuInDebugMode" | "onNodeClick" | "onHighlightChange" | "onSelectionChange" | "onNodeDragStart" | "onNodeDragOver" | "onNodeDrop" | "beforeDropCallback" | "beforeCopyCallback" | "beforeCutCallback" | "beforePasteCallback" | "getContextMenuItemsCallback" | "virtualScroll" | "virtualRowHeight" | "virtualOverscan" | "virtualContainerHeight" | "dragDropMode" | "dropZoneMode" | "bodyClass" | "expandIconClass" | "collapseIconClass" | "leafIconClass" | "toggleIconMode" | "highlightedNodeClass" | "focusedNodeClass" | "dragOverNodeClass" | "scrollHighlightTimeout" | "scrollHighlightClass" | "contextMenuXOffset" | "contextMenuYOffset" | "accordionExpand">>) => void;
346
368
  };
347
369
  bindings: "data" | "focusedNode" | "highlightedPaths" | "selectedPaths" | "searchText" | "insertResult" | "isRendering";
348
370
  slots: {};
@@ -441,8 +463,11 @@ declare class __sveltets_Render<T> {
441
463
  parentPathMember?: string | null | undefined;
442
464
  levelMember?: string | null | undefined;
443
465
  isExpandedMember?: string | null | undefined;
466
+ getIsExpandedCallback?: ((node: LTreeNode<T>) => boolean) | undefined;
444
467
  isSelectableMember?: string | null | undefined;
468
+ getIsSelectableCallback?: ((node: LTreeNode<T>) => boolean) | undefined;
445
469
  isSelectedMember?: string | null | undefined;
470
+ getIsSelectedCallback?: ((node: LTreeNode<T>) => boolean) | undefined;
446
471
  isDraggableMember?: string | null | undefined;
447
472
  getIsDraggableCallback?: ((node: LTreeNode<T>) => boolean) | undefined;
448
473
  isDropAllowedMember?: string | null | undefined;
@@ -475,8 +500,16 @@ declare class __sveltets_Render<T> {
475
500
  loadingPlaceholder?: any;
476
501
  expandLevel?: number | null | undefined;
477
502
  clickBehavior?: ClickBehavior | null | undefined;
503
+ /**
504
+ * `'single'` (default): plain click highlights one node; Ctrl/Shift+click
505
+ * degrade to plain click; Shift+Arrow / Enter are no-ops.
506
+ * `'multi'`: Ctrl-toggle, Shift-range, Shift+Arrow extend, Enter toggles
507
+ * highlight on the focused node.
508
+ */
509
+ selectionMode?: SelectionMode | null | undefined;
478
510
  showCheckboxes?: boolean | null | undefined;
479
511
  checkboxMode?: CheckboxMode | null | undefined;
512
+ clickTogglesCheckbox?: boolean | null | undefined;
480
513
  beforeCheckboxToggleCallback?: ((node: LTreeNode<T>, checked: boolean, affectedPaths: string[]) => boolean | string[] | void) | undefined;
481
514
  rangeSelectionMode?: "visual" | "logical";
482
515
  initializeIndexCallback?: (() => Index) | undefined;
@@ -562,7 +595,7 @@ declare class __sveltets_Render<T> {
562
595
  onTreeKeydown?: ((event: KeyboardEvent, controller: TreeController<T>) => boolean | void) | undefined;
563
596
  /** Override individual navigation methods (e.g. for custom ArrowDown/Up behavior) */
564
597
  navigationOverrides?: Partial<import("../core/navigation.js").TreeNavigation<T>> | undefined;
565
- }, "treeId" | "data" | "treePathSeparator" | "idMember" | "pathMember" | "parentPathMember" | "levelMember" | "hasChildrenMember" | "isExpandedMember" | "displayValueMember" | "getDisplayValueCallback" | "searchValueMember" | "getSearchValueCallback" | "orderMember" | "isSorted" | "sortCallback" | "isSelectableMember" | "isSelectedMember" | "isDraggableMember" | "getIsDraggableCallback" | "isDropAllowedMember" | "isCollapsibleMember" | "getIsCollapsibleCallback" | "shouldDisplayDebugInformation" | "onNodeDrop" | "beforeDropCallback" | "beforeCopyCallback" | "beforeCutCallback" | "beforePasteCallback" | "beforeCheckboxToggleCallback" | "getContextMenuItemsCallback" | "focusedNode" | "highlightedPaths" | "selectedPaths" | "expandLevel" | "clickBehavior" | "showCheckboxes" | "checkboxMode" | "rangeSelectionMode" | "initializeIndexCallback" | "searchText" | "shouldUseInternalSearchIndex" | "indexerBatchSize" | "indexerTimeout" | "shouldDisplayContextMenuInDebugMode" | "virtualScroll" | "virtualRowHeight" | "virtualOverscan" | "virtualContainerHeight" | "dragDropMode" | "dropZoneMode" | "accordionExpand" | "onNodeClick" | "onNodeDragStart" | "onNodeDragOver" | "onHighlightChange" | "onSelectionChange" | "bodyClass" | "highlightedNodeClass" | "focusedNodeClass" | "dragOverNodeClass" | "expandIconClass" | "collapseIconClass" | "leafIconClass" | "toggleIconMode" | "scrollHighlightTimeout" | "scrollHighlightClass" | "contextMenuXOffset" | "contextMenuYOffset">>) => void;
598
+ }, "treeId" | "data" | "treePathSeparator" | "idMember" | "pathMember" | "parentPathMember" | "levelMember" | "hasChildrenMember" | "isExpandedMember" | "getIsExpandedCallback" | "displayValueMember" | "getDisplayValueCallback" | "searchValueMember" | "getSearchValueCallback" | "orderMember" | "isSorted" | "sortCallback" | "isSelectableMember" | "getIsSelectableCallback" | "isSelectedMember" | "getIsSelectedCallback" | "isDraggableMember" | "getIsDraggableCallback" | "isDropAllowedMember" | "isCollapsibleMember" | "getIsCollapsibleCallback" | "shouldDisplayDebugInformation" | "onNodeDrop" | "beforeDropCallback" | "beforeCopyCallback" | "beforeCutCallback" | "beforePasteCallback" | "beforeCheckboxToggleCallback" | "getContextMenuItemsCallback" | "focusedNode" | "highlightedPaths" | "selectedPaths" | "expandLevel" | "clickBehavior" | "selectionMode" | "showCheckboxes" | "checkboxMode" | "clickTogglesCheckbox" | "rangeSelectionMode" | "initializeIndexCallback" | "searchText" | "shouldUseInternalSearchIndex" | "indexerBatchSize" | "indexerTimeout" | "shouldDisplayContextMenuInDebugMode" | "virtualScroll" | "virtualRowHeight" | "virtualOverscan" | "virtualContainerHeight" | "dragDropMode" | "dropZoneMode" | "accordionExpand" | "onNodeClick" | "onNodeDragStart" | "onNodeDragOver" | "onHighlightChange" | "onSelectionChange" | "bodyClass" | "highlightedNodeClass" | "focusedNodeClass" | "dragOverNodeClass" | "expandIconClass" | "collapseIconClass" | "leafIconClass" | "toggleIconMode" | "scrollHighlightTimeout" | "scrollHighlightClass" | "contextMenuXOffset" | "contextMenuYOffset">>) => void;
566
599
  };
567
600
  }
568
601
  interface $$IsomorphicComponent {
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "5.0.0-rc08";
1
+ export declare const VERSION = "5.0.0-rc09";
2
2
  export declare const PACKAGE_NAME = "@keenmate/svelte-treeview";
3
3
  export declare const AUTHOR = "KeenMate";
4
4
  export declare const LICENSE = "MIT";
@@ -1,6 +1,6 @@
1
1
  // Auto-generated file - do not edit manually
2
2
  // Generated by scripts/generate-constants.js
3
- export const VERSION = "5.0.0-rc08";
3
+ export const VERSION = "5.0.0-rc09";
4
4
  export const PACKAGE_NAME = "@keenmate/svelte-treeview";
5
5
  export const AUTHOR = "KeenMate";
6
6
  export const LICENSE = "MIT";