@djangocfg/ui-tools 2.1.413 → 2.1.416

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 (113) hide show
  1. package/dist/file-icon/index.d.cts +1 -1
  2. package/dist/file-icon/index.d.ts +1 -1
  3. package/dist/slots-ClRpIzoh.d.cts +88 -0
  4. package/dist/slots-ClRpIzoh.d.ts +88 -0
  5. package/dist/tree/index.cjs +1994 -276
  6. package/dist/tree/index.cjs.map +1 -1
  7. package/dist/tree/index.d.cts +717 -72
  8. package/dist/tree/index.d.ts +717 -72
  9. package/dist/tree/index.mjs +1984 -279
  10. package/dist/tree/index.mjs.map +1 -1
  11. package/package.json +10 -6
  12. package/src/tools/chat/README.md +111 -1
  13. package/src/tools/chat/composer/Composer.tsx +138 -17
  14. package/src/tools/chat/composer/ComposerRichTextarea.tsx +25 -0
  15. package/src/tools/chat/composer/index.ts +22 -0
  16. package/src/tools/chat/composer/slash/README.md +187 -0
  17. package/src/tools/chat/composer/slash/SlashHighlightTextarea.tsx +144 -0
  18. package/src/tools/chat/composer/slash/SlashMenu.tsx +142 -0
  19. package/src/tools/chat/composer/slash/SlashToken.tsx +57 -0
  20. package/src/tools/chat/composer/slash/index.ts +44 -0
  21. package/src/tools/chat/composer/slash/labels.ts +19 -0
  22. package/src/tools/chat/composer/slash/state.ts +168 -0
  23. package/src/tools/chat/composer/slash/types.ts +64 -0
  24. package/src/tools/chat/composer/slash/useSlashCommands.ts +204 -0
  25. package/src/tools/chat/composer/types.ts +8 -0
  26. package/src/tools/chat/shell/SuggestedPrompts.tsx +194 -0
  27. package/src/tools/chat/shell/index.ts +6 -0
  28. package/src/tools/data/Listbox/lazy.tsx +1 -1
  29. package/src/tools/data/Masonry/lazy.tsx +1 -1
  30. package/src/tools/data/Timeline/lazy.tsx +1 -1
  31. package/src/tools/data/Tree/FinderTree.tsx +42 -0
  32. package/src/tools/data/Tree/README.md +337 -208
  33. package/src/tools/data/Tree/TreeDndProvider.tsx +137 -0
  34. package/src/tools/data/Tree/TreeRoot.tsx +170 -55
  35. package/src/tools/data/Tree/__tests__/dnd.test.ts +160 -0
  36. package/src/tools/data/Tree/__tests__/keyboard.test.ts +137 -0
  37. package/src/tools/data/Tree/__tests__/renameUtils.test.ts +52 -0
  38. package/src/tools/data/Tree/__tests__/selection.test.ts +227 -0
  39. package/src/tools/data/Tree/components/TreeDropIndicator.tsx +65 -0
  40. package/src/tools/data/Tree/components/TreeEmptyArea.tsx +160 -0
  41. package/src/tools/data/Tree/components/TreeRenameInput.tsx +114 -0
  42. package/src/tools/data/Tree/components/TreeRow.tsx +92 -8
  43. package/src/tools/data/Tree/components/index.ts +6 -0
  44. package/src/tools/data/Tree/context/TreeContext.tsx +204 -363
  45. package/src/tools/data/Tree/context/TreeContextValue.ts +139 -0
  46. package/src/tools/data/Tree/context/async-children/collect-ids.ts +27 -0
  47. package/src/tools/data/Tree/context/async-children/index.ts +8 -0
  48. package/src/tools/data/Tree/context/async-children/use-async-children.ts +157 -0
  49. package/src/tools/data/Tree/context/clipboard/index.ts +4 -0
  50. package/src/tools/data/Tree/context/clipboard/use-clipboard.ts +115 -0
  51. package/src/tools/data/Tree/context/dnd/index.ts +8 -0
  52. package/src/tools/data/Tree/context/dnd/use-dnd.ts +194 -0
  53. package/src/tools/data/Tree/context/expansion/index.ts +4 -0
  54. package/src/tools/data/Tree/context/expansion/use-expansion.ts +55 -0
  55. package/src/tools/data/Tree/context/hooks.ts +68 -1
  56. package/src/tools/data/Tree/context/index.ts +3 -0
  57. package/src/tools/data/Tree/context/menu/builtin-actions.ts +357 -0
  58. package/src/tools/data/Tree/context/menu/index.ts +10 -0
  59. package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +127 -0
  60. package/src/tools/data/Tree/context/persist/index.ts +4 -0
  61. package/src/tools/data/Tree/context/persist/use-persist-sync.ts +74 -0
  62. package/src/tools/data/Tree/context/rename/index.ts +4 -0
  63. package/src/tools/data/Tree/context/rename/use-rename.ts +113 -0
  64. package/src/tools/data/Tree/context/selection/index.ts +4 -0
  65. package/src/tools/data/Tree/context/selection/use-selection.ts +146 -0
  66. package/src/tools/data/Tree/context/state/index.ts +6 -0
  67. package/src/tools/data/Tree/context/state/initial.ts +41 -0
  68. package/src/tools/data/Tree/context/state/reducer.ts +76 -0
  69. package/src/tools/data/Tree/context/state/types.ts +46 -0
  70. package/src/tools/data/Tree/data/clipboard.ts +33 -0
  71. package/src/tools/data/Tree/data/dnd.ts +123 -0
  72. package/src/tools/data/Tree/data/finderShortcuts.ts +67 -0
  73. package/src/tools/data/Tree/data/index.ts +19 -0
  74. package/src/tools/data/Tree/data/renameUtils.ts +51 -0
  75. package/src/tools/data/Tree/data/selection.ts +157 -0
  76. package/src/tools/data/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
  77. package/src/tools/data/Tree/hooks/finder-hotkeys/index.ts +8 -0
  78. package/src/tools/data/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
  79. package/src/tools/data/Tree/hooks/index.ts +23 -4
  80. package/src/tools/data/Tree/hooks/keyboard/activation.ts +27 -0
  81. package/src/tools/data/Tree/hooks/keyboard/arrow-nav.ts +26 -0
  82. package/src/tools/data/Tree/hooks/keyboard/expand-collapse.ts +54 -0
  83. package/src/tools/data/Tree/hooks/keyboard/index.ts +10 -0
  84. package/src/tools/data/Tree/hooks/keyboard/types.ts +39 -0
  85. package/src/tools/data/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
  86. package/src/tools/data/Tree/hooks/type-ahead/index.ts +5 -0
  87. package/src/tools/data/Tree/hooks/type-ahead/match-prefix.ts +42 -0
  88. package/src/tools/data/Tree/hooks/{useTreeTypeAhead.ts → type-ahead/use-tree-type-ahead.ts} +8 -19
  89. package/src/tools/data/Tree/index.tsx +25 -2
  90. package/src/tools/data/Tree/types/activation.ts +30 -0
  91. package/src/tools/data/Tree/types/adapter.ts +70 -0
  92. package/src/tools/data/Tree/types/index.ts +27 -0
  93. package/src/tools/data/Tree/types/labels.ts +97 -0
  94. package/src/tools/data/Tree/types/loader.ts +9 -0
  95. package/src/tools/data/Tree/types/node.ts +38 -0
  96. package/src/tools/data/Tree/types/root-props.ts +142 -0
  97. package/src/tools/data/Tree/types/selection.ts +3 -0
  98. package/src/tools/data/Tree/types/slots.ts +64 -0
  99. package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseBody.tsx +1 -1
  100. package/src/tools/dev/OpenapiViewer/components/shared/ResponsePanel/PrettyView.tsx +1 -1
  101. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +85 -0
  102. package/src/tools/forms/MarkdownEditor/index.ts +1 -0
  103. package/src/tools/forms/MarkdownEditor/lazy.tsx +6 -0
  104. package/src/tools/forms/MarkdownEditor/slash/SlashCommandNode.ts +162 -0
  105. package/src/tools/forms/MarkdownEditor/slash/index.ts +4 -0
  106. package/src/tools/forms/MarkdownEditor/slash/syncSlashNode.ts +97 -0
  107. package/src/tools/forms/MarkdownEditor/slash/types.ts +13 -0
  108. package/src/tools/forms/MarkdownEditor/styles.css +18 -0
  109. package/src/tools/index.ts +2 -2
  110. package/dist/types-j2vhn4Kv.d.cts +0 -241
  111. package/dist/types-j2vhn4Kv.d.ts +0 -241
  112. package/src/tools/data/Tree/hooks/useTreeKeyboard.ts +0 -171
  113. package/src/tools/data/Tree/types.ts +0 -217
@@ -1,6 +1,11 @@
1
1
  # Tree
2
2
 
3
- A decomposed, shadcn-styled tree for `@djangocfg/ui-tools`. Pure React engine, zero external tree libraries. Generic over `T`, slot-driven, async-friendly.
3
+ A decomposed, shadcn-styled tree for `@djangocfg/ui-tools`. Pure React engine, zero external tree libraries. Generic over `T`, slot-driven, async-friendly, **with built-in Finder/Explorer CRUD UX** when you provide an `adapter`.
4
+
5
+ Ships two entry points:
6
+
7
+ - **`<TreeRoot>`** — every prop is opt-in. Use for read-only / display trees.
8
+ - **`<FinderTree>`** — opinionated Finder/Explorer preset: multi-select, double-click activation, inline rename, indent guides, Finder hotkeys, cozy density. Override anything by passing the same prop.
4
9
 
5
10
  ## Why this exists
6
11
 
@@ -9,39 +14,74 @@ We tried popular headless tree engines first. They all leak React-integration bu
9
14
  ## Philosophy
10
15
 
11
16
  1. **No engine.** State lives in plain React. Every interaction goes through React's commit cycle.
12
- 2. **Generic over `T`.** Tree nodes carry your domain payload (`File`, `Project`, `JsonNode`, …). The component never assumes filesystem semantics.
17
+ 2. **Generic over `T`.** Tree nodes carry your domain payload (`File`, `Project`, `JsonNode`, …). The component never assumes filesystem semantics — but it provides a Finder-shaped affordance layer (`TreeAdapter`) for those that want one.
13
18
  3. **Sync or async.** Pass inline `children: TreeNode<T>[]` for sync data, or omit them and provide `loadChildren` for lazy loading. The async cache de-duplicates concurrent fetches.
14
19
  4. **Slots over props.** New visual needs add a slot, not a flag: `renderRow` / `renderIcon` / `renderLabel` / `renderActions` / `renderContextMenu`.
15
- 5. **VSCode-style highlights.** Hover, focus, and selection have distinct levels. Selection inside a focused tree gets the primary tint and a left active-indicator bar.
16
- 6. **CSS-variable theming.** Density, sizes, gaps, indent all exposed as `--tree-*` variables on the root. Override in any consumer without re-implementing components.
20
+ 5. **CRUD = adapter, not props.** The host app describes how to delete / rename / move / etc. through a single `TreeAdapter<T>` object. Tree owns the UX (dialogs, hotkeys, menus) and calls back into the adapter. No `onDelete`/`onRename`/... prop sprawl.
21
+ 6. **Dialogs come from `<DialogProvider>`.** Tree never re-implements its own dialogs. Built-in CRUD flows resolve `window.dialog` (installed by `@djangocfg/ui-core/lib/dialog-service`). If the host app hasn't mounted it, CRUD flows silently no-op with a dev-mode warning — Tree itself still renders.
22
+ 7. **CSS-variable theming.** Density, sizes, gaps, indent — all exposed as `--tree-*` variables on the root. Override in any consumer without re-implementing components.
17
23
 
18
24
  ## Layered architecture
19
25
 
20
26
  ```
21
- types.ts public types — generic over T, no `any`
22
- data/
23
- appearance.ts density / accent / radius / sizes → CSS vars + classes
24
- childCache.ts id { status, children, error }
25
- flatten.ts roots + expanded + cache → FlatRow<T>[]
26
- persist.ts versioned localStorage helper
27
- createDemoTree.ts deterministic synthetic tree for stories/tests
28
- context/
29
- TreeContext.tsx reducer + Provider + async loader
30
- hooks.ts useTreeSelection / Expansion / Focus / Search / Actions / Rows
31
- hooks/
32
- useTreeKeyboard.ts ↑↓ ←→ Home End Enter Esc on the container
33
- useTreeTypeAhead.ts Finder-style 600 ms prefix buffer
27
+ types/ public types — generic over T, no `any`, folders-per-concept
28
+ node.ts TreeItemId / TreeNode / FlatRow
29
+ selection.ts TreeSelectionMode
30
+ activation.ts TreeActivationMode / TreeActivateOptions
31
+ loader.ts TreeLoadChildren
32
+ labels.ts TreeLabels + DEFAULT_TREE_LABELS (CRUD copy too)
33
+ slots.ts TreeRowSlot / TreeContextMenuSlot / TreeContextMenuItem /
34
+ adapter.ts TreeAdapter / TreeBuiltinAction / TreeMovePosition
35
+ root-props.ts TreeRootProps
36
+
37
+ TreeRoot.tsx high-level entry — Provider + shell + content
38
+ TreeDndProvider.tsx thin <DndContext> wrapper (no-op when DnD off)
39
+ FinderTree.tsx Finder/Explorer preset over TreeRoot
40
+ lazy.tsx LazyTree via createLazyComponent
41
+
42
+ data/ pure helpers, zero React
43
+ appearance.ts density / accent / radius / sizes → CSS vars + classes
44
+ childCache.ts id → { status, children, error }
45
+ flatten.ts roots + expanded + cache → FlatRow<T>[]
46
+ persist.ts versioned localStorage helper
47
+ createDemoTree.ts deterministic synthetic tree for stories/tests
48
+ selection.ts Finder selection: anchor + shift-range + ⌘+A
49
+ clipboard.ts tree-local cut/copy state
50
+ renameUtils.ts splitFileName / autoSelectRange (base without ext)
51
+ finderShortcuts.ts Finder/Explorer keymap (mod+⌫, F2, ⌘D, …)
52
+ dnd.ts resolveDropZone + defaultCanDrop (cycle / self-drop)
53
+
54
+ context/ Provider + per-feature hooks (folders-per-feature)
55
+ TreeContext.tsx thin assembly: stitches the hooks below into one value
56
+ TreeContextValue.ts interface TreeContextValue<T>
57
+ hooks.ts public hooks: useTreeSelection / Expansion / Rename / Clipboard / Dnd / …
58
+ state/ reducer + initial state + action types
59
+ async-children/ cache + nodeById + fetchChildren + refresh / refreshAll
60
+ expansion/ expand / collapse / toggle / expandAll / collapseAll
61
+ selection/ clickSelect / moveSelect / selectAll + plain select / clear
62
+ rename/ startRename / cancelRename / commitRename (window.dialog.alert on error)
63
+ clipboard/ cutToClipboard / copyToClipboard / pasteFromClipboard
64
+ menu/ built-in actions registry + merged declarative resolver
65
+ dnd/ draggingIds / dropTarget / commitDrop / canDrop layering
66
+ persist/ localStorage + onSelectionChange / onExpansionChange notify
67
+
68
+ hooks/ container-level keyboard hooks (folders-per-scope)
69
+ keyboard/ ↑↓ ←→ Home/End Enter/Space Esc ⌘+A (Shift extends)
70
+ type-ahead/ Finder-style 600 ms prefix buffer
71
+ finder-hotkeys/ ⌘⌫ F2 ⌘D ⌘N ⌘⇧N ⌘C ⌘X ⌘V → adapter actions
72
+
34
73
  components/
35
- TreeRow.tsx default row: chevron + icon + label + actions + ctx-menu
74
+ TreeRow.tsx default row: chevron + icon + label + actions + ctx-menu
75
+ TreeRenameInput.tsx inline rename input (auto-selects base name without extension)
76
+ TreeDropIndicator.tsx drop indicator (before / after line, inside fill)
77
+ TreeEmptyArea.tsx fills space below last row — empty context menu + root drop target
36
78
  TreeChevron / TreeIcon / TreeLabel / TreeIndentGuides
37
- TreeSearchInput.tsx controlled search input
38
- TreeContent.tsx iterates flatRows, default-renders TreeRow
79
+ TreeSearchInput.tsx controlled search input
80
+ TreeContent.tsx iterates flatRows, default-renders TreeRow
39
81
  TreeEmpty / TreeSkeleton / TreeError
40
- TreeRoot.tsx high-level entry — Provider + shell + content
41
- lazy.tsx LazyTree via createLazyComponent
42
82
  ```
43
83
 
44
- Dependency direction: `components → context → data → types`. `hooks/` consume `context/`. Nothing cycles back.
84
+ Dependency direction: `components → context → data → types`. `hooks/` consume `context/` and `data/`. Pure helpers (`data/`, `hooks/*/match-prefix.ts`, `hooks/keyboard/arrow-nav.ts`, …) are unit-testable without a DOM.
45
85
 
46
86
  ## Quick start
47
87
 
@@ -54,9 +94,7 @@ const data: TreeNode<FsNode>[] = [
54
94
  {
55
95
  id: 'src',
56
96
  data: { name: 'src' },
57
- children: [
58
- { id: 'index.ts', data: { name: 'index.ts' } },
59
- ],
97
+ children: [{ id: 'index.ts', data: { name: 'index.ts' } }],
60
98
  },
61
99
  ];
62
100
 
@@ -71,6 +109,55 @@ const data: TreeNode<FsNode>[] = [
71
109
  />
72
110
  ```
73
111
 
112
+ ## Finder/Explorer preset
113
+
114
+ `<FinderTree>` is `<TreeRoot>` with sensible Finder defaults pre-set:
115
+
116
+ ```tsx
117
+ import { FinderTree, type TreeAdapter } from '@djangocfg/ui-tools/tree';
118
+
119
+ const fsAdapter: TreeAdapter<FsNode> = {
120
+ remove: async (nodes) => api.delete(nodes.map((n) => n.id)),
121
+ rename: async (node, name) => api.rename(node.id, name),
122
+ createFolder: async (parent, name) => api.mkdir(parent?.id ?? null, name),
123
+ move: async (nodes, target) => api.move(nodes.map((n) => n.id), target?.id ?? null),
124
+ // …
125
+ };
126
+
127
+ <FinderTree<FsNode>
128
+ data={data}
129
+ getItemName={(n) => n.data.name}
130
+ adapter={fsAdapter}
131
+ />
132
+ ```
133
+
134
+ Equivalent to:
135
+
136
+ ```tsx
137
+ <TreeRoot<FsNode>
138
+ data={data}
139
+ getItemName={(n) => n.data.name}
140
+ adapter={fsAdapter}
141
+ selectionMode="multiple"
142
+ activationMode="double-click"
143
+ enableInlineRename
144
+ enableFinderHotkeys
145
+ enableTypeAhead
146
+ showIndentGuides
147
+ appearance={{ density: 'cozy' }}
148
+ />
149
+ ```
150
+
151
+ Override any default by passing the prop:
152
+
153
+ ```tsx
154
+ <FinderTree
155
+ /* …data, getItemName, adapter… */
156
+ activationMode="single-click-preview" // VSCode/Cursor preview tabs
157
+ selectionMode="single" // disable multi-select
158
+ />
159
+ ```
160
+
74
161
  ## Async children
75
162
 
76
163
  ```tsx
@@ -110,14 +197,184 @@ function Toolbar() {
110
197
  }
111
198
  ```
112
199
 
113
- ## Appearance
200
+ ## Multi-selection (Finder / Explorer semantics)
201
+
202
+ When `selectionMode="multiple"`, Tree implements full file-manager selection:
203
+
204
+ | Gesture | Behaviour |
205
+ | --- | --- |
206
+ | **plain click** | replace selection, set anchor |
207
+ | **⌘ / Ctrl + click** | toggle row, set anchor |
208
+ | **shift + click** | range from anchor to clicked row |
209
+ | **shift + ⌘ + click** | union range with existing selection |
210
+ | **shift + ↑/↓** | extend range one row from anchor |
211
+ | **shift + Home / End** | extend range to top / bottom of visible rows |
212
+ | **⌘ / Ctrl + A** | select every visible row |
213
+ | **Esc** | clear selection (focused row stays) |
214
+
215
+ `anchor` is the pivot for shift-extend. Plain click and ⌘-click reset it to the clicked row; shift-click leaves it untouched. Access it from `useTreeSelection()`:
216
+
217
+ ```tsx
218
+ const {
219
+ selectedIds, anchor,
220
+ clickSelect, moveSelect, selectAll,
221
+ setSelectedIds, clear, isSelected,
222
+ } = useTreeSelection();
223
+ ```
224
+
225
+ `clickSelect(id, { shift, meta })` and `moveSelect(id, { extend })` are also exposed for consumers building custom row components.
226
+
227
+ ## CRUD adapter (delete / rename / new / cut+copy+paste)
228
+
229
+ ```ts
230
+ import type { TreeAdapter } from '@djangocfg/ui-tools/tree';
231
+
232
+ const adapter: TreeAdapter<FsNode> = {
233
+ remove?: (nodes) => Promise<void>;
234
+ rename?: (node, nextName) => Promise<void>;
235
+ createFile?: (parent, name) => Promise<void>; // parent === null → root
236
+ createFolder?: (parent, name) => Promise<void>;
237
+ duplicate?: (nodes) => Promise<void>;
238
+ move?: (nodes, target, position) => Promise<void>; // DnD + cut+paste
239
+ copy?: (nodes, target, position) => Promise<void>; // copy+paste
240
+ validateName?: (name, ctx) => string | null; // null = ok; string = error to show
241
+ };
242
+ ```
243
+
244
+ Every method is **optional**. Tree only exposes the corresponding context-menu item / hotkey when the matching method is defined. So an inspection-only tree (`adapter={{}}` or no adapter) gets no destructive actions — no greyed-out items, just nothing.
114
245
 
115
- Cosmetic configuration is a single optional prop. Defaults to a comfortable VSCode-Explorer density.
246
+ `window.dialog.confirm / prompt / alert` (from `@djangocfg/ui-core/lib/dialog-service`) drives every flow:
247
+
248
+ - **Delete** — confirms via `dialog.confirm({ variant: 'destructive' })`, then calls `adapter.remove`.
249
+ - **New file / folder** — prompts for a name via `dialog.prompt`, validates via `adapter.validateName`, then calls `adapter.createFile/createFolder`.
250
+ - **Rename (no inline)** — prompts via `dialog.prompt` and calls `adapter.rename`. When `enableInlineRename` is on, the menu item opens the in-row input instead.
251
+ - **Errors** — surface as `dialog.alert` and re-open the rename input on validation failure.
252
+
253
+ ## Inline rename
254
+
255
+ Set `enableInlineRename` (requires `adapter.rename`):
256
+
257
+ - F2 opens the inline `<input>` for the focused row.
258
+ - Enter / blur commits → `adapter.rename(node, nextName)`.
259
+ - Escape cancels.
260
+ - The base name is pre-selected (Finder style — `foo.txt` highlights `foo`).
261
+ - Container hotkeys (delete / arrows / F2) are paused while the input is mounted.
262
+
263
+ ```tsx
264
+ const { renamingId, startRename, cancelRename, commitRename } = useTreeRename();
265
+ ```
266
+
267
+ ## Finder hotkeys
268
+
269
+ Set `enableFinderHotkeys` (only fires when the tree container has focus):
270
+
271
+ | Combo | Action |
272
+ | --- | --- |
273
+ | `F2` | rename |
274
+ | `⌘⌫` / `Delete` | delete |
275
+ | `⌘D` | duplicate |
276
+ | `⌘N` | new file |
277
+ | `⌘⇧N` | new folder |
278
+ | `⌘C` / `⌘X` / `⌘V` | copy / cut / paste |
279
+
280
+ Each binding is further gated by the adapter — `⌘⌫` does nothing when `adapter.remove` is undefined. Descriptions register with `useHotkeyHelp` for the global cheat sheet.
281
+
282
+ ## Clipboard (cut / copy / paste)
283
+
284
+ Tree's clipboard is in-memory and tree-local (not the system clipboard) — that lets us dim cut rows the way Finder/Explorer do and gives a single source of truth across menu / hotkey / DnD entry points.
285
+
286
+ ```tsx
287
+ const { clipboard, isCut, cut, copy, paste, clear } = useTreeClipboard();
288
+ ```
289
+
290
+ Paste calls `adapter.move` for cut, `adapter.copy` for copy. Cut + paste clears the clipboard; copy + paste retains it. Errors → `dialog.alert`.
291
+
292
+ CSS hook for custom row styling: `[data-tree-row][data-clipboard="cut"]`.
293
+
294
+ ## Drag and drop
295
+
296
+ Set `enableDnD` (requires `adapter.move`). Powered by `@dnd-kit/core` with pointer + keyboard sensors.
297
+
298
+ | Gesture | Behaviour |
299
+ | --- | --- |
300
+ | Drag a row | If it's part of the current selection, the whole selection drags together; otherwise just that row |
301
+ | Hover over top third of a row | Drop indicator above (reorder `before` sibling) |
302
+ | Hover over middle of a folder | Folder lights up (drop `inside`) |
303
+ | Hover over bottom third of a row | Drop indicator below (reorder `after` sibling) |
304
+ | Hover over empty area below the last row | Root drop target (drop into project root) |
305
+ | Drop onto self / descendant | Rejected by `defaultCanDrop` — indicator turns red |
306
+
307
+ ```tsx
308
+ <TreeRoot
309
+ adapter={adapter}
310
+ enableDnD
311
+ canDrop={({ source, target, position }) => {
312
+ // Layer your domain rules on top of the built-in cycle check.
313
+ if (target?.data.isReadonly) return false;
314
+ return true;
315
+ }}
316
+ />
317
+ ```
318
+
319
+ `useTreeDnd()` exposes `draggingIds`, `dropTarget`, `commitDrop`, `cancelDrag`, and `isAllowedDrop` for custom row renderers.
320
+
321
+ CSS hooks: `[data-tree-row][data-dragging="true"]` on the source row, `[data-tree-drop="before|after|inside"]` on `<TreeDropIndicator>`.
322
+
323
+ ## Empty-area context menu
324
+
325
+ Tree's scroll container always ends with a `<TreeEmptyArea>` that fills the remaining vertical space. Right-clicking on whitespace below the last row opens a menu with the built-in actions that apply to "no row" — `New file`, `New folder`, and `Paste` (if clipboard has items). Items hide automatically when the matching adapter method is undefined; with no relevant items, the area renders as plain whitespace and right-click falls through to the browser default.
326
+
327
+ The empty area is also the **root drop target** during DnD — drop here to move/copy into the project root.
328
+
329
+ ## Context menu
330
+
331
+ Two APIs, pick the lighter one when it fits.
332
+
333
+ **Short-form — `contextMenuActions`.** Pass a resolver that returns a flat list of actions per row. Tree builds a themed `<ContextMenu>` for you and **merges your items with built-in adapter actions** automatically. Use the string `'separator'` for dividers; mark dangerous rows with `destructive: true`.
334
+
335
+ ```tsx
336
+ import { Star } from 'lucide-react';
337
+
338
+ <TreeRoot<FsNode>
339
+ data={data}
340
+ getItemName={(n) => n.data.name}
341
+ adapter={fsAdapter}
342
+ contextMenuActions={({ row, selectedNodes }) => [
343
+ { id: 'star', label: 'Star', icon: Star,
344
+ onSelect: () => star(selectedNodes) },
345
+ ]}
346
+ />
347
+ ```
348
+
349
+ Built-in delete / rename / duplicate / cut / copy / paste / new-file / new-folder appear automatically based on adapter methods.
350
+
351
+ - Right-clicking a row outside the current selection switches selection to that single row first (Finder/Explorer convention), so destructive actions on multi-selection stay predictable.
352
+ - Limit / reorder the built-in items via `defaultMenuItems={['rename', 'delete']}`. Pass `[]` to suppress them entirely while still using the adapter for hotkeys / DnD.
353
+
354
+ **Full control — `renderContextMenu`.** Drop down when you need submenus, checkbox / radio items, custom JSX, or want to compose with non-default ContextMenu primitives. `renderContextMenu` wins if both props are set.
355
+
356
+ ## Localisation
357
+
358
+ Every user-facing string lives on `TreeLabels` and is overridable via the `labels` prop:
359
+
360
+ ```tsx
361
+ <TreeRoot
362
+ labels={{
363
+ confirmDeleteTitle: (n) => n === 1 ? 'Удалить элемент?' : `Удалить ${n} элементов?`,
364
+ actionRename: 'Переименовать',
365
+ actionNewFolder: 'Новая папка',
366
+ invalidNameEmpty: 'Имя не может быть пустым',
367
+ // …all of TreeLabels is partial
368
+ }}
369
+ />
370
+ ```
371
+
372
+ Defaults are English. Function-shaped entries (`confirmDeleteTitle(count)`, `confirmDeleteMessage(names)`, `searchMatches(n)`, `duplicateSuffix(name)`) receive runtime context for proper plurals / interpolation.
373
+
374
+ ## Appearance
116
375
 
117
376
  ```tsx
118
377
  <TreeRoot
119
- data={…}
120
- getItemName={…}
121
378
  appearance={{
122
379
  density: 'cozy', // 'compact' | 'cozy' | 'comfortable'
123
380
  accent: 'default', // 'subtle' | 'default' | 'strong'
@@ -125,18 +382,12 @@ Cosmetic configuration is a single optional prop. Defaults to a comfortable VSCo
125
382
  iconStrokeWidth: 1.5,
126
383
  indentGuideOpacity: 0.4,
127
384
  showActiveIndicator: true,
128
-
129
- // fine-grained overrides (win over density):
130
- rowHeight: 30,
131
- iconSize: 18,
132
- fontSize: 14,
133
- gap: 10,
134
- indent: 20,
385
+ rowHeight: 30, iconSize: 18, fontSize: 14, gap: 10, indent: 20,
135
386
  }}
136
387
  />
137
388
  ```
138
389
 
139
- The resolved appearance is exposed on the root container as CSS variables, so any nested override (`className`, custom slot) can read them:
390
+ The resolved appearance is exposed on the root container as CSS variables:
140
391
 
141
392
  ```
142
393
  --tree-row-height
@@ -148,35 +399,43 @@ The resolved appearance is exposed on the root container as CSS variables, so an
148
399
  --tree-guide-opacity
149
400
  ```
150
401
 
151
- ### VSCode-style highlights
152
-
153
- Row state visuals follow VSCode's Explorer:
402
+ ### VSCode-style row state
154
403
 
155
404
  | State | Look |
156
405
  | --- | --- |
157
- | Hover | subtle neutral wash (`bg-foreground/[.06]`) |
406
+ | Hover | subtle neutral wash |
158
407
  | Focused (keyboard nav, not selected) | slightly stronger neutral |
159
408
  | Selected, tree NOT focused | muted neutral block |
160
409
  | Selected + tree focused-within | primary tint + colored text + left active bar |
161
410
  | Search match | thin primary ring |
411
+ | Cut (clipboard) | `opacity-60`, dimmed |
162
412
  | Disabled | dimmed + cursor-not-allowed |
163
413
 
164
- Toggle the bar with `appearance.showActiveIndicator`. Intensity scales with `appearance.accent`.
414
+ ## Activation modes
165
415
 
166
- ## Extension points
416
+ | Mode | Single click | Double click |
417
+ | --- | --- | --- |
418
+ | `'single-click'` *(default)* | activate `{ preview: false }` | activate `{ preview: false }` |
419
+ | `'double-click'` | select + focus only | activate `{ preview: false }` |
420
+ | `'single-click-preview'` *(VSCode / Cursor)* | activate `{ preview: true }` | activate `{ preview: false }` |
167
421
 
168
- | Need | Mechanism |
422
+ Folders ignore this they always toggle on single click and never call `onActivate`. Keyboard `Enter` / `Space` always activates with `{ preview: false }`.
423
+
424
+ ## Hooks
425
+
426
+ | Hook | What it exposes |
169
427
  | --- | --- |
170
- | Custom row markup | `renderRow={(row) => …}` |
171
- | Replace icon (per file type) | `renderIcon={(row) => …}` (see `WithIcons` story) |
172
- | Modified / error / disabled labels | `renderLabel={(row) => …}` (see `WithStatus` story) |
173
- | Right-side buttons (per row) | `renderActions={(row) => …}` (see `WithActions` story) |
174
- | Right-click menu (declarative, short-form) | `contextMenuActions={(row) => [{ id, label, icon, shortcut, onSelect, destructive }, 'separator', …]}` |
175
- | Right-click menu (full control: submenus, checkbox/radio items, custom JSX) | `renderContextMenu={(row, trigger) => <ContextMenu>…</ContextMenu>}` |
176
- | Localised copy | `labels={{ empty: '…', searchPlaceholder: '…' }}` |
177
- | Persist state | `persistKey="settings.fileTree"`, optional `persistSelection` |
178
- | Imperative actions | `useTreeActions()` |
179
- | Read raw flat rows | `useTreeRows()` |
428
+ | `useTreeContext()` | full `TreeContextValue<T>` — use as a last resort |
429
+ | `useTreeRows()` | flat row list (visible only) |
430
+ | `useTreeSelection()` | `selectedIds`, `anchor`, `clickSelect`, `moveSelect`, `selectAll`, `setSelectedIds`, `clear`, `isSelected` |
431
+ | `useTreeExpansion()` | `expandedIds`, `expand`, `collapse`, `toggle`, `expandAll`, `collapseAll`, `isExpanded` |
432
+ | `useTreeFocus()` | `focusedId`, `setFocus` |
433
+ | `useTreeSearch()` | `query`, `setQuery`, `matchingIds`, `matchCount` |
434
+ | `useTreeRename()` | `renamingId`, `enabled`, `startRename`, `cancelRename`, `commitRename` |
435
+ | `useTreeClipboard()` | `clipboard`, `isCut`, `cut`, `copy`, `paste`, `clear` |
436
+ | `useTreeDnd()` | `active`, `draggingIds`, `dropTarget`, `beginDrag`, `setDropTarget`, `commitDrop`, `cancelDrag`, `isAllowedDrop` |
437
+ | `useTreeActions()` | imperative bag: `expand*` / `collapse*` / `refresh*` / `activate` |
438
+ | `useTreeLabels()` | resolved `TreeLabels` (with overrides applied) |
180
439
 
181
440
  ## Defaults
182
441
 
@@ -186,6 +445,9 @@ Toggle the bar with `appearance.showActiveIndicator`. Intensity scales with `app
186
445
  | `activationMode` | `'single-click'` |
187
446
  | `enableSearch` | `false` |
188
447
  | `enableTypeAhead` | `true` |
448
+ | `enableInlineRename` | `false` |
449
+ | `enableFinderHotkeys` | `false` |
450
+ | `enableDnD` | `false` |
189
451
  | `showIndentGuides` | `false` |
190
452
  | `persistSelection` | `false` |
191
453
  | `appearance.density` | `'cozy'` |
@@ -195,31 +457,9 @@ Toggle the bar with `appearance.showActiveIndicator`. Intensity scales with `app
195
457
  | `appearance.iconStrokeWidth` | `1.5` |
196
458
  | `appearance.indent` | `16` |
197
459
 
198
- ## Stories
199
-
200
- | Story | Demonstrates |
201
- | --- | --- |
202
- | Default | sensible cozy defaults |
203
- | WithActivationModes | single-click / double-click / preview semantics |
204
- | WithHiddenFilter | `filterNode` toggle hides dot-files |
205
- | Densities | three density presets side-by-side |
206
- | WithIcons | file-type icons through `renderIcon` |
207
- | WithStatus | modified / error / disabled rows through `renderLabel` |
208
- | WithActions | rename / delete on hover through `renderActions` |
209
- | WithSearch | built-in search bar + match highlight |
210
- | WithIndentGuides | opt-in vertical guides |
211
- | WithContextMenu | declarative right-click via `contextMenuActions={(row) => […]}` |
212
- | AsyncLazyChildren | `loadChildren` + cache + dedup |
213
- | ExpandCollapseAll | composition mode with `useTreeActions` |
214
- | Persisted | localStorage round-trip |
215
- | LargeTree | ~500 nodes scalability check |
216
- | Playground | every knob exposed as a control |
217
-
218
460
  ## Filtering nodes
219
461
 
220
- `filterNode` is a single predicate that decides which nodes appear at all.
221
- Nodes returning `false` (and their descendants) are excluded from rendering,
222
- keyboard navigation, and search.
462
+ `filterNode` is a single predicate that decides which nodes appear at all. Nodes returning `false` (and their descendants) are excluded from rendering, keyboard navigation, and search.
223
463
 
224
464
  ```tsx
225
465
  const [showHidden, setShowHidden] = useState(false);
@@ -231,115 +471,13 @@ const [showHidden, setShowHidden] = useState(false);
231
471
  />
232
472
  ```
233
473
 
234
- This is intentionally minimal — Tree is generic over `T` and has no opinion
235
- on what "hidden" means in your domain. If your backend already provides
236
- flags like `entry.isHidden` / `entry.isSystem`, use them directly:
474
+ This is intentionally minimal — Tree is generic over `T` and has no opinion on what "hidden" means in your domain. If your backend already provides flags like `entry.isHidden`, use them directly.
237
475
 
238
- ```tsx
239
- filterNode={(n) => showHidden || (!n.data.isHidden && !n.data.isSystem)}
240
- ```
241
-
242
- > **Frontend note.** From the browser you cannot read OS-level hidden
243
- > attributes (Windows `FILE_ATTRIBUTE_HIDDEN`, macOS `kIsInvisible`).
244
- > Either filter by name (Unix dot-prefix is the de-facto convention), or
245
- > let your backend determine those flags and forward them in `node.data`.
246
-
247
- ## Activation modes
476
+ > **Frontend note.** From the browser you cannot read OS-level hidden attributes (`FILE_ATTRIBUTE_HIDDEN`, `kIsInvisible`). Either filter by name (Unix dot-prefix is the de-facto convention), or let your backend determine those flags and forward them in `node.data`.
248
477
 
249
- How a leaf becomes "activated" (opened) on pointer interaction is controlled
250
- by `activationMode`. Folders ignore this setting — they always toggle on
251
- single click and never call `onActivate`.
478
+ ## File icons
252
479
 
253
- | Mode | Single click | Double click |
254
- | --- | --- | --- |
255
- | `'single-click'` *(default)* | activate `{ preview: false }` | activate `{ preview: false }` |
256
- | `'double-click'` | select + focus only | activate `{ preview: false }` |
257
- | `'single-click-preview'` *(VSCode / Cursor)* | activate `{ preview: true }` | activate `{ preview: false }` |
258
-
259
- Keyboard `Enter` / `Space` always activates with `{ preview: false }` —
260
- keyboard input is treated as an explicit user action.
261
-
262
- ```tsx
263
- <TreeRoot<FsNode>
264
- data={data}
265
- getItemName={(n) => n.data.name}
266
- activationMode="single-click-preview"
267
- onActivate={(node, { preview }) =>
268
- preview ? openPreviewTab(node) : openPinnedTab(node)
269
- }
270
- />
271
- ```
272
-
273
- The active mode is also exposed on each row as
274
- `data-activation-mode="<mode>"` for CSS-level targeting.
275
-
276
- ## Right-click menus
277
-
278
- Two APIs, pick the lighter one when it fits.
279
-
280
- **Short-form — `contextMenuActions`.** Pass a resolver that returns a flat
281
- list of actions per row. Tree builds a themed `<ContextMenu>` for you. Use
282
- the string `'separator'` to insert dividers between groups; mark dangerous
283
- rows with `destructive: true`.
284
-
285
- ```tsx
286
- import { Pencil, Copy, Trash2 } from 'lucide-react';
287
-
288
- <TreeRoot<FsNode>
289
- data={data}
290
- getItemName={(n) => n.data.name}
291
- contextMenuActions={({ node, isFolder }) => [
292
- { id: 'rename', label: 'Rename', icon: Pencil, shortcut: '↵',
293
- onSelect: () => rename(node) },
294
- { id: 'copy', label: 'Copy', icon: Copy, shortcut: '⌘C',
295
- onSelect: () => copy(node) },
296
- 'separator',
297
- { id: 'delete', label: 'Delete', icon: Trash2, shortcut: '⌫',
298
- destructive: true,
299
- onSelect: () => del(node, { folder: isFolder }) },
300
- ]}
301
- />
302
- ```
303
-
304
- Return `null` / `undefined` / `[]` from the resolver to skip the menu for a
305
- specific row (e.g. read-only system entries).
306
-
307
- **Full control — `renderContextMenu`.** Drop down when you need submenus,
308
- checkbox / radio items, custom JSX, or want to compose with non-default
309
- ContextMenu primitives. `renderContextMenu` wins if both props are set.
310
-
311
- ```tsx
312
- import {
313
- ContextMenu, ContextMenuTrigger, ContextMenuContent,
314
- ContextMenuSub, ContextMenuSubTrigger, ContextMenuSubContent,
315
- ContextMenuCheckboxItem,
316
- } from '@djangocfg/ui-core/components';
317
-
318
- <TreeRoot<FsNode>
319
- data={data}
320
- getItemName={(n) => n.data.name}
321
- renderContextMenu={({ node }, trigger) => (
322
- <ContextMenu>
323
- <ContextMenuTrigger asChild>{trigger}</ContextMenuTrigger>
324
- <ContextMenuContent>
325
- <ContextMenuCheckboxItem checked={isPinned(node.id)}
326
- onCheckedChange={(v) => togglePin(node.id, v)}>
327
- Pin to top
328
- </ContextMenuCheckboxItem>
329
- <ContextMenuSub>
330
- <ContextMenuSubTrigger>Open with…</ContextMenuSubTrigger>
331
- <ContextMenuSubContent>{/* editors */}</ContextMenuSubContent>
332
- </ContextMenuSub>
333
- </ContextMenuContent>
334
- </ContextMenu>
335
- )}
336
- />
337
- ```
338
-
339
- ## VSCode-style file icons
340
-
341
- Tree is generic over `T` — it has no opinion on whether nodes are files. For a
342
- ready-made VSCode-style icon set, use the `file-icon` companion subpath:
480
+ Tree is generic over `T` it has no opinion on whether nodes are files. For a ready-made VSCode-style icon set, use the `file-icon` companion subpath:
343
481
 
344
482
  ```tsx
345
483
  import { TreeRoot } from '@djangocfg/ui-tools/tree';
@@ -352,29 +490,20 @@ import { createFileIconSlot } from '@djangocfg/ui-tools/file-icon';
352
490
  />
353
491
  ```
354
492
 
355
- The icon SVGs are vendored statically from `material-file-icons` (MIT) — no
356
- runtime dependency, no install step, and resolution is synchronous in any
357
- bundler.
358
-
359
- Folders use a small built-in mapping (`src` → `FolderCode`,
360
- `node_modules` → `Package`, `.git` → `FolderGit2`, `dist`/`build`/`.next`
361
- → `FolderOutput`, `tests`/`__tests__` → `FlaskConical`, …). Anything not
362
- in the table renders the generic `Folder` / `FolderOpen` pair. Override
363
- or extend the table per-call:
493
+ The icon SVGs are vendored statically from `material-file-icons` (MIT) — no runtime dependency, no install step, synchronous resolution.
364
494
 
365
- ```tsx
366
- import { FolderHeart } from 'lucide-react';
367
-
368
- renderIcon={createFileIconSlot({
369
- getName: (n) => n.data.name,
370
- folderOverrides: { favorites: FolderHeart },
371
- })}
372
- ```
495
+ ## Status
373
496
 
374
- ## Out of scope (today)
375
-
376
- - Inline rename UX
377
- - Drag-and-drop
378
- - Virtualization (wrap with `@tanstack/react-virtual` when needed)
379
- - Multi-tree (cross-tree DnD)
380
- - Live filesystem watchers / quick-open palette app-level concerns; build them on top of `useTreeActions()` and `useTreeContext()`.
497
+ | Feature | Status |
498
+ | --- | --- |
499
+ | Multi-selection (anchor / shift / ⌘+A) | ✅ |
500
+ | `TreeAdapter` + built-in CRUD via `window.dialog` | ✅ |
501
+ | `<FinderTree>` preset | |
502
+ | Inline rename (F2, auto-select base) | ✅ |
503
+ | Finder hotkeys (⌘⌫ F2 ⌘D ⌘N ⌘⇧N ⌘C ⌘X ⌘V) | |
504
+ | Clipboard (cut / copy / paste, dimmed cut state) | ✅ |
505
+ | Drag-and-drop (`@dnd-kit/core`) | ✅ |
506
+ | Empty-area context menu (`new file / new folder / paste` on background) | ✅ |
507
+ | Undo / Redo | n/a (host's job — implement in your adapter / Wails-Go layer) |
508
+ | Virtualization | out of scope (wrap with `@tanstack/react-virtual`) |
509
+ | Multi-tree cross-DnD | out of scope |