@djangocfg/ui-tools 2.1.413 → 2.1.416

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 (113) hide show
  1. package/dist/file-icon/index.d.cts +1 -1
  2. package/dist/file-icon/index.d.ts +1 -1
  3. package/dist/slots-ClRpIzoh.d.cts +88 -0
  4. package/dist/slots-ClRpIzoh.d.ts +88 -0
  5. package/dist/tree/index.cjs +1994 -276
  6. package/dist/tree/index.cjs.map +1 -1
  7. package/dist/tree/index.d.cts +717 -72
  8. package/dist/tree/index.d.ts +717 -72
  9. package/dist/tree/index.mjs +1984 -279
  10. package/dist/tree/index.mjs.map +1 -1
  11. package/package.json +10 -6
  12. package/src/tools/chat/README.md +111 -1
  13. package/src/tools/chat/composer/Composer.tsx +138 -17
  14. package/src/tools/chat/composer/ComposerRichTextarea.tsx +25 -0
  15. package/src/tools/chat/composer/index.ts +22 -0
  16. package/src/tools/chat/composer/slash/README.md +187 -0
  17. package/src/tools/chat/composer/slash/SlashHighlightTextarea.tsx +144 -0
  18. package/src/tools/chat/composer/slash/SlashMenu.tsx +142 -0
  19. package/src/tools/chat/composer/slash/SlashToken.tsx +57 -0
  20. package/src/tools/chat/composer/slash/index.ts +44 -0
  21. package/src/tools/chat/composer/slash/labels.ts +19 -0
  22. package/src/tools/chat/composer/slash/state.ts +168 -0
  23. package/src/tools/chat/composer/slash/types.ts +64 -0
  24. package/src/tools/chat/composer/slash/useSlashCommands.ts +204 -0
  25. package/src/tools/chat/composer/types.ts +8 -0
  26. package/src/tools/chat/shell/SuggestedPrompts.tsx +194 -0
  27. package/src/tools/chat/shell/index.ts +6 -0
  28. package/src/tools/data/Listbox/lazy.tsx +1 -1
  29. package/src/tools/data/Masonry/lazy.tsx +1 -1
  30. package/src/tools/data/Timeline/lazy.tsx +1 -1
  31. package/src/tools/data/Tree/FinderTree.tsx +42 -0
  32. package/src/tools/data/Tree/README.md +337 -208
  33. package/src/tools/data/Tree/TreeDndProvider.tsx +137 -0
  34. package/src/tools/data/Tree/TreeRoot.tsx +170 -55
  35. package/src/tools/data/Tree/__tests__/dnd.test.ts +160 -0
  36. package/src/tools/data/Tree/__tests__/keyboard.test.ts +137 -0
  37. package/src/tools/data/Tree/__tests__/renameUtils.test.ts +52 -0
  38. package/src/tools/data/Tree/__tests__/selection.test.ts +227 -0
  39. package/src/tools/data/Tree/components/TreeDropIndicator.tsx +65 -0
  40. package/src/tools/data/Tree/components/TreeEmptyArea.tsx +160 -0
  41. package/src/tools/data/Tree/components/TreeRenameInput.tsx +114 -0
  42. package/src/tools/data/Tree/components/TreeRow.tsx +92 -8
  43. package/src/tools/data/Tree/components/index.ts +6 -0
  44. package/src/tools/data/Tree/context/TreeContext.tsx +204 -363
  45. package/src/tools/data/Tree/context/TreeContextValue.ts +139 -0
  46. package/src/tools/data/Tree/context/async-children/collect-ids.ts +27 -0
  47. package/src/tools/data/Tree/context/async-children/index.ts +8 -0
  48. package/src/tools/data/Tree/context/async-children/use-async-children.ts +157 -0
  49. package/src/tools/data/Tree/context/clipboard/index.ts +4 -0
  50. package/src/tools/data/Tree/context/clipboard/use-clipboard.ts +115 -0
  51. package/src/tools/data/Tree/context/dnd/index.ts +8 -0
  52. package/src/tools/data/Tree/context/dnd/use-dnd.ts +194 -0
  53. package/src/tools/data/Tree/context/expansion/index.ts +4 -0
  54. package/src/tools/data/Tree/context/expansion/use-expansion.ts +55 -0
  55. package/src/tools/data/Tree/context/hooks.ts +68 -1
  56. package/src/tools/data/Tree/context/index.ts +3 -0
  57. package/src/tools/data/Tree/context/menu/builtin-actions.ts +357 -0
  58. package/src/tools/data/Tree/context/menu/index.ts +10 -0
  59. package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +127 -0
  60. package/src/tools/data/Tree/context/persist/index.ts +4 -0
  61. package/src/tools/data/Tree/context/persist/use-persist-sync.ts +74 -0
  62. package/src/tools/data/Tree/context/rename/index.ts +4 -0
  63. package/src/tools/data/Tree/context/rename/use-rename.ts +113 -0
  64. package/src/tools/data/Tree/context/selection/index.ts +4 -0
  65. package/src/tools/data/Tree/context/selection/use-selection.ts +146 -0
  66. package/src/tools/data/Tree/context/state/index.ts +6 -0
  67. package/src/tools/data/Tree/context/state/initial.ts +41 -0
  68. package/src/tools/data/Tree/context/state/reducer.ts +76 -0
  69. package/src/tools/data/Tree/context/state/types.ts +46 -0
  70. package/src/tools/data/Tree/data/clipboard.ts +33 -0
  71. package/src/tools/data/Tree/data/dnd.ts +123 -0
  72. package/src/tools/data/Tree/data/finderShortcuts.ts +67 -0
  73. package/src/tools/data/Tree/data/index.ts +19 -0
  74. package/src/tools/data/Tree/data/renameUtils.ts +51 -0
  75. package/src/tools/data/Tree/data/selection.ts +157 -0
  76. package/src/tools/data/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
  77. package/src/tools/data/Tree/hooks/finder-hotkeys/index.ts +8 -0
  78. package/src/tools/data/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
  79. package/src/tools/data/Tree/hooks/index.ts +23 -4
  80. package/src/tools/data/Tree/hooks/keyboard/activation.ts +27 -0
  81. package/src/tools/data/Tree/hooks/keyboard/arrow-nav.ts +26 -0
  82. package/src/tools/data/Tree/hooks/keyboard/expand-collapse.ts +54 -0
  83. package/src/tools/data/Tree/hooks/keyboard/index.ts +10 -0
  84. package/src/tools/data/Tree/hooks/keyboard/types.ts +39 -0
  85. package/src/tools/data/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
  86. package/src/tools/data/Tree/hooks/type-ahead/index.ts +5 -0
  87. package/src/tools/data/Tree/hooks/type-ahead/match-prefix.ts +42 -0
  88. package/src/tools/data/Tree/hooks/{useTreeTypeAhead.ts → type-ahead/use-tree-type-ahead.ts} +8 -19
  89. package/src/tools/data/Tree/index.tsx +25 -2
  90. package/src/tools/data/Tree/types/activation.ts +30 -0
  91. package/src/tools/data/Tree/types/adapter.ts +70 -0
  92. package/src/tools/data/Tree/types/index.ts +27 -0
  93. package/src/tools/data/Tree/types/labels.ts +97 -0
  94. package/src/tools/data/Tree/types/loader.ts +9 -0
  95. package/src/tools/data/Tree/types/node.ts +38 -0
  96. package/src/tools/data/Tree/types/root-props.ts +142 -0
  97. package/src/tools/data/Tree/types/selection.ts +3 -0
  98. package/src/tools/data/Tree/types/slots.ts +64 -0
  99. package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseBody.tsx +1 -1
  100. package/src/tools/dev/OpenapiViewer/components/shared/ResponsePanel/PrettyView.tsx +1 -1
  101. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +85 -0
  102. package/src/tools/forms/MarkdownEditor/index.ts +1 -0
  103. package/src/tools/forms/MarkdownEditor/lazy.tsx +6 -0
  104. package/src/tools/forms/MarkdownEditor/slash/SlashCommandNode.ts +162 -0
  105. package/src/tools/forms/MarkdownEditor/slash/index.ts +4 -0
  106. package/src/tools/forms/MarkdownEditor/slash/syncSlashNode.ts +97 -0
  107. package/src/tools/forms/MarkdownEditor/slash/types.ts +13 -0
  108. package/src/tools/forms/MarkdownEditor/styles.css +18 -0
  109. package/src/tools/index.ts +2 -2
  110. package/dist/types-j2vhn4Kv.d.cts +0 -241
  111. package/dist/types-j2vhn4Kv.d.ts +0 -241
  112. package/src/tools/data/Tree/hooks/useTreeKeyboard.ts +0 -171
  113. package/src/tools/data/Tree/types.ts +0 -217
@@ -116,6 +116,24 @@
116
116
  box-shadow: none;
117
117
  }
118
118
 
119
+ /* Slash-command inline chip — TipTap atom analogue of the plain
120
+ `<SlashHighlightTextarea>` mirror. Mirrors `bg-primary/15 text-primary`
121
+ so the two paths look identical. Atom nodes don't admit a caret, so
122
+ light horizontal padding is safe here (unlike the plain mirror,
123
+ which has to be padding-less to keep caret alignment). */
124
+ .markdown-slash-command {
125
+ background: color-mix(in oklab, var(--color-primary, var(--primary)) 15%, transparent);
126
+ color: var(--color-primary, var(--primary));
127
+ padding: 0 2px;
128
+ border-radius: 3px;
129
+ font-weight: 500;
130
+ /* Atom nodes can't be entered; reflect that visually so the user does
131
+ not try to click into the chip to edit it. */
132
+ cursor: default;
133
+ user-select: none;
134
+ display: inline;
135
+ }
136
+
119
137
  /* Mention inline chip */
120
138
  .markdown-mention {
121
139
  background: var(--color-primary, var(--primary));
@@ -97,8 +97,8 @@ export { Player as AudioPlayer } from './media/AudioPlayer';
97
97
  export type { PlayerProps as AudioPlayerProps } from './media/AudioPlayer';
98
98
 
99
99
  // Export ImageViewer
100
- export { ImageViewer } from './overlay/ImageViewer';
101
- export type { ImageViewerProps, ImageFile } from './overlay/ImageViewer';
100
+ export { ImageViewer } from './media/ImageViewer';
101
+ export type { ImageViewerProps, ImageFile } from './media/ImageViewer';
102
102
 
103
103
  // Export CronScheduler (~15KB)
104
104
  export {
@@ -1,241 +0,0 @@
1
- import { CSSProperties, ReactNode } from 'react';
2
-
3
- type TreeDensity = 'compact' | 'cozy' | 'comfortable';
4
- type TreeAccentIntensity = 'subtle' | 'default' | 'strong';
5
- type TreeRadius = 'none' | 'sm' | 'md';
6
- /**
7
- * Cosmetic configuration. Every field is optional; missing values fall
8
- * back to the `cozy` preset (a comfortable VSCode-Explorer-like density).
9
- *
10
- * Customize the look without re-implementing slots.
11
- */
12
- interface TreeAppearance {
13
- /** Built-in size preset. Default: `'cozy'`. */
14
- density?: TreeDensity;
15
- /** Override row height in px (wins over density). */
16
- rowHeight?: number;
17
- /** Override icon + chevron size in px (wins over density). */
18
- iconSize?: number;
19
- /** Lucide stroke width for icon + chevron. Default: 1.5. */
20
- iconStrokeWidth?: number;
21
- /** Override label font size in px (wins over density). */
22
- fontSize?: number;
23
- /** Pixels between chevron / icon / label. Default depends on density. */
24
- gap?: number;
25
- /** Pixels between nesting levels. Default: 16. */
26
- indent?: number;
27
- /** Hover / selected highlight intensity. Default: `'default'`. */
28
- accent?: TreeAccentIntensity;
29
- /** Row corner radius. Default: `'sm'`. */
30
- radius?: TreeRadius;
31
- /** Indent-guide line opacity (0..1). Default: 0.4. */
32
- indentGuideOpacity?: number;
33
- /**
34
- * Show a 2px primary-tinted bar on the left of the selected row.
35
- * Mimics the VSCode active-tab indicator. Default: `true`.
36
- */
37
- showActiveIndicator?: boolean;
38
- }
39
- interface ResolvedAppearance {
40
- density: TreeDensity;
41
- rowHeight: number;
42
- iconSize: number;
43
- iconStrokeWidth: number;
44
- fontSize: number;
45
- gap: number;
46
- indent: number;
47
- accent: TreeAccentIntensity;
48
- radius: TreeRadius;
49
- indentGuideOpacity: number;
50
- showActiveIndicator: boolean;
51
- }
52
- declare const DEFAULT_TREE_APPEARANCE: ResolvedAppearance;
53
- /**
54
- * Merge a partial appearance with the default + density preset.
55
- *
56
- * Explicit numeric overrides (e.g. `rowHeight`) win over the density preset.
57
- */
58
- declare function resolveAppearance(input?: TreeAppearance,
59
- /** Outer `indent` prop (kept on TreeRoot for back-compat). */
60
- outerIndent?: number): ResolvedAppearance;
61
- /**
62
- * Build the `style` object that exposes the resolved appearance to any
63
- * descendant via CSS variables. Set on `<TreeRoot>`'s outer div.
64
- */
65
- declare function appearanceToStyle(a: ResolvedAppearance): CSSProperties;
66
-
67
- type TreeItemId = string;
68
- /** A single node in the consumer's tree data. Generic over your payload. */
69
- interface TreeNode<T = unknown> {
70
- id: TreeItemId;
71
- data: T;
72
- /** Inline children. Omit (and provide a `loadChildren`) for async loading. */
73
- children?: TreeNode<T>[];
74
- /**
75
- * Set to `true` to mark a node as a folder even when its `children` array
76
- * is empty (e.g. an unloaded async folder). Default: derived from
77
- * `Array.isArray(children)`.
78
- */
79
- isFolder?: boolean;
80
- /** Disable interaction. */
81
- disabled?: boolean;
82
- }
83
- interface TreeLabels {
84
- loading: string;
85
- empty: string;
86
- error: string;
87
- searchPlaceholder: string;
88
- searchMatches: (count: number) => string;
89
- ariaLabel: string;
90
- }
91
- declare const DEFAULT_TREE_LABELS: TreeLabels;
92
- type TreeSelectionMode = 'none' | 'single' | 'multiple';
93
- /**
94
- * How a node becomes "activated" (i.e. opened) on pointer interaction.
95
- *
96
- * - `'single-click'` (default): single click activates a leaf immediately;
97
- * double-click also activates. Folders always toggle on single click.
98
- * - `'double-click'`: single click only selects + focuses; double-click is
99
- * required to activate. Mirrors classic file-manager behaviour.
100
- * - `'single-click-preview'`: VSCode Explorer / Cursor behaviour. Single
101
- * click activates with `{ preview: true }` (consumer renders a preview
102
- * tab); double-click activates with `{ preview: false }` (pinned tab).
103
- *
104
- * Folders ignore this setting — they always toggle on single click and
105
- * never call `onActivate`.
106
- */
107
- type TreeActivationMode = 'single-click' | 'double-click' | 'single-click-preview';
108
- interface TreeActivateOptions {
109
- /**
110
- * `true` when the activation came from a single click in
111
- * `'single-click-preview'` mode. `false` for double-click and for
112
- * non-preview modes. Consumers typically map this to a
113
- * preview-tab vs pinned-tab distinction.
114
- */
115
- preview: boolean;
116
- }
117
- /**
118
- * Async loader: called the first time a folder is expanded with no inline
119
- * `children`. Result is cached; concurrent expansions are de-duplicated.
120
- */
121
- type TreeLoadChildren<T> = (node: TreeNode<T>) => Promise<TreeNode<T>[]>;
122
- interface TreeRowRenderProps<T> {
123
- node: TreeNode<T>;
124
- level: number;
125
- isSelected: boolean;
126
- isExpanded: boolean;
127
- isFocused: boolean;
128
- isFolder: boolean;
129
- isLoading: boolean;
130
- isMatchingSearch: boolean;
131
- }
132
- type TreeRowSlot<T> = (props: TreeRowRenderProps<T>) => ReactNode;
133
- type TreeContextMenuSlot<T> = (props: TreeRowRenderProps<T>, trigger: ReactNode) => ReactNode;
134
- /**
135
- * Declarative context-menu item. Pass `'separator'` (string) in place of an
136
- * object to insert a `<ContextMenuSeparator />` between groups.
137
- *
138
- * For more advanced needs (submenus, checkbox items, custom JSX), drop down
139
- * to `renderContextMenu` instead.
140
- */
141
- interface TreeContextMenuAction<T> {
142
- /** Stable React key. */
143
- id: string;
144
- label: ReactNode;
145
- /** Lucide-style icon component. Rendered as `<icon className="size-4" />`. */
146
- icon?: React.ComponentType<{
147
- className?: string;
148
- }>;
149
- /** Right-aligned keyboard hint (e.g. `'⌘C'`, `'↵'`). Cosmetic. */
150
- shortcut?: ReactNode;
151
- /** Disable the item — still rendered, not selectable. */
152
- disabled?: boolean;
153
- /** Style as destructive (red). */
154
- destructive?: boolean;
155
- /** Click / Enter handler. Receives the row meta. */
156
- onSelect: (props: TreeRowRenderProps<T>) => void;
157
- }
158
- type TreeContextMenuItem<T> = TreeContextMenuAction<T> | 'separator';
159
- type TreeContextMenuActionsResolver<T> = (props: TreeRowRenderProps<T>) => TreeContextMenuItem<T>[] | null | undefined;
160
- interface TreeRootProps<T> {
161
- /** Root nodes. Top-level items are rendered directly (no synthetic root). */
162
- data: TreeNode<T>[];
163
- /** Returns the human-readable name for a node (used by search/type-ahead). */
164
- getItemName: (node: TreeNode<T>) => string;
165
- /** Async loader for folders without inline `children`. */
166
- loadChildren?: TreeLoadChildren<T>;
167
- /** Selection behaviour. Default: `'single'`. */
168
- selectionMode?: TreeSelectionMode;
169
- /** Pointer activation behaviour. Default: `'single-click'`. */
170
- activationMode?: TreeActivationMode;
171
- /** Initially expanded ids. */
172
- initialExpandedIds?: TreeItemId[];
173
- /** Initially selected ids. */
174
- initialSelectedIds?: TreeItemId[];
175
- /** Pixels of indent per nesting level. Default: 16. (Shortcut for `appearance.indent`.) */
176
- indent?: number;
177
- /** Cosmetic configuration: density, sizes, accent intensity, radius. */
178
- appearance?: TreeAppearance;
179
- /** Triggered when selection changes. */
180
- onSelectionChange?: (selectedIds: TreeItemId[]) => void;
181
- /** Triggered when expanded set changes. */
182
- onExpansionChange?: (expandedIds: TreeItemId[]) => void;
183
- /**
184
- * Triggered when a leaf is activated (Enter / dblclick / click depending
185
- * on `activationMode`). Folders never call this — they toggle instead.
186
- */
187
- onActivate?: (node: TreeNode<T>, opts: TreeActivateOptions) => void;
188
- /**
189
- * Optional predicate. Nodes returning `false` (and their descendants) are
190
- * not rendered and not searchable. Use this to hide dot-files, system
191
- * entries, or anything else the consumer wants to filter out.
192
- */
193
- filterNode?: (node: TreeNode<T>) => boolean;
194
- /** Show built-in search input. Default: false. */
195
- enableSearch?: boolean;
196
- /** Type printable letters to jump to a matching name. Default: true. */
197
- enableTypeAhead?: boolean;
198
- /** Render vertical indent guides under expanded folders. Default: false. */
199
- showIndentGuides?: boolean;
200
- /** Custom row renderer. Falls back to the default <TreeRow />. */
201
- renderRow?: TreeRowSlot<T>;
202
- /** Replace default folder/file icon. */
203
- renderIcon?: TreeRowSlot<T>;
204
- /** Replace default label rendering. */
205
- renderLabel?: TreeRowSlot<T>;
206
- /** Right-side actions slot (per row). */
207
- renderActions?: TreeRowSlot<T>;
208
- /** Wrap each row in a context menu (right-click). Receives the row meta + trigger element. */
209
- renderContextMenu?: TreeContextMenuSlot<T>;
210
- /**
211
- * Declarative right-click menu — short-form. Pass `(row) => [items]` and the
212
- * Tree builds a `<ContextMenu>` for you with sensible defaults. Ignored if
213
- * `renderContextMenu` is also set. Return `null`/`undefined`/`[]` to skip
214
- * the menu for that row.
215
- */
216
- contextMenuActions?: TreeContextMenuActionsResolver<T>;
217
- /** Override built-in copy in your locale. */
218
- labels?: Partial<TreeLabels>;
219
- /** Persist expanded + (optional) selected ids in localStorage under this key. */
220
- persistKey?: string;
221
- /** Persist selection alongside expansion. Default: false. */
222
- persistSelection?: boolean;
223
- className?: string;
224
- style?: React.CSSProperties;
225
- }
226
- /** Internal flat-row representation used by the renderer + keyboard nav. */
227
- interface FlatRow<T> {
228
- node: TreeNode<T>;
229
- level: number;
230
- parentId: TreeItemId | null;
231
- isFolder: boolean;
232
- isExpanded: boolean;
233
- isLoading: boolean;
234
- hasError: boolean;
235
- /** 1-based position among visible siblings (for `aria-posinset`). */
236
- posInSet: number;
237
- /** Count of visible siblings sharing this row's parent (for `aria-setsize`). */
238
- setSize: number;
239
- }
240
-
241
- export { DEFAULT_TREE_APPEARANCE as D, type FlatRow as F, type ResolvedAppearance as R, type TreeRootProps as T, type TreeItemId as a, type TreeNode as b, type TreeActivateOptions as c, type TreeLabels as d, type TreeSelectionMode as e, type TreeActivationMode as f, type TreeRowSlot as g, type TreeContextMenuSlot as h, appearanceToStyle as i, type TreeAppearance as j, type TreeDensity as k, type TreeAccentIntensity as l, type TreeRadius as m, DEFAULT_TREE_LABELS as n, type TreeRowRenderProps as o, type TreeContextMenuAction as p, type TreeContextMenuItem as q, resolveAppearance as r, type TreeContextMenuActionsResolver as s, type TreeLoadChildren as t };
@@ -1,241 +0,0 @@
1
- import { CSSProperties, ReactNode } from 'react';
2
-
3
- type TreeDensity = 'compact' | 'cozy' | 'comfortable';
4
- type TreeAccentIntensity = 'subtle' | 'default' | 'strong';
5
- type TreeRadius = 'none' | 'sm' | 'md';
6
- /**
7
- * Cosmetic configuration. Every field is optional; missing values fall
8
- * back to the `cozy` preset (a comfortable VSCode-Explorer-like density).
9
- *
10
- * Customize the look without re-implementing slots.
11
- */
12
- interface TreeAppearance {
13
- /** Built-in size preset. Default: `'cozy'`. */
14
- density?: TreeDensity;
15
- /** Override row height in px (wins over density). */
16
- rowHeight?: number;
17
- /** Override icon + chevron size in px (wins over density). */
18
- iconSize?: number;
19
- /** Lucide stroke width for icon + chevron. Default: 1.5. */
20
- iconStrokeWidth?: number;
21
- /** Override label font size in px (wins over density). */
22
- fontSize?: number;
23
- /** Pixels between chevron / icon / label. Default depends on density. */
24
- gap?: number;
25
- /** Pixels between nesting levels. Default: 16. */
26
- indent?: number;
27
- /** Hover / selected highlight intensity. Default: `'default'`. */
28
- accent?: TreeAccentIntensity;
29
- /** Row corner radius. Default: `'sm'`. */
30
- radius?: TreeRadius;
31
- /** Indent-guide line opacity (0..1). Default: 0.4. */
32
- indentGuideOpacity?: number;
33
- /**
34
- * Show a 2px primary-tinted bar on the left of the selected row.
35
- * Mimics the VSCode active-tab indicator. Default: `true`.
36
- */
37
- showActiveIndicator?: boolean;
38
- }
39
- interface ResolvedAppearance {
40
- density: TreeDensity;
41
- rowHeight: number;
42
- iconSize: number;
43
- iconStrokeWidth: number;
44
- fontSize: number;
45
- gap: number;
46
- indent: number;
47
- accent: TreeAccentIntensity;
48
- radius: TreeRadius;
49
- indentGuideOpacity: number;
50
- showActiveIndicator: boolean;
51
- }
52
- declare const DEFAULT_TREE_APPEARANCE: ResolvedAppearance;
53
- /**
54
- * Merge a partial appearance with the default + density preset.
55
- *
56
- * Explicit numeric overrides (e.g. `rowHeight`) win over the density preset.
57
- */
58
- declare function resolveAppearance(input?: TreeAppearance,
59
- /** Outer `indent` prop (kept on TreeRoot for back-compat). */
60
- outerIndent?: number): ResolvedAppearance;
61
- /**
62
- * Build the `style` object that exposes the resolved appearance to any
63
- * descendant via CSS variables. Set on `<TreeRoot>`'s outer div.
64
- */
65
- declare function appearanceToStyle(a: ResolvedAppearance): CSSProperties;
66
-
67
- type TreeItemId = string;
68
- /** A single node in the consumer's tree data. Generic over your payload. */
69
- interface TreeNode<T = unknown> {
70
- id: TreeItemId;
71
- data: T;
72
- /** Inline children. Omit (and provide a `loadChildren`) for async loading. */
73
- children?: TreeNode<T>[];
74
- /**
75
- * Set to `true` to mark a node as a folder even when its `children` array
76
- * is empty (e.g. an unloaded async folder). Default: derived from
77
- * `Array.isArray(children)`.
78
- */
79
- isFolder?: boolean;
80
- /** Disable interaction. */
81
- disabled?: boolean;
82
- }
83
- interface TreeLabels {
84
- loading: string;
85
- empty: string;
86
- error: string;
87
- searchPlaceholder: string;
88
- searchMatches: (count: number) => string;
89
- ariaLabel: string;
90
- }
91
- declare const DEFAULT_TREE_LABELS: TreeLabels;
92
- type TreeSelectionMode = 'none' | 'single' | 'multiple';
93
- /**
94
- * How a node becomes "activated" (i.e. opened) on pointer interaction.
95
- *
96
- * - `'single-click'` (default): single click activates a leaf immediately;
97
- * double-click also activates. Folders always toggle on single click.
98
- * - `'double-click'`: single click only selects + focuses; double-click is
99
- * required to activate. Mirrors classic file-manager behaviour.
100
- * - `'single-click-preview'`: VSCode Explorer / Cursor behaviour. Single
101
- * click activates with `{ preview: true }` (consumer renders a preview
102
- * tab); double-click activates with `{ preview: false }` (pinned tab).
103
- *
104
- * Folders ignore this setting — they always toggle on single click and
105
- * never call `onActivate`.
106
- */
107
- type TreeActivationMode = 'single-click' | 'double-click' | 'single-click-preview';
108
- interface TreeActivateOptions {
109
- /**
110
- * `true` when the activation came from a single click in
111
- * `'single-click-preview'` mode. `false` for double-click and for
112
- * non-preview modes. Consumers typically map this to a
113
- * preview-tab vs pinned-tab distinction.
114
- */
115
- preview: boolean;
116
- }
117
- /**
118
- * Async loader: called the first time a folder is expanded with no inline
119
- * `children`. Result is cached; concurrent expansions are de-duplicated.
120
- */
121
- type TreeLoadChildren<T> = (node: TreeNode<T>) => Promise<TreeNode<T>[]>;
122
- interface TreeRowRenderProps<T> {
123
- node: TreeNode<T>;
124
- level: number;
125
- isSelected: boolean;
126
- isExpanded: boolean;
127
- isFocused: boolean;
128
- isFolder: boolean;
129
- isLoading: boolean;
130
- isMatchingSearch: boolean;
131
- }
132
- type TreeRowSlot<T> = (props: TreeRowRenderProps<T>) => ReactNode;
133
- type TreeContextMenuSlot<T> = (props: TreeRowRenderProps<T>, trigger: ReactNode) => ReactNode;
134
- /**
135
- * Declarative context-menu item. Pass `'separator'` (string) in place of an
136
- * object to insert a `<ContextMenuSeparator />` between groups.
137
- *
138
- * For more advanced needs (submenus, checkbox items, custom JSX), drop down
139
- * to `renderContextMenu` instead.
140
- */
141
- interface TreeContextMenuAction<T> {
142
- /** Stable React key. */
143
- id: string;
144
- label: ReactNode;
145
- /** Lucide-style icon component. Rendered as `<icon className="size-4" />`. */
146
- icon?: React.ComponentType<{
147
- className?: string;
148
- }>;
149
- /** Right-aligned keyboard hint (e.g. `'⌘C'`, `'↵'`). Cosmetic. */
150
- shortcut?: ReactNode;
151
- /** Disable the item — still rendered, not selectable. */
152
- disabled?: boolean;
153
- /** Style as destructive (red). */
154
- destructive?: boolean;
155
- /** Click / Enter handler. Receives the row meta. */
156
- onSelect: (props: TreeRowRenderProps<T>) => void;
157
- }
158
- type TreeContextMenuItem<T> = TreeContextMenuAction<T> | 'separator';
159
- type TreeContextMenuActionsResolver<T> = (props: TreeRowRenderProps<T>) => TreeContextMenuItem<T>[] | null | undefined;
160
- interface TreeRootProps<T> {
161
- /** Root nodes. Top-level items are rendered directly (no synthetic root). */
162
- data: TreeNode<T>[];
163
- /** Returns the human-readable name for a node (used by search/type-ahead). */
164
- getItemName: (node: TreeNode<T>) => string;
165
- /** Async loader for folders without inline `children`. */
166
- loadChildren?: TreeLoadChildren<T>;
167
- /** Selection behaviour. Default: `'single'`. */
168
- selectionMode?: TreeSelectionMode;
169
- /** Pointer activation behaviour. Default: `'single-click'`. */
170
- activationMode?: TreeActivationMode;
171
- /** Initially expanded ids. */
172
- initialExpandedIds?: TreeItemId[];
173
- /** Initially selected ids. */
174
- initialSelectedIds?: TreeItemId[];
175
- /** Pixels of indent per nesting level. Default: 16. (Shortcut for `appearance.indent`.) */
176
- indent?: number;
177
- /** Cosmetic configuration: density, sizes, accent intensity, radius. */
178
- appearance?: TreeAppearance;
179
- /** Triggered when selection changes. */
180
- onSelectionChange?: (selectedIds: TreeItemId[]) => void;
181
- /** Triggered when expanded set changes. */
182
- onExpansionChange?: (expandedIds: TreeItemId[]) => void;
183
- /**
184
- * Triggered when a leaf is activated (Enter / dblclick / click depending
185
- * on `activationMode`). Folders never call this — they toggle instead.
186
- */
187
- onActivate?: (node: TreeNode<T>, opts: TreeActivateOptions) => void;
188
- /**
189
- * Optional predicate. Nodes returning `false` (and their descendants) are
190
- * not rendered and not searchable. Use this to hide dot-files, system
191
- * entries, or anything else the consumer wants to filter out.
192
- */
193
- filterNode?: (node: TreeNode<T>) => boolean;
194
- /** Show built-in search input. Default: false. */
195
- enableSearch?: boolean;
196
- /** Type printable letters to jump to a matching name. Default: true. */
197
- enableTypeAhead?: boolean;
198
- /** Render vertical indent guides under expanded folders. Default: false. */
199
- showIndentGuides?: boolean;
200
- /** Custom row renderer. Falls back to the default <TreeRow />. */
201
- renderRow?: TreeRowSlot<T>;
202
- /** Replace default folder/file icon. */
203
- renderIcon?: TreeRowSlot<T>;
204
- /** Replace default label rendering. */
205
- renderLabel?: TreeRowSlot<T>;
206
- /** Right-side actions slot (per row). */
207
- renderActions?: TreeRowSlot<T>;
208
- /** Wrap each row in a context menu (right-click). Receives the row meta + trigger element. */
209
- renderContextMenu?: TreeContextMenuSlot<T>;
210
- /**
211
- * Declarative right-click menu — short-form. Pass `(row) => [items]` and the
212
- * Tree builds a `<ContextMenu>` for you with sensible defaults. Ignored if
213
- * `renderContextMenu` is also set. Return `null`/`undefined`/`[]` to skip
214
- * the menu for that row.
215
- */
216
- contextMenuActions?: TreeContextMenuActionsResolver<T>;
217
- /** Override built-in copy in your locale. */
218
- labels?: Partial<TreeLabels>;
219
- /** Persist expanded + (optional) selected ids in localStorage under this key. */
220
- persistKey?: string;
221
- /** Persist selection alongside expansion. Default: false. */
222
- persistSelection?: boolean;
223
- className?: string;
224
- style?: React.CSSProperties;
225
- }
226
- /** Internal flat-row representation used by the renderer + keyboard nav. */
227
- interface FlatRow<T> {
228
- node: TreeNode<T>;
229
- level: number;
230
- parentId: TreeItemId | null;
231
- isFolder: boolean;
232
- isExpanded: boolean;
233
- isLoading: boolean;
234
- hasError: boolean;
235
- /** 1-based position among visible siblings (for `aria-posinset`). */
236
- posInSet: number;
237
- /** Count of visible siblings sharing this row's parent (for `aria-setsize`). */
238
- setSize: number;
239
- }
240
-
241
- export { DEFAULT_TREE_APPEARANCE as D, type FlatRow as F, type ResolvedAppearance as R, type TreeRootProps as T, type TreeItemId as a, type TreeNode as b, type TreeActivateOptions as c, type TreeLabels as d, type TreeSelectionMode as e, type TreeActivationMode as f, type TreeRowSlot as g, type TreeContextMenuSlot as h, appearanceToStyle as i, type TreeAppearance as j, type TreeDensity as k, type TreeAccentIntensity as l, type TreeRadius as m, DEFAULT_TREE_LABELS as n, type TreeRowRenderProps as o, type TreeContextMenuAction as p, type TreeContextMenuItem as q, resolveAppearance as r, type TreeContextMenuActionsResolver as s, type TreeLoadChildren as t };
@@ -1,171 +0,0 @@
1
- 'use client';
2
-
3
- import { useCallback, useRef } from 'react';
4
- import { useHotkey } from '@djangocfg/ui-core/hooks';
5
-
6
- import type { FlatRow, TreeItemId } from '../types';
7
-
8
- export interface UseTreeKeyboardOptions<T> {
9
- rows: FlatRow<T>[];
10
- focusedId: TreeItemId | null;
11
- enabled?: boolean;
12
- onFocus: (id: TreeItemId) => void;
13
- onSelect: (id: TreeItemId) => void;
14
- onActivate: (id: TreeItemId) => void;
15
- onExpand: (id: TreeItemId) => void;
16
- onCollapse: (id: TreeItemId) => void;
17
- onClearSelection: () => void;
18
- }
19
-
20
- export interface UseTreeKeyboardReturn {
21
- /** Attach to the tree container. Hotkeys only fire when focus is inside. */
22
- ref: (instance: HTMLElement | null) => void;
23
- }
24
-
25
- /**
26
- * Standard tree keyboard navigation, scoped to the container ref.
27
- *
28
- * - ↑ / ↓ : prev / next visible row
29
- * - Home / End : first / last visible row
30
- * - → : expand folder; if already expanded, jump to first child
31
- * - ← : collapse folder; if already collapsed (or leaf), jump to parent
32
- * - Enter / Space : activate (folder => toggle, leaf => onActivate)
33
- * - Esc : clear selection
34
- *
35
- * Built on `useHotkey` (react-hotkeys-hook) — focus gating is automatic; no
36
- * manual `addEventListener` or `data-scope` juggling.
37
- */
38
- export function useTreeKeyboard<T>({
39
- rows,
40
- focusedId,
41
- enabled = true,
42
- onFocus,
43
- onSelect,
44
- onActivate,
45
- onExpand,
46
- onCollapse,
47
- onClearSelection,
48
- }: UseTreeKeyboardOptions<T>): UseTreeKeyboardReturn {
49
- // Keep latest values in refs so the callbacks below stay stable across
50
- // renders — react-hotkeys-hook re-binds on dep change otherwise.
51
- const rowsRef = useRef(rows);
52
- const focusedIdRef = useRef(focusedId);
53
- rowsRef.current = rows;
54
- focusedIdRef.current = focusedId;
55
-
56
- const getCurrent = () => {
57
- const r = rowsRef.current;
58
- const id = focusedIdRef.current;
59
- const idx = id ? r.findIndex((x) => x.node.id === id) : -1;
60
- return { rows: r, idx, current: idx >= 0 ? r[idx] : null };
61
- };
62
-
63
- const refDown = useHotkey(
64
- 'down',
65
- () => {
66
- const { rows: r, idx } = getCurrent();
67
- if (r.length === 0) return;
68
- const next = r[Math.min(idx + 1, r.length - 1)] ?? r[0];
69
- onFocus(next.node.id);
70
- },
71
- { enabled, preventDefault: true, description: 'Next row' },
72
- );
73
-
74
- const refUp = useHotkey(
75
- 'up',
76
- () => {
77
- const { rows: r, idx } = getCurrent();
78
- if (r.length === 0) return;
79
- const prev = r[Math.max(idx - 1, 0)] ?? r[0];
80
- onFocus(prev.node.id);
81
- },
82
- { enabled, preventDefault: true, description: 'Previous row' },
83
- );
84
-
85
- const refHome = useHotkey(
86
- 'home',
87
- () => {
88
- const { rows: r } = getCurrent();
89
- if (r.length === 0) return;
90
- onFocus(r[0].node.id);
91
- },
92
- { enabled, preventDefault: true, description: 'First row' },
93
- );
94
-
95
- const refEnd = useHotkey(
96
- 'end',
97
- () => {
98
- const { rows: r } = getCurrent();
99
- if (r.length === 0) return;
100
- onFocus(r[r.length - 1].node.id);
101
- },
102
- { enabled, preventDefault: true, description: 'Last row' },
103
- );
104
-
105
- const refRight = useHotkey(
106
- 'right',
107
- () => {
108
- const { rows: r, idx, current } = getCurrent();
109
- if (!current) return;
110
- if (current.isFolder && !current.isExpanded) {
111
- onExpand(current.node.id);
112
- } else if (current.isFolder && current.isExpanded) {
113
- const next = r[idx + 1];
114
- if (next) onFocus(next.node.id);
115
- }
116
- },
117
- { enabled, preventDefault: true, description: 'Expand / first child' },
118
- );
119
-
120
- const refLeft = useHotkey(
121
- 'left',
122
- () => {
123
- const { current } = getCurrent();
124
- if (!current) return;
125
- if (current.isFolder && current.isExpanded) {
126
- onCollapse(current.node.id);
127
- } else if (current.parentId) {
128
- onFocus(current.parentId);
129
- }
130
- },
131
- { enabled, preventDefault: true, description: 'Collapse / parent' },
132
- );
133
-
134
- const refActivate = useHotkey(
135
- ['enter', 'space'],
136
- () => {
137
- const { current } = getCurrent();
138
- if (!current) return;
139
- onSelect(current.node.id);
140
- if (current.isFolder) {
141
- if (current.isExpanded) onCollapse(current.node.id);
142
- else onExpand(current.node.id);
143
- } else {
144
- onActivate(current.node.id);
145
- }
146
- },
147
- { enabled, preventDefault: true, description: 'Activate / toggle' },
148
- );
149
-
150
- const refEscape = useHotkey(
151
- 'escape',
152
- () => onClearSelection(),
153
- { enabled, preventDefault: true, description: 'Clear selection' },
154
- );
155
-
156
- const ref = useCallback(
157
- (instance: HTMLElement | null) => {
158
- refDown(instance);
159
- refUp(instance);
160
- refHome(instance);
161
- refEnd(instance);
162
- refRight(instance);
163
- refLeft(instance);
164
- refActivate(instance);
165
- refEscape(instance);
166
- },
167
- [refDown, refUp, refHome, refEnd, refRight, refLeft, refActivate, refEscape],
168
- );
169
-
170
- return { ref };
171
- }