@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,237 @@
1
+ 'use client';
2
+
3
+ import type { CSSProperties } from 'react';
4
+
5
+ export type TreeDensity = 'compact' | 'cozy' | 'comfortable';
6
+ export type TreeAccentIntensity = 'subtle' | 'default' | 'strong';
7
+ export type TreeRadius = 'none' | 'sm' | 'md';
8
+
9
+ /**
10
+ * High-level look. Sets sensible defaults for icons / row sizing / the
11
+ * active indicator; any individual `TreeAppearance` field still overrides.
12
+ *
13
+ * - `'explorer'` (default) — classic file/folder tree: leaf + folder
14
+ * icons, fixed single-line rows, VSCode active-bar.
15
+ * - `'list'` — macOS-sidebar / chat-history look: no icons (chevron
16
+ * only on groups), auto-height rows (multi-line label friendly), quiet
17
+ * selection (no active-bar).
18
+ */
19
+ export type TreeVariant = 'explorer' | 'list';
20
+
21
+ export type TreeRowSizing = 'fixed' | 'auto';
22
+
23
+ /**
24
+ * Cosmetic configuration. Every field is optional; missing values fall
25
+ * back to the `cozy` preset (a comfortable VSCode-Explorer-like density).
26
+ *
27
+ * Customize the look without re-implementing slots.
28
+ */
29
+ export interface TreeAppearance {
30
+ /** Built-in size preset. Default: `'cozy'`. */
31
+ density?: TreeDensity;
32
+ /** Override row height in px (wins over density). */
33
+ rowHeight?: number;
34
+ /** Override icon + chevron size in px (wins over density). */
35
+ iconSize?: number;
36
+ /** Lucide stroke width for icon + chevron. Default: 1.5. */
37
+ iconStrokeWidth?: number;
38
+ /** Override label font size in px (wins over density). */
39
+ fontSize?: number;
40
+ /** Pixels between chevron / icon / label. Default depends on density. */
41
+ gap?: number;
42
+ /** Pixels between nesting levels. Default: 16. */
43
+ indent?: number;
44
+ /** Hover / selected highlight intensity. Default: `'default'`. */
45
+ accent?: TreeAccentIntensity;
46
+ /** Row corner radius. Default: `'sm'`. */
47
+ radius?: TreeRadius;
48
+ /** Indent-guide line opacity (0..1). Default: 0.4. */
49
+ indentGuideOpacity?: number;
50
+ /**
51
+ * Show a 2px primary-tinted bar on the left of the selected row.
52
+ * Mimics the VSCode active-tab indicator. Default: `true`.
53
+ */
54
+ showActiveIndicator?: boolean;
55
+ /**
56
+ * High-level look. Default: `'explorer'`. Sets defaults for the three
57
+ * fields below; pass any of them explicitly to override the variant.
58
+ */
59
+ variant?: TreeVariant;
60
+ /** Hide per-leaf icons (chevron + label only). Variant default. */
61
+ hideLeafIcons?: boolean;
62
+ /** Hide folder icons too (chevron stays). Variant default. */
63
+ hideFolderIcons?: boolean;
64
+ /**
65
+ * `'fixed'` — every row is exactly `rowHeight` (single-line explorer).
66
+ * `'auto'` — `rowHeight` is a *minimum*; rows grow to fit a multi-line
67
+ * label (title + meta). Variant default.
68
+ */
69
+ rowSizing?: TreeRowSizing;
70
+ }
71
+
72
+ export interface ResolvedAppearance {
73
+ density: TreeDensity;
74
+ rowHeight: number;
75
+ iconSize: number;
76
+ iconStrokeWidth: number;
77
+ fontSize: number;
78
+ gap: number;
79
+ indent: number;
80
+ accent: TreeAccentIntensity;
81
+ radius: TreeRadius;
82
+ indentGuideOpacity: number;
83
+ showActiveIndicator: boolean;
84
+ hideLeafIcons: boolean;
85
+ hideFolderIcons: boolean;
86
+ rowSizing: TreeRowSizing;
87
+ }
88
+
89
+ const DENSITY_PRESETS: Record<
90
+ TreeDensity,
91
+ Pick<ResolvedAppearance, 'rowHeight' | 'iconSize' | 'fontSize' | 'gap'>
92
+ > = {
93
+ compact: { rowHeight: 24, iconSize: 14, fontSize: 13, gap: 6 },
94
+ cozy: { rowHeight: 28, iconSize: 16, fontSize: 13, gap: 8 },
95
+ comfortable: { rowHeight: 32, iconSize: 16, fontSize: 14, gap: 8 },
96
+ };
97
+
98
+ /**
99
+ * Per-variant defaults for the icon / sizing / indicator fields. The
100
+ * `'list'` variant is the macOS-sidebar / chat-history look.
101
+ */
102
+ const VARIANT_DEFAULTS: Record<
103
+ TreeVariant,
104
+ Pick<
105
+ ResolvedAppearance,
106
+ 'hideLeafIcons' | 'hideFolderIcons' | 'rowSizing' | 'showActiveIndicator'
107
+ >
108
+ > = {
109
+ explorer: {
110
+ hideLeafIcons: false,
111
+ hideFolderIcons: false,
112
+ rowSizing: 'fixed',
113
+ showActiveIndicator: true,
114
+ },
115
+ list: {
116
+ hideLeafIcons: true,
117
+ hideFolderIcons: true,
118
+ rowSizing: 'auto',
119
+ showActiveIndicator: false,
120
+ },
121
+ };
122
+
123
+ export const DEFAULT_TREE_APPEARANCE: ResolvedAppearance = {
124
+ density: 'cozy',
125
+ ...DENSITY_PRESETS.cozy,
126
+ iconStrokeWidth: 1.5,
127
+ indent: 16,
128
+ accent: 'default',
129
+ radius: 'sm',
130
+ indentGuideOpacity: 0.4,
131
+ ...VARIANT_DEFAULTS.explorer,
132
+ };
133
+
134
+ /**
135
+ * Merge a partial appearance with the default + density preset.
136
+ *
137
+ * Explicit numeric overrides (e.g. `rowHeight`) win over the density preset.
138
+ */
139
+ export function resolveAppearance(
140
+ input?: TreeAppearance,
141
+ /** Outer `indent` prop (kept on TreeRoot for back-compat). */
142
+ outerIndent?: number,
143
+ ): ResolvedAppearance {
144
+ if (!input && outerIndent === undefined) return DEFAULT_TREE_APPEARANCE;
145
+
146
+ const density: TreeDensity = input?.density ?? 'cozy';
147
+ const preset = DENSITY_PRESETS[density];
148
+ // Variant sets the icon/sizing/indicator defaults; explicit fields win.
149
+ const variant: TreeVariant = input?.variant ?? 'explorer';
150
+ const v = VARIANT_DEFAULTS[variant];
151
+
152
+ return {
153
+ density,
154
+ rowHeight: input?.rowHeight ?? preset.rowHeight,
155
+ iconSize: input?.iconSize ?? preset.iconSize,
156
+ iconStrokeWidth: input?.iconStrokeWidth ?? DEFAULT_TREE_APPEARANCE.iconStrokeWidth,
157
+ fontSize: input?.fontSize ?? preset.fontSize,
158
+ gap: input?.gap ?? preset.gap,
159
+ indent: input?.indent ?? outerIndent ?? DEFAULT_TREE_APPEARANCE.indent,
160
+ accent: input?.accent ?? DEFAULT_TREE_APPEARANCE.accent,
161
+ radius: input?.radius ?? DEFAULT_TREE_APPEARANCE.radius,
162
+ indentGuideOpacity:
163
+ input?.indentGuideOpacity ?? DEFAULT_TREE_APPEARANCE.indentGuideOpacity,
164
+ showActiveIndicator: input?.showActiveIndicator ?? v.showActiveIndicator,
165
+ hideLeafIcons: input?.hideLeafIcons ?? v.hideLeafIcons,
166
+ hideFolderIcons: input?.hideFolderIcons ?? v.hideFolderIcons,
167
+ rowSizing: input?.rowSizing ?? v.rowSizing,
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Build the `style` object that exposes the resolved appearance to any
173
+ * descendant via CSS variables. Set on `<TreeRoot>`'s outer div.
174
+ */
175
+ export function appearanceToStyle(a: ResolvedAppearance): CSSProperties {
176
+ return {
177
+ ['--tree-row-height' as string]: `${a.rowHeight}px`,
178
+ ['--tree-icon-size' as string]: `${a.iconSize}px`,
179
+ ['--tree-icon-stroke' as string]: a.iconStrokeWidth,
180
+ ['--tree-font-size' as string]: `${a.fontSize}px`,
181
+ ['--tree-gap' as string]: `${a.gap}px`,
182
+ ['--tree-indent' as string]: `${a.indent}px`,
183
+ ['--tree-guide-opacity' as string]: a.indentGuideOpacity,
184
+ };
185
+ }
186
+
187
+ const RADIUS_CLASS: Record<TreeRadius, string> = {
188
+ none: 'rounded-none',
189
+ sm: 'rounded-sm',
190
+ md: 'rounded-md',
191
+ };
192
+ export function radiusClass(a: ResolvedAppearance) {
193
+ return RADIUS_CLASS[a.radius];
194
+ }
195
+
196
+ /**
197
+ * VSCode-style row state classes.
198
+ *
199
+ * - hover: subtle neutral wash
200
+ * - focused (keyboard nav, not yet selected): slightly stronger neutral
201
+ * - selected (tree NOT focused): muted neutral block
202
+ * - selected + tree focused-within: primary-tinted block + colored text
203
+ *
204
+ * Intensity scales with `appearance.accent`.
205
+ */
206
+ const HOVER: Record<TreeAccentIntensity, string> = {
207
+ subtle: 'hover:bg-foreground/[.03]',
208
+ default: 'hover:bg-foreground/[.06]',
209
+ strong: 'hover:bg-foreground/[.09]',
210
+ };
211
+ const FOCUSED_NOT_SELECTED: Record<TreeAccentIntensity, string> = {
212
+ subtle: 'data-[focused=true]:bg-foreground/[.05]',
213
+ default: 'data-[focused=true]:bg-foreground/[.08]',
214
+ strong: 'data-[focused=true]:bg-foreground/[.12]',
215
+ };
216
+ const SELECTED_INACTIVE: Record<TreeAccentIntensity, string> = {
217
+ subtle: 'data-[selected=true]:bg-foreground/[.06]',
218
+ default: 'data-[selected=true]:bg-foreground/[.10]',
219
+ strong: 'data-[selected=true]:bg-foreground/[.14]',
220
+ };
221
+ const SELECTED_ACTIVE: Record<TreeAccentIntensity, string> = {
222
+ subtle:
223
+ 'data-[selected=true]:group-focus-within/tree:bg-primary/10 data-[selected=true]:group-focus-within/tree:text-primary',
224
+ default:
225
+ 'data-[selected=true]:group-focus-within/tree:bg-primary/15 data-[selected=true]:group-focus-within/tree:text-primary',
226
+ strong:
227
+ 'data-[selected=true]:group-focus-within/tree:bg-primary/25 data-[selected=true]:group-focus-within/tree:text-primary',
228
+ };
229
+
230
+ export function rowStateClasses(a: ResolvedAppearance) {
231
+ return [
232
+ HOVER[a.accent],
233
+ FOCUSED_NOT_SELECTED[a.accent],
234
+ SELECTED_INACTIVE[a.accent],
235
+ SELECTED_ACTIVE[a.accent],
236
+ ].join(' ');
237
+ }
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+
3
+ import type { TreeItemId, TreeNode } from '../types';
4
+
5
+ export type ChildEntryStatus = 'idle' | 'loading' | 'loaded' | 'error';
6
+
7
+ export interface ChildEntry<T> {
8
+ status: ChildEntryStatus;
9
+ children: TreeNode<T>[];
10
+ error?: string;
11
+ }
12
+
13
+ export type ChildCache<T> = Map<TreeItemId, ChildEntry<T>>;
14
+
15
+ export const createChildCache = <T>(): ChildCache<T> => new Map();
16
+
17
+ export const getEntry = <T>(
18
+ cache: ChildCache<T>,
19
+ id: TreeItemId,
20
+ ): ChildEntry<T> | undefined => cache.get(id);
21
+
22
+ /**
23
+ * Resolve a node's children for the current render.
24
+ *
25
+ * - If the node carries inline `children`, those win (no async fetch).
26
+ * - Otherwise we look in the cache.
27
+ *
28
+ * Returns `null` when nothing is loaded yet (caller may show a skeleton).
29
+ */
30
+ export const resolveChildren = <T>(
31
+ cache: ChildCache<T>,
32
+ node: TreeNode<T>,
33
+ ): { children: TreeNode<T>[] | null; status: ChildEntryStatus; error?: string } => {
34
+ if (Array.isArray(node.children)) {
35
+ return { children: node.children, status: 'loaded' };
36
+ }
37
+ const entry = cache.get(node.id);
38
+ if (!entry) return { children: null, status: 'idle' };
39
+ if (entry.status === 'loaded') {
40
+ return { children: entry.children, status: 'loaded' };
41
+ }
42
+ return { children: null, status: entry.status, error: entry.error };
43
+ };
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+
3
+ import type { TreeItemId } from '../types';
4
+
5
+ // =====================================================================
6
+ // Tree-local clipboard.
7
+ //
8
+ // Why not the system clipboard? Because we need a visual "cut" state
9
+ // (Finder/Explorer dim cut items) and a single source of truth across
10
+ // Tree's own keyboard / menu / DnD entry points. The system clipboard
11
+ // can't carry that semantics in a way that survives focus changes.
12
+ //
13
+ // Clipboard owns *ids*, not full nodes — Tree resolves them through
14
+ // `getNodeById` at paste time. This keeps the model immutable to the
15
+ // underlying data refreshing under us (async load, refresh, etc.).
16
+ // =====================================================================
17
+
18
+ export type ClipboardKind = 'cut' | 'copy';
19
+
20
+ export interface ClipboardEntry {
21
+ kind: ClipboardKind;
22
+ ids: TreeItemId[];
23
+ }
24
+
25
+ export type ClipboardState = ClipboardEntry | null;
26
+
27
+ /**
28
+ * Was this id put on the clipboard with `kind: 'cut'`? Used by `TreeRow`
29
+ * to render dimmed (`opacity-60`) rows the way Finder does.
30
+ */
31
+ export function isCutId(state: ClipboardState, id: TreeItemId): boolean {
32
+ return state?.kind === 'cut' && state.ids.includes(id);
33
+ }
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import type { TreeNode } from '../types';
4
+
5
+ export interface DemoNode {
6
+ name: string;
7
+ }
8
+
9
+ /**
10
+ * Build a deterministic synthetic tree for stories and tests.
11
+ *
12
+ * @example
13
+ * const data = createDemoTree({ depth: 4, breadth: 3 });
14
+ * <TreeRoot data={data} getItemName={(n) => n.data.name} />
15
+ */
16
+ export function createDemoTree({
17
+ depth = 3,
18
+ breadth = 4,
19
+ rootPrefix = 'node',
20
+ }: {
21
+ depth?: number;
22
+ breadth?: number;
23
+ rootPrefix?: string;
24
+ } = {}): TreeNode<DemoNode>[] {
25
+ const make = (id: string, name: string, level: number): TreeNode<DemoNode> => {
26
+ if (level >= depth) {
27
+ return { id, data: { name } };
28
+ }
29
+ const children: TreeNode<DemoNode>[] = [];
30
+ for (let i = 0; i < breadth; i++) {
31
+ const childId = `${id}/${i}`;
32
+ children.push(make(childId, `${name}-${i}`, level + 1));
33
+ }
34
+ return { id, data: { name }, children };
35
+ };
36
+
37
+ const roots: TreeNode<DemoNode>[] = [];
38
+ for (let i = 0; i < breadth; i++) {
39
+ roots.push(make(`${rootPrefix}-${i}`, `${rootPrefix} ${i}`, 1));
40
+ }
41
+ return roots;
42
+ }
@@ -0,0 +1,123 @@
1
+ 'use client';
2
+
3
+ import type { FlatRow, TreeItemId, TreeMovePosition, TreeNode } from '../types';
4
+
5
+ // =====================================================================
6
+ // Drag-and-drop primitives — pure, framework-agnostic.
7
+ //
8
+ // @dnd-kit handles the pointer/keyboard sensors and overlay rendering;
9
+ // these helpers only translate pointer geometry into a domain-level
10
+ // drop position (`before` / `inside` / `after`) and validate whether
11
+ // a given drag/drop combo is allowed.
12
+ //
13
+ // Folder vs leaf semantics:
14
+ // - leaves → no `inside` zone; row splits into top/bottom halves
15
+ // for `before` / `after` reorder
16
+ // - folders → top third = `before`, middle third = `inside`,
17
+ // bottom third = `after`
18
+ // =====================================================================
19
+
20
+ export interface DropZoneInput {
21
+ /** Pointer Y in viewport coordinates. */
22
+ pointerY: number;
23
+ /** Row bounding box (`getBoundingClientRect()`). */
24
+ rowRect: { top: number; bottom: number; height: number };
25
+ /** Folders accept `inside` drops; leaves only reorder via before/after. */
26
+ isFolder: boolean;
27
+ }
28
+
29
+ /**
30
+ * Translate pointer geometry into a drop position relative to the row.
31
+ *
32
+ * For folders the row is split into three zones (top third / middle /
33
+ * bottom third). For leaves it's split in half (before / after).
34
+ */
35
+ export function resolveDropZone(input: DropZoneInput): TreeMovePosition {
36
+ const { pointerY, rowRect, isFolder } = input;
37
+ const offset = pointerY - rowRect.top;
38
+ const ratio = rowRect.height > 0 ? offset / rowRect.height : 0.5;
39
+
40
+ if (isFolder) {
41
+ if (ratio < 0.33) return 'before';
42
+ if (ratio > 0.66) return 'after';
43
+ return 'inside';
44
+ }
45
+ return ratio < 0.5 ? 'before' : 'after';
46
+ }
47
+
48
+ // =====================================================================
49
+ // canDrop — default validator.
50
+ //
51
+ // Rejects:
52
+ // 1. Dropping a node onto itself.
53
+ // 2. Dropping a folder into one of its own descendants (would create
54
+ // a cycle).
55
+ // 3. Dropping `inside` a non-folder (defensive — UI already routes
56
+ // these as `before`/`after`, but the API can be called directly).
57
+ //
58
+ // Consumers can pass their own `canDrop` via `<TreeRoot canDrop={…}>`
59
+ // to layer extra rules on top (e.g. read-only branches, type matching).
60
+ // =====================================================================
61
+
62
+ export interface CanDropInput<T> {
63
+ /** Nodes being dragged. */
64
+ source: TreeNode<T>[];
65
+ /** Row under the pointer (`null` = root drop zone). */
66
+ target: TreeNode<T> | null;
67
+ /** Resolved drop position. */
68
+ position: TreeMovePosition;
69
+ /** Tree's id→node lookup, used to walk descendants. */
70
+ getNodeById: (id: TreeItemId) => TreeNode<T> | undefined;
71
+ }
72
+
73
+ export function defaultCanDrop<T>(input: CanDropInput<T>): boolean {
74
+ const { source, target, position } = input;
75
+ if (source.length === 0) return false;
76
+ if (!target) return true; // root drop is always allowed when no other rule rejects.
77
+
78
+ // Inside a leaf doesn't make sense.
79
+ if (position === 'inside') {
80
+ const isFolder = Array.isArray(target.children) || !!target.isFolder;
81
+ if (!isFolder) return false;
82
+ }
83
+
84
+ for (const node of source) {
85
+ if (node.id === target.id) return false;
86
+ if (isDescendant(node, target.id)) return false;
87
+ }
88
+ return true;
89
+ }
90
+
91
+ /**
92
+ * Walk `root`'s inline children searching for `id`. Cached async children
93
+ * are *not* walked here because the cycle check needs to be cheap and
94
+ * deterministic — a missing cycle in unloaded branches is acceptable
95
+ * (the consumer's `adapter.move` is the final gatekeeper).
96
+ */
97
+ function isDescendant<T>(root: TreeNode<T>, id: TreeItemId): boolean {
98
+ if (!Array.isArray(root.children)) return false;
99
+ for (const child of root.children) {
100
+ if (child.id === id) return true;
101
+ if (isDescendant(child, id)) return true;
102
+ }
103
+ return false;
104
+ }
105
+
106
+ // =====================================================================
107
+ // DataTransfer mime — used both for in-tree drags and (later) for
108
+ // receiving dragged files from the OS. The id payload is the JSON-
109
+ // encoded list of `TreeItemId`s.
110
+ // =====================================================================
111
+
112
+ export const TREE_DND_MIME = 'application/x-djangocfg-tree';
113
+
114
+ /**
115
+ * Sentinel droppable id used by `<TreeEmptyArea>` to claim the
116
+ * "root drop target" slot. `<TreeDndProvider>` recognises this id and
117
+ * maps it to `{ id: null, position: 'inside' }` in `dnd.dropTarget`.
118
+ *
119
+ * Lives in `data/` (not in the component) so both producer and
120
+ * consumer reference one constant instead of duplicating the magic
121
+ * string.
122
+ */
123
+ export const TREE_ROOT_DROP_ID = '__tree_root_drop__';
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+
3
+ import type { TreeBuiltinAction } from '../types';
4
+
5
+ // =====================================================================
6
+ // Finder / Explorer keyboard shortcuts.
7
+ //
8
+ // Each entry maps a built-in action to a list of `react-hotkeys-hook`
9
+ // combos. `mod` resolves to ⌘ on macOS and Ctrl on Win/Linux automatically,
10
+ // so a single declaration works cross-platform.
11
+ //
12
+ // Tree only binds shortcuts whose corresponding adapter method is
13
+ // defined. So `delete` only fires if `adapter.remove` exists — same
14
+ // gating used for context-menu items, no duplicated source of truth.
15
+ // =====================================================================
16
+
17
+ export interface FinderShortcut {
18
+ /** Built-in action id this shortcut runs. */
19
+ action: TreeBuiltinAction;
20
+ /** `react-hotkeys-hook` key combos. */
21
+ keys: string[];
22
+ /** Cheat-sheet description (registered via `useHotkey`). */
23
+ description: string;
24
+ }
25
+
26
+ export const FINDER_SHORTCUTS: FinderShortcut[] = [
27
+ {
28
+ action: 'delete',
29
+ keys: ['mod+backspace', 'delete'],
30
+ description: 'Delete selected items',
31
+ },
32
+ {
33
+ action: 'rename',
34
+ keys: ['f2'],
35
+ description: 'Rename selected item',
36
+ },
37
+ {
38
+ action: 'duplicate',
39
+ keys: ['mod+d'],
40
+ description: 'Duplicate selected items',
41
+ },
42
+ {
43
+ action: 'new-folder',
44
+ keys: ['mod+shift+n'],
45
+ description: 'New folder',
46
+ },
47
+ {
48
+ action: 'new-file',
49
+ keys: ['mod+n'],
50
+ description: 'New file',
51
+ },
52
+ {
53
+ action: 'cut',
54
+ keys: ['mod+x'],
55
+ description: 'Cut',
56
+ },
57
+ {
58
+ action: 'copy',
59
+ keys: ['mod+c'],
60
+ description: 'Copy',
61
+ },
62
+ {
63
+ action: 'paste',
64
+ keys: ['mod+v'],
65
+ description: 'Paste',
66
+ },
67
+ ];
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+
3
+ import type { FlatRow, TreeItemId, TreeNode } from '../types';
4
+ import { resolveChildren, type ChildCache } from './childCache';
5
+
6
+ export interface FlattenInput<T> {
7
+ roots: TreeNode<T>[];
8
+ expandedIds: ReadonlySet<TreeItemId>;
9
+ cache: ChildCache<T>;
10
+ /** Optional predicate. Nodes returning `false` (and their descendants) are excluded. */
11
+ filterNode?: (node: TreeNode<T>) => boolean;
12
+ }
13
+
14
+ const isNodeFolder = <T>(node: TreeNode<T>): boolean => {
15
+ if (typeof node.isFolder === 'boolean') return node.isFolder;
16
+ return Array.isArray(node.children);
17
+ };
18
+
19
+ /**
20
+ * Walk the tree top-to-bottom and produce a flat list of visible rows.
21
+ *
22
+ * Visibility rule: a child row appears only when every ancestor is in
23
+ * `expandedIds`. The output is ordered exactly as it should render,
24
+ * which keeps keyboard navigation (next/prev row) trivial.
25
+ */
26
+ export function flattenTree<T>({
27
+ roots,
28
+ expandedIds,
29
+ cache,
30
+ filterNode,
31
+ }: FlattenInput<T>): FlatRow<T>[] {
32
+ const out: FlatRow<T>[] = [];
33
+
34
+ const walk = (nodes: TreeNode<T>[], level: number, parentId: TreeItemId | null) => {
35
+ // Visible siblings only — `aria-setsize`/`posinset` must ignore filtered nodes.
36
+ const visible = filterNode ? nodes.filter(filterNode) : nodes;
37
+ const setSize = visible.length;
38
+
39
+ visible.forEach((node, index) => {
40
+ const isFolder = isNodeFolder(node);
41
+ const isExpanded = expandedIds.has(node.id);
42
+ const resolved = isFolder
43
+ ? resolveChildren(cache, node)
44
+ : { children: [], status: 'loaded' as const };
45
+
46
+ out.push({
47
+ node,
48
+ level,
49
+ parentId,
50
+ isFolder,
51
+ isExpanded,
52
+ isLoading: resolved.status === 'loading',
53
+ hasError: resolved.status === 'error',
54
+ posInSet: index + 1,
55
+ setSize,
56
+ });
57
+
58
+ if (isFolder && isExpanded && resolved.children) {
59
+ walk(resolved.children, level + 1, node.id);
60
+ }
61
+ });
62
+ };
63
+
64
+ walk(roots, 0, null);
65
+ return out;
66
+ }
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ export { createChildCache, getEntry, resolveChildren } from './childCache';
4
+ export type { ChildCache, ChildEntry, ChildEntryStatus } from './childCache';
5
+ export { flattenTree } from './flatten';
6
+ export type { FlattenInput } from './flatten';
7
+ export { loadTreeState, saveTreeState, clearTreeState } from './persist';
8
+ export type { PersistedTreeState } from './persist';
9
+ export { createDemoTree } from './createDemoTree';
10
+ export type { DemoNode } from './createDemoTree';
11
+ export {
12
+ computeRange,
13
+ selectionFromClick,
14
+ selectionFromMove,
15
+ selectionSelectAll,
16
+ selectionClear,
17
+ } from './selection';
18
+ export type { SelectionState, ClickModifiers } from './selection';
19
+ export { splitFileName, autoSelectRange } from './renameUtils';
20
+ export type { SplitName } from './renameUtils';
21
+ export { isCutId } from './clipboard';
22
+ export type { ClipboardKind, ClipboardEntry, ClipboardState } from './clipboard';
23
+ export {
24
+ resolveDropZone,
25
+ defaultCanDrop,
26
+ TREE_DND_MIME,
27
+ TREE_ROOT_DROP_ID,
28
+ } from './dnd';
29
+ export type { DropZoneInput, CanDropInput } from './dnd';
30
+ export {
31
+ DEFAULT_TREE_APPEARANCE,
32
+ resolveAppearance,
33
+ appearanceToStyle,
34
+ radiusClass,
35
+ rowStateClasses,
36
+ } from './appearance';
37
+ export type {
38
+ TreeAppearance,
39
+ ResolvedAppearance,
40
+ TreeDensity,
41
+ TreeAccentIntensity,
42
+ TreeRadius,
43
+ TreeVariant,
44
+ TreeRowSizing,
45
+ } from './appearance';