@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.
Files changed (56) hide show
  1. package/dist/TreeRoot-A25RIGYE.cjs +19 -0
  2. package/dist/TreeRoot-A25RIGYE.cjs.map +1 -0
  3. package/dist/TreeRoot-HBRJEHBH.mjs +4 -0
  4. package/dist/TreeRoot-HBRJEHBH.mjs.map +1 -0
  5. package/dist/chunk-4CEOJDMB.cjs +1300 -0
  6. package/dist/chunk-4CEOJDMB.cjs.map +1 -0
  7. package/dist/chunk-KR6B3LVY.mjs +59 -0
  8. package/dist/chunk-KR6B3LVY.mjs.map +1 -0
  9. package/dist/chunk-NFIMVYJU.mjs +1249 -0
  10. package/dist/chunk-NFIMVYJU.mjs.map +1 -0
  11. package/dist/chunk-YXBOAGIM.cjs +63 -0
  12. package/dist/chunk-YXBOAGIM.cjs.map +1 -0
  13. package/dist/index.cjs +151 -5
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +5 -1
  16. package/dist/index.d.ts +5 -1
  17. package/dist/index.mjs +11 -2
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/tree/index.cjs +152 -0
  20. package/dist/tree/index.cjs.map +1 -0
  21. package/dist/tree/index.d.cts +442 -0
  22. package/dist/tree/index.d.ts +442 -0
  23. package/dist/tree/index.mjs +5 -0
  24. package/dist/tree/index.mjs.map +1 -0
  25. package/package.json +11 -6
  26. package/src/index.ts +4 -0
  27. package/src/tools/Tree/README.md +220 -0
  28. package/src/tools/Tree/Tree.story.tsx +536 -0
  29. package/src/tools/Tree/TreeRoot.tsx +164 -0
  30. package/src/tools/Tree/components/TreeChevron.tsx +39 -0
  31. package/src/tools/Tree/components/TreeContent.tsx +48 -0
  32. package/src/tools/Tree/components/TreeEmpty.tsx +21 -0
  33. package/src/tools/Tree/components/TreeError.tsx +24 -0
  34. package/src/tools/Tree/components/TreeIcon.tsx +29 -0
  35. package/src/tools/Tree/components/TreeIndentGuides.tsx +33 -0
  36. package/src/tools/Tree/components/TreeLabel.tsx +24 -0
  37. package/src/tools/Tree/components/TreeRow.tsx +163 -0
  38. package/src/tools/Tree/components/TreeSearchInput.tsx +50 -0
  39. package/src/tools/Tree/components/TreeSkeleton.tsx +22 -0
  40. package/src/tools/Tree/components/index.ts +22 -0
  41. package/src/tools/Tree/context/TreeContext.tsx +538 -0
  42. package/src/tools/Tree/context/hooks.ts +110 -0
  43. package/src/tools/Tree/context/index.ts +13 -0
  44. package/src/tools/Tree/data/appearance.ts +175 -0
  45. package/src/tools/Tree/data/childCache.ts +43 -0
  46. package/src/tools/Tree/data/createDemoTree.ts +42 -0
  47. package/src/tools/Tree/data/flatten.ts +51 -0
  48. package/src/tools/Tree/data/index.ts +24 -0
  49. package/src/tools/Tree/data/persist.ts +62 -0
  50. package/src/tools/Tree/hooks/index.ts +6 -0
  51. package/src/tools/Tree/hooks/useTreeKeyboard.ts +171 -0
  52. package/src/tools/Tree/hooks/useTreeTypeAhead.ts +100 -0
  53. package/src/tools/Tree/index.tsx +99 -0
  54. package/src/tools/Tree/lazy.tsx +14 -0
  55. package/src/tools/Tree/types.ts +136 -0
  56. 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()`.