@djangocfg/ui-tools 2.1.315 → 2.1.317

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/TreeRoot-R6XVHYQK.mjs +4 -0
  2. package/dist/{TreeRoot-DO33TIS5.mjs.map → TreeRoot-R6XVHYQK.mjs.map} +1 -1
  3. package/dist/TreeRoot-RAMQSBMO.cjs +19 -0
  4. package/dist/{TreeRoot-NJOZ2DMV.cjs.map → TreeRoot-RAMQSBMO.cjs.map} +1 -1
  5. package/dist/{chunk-MA552EWC.cjs → chunk-44ZTWYAF.cjs} +139 -111
  6. package/dist/chunk-44ZTWYAF.cjs.map +1 -0
  7. package/dist/chunk-KR6B3LVY.mjs +59 -0
  8. package/dist/chunk-KR6B3LVY.mjs.map +1 -0
  9. package/dist/{chunk-E5BP4IXF.mjs → chunk-NTJL2SXK.mjs} +139 -111
  10. package/dist/chunk-NTJL2SXK.mjs.map +1 -0
  11. package/dist/chunk-YXBOAGIM.cjs +63 -0
  12. package/dist/chunk-YXBOAGIM.cjs.map +1 -0
  13. package/dist/file-icon/index.cjs +117 -0
  14. package/dist/file-icon/index.cjs.map +1 -0
  15. package/dist/file-icon/index.d.cts +69 -0
  16. package/dist/file-icon/index.d.ts +69 -0
  17. package/dist/file-icon/index.mjs +112 -0
  18. package/dist/file-icon/index.mjs.map +1 -0
  19. package/dist/index.cjs +140 -180
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +5 -433
  22. package/dist/index.d.ts +5 -433
  23. package/dist/index.mjs +7 -56
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/tree/index.cjs +152 -0
  26. package/dist/tree/index.cjs.map +1 -0
  27. package/dist/tree/index.d.cts +278 -0
  28. package/dist/tree/index.d.ts +278 -0
  29. package/dist/tree/index.mjs +5 -0
  30. package/dist/tree/index.mjs.map +1 -0
  31. package/dist/types-Cclwv4Hl.d.cts +198 -0
  32. package/dist/types-Cclwv4Hl.d.ts +198 -0
  33. package/package.json +16 -10
  34. package/src/tools/FileIcon/FileIcon.tsx +91 -0
  35. package/src/tools/FileIcon/index.ts +9 -0
  36. package/src/tools/FileIcon/loader.ts +47 -0
  37. package/src/tools/FileIcon/treeAdapter.tsx +41 -0
  38. package/src/tools/Tree/README.md +56 -0
  39. package/src/tools/Tree/Tree.story.tsx +48 -0
  40. package/src/tools/Tree/TreeRoot.tsx +15 -5
  41. package/src/tools/Tree/components/TreeRow.tsx +17 -18
  42. package/src/tools/Tree/context/TreeContext.tsx +10 -2
  43. package/src/tools/Tree/hooks/useTreeKeyboard.ts +133 -99
  44. package/src/tools/Tree/index.tsx +2 -0
  45. package/src/tools/Tree/types.ts +36 -2
  46. package/dist/TreeRoot-DO33TIS5.mjs +0 -4
  47. package/dist/TreeRoot-NJOZ2DMV.cjs +0 -19
  48. package/dist/chunk-E5BP4IXF.mjs.map +0 -1
  49. package/dist/chunk-MA552EWC.cjs.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-tools",
3
- "version": "2.1.315",
3
+ "version": "2.1.317",
4
4
  "description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
5
5
  "keywords": [
6
6
  "ui-tools",
@@ -75,9 +75,14 @@
75
75
  "require": "./src/tools/CodeEditor/index.ts"
76
76
  },
77
77
  "./tree": {
78
- "types": "./src/tools/Tree/index.tsx",
79
- "import": "./src/tools/Tree/index.tsx",
80
- "require": "./src/tools/Tree/index.tsx"
78
+ "types": "./dist/tree/index.d.ts",
79
+ "import": "./dist/tree/index.mjs",
80
+ "require": "./dist/tree/index.cjs"
81
+ },
82
+ "./file-icon": {
83
+ "types": "./dist/file-icon/index.d.ts",
84
+ "import": "./dist/file-icon/index.mjs",
85
+ "require": "./dist/file-icon/index.cjs"
81
86
  },
82
87
  "./styles": "./src/styles/index.css",
83
88
  "./dist.css": "./dist/index.css"
@@ -96,8 +101,8 @@
96
101
  "check": "tsc --noEmit"
97
102
  },
98
103
  "peerDependencies": {
99
- "@djangocfg/i18n": "^2.1.315",
100
- "@djangocfg/ui-core": "^2.1.315",
104
+ "@djangocfg/i18n": "^2.1.317",
105
+ "@djangocfg/ui-core": "^2.1.317",
101
106
  "consola": "^3.4.2",
102
107
  "lodash-es": "^4.18.1",
103
108
  "lucide-react": "^0.545.0",
@@ -146,13 +151,14 @@
146
151
  },
147
152
  "optionalDependencies": {
148
153
  "@mapbox/mapbox-gl-draw": "^1.4.3",
149
- "@maplibre/maplibre-gl-geocoder": "^1.7.0"
154
+ "@maplibre/maplibre-gl-geocoder": "^1.7.0",
155
+ "material-file-icons": "^2.4.0"
150
156
  },
151
157
  "devDependencies": {
152
- "@djangocfg/i18n": "^2.1.315",
158
+ "@djangocfg/i18n": "^2.1.317",
153
159
  "@djangocfg/playground": "workspace:*",
154
- "@djangocfg/typescript-config": "^2.1.315",
155
- "@djangocfg/ui-core": "^2.1.315",
160
+ "@djangocfg/typescript-config": "^2.1.317",
161
+ "@djangocfg/ui-core": "^2.1.317",
156
162
  "@types/lodash-es": "^4.17.12",
157
163
  "@types/mapbox__mapbox-gl-draw": "^1.4.8",
158
164
  "@types/node": "^24.7.2",
@@ -0,0 +1,91 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { File as FileFallback, Folder, FolderOpen } from 'lucide-react';
5
+ import { cn } from '@djangocfg/ui-core/lib';
6
+
7
+ import { getMaterialIconsSync, loadMaterialIcons } from './loader';
8
+
9
+ export type FileIconSize = 12 | 14 | 16 | 18 | 20 | 24 | 28 | 32 | 40 | 48 | 64;
10
+
11
+ export interface FileIconProps {
12
+ /** File name (with extension) or folder name. */
13
+ name: string;
14
+ /** Render a folder icon instead of a file icon. */
15
+ isFolder?: boolean;
16
+ /** Folder open / closed state. Ignored when `isFolder` is false. */
17
+ isExpanded?: boolean;
18
+ /** Pixel size of the icon. */
19
+ size?: FileIconSize;
20
+ className?: string;
21
+ }
22
+
23
+ /**
24
+ * VSCode-style file icon.
25
+ *
26
+ * - Folders render Lucide `Folder` / `FolderOpen` (amber tint).
27
+ * - Files use `material-file-icons` if it's installed in the consumer; otherwise
28
+ * fall back to a neutral Lucide `File` icon.
29
+ *
30
+ * The optional dependency is loaded lazily on first render — there is zero
31
+ * bundle cost for consumers who never mount this component.
32
+ */
33
+ export function FileIcon({
34
+ name,
35
+ isFolder = false,
36
+ isExpanded = false,
37
+ size = 16,
38
+ className,
39
+ }: FileIconProps) {
40
+ const [svg, setSvg] = useState<string | null>(() => {
41
+ if (isFolder) return null;
42
+ const fn = getMaterialIconsSync();
43
+ return fn ? (fn(name)?.svg ?? null) : null;
44
+ });
45
+
46
+ useEffect(() => {
47
+ if (isFolder) {
48
+ setSvg(null);
49
+ return;
50
+ }
51
+ let cancelled = false;
52
+ void loadMaterialIcons().then((fn) => {
53
+ if (cancelled) return;
54
+ setSvg(fn ? (fn(name)?.svg ?? null) : null);
55
+ });
56
+ return () => {
57
+ cancelled = true;
58
+ };
59
+ }, [isFolder, name]);
60
+
61
+ if (isFolder) {
62
+ const Icon = isExpanded ? FolderOpen : Folder;
63
+ return (
64
+ <Icon
65
+ className={cn('shrink-0 text-amber-500', className)}
66
+ style={{ width: size, height: size }}
67
+ />
68
+ );
69
+ }
70
+
71
+ if (!svg) {
72
+ return (
73
+ <FileFallback
74
+ className={cn('shrink-0 text-muted-foreground/80', className)}
75
+ style={{ width: size, height: size }}
76
+ />
77
+ );
78
+ }
79
+
80
+ return (
81
+ <span
82
+ role="img"
83
+ aria-hidden
84
+ className={cn('inline-block shrink-0', className)}
85
+ style={{ width: size, height: size }}
86
+ dangerouslySetInnerHTML={{ __html: svg }}
87
+ />
88
+ );
89
+ }
90
+
91
+ export default FileIcon;
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+
3
+ export { FileIcon } from './FileIcon';
4
+ export type { FileIconProps, FileIconSize } from './FileIcon';
5
+
6
+ export { createFileIconSlot } from './treeAdapter';
7
+ export type { CreateFileIconSlotOptions } from './treeAdapter';
8
+
9
+ export { loadMaterialIcons, getMaterialIconsSync } from './loader';
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Lazy loader for `material-file-icons`. The package is an optional dependency:
5
+ * if it's installed in the consumer's tree, we render its rich VSCode-style
6
+ * SVGs; if not, the loader resolves to `null` and `<FileIcon>` falls back to a
7
+ * Lucide icon. Either way, ui-tools never throws or warns about a missing
8
+ * package.
9
+ *
10
+ * Resolution is cached per-process so we don't re-import on every render.
11
+ */
12
+
13
+ type GetIconFn = (name: string) => { svg: string } | undefined;
14
+
15
+ let cached: GetIconFn | null | undefined;
16
+ let inflight: Promise<GetIconFn | null> | null = null;
17
+
18
+ export async function loadMaterialIcons(): Promise<GetIconFn | null> {
19
+ if (cached !== undefined) return cached;
20
+ if (inflight) return inflight;
21
+
22
+ inflight = (async () => {
23
+ try {
24
+ // Computed specifier so bundlers don't try to eagerly resolve it.
25
+ const specifier = 'material-file-icons';
26
+ const mod = (await import(/* @vite-ignore */ specifier)) as {
27
+ getIcon?: GetIconFn;
28
+ default?: { getIcon?: GetIconFn };
29
+ };
30
+ const fn = mod.getIcon ?? mod.default?.getIcon ?? null;
31
+ cached = fn;
32
+ return fn;
33
+ } catch {
34
+ cached = null;
35
+ return null;
36
+ } finally {
37
+ inflight = null;
38
+ }
39
+ })();
40
+
41
+ return inflight;
42
+ }
43
+
44
+ /** Synchronous accessor — returns null until `loadMaterialIcons` resolves. */
45
+ export function getMaterialIconsSync(): GetIconFn | null {
46
+ return cached ?? null;
47
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import type { TreeNode, TreeRowSlot } from '../Tree/types';
4
+ import { FileIcon, type FileIconSize } from './FileIcon';
5
+
6
+ export interface CreateFileIconSlotOptions<T> {
7
+ /**
8
+ * How to read the displayed name (with extension) for a node. Use the same
9
+ * accessor you pass to `<TreeRoot getItemName={...} />`.
10
+ */
11
+ getName: (node: TreeNode<T>) => string;
12
+ /** Pixel size for both file and folder icons. Default: 16. */
13
+ size?: FileIconSize;
14
+ }
15
+
16
+ /**
17
+ * Build a `renderIcon` slot for `<TreeRoot>` that uses `<FileIcon>` for both
18
+ * leaves and folders.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <TreeRoot
23
+ * data={data}
24
+ * getItemName={(n) => n.data.name}
25
+ * renderIcon={createFileIconSlot({ getName: (n) => n.data.name })}
26
+ * />
27
+ * ```
28
+ */
29
+ export function createFileIconSlot<T>({
30
+ getName,
31
+ size = 16,
32
+ }: CreateFileIconSlotOptions<T>): TreeRowSlot<T> {
33
+ return ({ node, isFolder, isExpanded }) => (
34
+ <FileIcon
35
+ name={getName(node)}
36
+ isFolder={isFolder}
37
+ isExpanded={isExpanded}
38
+ size={size}
39
+ />
40
+ );
41
+ }
@@ -182,6 +182,7 @@ Toggle the bar with `appearance.showActiveIndicator`. Intensity scales with `app
182
182
  | Option | Default |
183
183
  | --- | --- |
184
184
  | `selectionMode` | `'single'` |
185
+ | `activationMode` | `'single-click'` |
185
186
  | `enableSearch` | `false` |
186
187
  | `enableTypeAhead` | `true` |
187
188
  | `showIndentGuides` | `false` |
@@ -198,6 +199,7 @@ Toggle the bar with `appearance.showActiveIndicator`. Intensity scales with `app
198
199
  | Story | Demonstrates |
199
200
  | --- | --- |
200
201
  | Default | sensible cozy defaults |
202
+ | WithActivationModes | single-click / double-click / preview semantics |
201
203
  | Densities | three density presets side-by-side |
202
204
  | WithIcons | file-type icons through `renderIcon` |
203
205
  | WithStatus | modified / error / disabled rows through `renderLabel` |
@@ -211,6 +213,60 @@ Toggle the bar with `appearance.showActiveIndicator`. Intensity scales with `app
211
213
  | LargeTree | ~500 nodes scalability check |
212
214
  | Playground | every knob exposed as a control |
213
215
 
216
+ ## Activation modes
217
+
218
+ How a leaf becomes "activated" (opened) on pointer interaction is controlled
219
+ by `activationMode`. Folders ignore this setting — they always toggle on
220
+ single click and never call `onActivate`.
221
+
222
+ | Mode | Single click | Double click |
223
+ | --- | --- | --- |
224
+ | `'single-click'` *(default)* | activate `{ preview: false }` | activate `{ preview: false }` |
225
+ | `'double-click'` | select + focus only | activate `{ preview: false }` |
226
+ | `'single-click-preview'` *(VSCode / Cursor)* | activate `{ preview: true }` | activate `{ preview: false }` |
227
+
228
+ Keyboard `Enter` / `Space` always activates with `{ preview: false }` —
229
+ keyboard input is treated as an explicit user action.
230
+
231
+ ```tsx
232
+ <TreeRoot<FsNode>
233
+ data={data}
234
+ getItemName={(n) => n.data.name}
235
+ activationMode="single-click-preview"
236
+ onActivate={(node, { preview }) =>
237
+ preview ? openPreviewTab(node) : openPinnedTab(node)
238
+ }
239
+ />
240
+ ```
241
+
242
+ The active mode is also exposed on each row as
243
+ `data-activation-mode="<mode>"` for CSS-level targeting.
244
+
245
+ ## VSCode-style file icons
246
+
247
+ Tree is generic over `T` — it has no opinion on whether nodes are files. For a
248
+ ready-made VSCode-style icon set, install the optional companion subpath:
249
+
250
+ ```bash
251
+ pnpm add material-file-icons
252
+ ```
253
+
254
+ ```tsx
255
+ import { TreeRoot } from '@djangocfg/ui-tools/tree';
256
+ import { createFileIconSlot } from '@djangocfg/ui-tools/file-icon';
257
+
258
+ <TreeRoot
259
+ data={data}
260
+ getItemName={(n) => n.data.name}
261
+ renderIcon={createFileIconSlot({ getName: (n) => n.data.name })}
262
+ />
263
+ ```
264
+
265
+ `material-file-icons` is declared in `optionalDependencies`. If it's not
266
+ installed, `<FileIcon>` falls back to a Lucide `File` icon — no warnings, no
267
+ runtime errors. Folders always render Lucide `Folder` / `FolderOpen` (amber
268
+ tint), regardless of whether the optional package is present.
269
+
214
270
  ## Out of scope (today)
215
271
 
216
272
  - Inline rename UX
@@ -111,6 +111,54 @@ export const Default = () => (
111
111
  </div>
112
112
  );
113
113
 
114
+ // ---------------------------------------------------------------------------
115
+ // 1.5) WithActivationModes — VSCode-style click semantics
116
+ // ---------------------------------------------------------------------------
117
+
118
+ export const WithActivationModes = () => {
119
+ const [log, setLog] = useState<string[]>([]);
120
+ const append = (s: string) =>
121
+ setLog((prev) => [s, ...prev].slice(0, 8));
122
+
123
+ return (
124
+ <div className="flex flex-col gap-3">
125
+ <div className="grid grid-cols-3 gap-3">
126
+ {(['single-click', 'double-click', 'single-click-preview'] as const).map(
127
+ (mode) => (
128
+ <div key={mode} className="flex flex-col gap-2">
129
+ <span className="text-xs font-medium text-muted-foreground">
130
+ {mode}
131
+ </span>
132
+ <div className="h-72 w-64 rounded-md border border-border bg-card">
133
+ <TreeRoot<FsNode>
134
+ data={fs}
135
+ getItemName={getName}
136
+ initialExpandedIds={['src']}
137
+ activationMode={mode}
138
+ onActivate={(node, { preview }) =>
139
+ append(
140
+ `[${mode}] ${node.data.name}${preview ? ' (preview)' : ''}`,
141
+ )
142
+ }
143
+ />
144
+ </div>
145
+ </div>
146
+ ),
147
+ )}
148
+ </div>
149
+ <div className="rounded-md border border-border bg-muted/30 p-3 font-mono text-xs">
150
+ {log.length === 0 ? (
151
+ <span className="text-muted-foreground">
152
+ click / double-click a file to log activations
153
+ </span>
154
+ ) : (
155
+ log.map((line, i) => <div key={i}>{line}</div>)
156
+ )}
157
+ </div>
158
+ </div>
159
+ );
160
+ };
161
+
114
162
  // ---------------------------------------------------------------------------
115
163
  // 2) Densities — three presets side-by-side for comparison
116
164
  // ---------------------------------------------------------------------------
@@ -23,6 +23,7 @@ function TreeRoot<T>(props: TreeRootProps<T>) {
23
23
  getItemName,
24
24
  loadChildren,
25
25
  selectionMode,
26
+ activationMode,
26
27
  initialExpandedIds,
27
28
  initialSelectedIds,
28
29
  indent,
@@ -51,6 +52,7 @@ function TreeRoot<T>(props: TreeRootProps<T>) {
51
52
  getItemName={getItemName}
52
53
  loadChildren={loadChildren}
53
54
  selectionMode={selectionMode}
55
+ activationMode={activationMode}
54
56
  initialExpandedIds={initialExpandedIds}
55
57
  initialSelectedIds={initialSelectedIds}
56
58
  indent={indent}
@@ -97,22 +99,30 @@ function TreeRootShell<T>({
97
99
  const containerRef = useRef<HTMLDivElement>(null);
98
100
  const ctx = useTreeContext<T>();
99
101
 
100
- // Keyboard navigation (↑↓ ←→ Home/End Enter Esc).
101
- useTreeKeyboard<T>({
102
- containerRef,
102
+ // Keyboard navigation (↑↓ ←→ Home/End Enter Esc) — scoped via callback ref.
103
+ const { ref: keyboardRef } = useTreeKeyboard<T>({
103
104
  rows: ctx.flatRows,
104
105
  focusedId: ctx.focused,
105
106
  onFocus: ctx.setFocus,
106
107
  onSelect: ctx.select,
107
108
  onActivate: (id) => {
109
+ // Keyboard Enter / Space is always an explicit action — pin (no preview).
108
110
  const row = ctx.flatRows.find((r) => r.node.id === id);
109
- if (row) ctx.activate(row.node);
111
+ if (row) ctx.activate(row.node, { preview: false });
110
112
  },
111
113
  onExpand: ctx.expand,
112
114
  onCollapse: ctx.collapse,
113
115
  onClearSelection: ctx.clearSelection,
114
116
  });
115
117
 
118
+ const setContainerRef = useCallback(
119
+ (instance: HTMLDivElement | null) => {
120
+ containerRef.current = instance;
121
+ keyboardRef(instance);
122
+ },
123
+ [keyboardRef],
124
+ );
125
+
116
126
  // Type-ahead jump.
117
127
  const onTypeAheadMatch = useCallback(
118
128
  (id: string) => {
@@ -136,7 +146,7 @@ function TreeRootShell<T>({
136
146
 
137
147
  return (
138
148
  <div
139
- ref={containerRef}
149
+ ref={setContainerRef}
140
150
  tabIndex={0}
141
151
  className={cn(
142
152
  'group/tree flex h-full w-full flex-col gap-2 outline-none',
@@ -20,6 +20,7 @@ export function TreeRow<T>({ row, className }: TreeRowProps<T>) {
20
20
  const ctx = useTreeContext<T>();
21
21
  const {
22
22
  appearance,
23
+ activationMode,
23
24
  showIndentGuides,
24
25
  selected,
25
26
  focused,
@@ -51,31 +52,29 @@ export function TreeRow<T>({ row, className }: TreeRowProps<T>) {
51
52
  isMatchingSearch,
52
53
  };
53
54
 
54
- const handleActivate = () => {
55
+ // Folders always toggle on single click regardless of `activationMode`.
56
+ // Leaves dispatch by mode:
57
+ // single-click → click activates {preview:false}
58
+ // double-click → click only selects; dblclick activates {preview:false}
59
+ // single-click-preview → click activates {preview:true}; dblclick activates {preview:false}
60
+ const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
55
61
  if (node.disabled) return;
56
62
  setFocus(node.id);
57
63
  select(node.id);
58
- if (isFolder) toggle(node.id);
59
- else activate(node);
60
- };
61
-
62
- const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
63
- handleActivate();
64
+ if (isFolder) {
65
+ toggle(node.id);
66
+ } else if (activationMode === 'single-click') {
67
+ activate(node, { preview: false });
68
+ } else if (activationMode === 'single-click-preview') {
69
+ activate(node, { preview: true });
70
+ }
64
71
  e.currentTarget.scrollIntoView?.({ block: 'nearest' });
65
72
  };
66
73
 
67
74
  const handleDoubleClick = () => {
68
75
  if (node.disabled) return;
69
- if (!isFolder) activate(node);
70
- };
71
-
72
- const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
73
- // Match native button keyboard semantics on the row itself.
74
- if (node.disabled) return;
75
- if (e.key === 'Enter' || e.key === ' ') {
76
- e.preventDefault();
77
- handleActivate();
78
- }
76
+ if (isFolder) return;
77
+ activate(node, { preview: false });
79
78
  };
80
79
 
81
80
  const trigger = (
@@ -88,6 +87,7 @@ export function TreeRow<T>({ row, className }: TreeRowProps<T>) {
88
87
  aria-disabled={node.disabled || undefined}
89
88
  data-tree-row=""
90
89
  data-id={node.id}
90
+ data-activation-mode={activationMode}
91
91
  data-selected={isSelected ? 'true' : undefined}
92
92
  data-focused={isFocused && !isSelected ? 'true' : undefined}
93
93
  data-folder={isFolder || undefined}
@@ -100,7 +100,6 @@ export function TreeRow<T>({ row, className }: TreeRowProps<T>) {
100
100
  }}
101
101
  onClick={handleClick}
102
102
  onDoubleClick={handleDoubleClick}
103
- onKeyDown={handleKeyDown}
104
103
  onFocus={() => setFocus(node.id)}
105
104
  className={cn(
106
105
  'group/row relative flex w-full select-none items-center pr-2 text-left',
@@ -18,6 +18,8 @@ import {
18
18
  type TreeLabels,
19
19
  type TreeLoadChildren,
20
20
  type TreeNode,
21
+ type TreeActivateOptions,
22
+ type TreeActivationMode,
21
23
  type TreeRootProps,
22
24
  type TreeRowSlot,
23
25
  type TreeSelectionMode,
@@ -136,7 +138,7 @@ export interface TreeContextValue<T> {
136
138
  setQuery: (q: string) => void;
137
139
  refresh: (id: TreeItemId) => Promise<void>;
138
140
  refreshAll: () => Promise<void>;
139
- activate: (node: TreeNode<T>) => void;
141
+ activate: (node: TreeNode<T>, opts?: TreeActivateOptions) => void;
140
142
 
141
143
  // Config / slots
142
144
  labels: TreeLabels;
@@ -145,6 +147,7 @@ export interface TreeContextValue<T> {
145
147
  /** Convenience alias for `appearance.indent`. */
146
148
  indent: number;
147
149
  selectionMode: TreeSelectionMode;
150
+ activationMode: TreeActivationMode;
148
151
  enableSearch: boolean;
149
152
  showIndentGuides: boolean;
150
153
  getItemName: (node: TreeNode<T>) => string;
@@ -176,6 +179,7 @@ export interface TreeProviderProps<T>
176
179
  | 'getItemName'
177
180
  | 'loadChildren'
178
181
  | 'selectionMode'
182
+ | 'activationMode'
179
183
  | 'initialExpandedIds'
180
184
  | 'initialSelectedIds'
181
185
  | 'indent'
@@ -225,6 +229,7 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
225
229
  getItemName,
226
230
  loadChildren,
227
231
  selectionMode = 'single',
232
+ activationMode = 'single-click',
228
233
  initialExpandedIds,
229
234
  initialSelectedIds,
230
235
  indent,
@@ -457,7 +462,8 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
457
462
  }, [loadChildren, state.expanded, nodeById, fetchChildren]);
458
463
 
459
464
  const activate = useCallback(
460
- (node: TreeNode<T>) => onActivateRef.current?.(node),
465
+ (node: TreeNode<T>, opts: TreeActivateOptions = { preview: false }) =>
466
+ onActivateRef.current?.(node, opts),
461
467
  [],
462
468
  );
463
469
 
@@ -486,6 +492,7 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
486
492
  appearance: resolvedAppearance,
487
493
  indent: resolvedAppearance.indent,
488
494
  selectionMode,
495
+ activationMode,
489
496
  enableSearch,
490
497
  showIndentGuides,
491
498
  getItemName,
@@ -517,6 +524,7 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
517
524
  labels,
518
525
  resolvedAppearance,
519
526
  selectionMode,
527
+ activationMode,
520
528
  enableSearch,
521
529
  showIndentGuides,
522
530
  getItemName,