@djangocfg/ui-tools 2.1.314 → 2.1.316
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/dist/TreeRoot-A25RIGYE.cjs +19 -0
- package/dist/TreeRoot-A25RIGYE.cjs.map +1 -0
- package/dist/TreeRoot-HBRJEHBH.mjs +4 -0
- package/dist/TreeRoot-HBRJEHBH.mjs.map +1 -0
- package/dist/chunk-4CEOJDMB.cjs +1300 -0
- package/dist/chunk-4CEOJDMB.cjs.map +1 -0
- package/dist/chunk-KR6B3LVY.mjs +59 -0
- package/dist/chunk-KR6B3LVY.mjs.map +1 -0
- package/dist/chunk-NFIMVYJU.mjs +1249 -0
- package/dist/chunk-NFIMVYJU.mjs.map +1 -0
- package/dist/chunk-YXBOAGIM.cjs +63 -0
- package/dist/chunk-YXBOAGIM.cjs.map +1 -0
- package/dist/index.cjs +151 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.mjs +11 -2
- package/dist/index.mjs.map +1 -1
- package/dist/tree/index.cjs +152 -0
- package/dist/tree/index.cjs.map +1 -0
- package/dist/tree/index.d.cts +442 -0
- package/dist/tree/index.d.ts +442 -0
- package/dist/tree/index.mjs +5 -0
- package/dist/tree/index.mjs.map +1 -0
- package/package.json +11 -6
- package/src/index.ts +4 -0
- package/src/tools/Tree/README.md +220 -0
- package/src/tools/Tree/Tree.story.tsx +536 -0
- package/src/tools/Tree/TreeRoot.tsx +164 -0
- package/src/tools/Tree/components/TreeChevron.tsx +39 -0
- package/src/tools/Tree/components/TreeContent.tsx +48 -0
- package/src/tools/Tree/components/TreeEmpty.tsx +21 -0
- package/src/tools/Tree/components/TreeError.tsx +24 -0
- package/src/tools/Tree/components/TreeIcon.tsx +29 -0
- package/src/tools/Tree/components/TreeIndentGuides.tsx +33 -0
- package/src/tools/Tree/components/TreeLabel.tsx +24 -0
- package/src/tools/Tree/components/TreeRow.tsx +163 -0
- package/src/tools/Tree/components/TreeSearchInput.tsx +50 -0
- package/src/tools/Tree/components/TreeSkeleton.tsx +22 -0
- package/src/tools/Tree/components/index.ts +22 -0
- package/src/tools/Tree/context/TreeContext.tsx +538 -0
- package/src/tools/Tree/context/hooks.ts +110 -0
- package/src/tools/Tree/context/index.ts +13 -0
- package/src/tools/Tree/data/appearance.ts +175 -0
- package/src/tools/Tree/data/childCache.ts +43 -0
- package/src/tools/Tree/data/createDemoTree.ts +42 -0
- package/src/tools/Tree/data/flatten.ts +51 -0
- package/src/tools/Tree/data/index.ts +24 -0
- package/src/tools/Tree/data/persist.ts +62 -0
- package/src/tools/Tree/hooks/index.ts +6 -0
- package/src/tools/Tree/hooks/useTreeKeyboard.ts +171 -0
- package/src/tools/Tree/hooks/useTreeTypeAhead.ts +100 -0
- package/src/tools/Tree/index.tsx +99 -0
- package/src/tools/Tree/lazy.tsx +14 -0
- package/src/tools/Tree/types.ts +136 -0
- package/src/tools/index.ts +75 -0
|
@@ -0,0 +1,220 @@
|
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
## Philosophy
|
|
10
|
+
|
|
11
|
+
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.
|
|
13
|
+
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
|
+
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.
|
|
17
|
+
|
|
18
|
+
## Layered architecture
|
|
19
|
+
|
|
20
|
+
```
|
|
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
|
|
34
|
+
components/
|
|
35
|
+
TreeRow.tsx default row: chevron + icon + label + actions + ctx-menu
|
|
36
|
+
TreeChevron / TreeIcon / TreeLabel / TreeIndentGuides
|
|
37
|
+
TreeSearchInput.tsx controlled search input
|
|
38
|
+
TreeContent.tsx iterates flatRows, default-renders TreeRow
|
|
39
|
+
TreeEmpty / TreeSkeleton / TreeError
|
|
40
|
+
TreeRoot.tsx high-level entry — Provider + shell + content
|
|
41
|
+
lazy.tsx LazyTree via createLazyComponent
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Dependency direction: `components → context → data → types`. `hooks/` consume `context/`. Nothing cycles back.
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { TreeRoot, type TreeNode } from '@djangocfg/ui-tools/tree';
|
|
50
|
+
|
|
51
|
+
interface FsNode { name: string }
|
|
52
|
+
|
|
53
|
+
const data: TreeNode<FsNode>[] = [
|
|
54
|
+
{
|
|
55
|
+
id: 'src',
|
|
56
|
+
data: { name: 'src' },
|
|
57
|
+
children: [
|
|
58
|
+
{ id: 'index.ts', data: { name: 'index.ts' } },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
<TreeRoot<FsNode>
|
|
64
|
+
data={data}
|
|
65
|
+
getItemName={(n) => n.data.name}
|
|
66
|
+
onSelectionChange={(ids) => console.log(ids)}
|
|
67
|
+
onActivate={(node) => openFile(node.id)}
|
|
68
|
+
enableSearch
|
|
69
|
+
showIndentGuides
|
|
70
|
+
persistKey="settings.fileTree"
|
|
71
|
+
/>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Async children
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
const data: TreeNode<FsNode>[] = [
|
|
78
|
+
{ id: 'root', data: { name: 'remote' }, isFolder: true },
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
<TreeRoot<FsNode>
|
|
82
|
+
data={data}
|
|
83
|
+
getItemName={(n) => n.data.name}
|
|
84
|
+
loadChildren={async (node) => {
|
|
85
|
+
const list = await fetchChildren(node.id);
|
|
86
|
+
return list.map((it) => ({ id: it.id, data: it, isFolder: it.isDir }));
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
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.
|
|
92
|
+
|
|
93
|
+
## Composition mode
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import {
|
|
97
|
+
TreeProvider, TreeContent, TreeSearchInput,
|
|
98
|
+
useTreeSelection, useTreeActions,
|
|
99
|
+
} from '@djangocfg/ui-tools/tree';
|
|
100
|
+
|
|
101
|
+
<TreeProvider data={data} getItemName={getName}>
|
|
102
|
+
<Toolbar />
|
|
103
|
+
<TreeSearchInput />
|
|
104
|
+
<TreeContent>{(row) => <CustomRow {...row} />}</TreeContent>
|
|
105
|
+
</TreeProvider>
|
|
106
|
+
|
|
107
|
+
function Toolbar() {
|
|
108
|
+
const { expandAll, collapseAll, refreshAll } = useTreeActions();
|
|
109
|
+
// …
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Appearance
|
|
114
|
+
|
|
115
|
+
Cosmetic configuration is a single optional prop. Defaults to a comfortable VSCode-Explorer density.
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
<TreeRoot
|
|
119
|
+
data={…}
|
|
120
|
+
getItemName={…}
|
|
121
|
+
appearance={{
|
|
122
|
+
density: 'cozy', // 'compact' | 'cozy' | 'comfortable'
|
|
123
|
+
accent: 'default', // 'subtle' | 'default' | 'strong'
|
|
124
|
+
radius: 'sm', // 'none' | 'sm' | 'md'
|
|
125
|
+
iconStrokeWidth: 1.5,
|
|
126
|
+
indentGuideOpacity: 0.4,
|
|
127
|
+
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,
|
|
135
|
+
}}
|
|
136
|
+
/>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The resolved appearance is exposed on the root container as CSS variables, so any nested override (`className`, custom slot) can read them:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
--tree-row-height
|
|
143
|
+
--tree-icon-size
|
|
144
|
+
--tree-icon-stroke
|
|
145
|
+
--tree-font-size
|
|
146
|
+
--tree-gap
|
|
147
|
+
--tree-indent
|
|
148
|
+
--tree-guide-opacity
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### VSCode-style highlights
|
|
152
|
+
|
|
153
|
+
Row state visuals follow VSCode's Explorer:
|
|
154
|
+
|
|
155
|
+
| State | Look |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| Hover | subtle neutral wash (`bg-foreground/[.06]`) |
|
|
158
|
+
| Focused (keyboard nav, not selected) | slightly stronger neutral |
|
|
159
|
+
| Selected, tree NOT focused | muted neutral block |
|
|
160
|
+
| Selected + tree focused-within | primary tint + colored text + left active bar |
|
|
161
|
+
| Search match | thin primary ring |
|
|
162
|
+
| Disabled | dimmed + cursor-not-allowed |
|
|
163
|
+
|
|
164
|
+
Toggle the bar with `appearance.showActiveIndicator`. Intensity scales with `appearance.accent`.
|
|
165
|
+
|
|
166
|
+
## Extension points
|
|
167
|
+
|
|
168
|
+
| Need | Mechanism |
|
|
169
|
+
| --- | --- |
|
|
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 | `renderContextMenu={(row, trigger) => <ContextMenu>…</ContextMenu>}` |
|
|
175
|
+
| Localised copy | `labels={{ empty: '…', searchPlaceholder: '…' }}` |
|
|
176
|
+
| Persist state | `persistKey="settings.fileTree"`, optional `persistSelection` |
|
|
177
|
+
| Imperative actions | `useTreeActions()` |
|
|
178
|
+
| Read raw flat rows | `useTreeRows()` |
|
|
179
|
+
|
|
180
|
+
## Defaults
|
|
181
|
+
|
|
182
|
+
| Option | Default |
|
|
183
|
+
| --- | --- |
|
|
184
|
+
| `selectionMode` | `'single'` |
|
|
185
|
+
| `enableSearch` | `false` |
|
|
186
|
+
| `enableTypeAhead` | `true` |
|
|
187
|
+
| `showIndentGuides` | `false` |
|
|
188
|
+
| `persistSelection` | `false` |
|
|
189
|
+
| `appearance.density` | `'cozy'` |
|
|
190
|
+
| `appearance.accent` | `'default'` |
|
|
191
|
+
| `appearance.radius` | `'sm'` |
|
|
192
|
+
| `appearance.showActiveIndicator` | `true` |
|
|
193
|
+
| `appearance.iconStrokeWidth` | `1.5` |
|
|
194
|
+
| `appearance.indent` | `16` |
|
|
195
|
+
|
|
196
|
+
## Stories
|
|
197
|
+
|
|
198
|
+
| Story | Demonstrates |
|
|
199
|
+
| --- | --- |
|
|
200
|
+
| Default | sensible cozy defaults |
|
|
201
|
+
| Densities | three density presets side-by-side |
|
|
202
|
+
| WithIcons | file-type icons through `renderIcon` |
|
|
203
|
+
| WithStatus | modified / error / disabled rows through `renderLabel` |
|
|
204
|
+
| WithActions | rename / delete on hover through `renderActions` |
|
|
205
|
+
| WithSearch | built-in search bar + match highlight |
|
|
206
|
+
| WithIndentGuides | opt-in vertical guides |
|
|
207
|
+
| WithContextMenu | right-click via `renderContextMenu` + `ui-core/ContextMenu` |
|
|
208
|
+
| AsyncLazyChildren | `loadChildren` + cache + dedup |
|
|
209
|
+
| ExpandCollapseAll | composition mode with `useTreeActions` |
|
|
210
|
+
| Persisted | localStorage round-trip |
|
|
211
|
+
| LargeTree | ~500 nodes scalability check |
|
|
212
|
+
| Playground | every knob exposed as a control |
|
|
213
|
+
|
|
214
|
+
## Out of scope (today)
|
|
215
|
+
|
|
216
|
+
- Inline rename UX
|
|
217
|
+
- Drag-and-drop
|
|
218
|
+
- Virtualization (wrap with `@tanstack/react-virtual` when needed)
|
|
219
|
+
- Multi-tree (cross-tree DnD)
|
|
220
|
+
- Live filesystem watchers / quick-open palette — app-level concerns; build them on top of `useTreeActions()` and `useTreeContext()`.
|