@djangocfg/widget-data 0.1.1

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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -0
  3. package/package.json +122 -0
  4. package/src/DataGrid/README.md +48 -0
  5. package/src/DataGrid/context/DataGridContext.tsx +442 -0
  6. package/src/DataGrid/context/index.ts +4 -0
  7. package/src/DataGrid/index.ts +34 -0
  8. package/src/DataGrid/lazy.tsx +23 -0
  9. package/src/DataGrid/parts/DataGrid.tsx +125 -0
  10. package/src/DataGrid/parts/DataGridCell.tsx +30 -0
  11. package/src/DataGrid/parts/DataGridHeader.tsx +111 -0
  12. package/src/DataGrid/parts/DataGridPagination.tsx +105 -0
  13. package/src/DataGrid/parts/DataGridRow.tsx +84 -0
  14. package/src/DataGrid/parts/index.ts +7 -0
  15. package/src/DataGrid/types.ts +95 -0
  16. package/src/DataTable/README.md +42 -0
  17. package/src/DataTable/context/DataTableContext.tsx +434 -0
  18. package/src/DataTable/context/index.ts +4 -0
  19. package/src/DataTable/index.ts +33 -0
  20. package/src/DataTable/lazy.tsx +23 -0
  21. package/src/DataTable/parts/DataTable.tsx +142 -0
  22. package/src/DataTable/parts/DataTableHeader.tsx +103 -0
  23. package/src/DataTable/parts/DataTablePagination.tsx +115 -0
  24. package/src/DataTable/parts/DataTableRow.tsx +96 -0
  25. package/src/DataTable/parts/index.ts +6 -0
  26. package/src/DataTable/types.ts +88 -0
  27. package/src/FileIcon/FileIcon.tsx +87 -0
  28. package/src/FileIcon/get-file-icon.ts +73 -0
  29. package/src/FileIcon/icons/icon-data.ts +399 -0
  30. package/src/FileIcon/index.ts +19 -0
  31. package/src/FileIcon/loader.ts +29 -0
  32. package/src/FileIcon/specialFolders.ts +110 -0
  33. package/src/FileIcon/treeAdapter.tsx +49 -0
  34. package/src/JsonTree/JsonViewer.tsx +720 -0
  35. package/src/JsonTree/README.md +142 -0
  36. package/src/JsonTree/index.tsx +11 -0
  37. package/src/JsonTree/lazy.tsx +22 -0
  38. package/src/JsonTree/types.ts +91 -0
  39. package/src/Kanban/context/KanbanContext.tsx +165 -0
  40. package/src/Kanban/context/index.ts +4 -0
  41. package/src/Kanban/index.ts +31 -0
  42. package/src/Kanban/lazy.tsx +23 -0
  43. package/src/Kanban/parts/Kanban.tsx +77 -0
  44. package/src/Kanban/parts/KanbanCard.tsx +76 -0
  45. package/src/Kanban/parts/KanbanColumn.tsx +79 -0
  46. package/src/Kanban/parts/KanbanDragOverlay.tsx +46 -0
  47. package/src/Kanban/parts/index.ts +6 -0
  48. package/src/Kanban/types.ts +45 -0
  49. package/src/Listbox/context/ListboxProvider.tsx +616 -0
  50. package/src/Listbox/context/index.ts +8 -0
  51. package/src/Listbox/index.ts +28 -0
  52. package/src/Listbox/lazy.tsx +31 -0
  53. package/src/Listbox/parts/ListboxGroup.tsx +71 -0
  54. package/src/Listbox/parts/ListboxItem.tsx +151 -0
  55. package/src/Listbox/parts/index.ts +12 -0
  56. package/src/Listbox/types.ts +98 -0
  57. package/src/Masonry/README.md +79 -0
  58. package/src/Masonry/context/MasonryProvider.tsx +1205 -0
  59. package/src/Masonry/context/index.ts +8 -0
  60. package/src/Masonry/index.ts +18 -0
  61. package/src/Masonry/lazy.tsx +20 -0
  62. package/src/Masonry/parts/index.ts +2 -0
  63. package/src/Masonry/types.ts +44 -0
  64. package/src/Timeline/context/TimelineProvider.tsx +474 -0
  65. package/src/Timeline/context/index.ts +16 -0
  66. package/src/Timeline/index.ts +29 -0
  67. package/src/Timeline/lazy.tsx +27 -0
  68. package/src/Timeline/parts/index.ts +2 -0
  69. package/src/Timeline/types.ts +25 -0
  70. package/src/Tree/FinderTree.tsx +42 -0
  71. package/src/Tree/README.md +542 -0
  72. package/src/Tree/TreeDndProvider.tsx +137 -0
  73. package/src/Tree/TreeRoot.tsx +290 -0
  74. package/src/Tree/components/TreeChevron.tsx +48 -0
  75. package/src/Tree/components/TreeContent.tsx +70 -0
  76. package/src/Tree/components/TreeDropIndicator.tsx +65 -0
  77. package/src/Tree/components/TreeEmpty.tsx +22 -0
  78. package/src/Tree/components/TreeEmptyArea.tsx +160 -0
  79. package/src/Tree/components/TreeError.tsx +24 -0
  80. package/src/Tree/components/TreeIcon.tsx +39 -0
  81. package/src/Tree/components/TreeIndentGuides.tsx +33 -0
  82. package/src/Tree/components/TreeLabel.tsx +39 -0
  83. package/src/Tree/components/TreeRenameInput.tsx +114 -0
  84. package/src/Tree/components/TreeRow.tsx +300 -0
  85. package/src/Tree/components/TreeSearchInput.tsx +50 -0
  86. package/src/Tree/components/TreeSkeleton.tsx +22 -0
  87. package/src/Tree/components/index.ts +28 -0
  88. package/src/Tree/context/TreeContext.tsx +409 -0
  89. package/src/Tree/context/TreeContextValue.ts +139 -0
  90. package/src/Tree/context/async-children/collect-ids.ts +27 -0
  91. package/src/Tree/context/async-children/index.ts +8 -0
  92. package/src/Tree/context/async-children/use-async-children.ts +157 -0
  93. package/src/Tree/context/clipboard/index.ts +4 -0
  94. package/src/Tree/context/clipboard/use-clipboard.ts +115 -0
  95. package/src/Tree/context/dnd/index.ts +8 -0
  96. package/src/Tree/context/dnd/use-dnd.ts +194 -0
  97. package/src/Tree/context/expansion/index.ts +4 -0
  98. package/src/Tree/context/expansion/use-expansion.ts +55 -0
  99. package/src/Tree/context/hooks.ts +177 -0
  100. package/src/Tree/context/index.ts +16 -0
  101. package/src/Tree/context/menu/builtin-actions.ts +358 -0
  102. package/src/Tree/context/menu/index.ts +11 -0
  103. package/src/Tree/context/menu/render.tsx +104 -0
  104. package/src/Tree/context/menu/use-resolved-menu.ts +141 -0
  105. package/src/Tree/context/persist/index.ts +4 -0
  106. package/src/Tree/context/persist/use-persist-sync.ts +74 -0
  107. package/src/Tree/context/rename/index.ts +4 -0
  108. package/src/Tree/context/rename/use-rename.ts +113 -0
  109. package/src/Tree/context/selection/index.ts +4 -0
  110. package/src/Tree/context/selection/use-selection.ts +146 -0
  111. package/src/Tree/context/state/index.ts +6 -0
  112. package/src/Tree/context/state/initial.ts +41 -0
  113. package/src/Tree/context/state/reducer.ts +76 -0
  114. package/src/Tree/context/state/types.ts +46 -0
  115. package/src/Tree/data/appearance.ts +237 -0
  116. package/src/Tree/data/childCache.ts +43 -0
  117. package/src/Tree/data/clipboard.ts +33 -0
  118. package/src/Tree/data/createDemoTree.ts +42 -0
  119. package/src/Tree/data/dnd.ts +123 -0
  120. package/src/Tree/data/finderShortcuts.ts +67 -0
  121. package/src/Tree/data/flatten.ts +66 -0
  122. package/src/Tree/data/index.ts +45 -0
  123. package/src/Tree/data/persist.ts +62 -0
  124. package/src/Tree/data/renameUtils.ts +51 -0
  125. package/src/Tree/data/selection.ts +161 -0
  126. package/src/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
  127. package/src/Tree/hooks/finder-hotkeys/index.ts +8 -0
  128. package/src/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
  129. package/src/Tree/hooks/index.ts +25 -0
  130. package/src/Tree/hooks/keyboard/activation.ts +27 -0
  131. package/src/Tree/hooks/keyboard/arrow-nav.ts +26 -0
  132. package/src/Tree/hooks/keyboard/expand-collapse.ts +54 -0
  133. package/src/Tree/hooks/keyboard/index.ts +10 -0
  134. package/src/Tree/hooks/keyboard/types.ts +39 -0
  135. package/src/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
  136. package/src/Tree/hooks/type-ahead/index.ts +5 -0
  137. package/src/Tree/hooks/type-ahead/match-prefix.ts +42 -0
  138. package/src/Tree/hooks/type-ahead/use-tree-type-ahead.ts +89 -0
  139. package/src/Tree/index.tsx +135 -0
  140. package/src/Tree/lazy.tsx +14 -0
  141. package/src/Tree/types/activation.ts +30 -0
  142. package/src/Tree/types/adapter.ts +70 -0
  143. package/src/Tree/types/index.ts +27 -0
  144. package/src/Tree/types/labels.ts +97 -0
  145. package/src/Tree/types/loader.ts +9 -0
  146. package/src/Tree/types/node.ts +38 -0
  147. package/src/Tree/types/root-props.ts +158 -0
  148. package/src/Tree/types/selection.ts +3 -0
  149. package/src/Tree/types/slots.ts +64 -0
  150. package/src/index.ts +21 -0
  151. package/src/stories/_fixtures/categories.ts +51 -0
  152. package/src/stories/_fixtures/finder-store.ts +196 -0
  153. package/src/stories/_fixtures/fs.ts +83 -0
  154. package/src/stories/_fixtures/index.ts +5 -0
  155. package/src/stories/_fixtures/org.ts +46 -0
  156. package/src/stories/_fixtures/status.ts +21 -0
  157. package/src/styles/index.css +10 -0
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import { TreeRoot } from './TreeRoot';
4
+ import type { TreeRootProps } from './types';
5
+
6
+ /**
7
+ * `<FinderTree>` — opinionated Finder/Explorer-style preset.
8
+ *
9
+ * Equivalent to `<TreeRoot>` with multi-selection, double-click activation,
10
+ * inline rename, indent guides, and a cozy appearance turned on. Pass an
11
+ * `adapter` to get the built-in CRUD menu wired to `window.dialog.*`.
12
+ *
13
+ * Override any preset default by simply passing the same prop:
14
+ *
15
+ * ```tsx
16
+ * <FinderTree<FsNode>
17
+ * data={data}
18
+ * getItemName={(n) => n.data.name}
19
+ * adapter={fsAdapter}
20
+ * // override one preset default — everything else stays Finder-y:
21
+ * activationMode="single-click-preview"
22
+ * />
23
+ * ```
24
+ */
25
+ export function FinderTree<T>(props: TreeRootProps<T>) {
26
+ return (
27
+ <TreeRoot<T>
28
+ // Finder/Explorer defaults — every one is overridable via `props`.
29
+ selectionMode="multiple"
30
+ activationMode="double-click"
31
+ enableInlineRename
32
+ enableFinderHotkeys
33
+ enableDnD
34
+ enableTypeAhead
35
+ showIndentGuides
36
+ appearance={{ density: 'cozy' }}
37
+ {...props}
38
+ />
39
+ );
40
+ }
41
+
42
+ export default FinderTree;
@@ -0,0 +1,542 @@
1
+ # Tree
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, **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.
9
+
10
+ ## Why this exists
11
+
12
+ We tried popular headless tree engines first. They all leak React-integration bugs (state references, mounting order, click handlers desyncing from re-renders). So this tree is intentionally small and predictable: a `useReducer` holds the state, a `flattenTree` walk produces visible rows, components consume those rows. No black boxes.
13
+
14
+ ## Philosophy
15
+
16
+ 1. **No engine.** State lives in plain React. Every interaction goes through React's commit cycle.
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.
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.
19
+ 4. **Slots over props.** New visual needs add a slot, not a flag: `renderRow` / `renderIcon` / `renderLabel` / `renderActions` / `renderContextMenu`.
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.
23
+ 8. **Honest empty semantics.** An empty collection is announced as a status and
24
+ does not expose an invalid, focusable ARIA tree until at least one tree item
25
+ exists.
26
+
27
+ ## Layered architecture
28
+
29
+ ```
30
+ types/ public types — generic over T, no `any`, folders-per-concept
31
+ node.ts TreeItemId / TreeNode / FlatRow
32
+ selection.ts TreeSelectionMode
33
+ activation.ts TreeActivationMode / TreeActivateOptions
34
+ loader.ts TreeLoadChildren
35
+ labels.ts TreeLabels + DEFAULT_TREE_LABELS (CRUD copy too)
36
+ slots.ts TreeRowSlot / TreeContextMenuSlot / TreeContextMenuItem / …
37
+ adapter.ts TreeAdapter / TreeBuiltinAction / TreeMovePosition
38
+ root-props.ts TreeRootProps
39
+
40
+ TreeRoot.tsx high-level entry — Provider + shell + content
41
+ TreeDndProvider.tsx thin <DndContext> wrapper (no-op when DnD off)
42
+ FinderTree.tsx Finder/Explorer preset over TreeRoot
43
+ lazy.tsx LazyTree via createLazyComponent
44
+
45
+ data/ pure helpers, zero React
46
+ appearance.ts density / accent / radius / sizes → CSS vars + classes
47
+ childCache.ts id → { status, children, error }
48
+ flatten.ts roots + expanded + cache → FlatRow<T>[]
49
+ persist.ts versioned localStorage helper
50
+ createDemoTree.ts deterministic synthetic tree for stories/tests
51
+ selection.ts Finder selection: anchor + shift-range + ⌘+A
52
+ clipboard.ts tree-local cut/copy state
53
+ renameUtils.ts splitFileName / autoSelectRange (base without ext)
54
+ finderShortcuts.ts Finder/Explorer keymap (mod+⌫, F2, ⌘D, …)
55
+ dnd.ts resolveDropZone + defaultCanDrop (cycle / self-drop)
56
+
57
+ context/ Provider + per-feature hooks (folders-per-feature)
58
+ TreeContext.tsx thin assembly: stitches the hooks below into one value
59
+ TreeContextValue.ts interface TreeContextValue<T>
60
+ hooks.ts public hooks: useTreeSelection / Expansion / Rename / Clipboard / Dnd / …
61
+ state/ reducer + initial state + action types
62
+ async-children/ cache + nodeById + fetchChildren + refresh / refreshAll
63
+ expansion/ expand / collapse / toggle / expandAll / collapseAll
64
+ selection/ clickSelect / moveSelect / selectAll + plain select / clear
65
+ rename/ startRename / cancelRename / commitRename (window.dialog.alert on error)
66
+ clipboard/ cutToClipboard / copyToClipboard / pasteFromClipboard
67
+ menu/ built-in actions registry + merged declarative resolver
68
+ dnd/ draggingIds / dropTarget / commitDrop / canDrop layering
69
+ persist/ localStorage + onSelectionChange / onExpansionChange notify
70
+
71
+ hooks/ container-level keyboard hooks (folders-per-scope)
72
+ keyboard/ ↑↓ ←→ Home/End Enter/Space Esc ⌘+A (Shift extends)
73
+ type-ahead/ Finder-style 600 ms prefix buffer
74
+ finder-hotkeys/ ⌘⌫ F2 ⌘D ⌘N ⌘⇧N ⌘C ⌘X ⌘V → adapter actions
75
+
76
+ components/
77
+ TreeRow.tsx default row: chevron + icon + label + actions + ctx-menu
78
+ TreeRenameInput.tsx inline rename input (auto-selects base name without extension)
79
+ TreeDropIndicator.tsx drop indicator (before / after line, inside fill)
80
+ TreeEmptyArea.tsx fills space below last row — empty context menu + root drop target
81
+ TreeChevron / TreeIcon / TreeLabel / TreeIndentGuides
82
+ TreeSearchInput.tsx controlled search input
83
+ TreeContent.tsx iterates flatRows, default-renders TreeRow
84
+ TreeEmpty / TreeSkeleton / TreeError
85
+ ```
86
+
87
+ 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.
88
+
89
+ ## Quick start
90
+
91
+ ```tsx
92
+ import { TreeRoot, type TreeNode } from '@djangocfg/ui-tools/tree';
93
+
94
+ interface FsNode { name: string }
95
+
96
+ const data: TreeNode<FsNode>[] = [
97
+ {
98
+ id: 'src',
99
+ data: { name: 'src' },
100
+ children: [{ id: 'index.ts', data: { name: 'index.ts' } }],
101
+ },
102
+ ];
103
+
104
+ <TreeRoot<FsNode>
105
+ data={data}
106
+ getItemName={(n) => n.data.name}
107
+ onSelectionChange={(ids) => console.log(ids)}
108
+ onActivate={(node) => openFile(node.id)}
109
+ enableSearch
110
+ showIndentGuides
111
+ persistKey="settings.fileTree"
112
+ />
113
+ ```
114
+
115
+ ## Finder/Explorer preset
116
+
117
+ `<FinderTree>` is `<TreeRoot>` with sensible Finder defaults pre-set:
118
+
119
+ ```tsx
120
+ import { FinderTree, type TreeAdapter } from '@djangocfg/ui-tools/tree';
121
+
122
+ const fsAdapter: TreeAdapter<FsNode> = {
123
+ remove: async (nodes) => api.delete(nodes.map((n) => n.id)),
124
+ rename: async (node, name) => api.rename(node.id, name),
125
+ createFolder: async (parent, name) => api.mkdir(parent?.id ?? null, name),
126
+ move: async (nodes, target) => api.move(nodes.map((n) => n.id), target?.id ?? null),
127
+ // …
128
+ };
129
+
130
+ <FinderTree<FsNode>
131
+ data={data}
132
+ getItemName={(n) => n.data.name}
133
+ adapter={fsAdapter}
134
+ />
135
+ ```
136
+
137
+ Equivalent to:
138
+
139
+ ```tsx
140
+ <TreeRoot<FsNode>
141
+ data={data}
142
+ getItemName={(n) => n.data.name}
143
+ adapter={fsAdapter}
144
+ selectionMode="multiple"
145
+ activationMode="double-click"
146
+ enableInlineRename
147
+ enableFinderHotkeys
148
+ enableTypeAhead
149
+ showIndentGuides
150
+ appearance={{ density: 'cozy' }}
151
+ />
152
+ ```
153
+
154
+ Override any default by passing the prop:
155
+
156
+ ```tsx
157
+ <FinderTree
158
+ /* …data, getItemName, adapter… */
159
+ activationMode="single-click-preview" // VSCode/Cursor preview tabs
160
+ selectionMode="single" // disable multi-select
161
+ />
162
+ ```
163
+
164
+ ## Async children
165
+
166
+ ```tsx
167
+ const data: TreeNode<FsNode>[] = [
168
+ { id: 'root', data: { name: 'remote' }, isFolder: true },
169
+ ];
170
+
171
+ <TreeRoot<FsNode>
172
+ data={data}
173
+ getItemName={(n) => n.data.name}
174
+ loadChildren={async (node) => {
175
+ const list = await fetchChildren(node.id);
176
+ return list.map((it) => ({ id: it.id, data: it, isFolder: it.isDir }));
177
+ }}
178
+ />
179
+ ```
180
+
181
+ The provider caches resolved children, de-duplicates concurrent fetches per id, and re-renders when the cache mutates. Use `useTreeActions().refresh(id)` to invalidate one node, or `refreshAll()` after a backend signal.
182
+
183
+ ## Composition mode
184
+
185
+ ```tsx
186
+ import {
187
+ TreeProvider, TreeContent, TreeSearchInput,
188
+ useTreeSelection, useTreeActions,
189
+ } from '@djangocfg/ui-tools/tree';
190
+
191
+ <TreeProvider data={data} getItemName={getName}>
192
+ <Toolbar />
193
+ <TreeSearchInput />
194
+ <TreeContent>{(row) => <CustomRow {...row} />}</TreeContent>
195
+ </TreeProvider>
196
+
197
+ function Toolbar() {
198
+ const { expandAll, collapseAll, refreshAll } = useTreeActions();
199
+ // …
200
+ }
201
+ ```
202
+
203
+ ## Multi-selection (Finder / Explorer semantics)
204
+
205
+ When `selectionMode="multiple"`, Tree implements full file-manager selection:
206
+
207
+ | Gesture | Behaviour |
208
+ | --- | --- |
209
+ | **plain click** | replace selection, set anchor |
210
+ | **⌘ / Ctrl + click** | toggle row, set anchor |
211
+ | **shift + click** | range from anchor to clicked row |
212
+ | **shift + ⌘ + click** | union range with existing selection |
213
+ | **shift + ↑/↓** | extend range one row from anchor |
214
+ | **shift + Home / End** | extend range to top / bottom of visible rows |
215
+ | **⌘ / Ctrl + A** | select every visible row |
216
+ | **Esc** | clear selection (focused row stays) |
217
+
218
+ `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()`:
219
+
220
+ ```tsx
221
+ const {
222
+ selectedIds, anchor,
223
+ clickSelect, moveSelect, selectAll,
224
+ setSelectedIds, clear, isSelected,
225
+ } = useTreeSelection();
226
+ ```
227
+
228
+ `clickSelect(id, { shift, meta })` and `moveSelect(id, { extend })` are also exposed for consumers building custom row components.
229
+
230
+ ## CRUD adapter (delete / rename / new / cut+copy+paste)
231
+
232
+ ```ts
233
+ import type { TreeAdapter } from '@djangocfg/ui-tools/tree';
234
+
235
+ const adapter: TreeAdapter<FsNode> = {
236
+ remove?: (nodes) => Promise<void>;
237
+ rename?: (node, nextName) => Promise<void>;
238
+ createFile?: (parent, name) => Promise<void>; // parent === null → root
239
+ createFolder?: (parent, name) => Promise<void>;
240
+ duplicate?: (nodes) => Promise<void>;
241
+ move?: (nodes, target, position) => Promise<void>; // DnD + cut+paste
242
+ copy?: (nodes, target, position) => Promise<void>; // copy+paste
243
+ validateName?: (name, ctx) => string | null; // null = ok; string = error to show
244
+ };
245
+ ```
246
+
247
+ 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.
248
+
249
+ `window.dialog.confirm / prompt / alert` (from `@djangocfg/ui-core/lib/dialog-service`) drives every flow:
250
+
251
+ - **Delete** — confirms via `dialog.confirm({ variant: 'destructive' })`, then calls `adapter.remove`.
252
+ - **New file / folder** — prompts for a name via `dialog.prompt`, validates via `adapter.validateName`, then calls `adapter.createFile/createFolder`.
253
+ - **Rename (no inline)** — prompts via `dialog.prompt` and calls `adapter.rename`. When `enableInlineRename` is on, the menu item opens the in-row input instead.
254
+ - **Errors** — surface as `dialog.alert` and re-open the rename input on validation failure.
255
+
256
+ ## Inline rename
257
+
258
+ Set `enableInlineRename` (requires `adapter.rename`):
259
+
260
+ - F2 opens the inline `<input>` for the focused row.
261
+ - Enter / blur commits → `adapter.rename(node, nextName)`.
262
+ - Escape cancels.
263
+ - The base name is pre-selected (Finder style — `foo.txt` highlights `foo`).
264
+ - Container hotkeys (delete / arrows / F2) are paused while the input is mounted.
265
+
266
+ ```tsx
267
+ const { renamingId, startRename, cancelRename, commitRename } = useTreeRename();
268
+ ```
269
+
270
+ ## Finder hotkeys
271
+
272
+ Set `enableFinderHotkeys` (only fires when the tree container has focus):
273
+
274
+ | Combo | Action |
275
+ | --- | --- |
276
+ | `F2` | rename |
277
+ | `⌘⌫` / `Delete` | delete |
278
+ | `⌘D` | duplicate |
279
+ | `⌘N` | new file |
280
+ | `⌘⇧N` | new folder |
281
+ | `⌘C` / `⌘X` / `⌘V` | copy / cut / paste |
282
+
283
+ Each binding is further gated by the adapter — `⌘⌫` does nothing when `adapter.remove` is undefined. Descriptions register with `useHotkeyHelp` for the global cheat sheet.
284
+
285
+ ## Clipboard (cut / copy / paste)
286
+
287
+ 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.
288
+
289
+ ```tsx
290
+ const { clipboard, isCut, cut, copy, paste, clear } = useTreeClipboard();
291
+ ```
292
+
293
+ Paste calls `adapter.move` for cut, `adapter.copy` for copy. Cut + paste clears the clipboard; copy + paste retains it. Errors → `dialog.alert`.
294
+
295
+ CSS hook for custom row styling: `[data-tree-row][data-clipboard="cut"]`.
296
+
297
+ ## Drag and drop
298
+
299
+ Set `enableDnD` (requires `adapter.move`). Powered by `@dnd-kit/core` with pointer + keyboard sensors.
300
+
301
+ | Gesture | Behaviour |
302
+ | --- | --- |
303
+ | Drag a row | If it's part of the current selection, the whole selection drags together; otherwise just that row |
304
+ | Hover over top third of a row | Drop indicator above (reorder `before` sibling) |
305
+ | Hover over middle of a folder | Folder lights up (drop `inside`) |
306
+ | Hover over bottom third of a row | Drop indicator below (reorder `after` sibling) |
307
+ | Hover over empty area below the last row | Root drop target (drop into project root) |
308
+ | Drop onto self / descendant | Rejected by `defaultCanDrop` — indicator turns red |
309
+
310
+ ```tsx
311
+ <TreeRoot
312
+ adapter={adapter}
313
+ enableDnD
314
+ canDrop={({ source, target, position }) => {
315
+ // Layer your domain rules on top of the built-in cycle check.
316
+ if (target?.data.isReadonly) return false;
317
+ return true;
318
+ }}
319
+ />
320
+ ```
321
+
322
+ `useTreeDnd()` exposes `draggingIds`, `dropTarget`, `commitDrop`, `cancelDrag`, and `isAllowedDrop` for custom row renderers.
323
+
324
+ CSS hooks: `[data-tree-row][data-dragging="true"]` on the source row, `[data-tree-drop="before|after|inside"]` on `<TreeDropIndicator>`.
325
+
326
+ ## Empty-area context menu
327
+
328
+ 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.
329
+
330
+ The empty area is also the **root drop target** during DnD — drop here to move/copy into the project root.
331
+
332
+ ## Context menu
333
+
334
+ Two APIs, pick the lighter one when it fits.
335
+
336
+ **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`.
337
+
338
+ ```tsx
339
+ import { Star } from 'lucide-react';
340
+
341
+ <TreeRoot<FsNode>
342
+ data={data}
343
+ getItemName={(n) => n.data.name}
344
+ adapter={fsAdapter}
345
+ contextMenuActions={({ row, selectedNodes }) => [
346
+ { id: 'star', label: 'Star', icon: Star,
347
+ onSelect: () => star(selectedNodes) },
348
+ ]}
349
+ />
350
+ ```
351
+
352
+ Built-in delete / rename / duplicate / cut / copy / paste / new-file / new-folder appear automatically based on adapter methods.
353
+
354
+ - 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.
355
+ - Limit / reorder the built-in items via `defaultMenuItems={['rename', 'delete']}`. Pass `[]` to suppress them entirely while still using the adapter for hotkeys / DnD.
356
+
357
+ **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.
358
+
359
+ ## Localisation
360
+
361
+ Every user-facing string lives on `TreeLabels` and is overridable via the `labels` prop:
362
+
363
+ ```tsx
364
+ <TreeRoot
365
+ labels={{
366
+ confirmDeleteTitle: (n) => n === 1 ? 'Удалить элемент?' : `Удалить ${n} элементов?`,
367
+ actionRename: 'Переименовать',
368
+ actionNewFolder: 'Новая папка',
369
+ invalidNameEmpty: 'Имя не может быть пустым',
370
+ // …all of TreeLabels is partial
371
+ }}
372
+ />
373
+ ```
374
+
375
+ Defaults are English. Function-shaped entries (`confirmDeleteTitle(count)`, `confirmDeleteMessage(names)`, `searchMatches(n)`, `duplicateSuffix(name)`) receive runtime context for proper plurals / interpolation.
376
+
377
+ ## Appearance
378
+
379
+ ```tsx
380
+ <TreeRoot
381
+ appearance={{
382
+ variant: 'explorer', // 'explorer' (default) | 'list'
383
+ density: 'cozy', // 'compact' | 'cozy' | 'comfortable'
384
+ accent: 'default', // 'subtle' | 'default' | 'strong'
385
+ radius: 'sm', // 'none' | 'sm' | 'md'
386
+ iconStrokeWidth: 1.5,
387
+ indentGuideOpacity: 0.4,
388
+ showActiveIndicator: true,
389
+ rowHeight: 30, iconSize: 18, fontSize: 14, gap: 10, indent: 20,
390
+ }}
391
+ />
392
+ ```
393
+
394
+ ### Variant — `explorer` vs `list`
395
+
396
+ `variant` is a high-level look that sets sensible defaults for three
397
+ fields (`hideLeafIcons`, `hideFolderIcons`, `rowSizing`,
398
+ `showActiveIndicator`); any of those passed explicitly still wins.
399
+
400
+ | Variant | Look | Defaults |
401
+ | --- | --- | --- |
402
+ | `'explorer'` (default) | Classic file/folder tree | leaf + folder icons, **fixed** single-line rows, active bar on |
403
+ | `'list'` | macOS-sidebar / **chat-history** list | **no icons** (chevron only on groups), **auto-height** rows (multi-line label friendly), quiet selection (no active bar) |
404
+
405
+ ```tsx
406
+ // A grouped chat-history sidebar (date / topic buckets → sessions),
407
+ // multi-line rows (title + dimmed meta), no file glyphs:
408
+ <TreeRoot<Payload>
409
+ data={groupedSessions}
410
+ getItemName={(n) => label(n)}
411
+ appearance={{ variant: 'list', indent: 10 }}
412
+ showIndentGuides={false}
413
+ renderLabel={({ node }) => /* title + meta */ null}
414
+ />
415
+ ```
416
+
417
+ Fine-grained fields (override the variant):
418
+
419
+ - `hideLeafIcons` / `hideFolderIcons` — drop the per-row icon (chevron stays).
420
+ - `rowSizing: 'fixed' | 'auto'` — `auto` turns `rowHeight` into a *minimum*
421
+ so a multi-line label grows the row instead of overflowing.
422
+
423
+ The resolved appearance is exposed on the root container as CSS variables:
424
+
425
+ ```
426
+ --tree-row-height
427
+ --tree-icon-size
428
+ --tree-icon-stroke
429
+ --tree-font-size
430
+ --tree-gap
431
+ --tree-indent
432
+ --tree-guide-opacity
433
+ ```
434
+
435
+ ### VSCode-style row state
436
+
437
+ | State | Look |
438
+ | --- | --- |
439
+ | Hover | subtle neutral wash |
440
+ | Focused (keyboard nav, not selected) | slightly stronger neutral |
441
+ | Selected, tree NOT focused | muted neutral block |
442
+ | Selected + tree focused-within | primary tint + colored text + left active bar |
443
+ | Search match | thin primary ring |
444
+ | Cut (clipboard) | `opacity-60`, dimmed |
445
+ | Disabled | dimmed + cursor-not-allowed |
446
+
447
+ ## Activation modes
448
+
449
+ | Mode | Single click | Double click |
450
+ | --- | --- | --- |
451
+ | `'single-click'` *(default)* | activate `{ preview: false }` | activate `{ preview: false }` |
452
+ | `'double-click'` | select + focus only | activate `{ preview: false }` |
453
+ | `'single-click-preview'` *(VSCode / Cursor)* | activate `{ preview: true }` | activate `{ preview: false }` |
454
+
455
+ Folders ignore this — they always toggle on single click and never call `onActivate`. Keyboard `Enter` / `Space` always activates with `{ preview: false }`.
456
+
457
+ ## Hooks
458
+
459
+ | Hook | What it exposes |
460
+ | --- | --- |
461
+ | `useTreeContext()` | full `TreeContextValue<T>` — use as a last resort |
462
+ | `useTreeRows()` | flat row list (visible only) |
463
+ | `useTreeSelection()` | `selectedIds`, `anchor`, `clickSelect`, `moveSelect`, `selectAll`, `setSelectedIds`, `clear`, `isSelected` |
464
+ | `useTreeExpansion()` | `expandedIds`, `expand`, `collapse`, `toggle`, `expandAll`, `collapseAll`, `isExpanded` |
465
+ | `useTreeFocus()` | `focusedId`, `setFocus` |
466
+ | `useTreeSearch()` | `query`, `setQuery`, `matchingIds`, `matchCount` |
467
+ | `useTreeRename()` | `renamingId`, `enabled`, `startRename`, `cancelRename`, `commitRename` |
468
+ | `useTreeClipboard()` | `clipboard`, `isCut`, `cut`, `copy`, `paste`, `clear` |
469
+ | `useTreeDnd()` | `active`, `draggingIds`, `dropTarget`, `beginDrag`, `setDropTarget`, `commitDrop`, `cancelDrag`, `isAllowedDrop` |
470
+ | `useTreeActions()` | imperative bag: `expand*` / `collapse*` / `refresh*` / `activate` |
471
+ | `useTreeLabels()` | resolved `TreeLabels` (with overrides applied) |
472
+
473
+ ## Defaults
474
+
475
+ | Option | Default |
476
+ | --- | --- |
477
+ | `selectionMode` | `'single'` |
478
+ | `activationMode` | `'single-click'` |
479
+ | `enableSearch` | `false` |
480
+ | `enableTypeAhead` | `true` |
481
+ | `enableInlineRename` | `false` |
482
+ | `enableFinderHotkeys` | `false` |
483
+ | `enableDnD` | `false` |
484
+ | `showIndentGuides` | `false` |
485
+ | `persistSelection` | `false` |
486
+ | `appearance.density` | `'cozy'` |
487
+ | `appearance.accent` | `'default'` |
488
+ | `appearance.radius` | `'sm'` |
489
+ | `appearance.showActiveIndicator` | `true` |
490
+ | `appearance.iconStrokeWidth` | `1.5` |
491
+ | `appearance.indent` | `16` |
492
+
493
+ ## Filtering nodes
494
+
495
+ `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.
496
+
497
+ ```tsx
498
+ const [showHidden, setShowHidden] = useState(false);
499
+
500
+ <TreeRoot
501
+ data={data}
502
+ getItemName={(n) => n.data.name}
503
+ filterNode={(n) => showHidden || !n.data.name.startsWith('.')}
504
+ />
505
+ ```
506
+
507
+ 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.
508
+
509
+ > **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`.
510
+
511
+ ## File icons
512
+
513
+ 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:
514
+
515
+ ```tsx
516
+ import { TreeRoot } from '@djangocfg/ui-tools/tree';
517
+ import { createFileIconSlot } from '@djangocfg/ui-tools/file-icon';
518
+
519
+ <TreeRoot
520
+ data={data}
521
+ getItemName={(n) => n.data.name}
522
+ renderIcon={createFileIconSlot({ getName: (n) => n.data.name })}
523
+ />
524
+ ```
525
+
526
+ The icon SVGs are vendored statically from `material-file-icons` (MIT) — no runtime dependency, no install step, synchronous resolution.
527
+
528
+ ## Status
529
+
530
+ | Feature | Status |
531
+ | --- | --- |
532
+ | Multi-selection (anchor / shift / ⌘+A) | ✅ |
533
+ | `TreeAdapter` + built-in CRUD via `window.dialog` | ✅ |
534
+ | `<FinderTree>` preset | ✅ |
535
+ | Inline rename (F2, auto-select base) | ✅ |
536
+ | Finder hotkeys (⌘⌫ F2 ⌘D ⌘N ⌘⇧N ⌘C ⌘X ⌘V) | ✅ |
537
+ | Clipboard (cut / copy / paste, dimmed cut state) | ✅ |
538
+ | Drag-and-drop (`@dnd-kit/core`) | ✅ |
539
+ | Empty-area context menu (`new file / new folder / paste` on background) | ✅ |
540
+ | Undo / Redo | n/a (host's job — implement in your adapter / Wails-Go layer) |
541
+ | Virtualization | out of scope (wrap with `@tanstack/react-virtual`) |
542
+ | Multi-tree cross-DnD | out of scope |