@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.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/package.json +122 -0
- package/src/DataGrid/README.md +48 -0
- package/src/DataGrid/context/DataGridContext.tsx +442 -0
- package/src/DataGrid/context/index.ts +4 -0
- package/src/DataGrid/index.ts +34 -0
- package/src/DataGrid/lazy.tsx +23 -0
- package/src/DataGrid/parts/DataGrid.tsx +125 -0
- package/src/DataGrid/parts/DataGridCell.tsx +30 -0
- package/src/DataGrid/parts/DataGridHeader.tsx +111 -0
- package/src/DataGrid/parts/DataGridPagination.tsx +105 -0
- package/src/DataGrid/parts/DataGridRow.tsx +84 -0
- package/src/DataGrid/parts/index.ts +7 -0
- package/src/DataGrid/types.ts +95 -0
- package/src/DataTable/README.md +42 -0
- package/src/DataTable/context/DataTableContext.tsx +434 -0
- package/src/DataTable/context/index.ts +4 -0
- package/src/DataTable/index.ts +33 -0
- package/src/DataTable/lazy.tsx +23 -0
- package/src/DataTable/parts/DataTable.tsx +142 -0
- package/src/DataTable/parts/DataTableHeader.tsx +103 -0
- package/src/DataTable/parts/DataTablePagination.tsx +115 -0
- package/src/DataTable/parts/DataTableRow.tsx +96 -0
- package/src/DataTable/parts/index.ts +6 -0
- package/src/DataTable/types.ts +88 -0
- package/src/FileIcon/FileIcon.tsx +87 -0
- package/src/FileIcon/get-file-icon.ts +73 -0
- package/src/FileIcon/icons/icon-data.ts +399 -0
- package/src/FileIcon/index.ts +19 -0
- package/src/FileIcon/loader.ts +29 -0
- package/src/FileIcon/specialFolders.ts +110 -0
- package/src/FileIcon/treeAdapter.tsx +49 -0
- package/src/JsonTree/JsonViewer.tsx +720 -0
- package/src/JsonTree/README.md +142 -0
- package/src/JsonTree/index.tsx +11 -0
- package/src/JsonTree/lazy.tsx +22 -0
- package/src/JsonTree/types.ts +91 -0
- package/src/Kanban/context/KanbanContext.tsx +165 -0
- package/src/Kanban/context/index.ts +4 -0
- package/src/Kanban/index.ts +31 -0
- package/src/Kanban/lazy.tsx +23 -0
- package/src/Kanban/parts/Kanban.tsx +77 -0
- package/src/Kanban/parts/KanbanCard.tsx +76 -0
- package/src/Kanban/parts/KanbanColumn.tsx +79 -0
- package/src/Kanban/parts/KanbanDragOverlay.tsx +46 -0
- package/src/Kanban/parts/index.ts +6 -0
- package/src/Kanban/types.ts +45 -0
- package/src/Listbox/context/ListboxProvider.tsx +616 -0
- package/src/Listbox/context/index.ts +8 -0
- package/src/Listbox/index.ts +28 -0
- package/src/Listbox/lazy.tsx +31 -0
- package/src/Listbox/parts/ListboxGroup.tsx +71 -0
- package/src/Listbox/parts/ListboxItem.tsx +151 -0
- package/src/Listbox/parts/index.ts +12 -0
- package/src/Listbox/types.ts +98 -0
- package/src/Masonry/README.md +79 -0
- package/src/Masonry/context/MasonryProvider.tsx +1205 -0
- package/src/Masonry/context/index.ts +8 -0
- package/src/Masonry/index.ts +18 -0
- package/src/Masonry/lazy.tsx +20 -0
- package/src/Masonry/parts/index.ts +2 -0
- package/src/Masonry/types.ts +44 -0
- package/src/Timeline/context/TimelineProvider.tsx +474 -0
- package/src/Timeline/context/index.ts +16 -0
- package/src/Timeline/index.ts +29 -0
- package/src/Timeline/lazy.tsx +27 -0
- package/src/Timeline/parts/index.ts +2 -0
- package/src/Timeline/types.ts +25 -0
- package/src/Tree/FinderTree.tsx +42 -0
- package/src/Tree/README.md +542 -0
- package/src/Tree/TreeDndProvider.tsx +137 -0
- package/src/Tree/TreeRoot.tsx +290 -0
- package/src/Tree/components/TreeChevron.tsx +48 -0
- package/src/Tree/components/TreeContent.tsx +70 -0
- package/src/Tree/components/TreeDropIndicator.tsx +65 -0
- package/src/Tree/components/TreeEmpty.tsx +22 -0
- package/src/Tree/components/TreeEmptyArea.tsx +160 -0
- package/src/Tree/components/TreeError.tsx +24 -0
- package/src/Tree/components/TreeIcon.tsx +39 -0
- package/src/Tree/components/TreeIndentGuides.tsx +33 -0
- package/src/Tree/components/TreeLabel.tsx +39 -0
- package/src/Tree/components/TreeRenameInput.tsx +114 -0
- package/src/Tree/components/TreeRow.tsx +300 -0
- package/src/Tree/components/TreeSearchInput.tsx +50 -0
- package/src/Tree/components/TreeSkeleton.tsx +22 -0
- package/src/Tree/components/index.ts +28 -0
- package/src/Tree/context/TreeContext.tsx +409 -0
- package/src/Tree/context/TreeContextValue.ts +139 -0
- package/src/Tree/context/async-children/collect-ids.ts +27 -0
- package/src/Tree/context/async-children/index.ts +8 -0
- package/src/Tree/context/async-children/use-async-children.ts +157 -0
- package/src/Tree/context/clipboard/index.ts +4 -0
- package/src/Tree/context/clipboard/use-clipboard.ts +115 -0
- package/src/Tree/context/dnd/index.ts +8 -0
- package/src/Tree/context/dnd/use-dnd.ts +194 -0
- package/src/Tree/context/expansion/index.ts +4 -0
- package/src/Tree/context/expansion/use-expansion.ts +55 -0
- package/src/Tree/context/hooks.ts +177 -0
- package/src/Tree/context/index.ts +16 -0
- package/src/Tree/context/menu/builtin-actions.ts +358 -0
- package/src/Tree/context/menu/index.ts +11 -0
- package/src/Tree/context/menu/render.tsx +104 -0
- package/src/Tree/context/menu/use-resolved-menu.ts +141 -0
- package/src/Tree/context/persist/index.ts +4 -0
- package/src/Tree/context/persist/use-persist-sync.ts +74 -0
- package/src/Tree/context/rename/index.ts +4 -0
- package/src/Tree/context/rename/use-rename.ts +113 -0
- package/src/Tree/context/selection/index.ts +4 -0
- package/src/Tree/context/selection/use-selection.ts +146 -0
- package/src/Tree/context/state/index.ts +6 -0
- package/src/Tree/context/state/initial.ts +41 -0
- package/src/Tree/context/state/reducer.ts +76 -0
- package/src/Tree/context/state/types.ts +46 -0
- package/src/Tree/data/appearance.ts +237 -0
- package/src/Tree/data/childCache.ts +43 -0
- package/src/Tree/data/clipboard.ts +33 -0
- package/src/Tree/data/createDemoTree.ts +42 -0
- package/src/Tree/data/dnd.ts +123 -0
- package/src/Tree/data/finderShortcuts.ts +67 -0
- package/src/Tree/data/flatten.ts +66 -0
- package/src/Tree/data/index.ts +45 -0
- package/src/Tree/data/persist.ts +62 -0
- package/src/Tree/data/renameUtils.ts +51 -0
- package/src/Tree/data/selection.ts +161 -0
- package/src/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
- package/src/Tree/hooks/finder-hotkeys/index.ts +8 -0
- package/src/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
- package/src/Tree/hooks/index.ts +25 -0
- package/src/Tree/hooks/keyboard/activation.ts +27 -0
- package/src/Tree/hooks/keyboard/arrow-nav.ts +26 -0
- package/src/Tree/hooks/keyboard/expand-collapse.ts +54 -0
- package/src/Tree/hooks/keyboard/index.ts +10 -0
- package/src/Tree/hooks/keyboard/types.ts +39 -0
- package/src/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
- package/src/Tree/hooks/type-ahead/index.ts +5 -0
- package/src/Tree/hooks/type-ahead/match-prefix.ts +42 -0
- package/src/Tree/hooks/type-ahead/use-tree-type-ahead.ts +89 -0
- package/src/Tree/index.tsx +135 -0
- package/src/Tree/lazy.tsx +14 -0
- package/src/Tree/types/activation.ts +30 -0
- package/src/Tree/types/adapter.ts +70 -0
- package/src/Tree/types/index.ts +27 -0
- package/src/Tree/types/labels.ts +97 -0
- package/src/Tree/types/loader.ts +9 -0
- package/src/Tree/types/node.ts +38 -0
- package/src/Tree/types/root-props.ts +158 -0
- package/src/Tree/types/selection.ts +3 -0
- package/src/Tree/types/slots.ts +64 -0
- package/src/index.ts +21 -0
- package/src/stories/_fixtures/categories.ts +51 -0
- package/src/stories/_fixtures/finder-store.ts +196 -0
- package/src/stories/_fixtures/fs.ts +83 -0
- package/src/stories/_fixtures/index.ts +5 -0
- package/src/stories/_fixtures/org.ts +46 -0
- package/src/stories/_fixtures/status.ts +21 -0
- 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 |
|