@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
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+
3
+ import type { FlatRow, TreeItemId } from '../../types';
4
+
5
+ export interface UseTreeKeyboardOptions<T> {
6
+ rows: FlatRow<T>[];
7
+ focusedId: TreeItemId | null;
8
+ enabled?: boolean;
9
+ /**
10
+ * `true` when `selectionMode === 'multiple'` — enables shift-extend on
11
+ * arrow keys, Home / End, and `Cmd/Ctrl+A` select-all. Without it the
12
+ * shift-modifier just moves focus.
13
+ */
14
+ multiSelect?: boolean;
15
+ /**
16
+ * Move focus to `id`. When `extend` is true and multi-select is enabled,
17
+ * the consumer should extend the selection range from anchor through id.
18
+ */
19
+ onFocus: (id: TreeItemId, opts: { extend: boolean }) => void;
20
+ onSelect: (id: TreeItemId) => void;
21
+ onActivate: (id: TreeItemId) => void;
22
+ onExpand: (id: TreeItemId) => void;
23
+ onCollapse: (id: TreeItemId) => void;
24
+ onClearSelection: () => void;
25
+ /** Cmd/Ctrl+A — select all visible rows. Ignored if multiSelect is false. */
26
+ onSelectAll?: () => void;
27
+ }
28
+
29
+ export interface UseTreeKeyboardReturn {
30
+ /** Attach to the tree container. Hotkeys only fire when focus is inside. */
31
+ ref: (instance: HTMLElement | null) => void;
32
+ }
33
+
34
+ /** Reduced state shape passed to action helpers — keeps them pure. */
35
+ export interface CurrentRow<T> {
36
+ rows: FlatRow<T>[];
37
+ idx: number;
38
+ current: FlatRow<T> | null;
39
+ }
@@ -0,0 +1,196 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useRef } from 'react';
4
+ import { useHotkey } from '@djangocfg/ui-core/hooks';
5
+
6
+ import { edgeRowId, nextRowId, prevRowId } from './arrow-nav';
7
+ import { resolveLeftArrow, resolveRightArrow } from './expand-collapse';
8
+ import { resolveActivate } from './activation';
9
+ import type {
10
+ CurrentRow,
11
+ UseTreeKeyboardOptions,
12
+ UseTreeKeyboardReturn,
13
+ } from './types';
14
+
15
+ /**
16
+ * Standard tree keyboard navigation, scoped to the container ref.
17
+ *
18
+ * - ↑ / ↓ : prev / next visible row (Shift extends range)
19
+ * - Home / End : first / last visible row (Shift extends range)
20
+ * - → / ← : expand-or-jump-to-child / collapse-or-jump-to-parent
21
+ * - Enter / Space : activate (folder → toggle, leaf → onActivate)
22
+ * - Esc : clear selection
23
+ * - Cmd/Ctrl + A : select all (multi-select only)
24
+ *
25
+ * Pure decision-making lives in the sibling helpers (`arrow-nav.ts`,
26
+ * `expand-collapse.ts`, `activation.ts`) so it's unit-testable without
27
+ * a DOM. This file only wires up `useHotkey` bindings and dispatches
28
+ * the helper outcomes back to the consumer's callbacks.
29
+ */
30
+ export function useTreeKeyboard<T>({
31
+ rows,
32
+ focusedId,
33
+ enabled = true,
34
+ multiSelect = false,
35
+ onFocus,
36
+ onSelect,
37
+ onActivate,
38
+ onExpand,
39
+ onCollapse,
40
+ onClearSelection,
41
+ onSelectAll,
42
+ }: UseTreeKeyboardOptions<T>): UseTreeKeyboardReturn {
43
+ // Keep latest values in refs so the callbacks below stay stable across
44
+ // renders — react-hotkeys-hook re-binds on dep change otherwise.
45
+ const rowsRef = useRef(rows);
46
+ const focusedIdRef = useRef(focusedId);
47
+ rowsRef.current = rows;
48
+ focusedIdRef.current = focusedId;
49
+
50
+ const getCurrent = (): CurrentRow<T> => {
51
+ const r = rowsRef.current;
52
+ const id = focusedIdRef.current;
53
+ const idx = id ? r.findIndex((x) => x.node.id === id) : -1;
54
+ return { rows: r, idx, current: idx >= 0 ? r[idx] : null };
55
+ };
56
+
57
+ // Down / Shift+Down. Plain moves focus, shift extends selection range.
58
+ const refDown = useHotkey(
59
+ ['down', 'shift+down'],
60
+ (e) => {
61
+ const { rows: r, idx } = getCurrent();
62
+ const id = nextRowId(r, idx);
63
+ if (id) onFocus(id, { extend: multiSelect && e.shiftKey });
64
+ },
65
+ { enabled, preventDefault: true, description: 'Next row (Shift extends)' },
66
+ );
67
+
68
+ const refUp = useHotkey(
69
+ ['up', 'shift+up'],
70
+ (e) => {
71
+ const { rows: r, idx } = getCurrent();
72
+ const id = prevRowId(r, idx);
73
+ if (id) onFocus(id, { extend: multiSelect && e.shiftKey });
74
+ },
75
+ { enabled, preventDefault: true, description: 'Previous row (Shift extends)' },
76
+ );
77
+
78
+ const refHome = useHotkey(
79
+ ['home', 'shift+home'],
80
+ (e) => {
81
+ const id = edgeRowId(rowsRef.current, 'first');
82
+ if (id) onFocus(id, { extend: multiSelect && e.shiftKey });
83
+ },
84
+ { enabled, preventDefault: true, description: 'First row (Shift extends)' },
85
+ );
86
+
87
+ const refEnd = useHotkey(
88
+ ['end', 'shift+end'],
89
+ (e) => {
90
+ const id = edgeRowId(rowsRef.current, 'last');
91
+ if (id) onFocus(id, { extend: multiSelect && e.shiftKey });
92
+ },
93
+ { enabled, preventDefault: true, description: 'Last row (Shift extends)' },
94
+ );
95
+
96
+ const refSelectAll = useHotkey(
97
+ 'mod+a',
98
+ () => {
99
+ if (!multiSelect) return;
100
+ onSelectAll?.();
101
+ },
102
+ {
103
+ enabled: enabled && multiSelect,
104
+ preventDefault: true,
105
+ description: 'Select all visible rows',
106
+ },
107
+ );
108
+
109
+ const refRight = useHotkey(
110
+ 'right',
111
+ () => {
112
+ const { rows: r, idx, current } = getCurrent();
113
+ const out = resolveRightArrow(current, r, idx);
114
+ switch (out.kind) {
115
+ case 'expand':
116
+ onExpand(out.id);
117
+ return;
118
+ case 'focus':
119
+ onFocus(out.id, { extend: false });
120
+ return;
121
+ case 'noop':
122
+ return;
123
+ }
124
+ },
125
+ { enabled, preventDefault: true, description: 'Expand / first child' },
126
+ );
127
+
128
+ const refLeft = useHotkey(
129
+ 'left',
130
+ () => {
131
+ const { current } = getCurrent();
132
+ const out = resolveLeftArrow(current);
133
+ switch (out.kind) {
134
+ case 'collapse':
135
+ onCollapse(out.id);
136
+ return;
137
+ case 'focus':
138
+ onFocus(out.id, { extend: false });
139
+ return;
140
+ case 'noop':
141
+ return;
142
+ }
143
+ },
144
+ { enabled, preventDefault: true, description: 'Collapse / parent' },
145
+ );
146
+
147
+ const refActivate = useHotkey(
148
+ ['enter', 'space'],
149
+ () => {
150
+ const { current } = getCurrent();
151
+ const out = resolveActivate(current);
152
+ if (out.kind === 'noop') return;
153
+ onSelect(out.kind === 'activate-leaf' ? out.id : out.id);
154
+ if (out.kind === 'toggle-folder') {
155
+ if (out.willExpand) onExpand(out.id);
156
+ else onCollapse(out.id);
157
+ } else {
158
+ onActivate(out.id);
159
+ }
160
+ },
161
+ { enabled, preventDefault: true, description: 'Activate / toggle' },
162
+ );
163
+
164
+ const refEscape = useHotkey(
165
+ 'escape',
166
+ () => onClearSelection(),
167
+ { enabled, preventDefault: true, description: 'Clear selection' },
168
+ );
169
+
170
+ const ref = useCallback(
171
+ (instance: HTMLElement | null) => {
172
+ refDown(instance);
173
+ refUp(instance);
174
+ refHome(instance);
175
+ refEnd(instance);
176
+ refRight(instance);
177
+ refLeft(instance);
178
+ refActivate(instance);
179
+ refEscape(instance);
180
+ refSelectAll(instance);
181
+ },
182
+ [
183
+ refDown,
184
+ refUp,
185
+ refHome,
186
+ refEnd,
187
+ refRight,
188
+ refLeft,
189
+ refActivate,
190
+ refEscape,
191
+ refSelectAll,
192
+ ],
193
+ );
194
+
195
+ return { ref };
196
+ }
@@ -0,0 +1,5 @@
1
+ 'use client';
2
+
3
+ export { useTreeTypeAhead } from './use-tree-type-ahead';
4
+ export type { UseTreeTypeAheadOptions } from './use-tree-type-ahead';
5
+ export { findRowByPrefix, isResetKey, isTypingTarget } from './match-prefix';
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import type { FlatRow, TreeNode } from '../../types';
4
+
5
+ /** Find the first row whose name starts with `prefix` (case-insensitive). */
6
+ export function findRowByPrefix<T>(
7
+ rows: readonly FlatRow<T>[],
8
+ getName: (node: TreeNode<T>) => string,
9
+ prefix: string,
10
+ ): FlatRow<T> | undefined {
11
+ if (prefix.length === 0) return undefined;
12
+ return rows.find((row) => getName(row.node).toLowerCase().startsWith(prefix));
13
+ }
14
+
15
+ /**
16
+ * Should this key terminate type-ahead instead of extending the buffer?
17
+ * Navigation, Enter, Tab, Escape — all reset; everything else continues.
18
+ */
19
+ export function isResetKey(key: string): boolean {
20
+ return (
21
+ key === 'Escape' ||
22
+ key === 'Enter' ||
23
+ key === 'Tab' ||
24
+ key.startsWith('Arrow') ||
25
+ key === 'Home' ||
26
+ key === 'End' ||
27
+ key === 'PageUp' ||
28
+ key === 'PageDown'
29
+ );
30
+ }
31
+
32
+ /**
33
+ * Skip keystrokes that originate inside a text field so type-ahead
34
+ * doesn't yank focus while the user is filling in a form.
35
+ */
36
+ export function isTypingTarget(target: EventTarget | null): boolean {
37
+ const el = target as HTMLElement | null;
38
+ if (!el) return false;
39
+ const tag = el.tagName;
40
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
41
+ return el.isContentEditable === true;
42
+ }
@@ -2,7 +2,8 @@
2
2
 
3
3
  import { useEffect, useRef } from 'react';
4
4
 
5
- import type { FlatRow, TreeNode } from '../types';
5
+ import type { FlatRow, TreeNode } from '../../types';
6
+ import { findRowByPrefix, isResetKey, isTypingTarget } from './match-prefix';
6
7
 
7
8
  const FLUSH_MS = 600;
8
9
 
@@ -56,34 +57,22 @@ export function useTreeTypeAhead<T>({
56
57
  };
57
58
 
58
59
  const handler = (e: KeyboardEvent) => {
59
- const tag = (e.target as HTMLElement | null)?.tagName;
60
- if (tag === 'INPUT' || tag === 'TEXTAREA') return;
61
- if ((e.target as HTMLElement | null)?.isContentEditable) return;
60
+ if (isTypingTarget(e.target)) return;
62
61
  if (e.metaKey || e.ctrlKey || e.altKey) return;
63
-
64
- if (
65
- e.key === 'Escape' ||
66
- e.key === 'Enter' ||
67
- e.key === 'Tab' ||
68
- e.key.startsWith('Arrow') ||
69
- e.key === 'Home' ||
70
- e.key === 'End' ||
71
- e.key === 'PageUp' ||
72
- e.key === 'PageDown'
73
- ) {
62
+ if (isResetKey(e.key)) {
74
63
  reset();
75
64
  return;
76
65
  }
77
-
78
66
  if (e.key.length !== 1) return;
79
67
 
80
68
  bufferRef.current += e.key.toLowerCase();
81
69
  if (timerRef.current) clearTimeout(timerRef.current);
82
70
  timerRef.current = setTimeout(reset, FLUSH_MS);
83
71
 
84
- const prefix = bufferRef.current;
85
- const hit = rowsRef.current.find((row) =>
86
- getNameRef.current(row.node).toLowerCase().startsWith(prefix),
72
+ const hit = findRowByPrefix(
73
+ rowsRef.current,
74
+ getNameRef.current,
75
+ bufferRef.current,
87
76
  );
88
77
  if (hit) {
89
78
  e.preventDefault();
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  export { TreeRoot, TreeRoot as Tree, default as default } from './TreeRoot';
19
+ export { FinderTree } from './FinderTree';
19
20
 
20
21
  export {
21
22
  TreeProvider,
@@ -26,12 +27,19 @@ export {
26
27
  useTreeExpansion,
27
28
  useTreeFocus,
28
29
  useTreeSearch,
30
+ useTreeRename,
31
+ useTreeClipboard,
32
+ useTreeDnd,
29
33
  useTreeActions,
30
34
  } from './context';
31
35
  export type { TreeProviderProps, TreeContextValue } from './context';
32
36
 
33
- export { useTreeTypeAhead, useTreeKeyboard } from './hooks';
34
- export type { UseTreeTypeAheadOptions, UseTreeKeyboardOptions } from './hooks';
37
+ export { useTreeTypeAhead, useTreeKeyboard, useTreeFinderHotkeys } from './hooks';
38
+ export type {
39
+ UseTreeTypeAheadOptions,
40
+ UseTreeKeyboardOptions,
41
+ UseTreeFinderHotkeysOptions,
42
+ } from './hooks';
35
43
 
36
44
  export {
37
45
  TreeContent,
@@ -44,6 +52,9 @@ export {
44
52
  TreeSkeleton,
45
53
  TreeError,
46
54
  TreeIndentGuides,
55
+ TreeRenameInput,
56
+ TreeDropIndicator,
57
+ TreeEmptyArea,
47
58
  } from './components';
48
59
  export type {
49
60
  TreeContentProps,
@@ -56,6 +67,9 @@ export type {
56
67
  TreeSkeletonProps,
57
68
  TreeErrorProps,
58
69
  TreeIndentGuidesProps,
70
+ TreeRenameInputProps,
71
+ TreeDropIndicatorProps,
72
+ TreeEmptyAreaProps,
59
73
  } from './components';
60
74
 
61
75
  export {
@@ -69,6 +83,11 @@ export {
69
83
  DEFAULT_TREE_APPEARANCE,
70
84
  resolveAppearance,
71
85
  appearanceToStyle,
86
+ splitFileName,
87
+ autoSelectRange,
88
+ resolveDropZone,
89
+ defaultCanDrop,
90
+ TREE_DND_MIME,
72
91
  } from './data';
73
92
  export type {
74
93
  ChildCache,
@@ -98,7 +117,11 @@ export type {
98
117
  TreeContextMenuSlot,
99
118
  TreeContextMenuAction,
100
119
  TreeContextMenuItem,
120
+ TreeContextMenuActionsContext,
101
121
  TreeContextMenuActionsResolver,
102
122
  TreeLoadChildren,
123
+ TreeAdapter,
124
+ TreeBuiltinAction,
125
+ TreeMovePosition,
103
126
  FlatRow,
104
127
  } from './types';
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * How a node becomes "activated" (i.e. opened) on pointer interaction.
5
+ *
6
+ * - `'single-click'` (default): single click activates a leaf immediately;
7
+ * double-click also activates. Folders always toggle on single click.
8
+ * - `'double-click'`: single click only selects + focuses; double-click is
9
+ * required to activate. Mirrors classic file-manager behaviour.
10
+ * - `'single-click-preview'`: VSCode Explorer / Cursor behaviour. Single
11
+ * click activates with `{ preview: true }` (consumer renders a preview
12
+ * tab); double-click activates with `{ preview: false }` (pinned tab).
13
+ *
14
+ * Folders ignore this setting — they always toggle on single click and
15
+ * never call `onActivate`.
16
+ */
17
+ export type TreeActivationMode =
18
+ | 'single-click'
19
+ | 'double-click'
20
+ | 'single-click-preview';
21
+
22
+ export interface TreeActivateOptions {
23
+ /**
24
+ * `true` when the activation came from a single click in
25
+ * `'single-click-preview'` mode. `false` for double-click and for
26
+ * non-preview modes. Consumers typically map this to a
27
+ * preview-tab vs pinned-tab distinction.
28
+ */
29
+ preview: boolean;
30
+ }
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+
3
+ import type { TreeNode } from './node';
4
+
5
+ /**
6
+ * Position of a drop / move relative to the target row. `inside` means
7
+ * "drop into this folder"; `before`/`after` are sibling reorder hints.
8
+ */
9
+ export type TreeMovePosition = 'before' | 'inside' | 'after';
10
+
11
+ /**
12
+ * CRUD adapter. Every method is optional — Tree only surfaces built-in
13
+ * menu items / hotkeys whose adapter method is defined. So an
14
+ * inspection-only tree just passes `{}` (or no adapter) and gets no
15
+ * destructive menu actions.
16
+ *
17
+ * Dialogs (`alert` / `confirm` / `prompt`) are taken from `window.dialog`
18
+ * exposed by `<DialogProvider />` in `@djangocfg/ui-core`. The host app
19
+ * is expected to mount that provider once at the layout level — Tree
20
+ * never re-implements its own dialogs.
21
+ */
22
+ export interface TreeAdapter<T = unknown> {
23
+ /** Delete the given nodes. Tree calls `window.dialog.confirm` first. */
24
+ remove?: (nodes: TreeNode<T>[]) => Promise<void>;
25
+ /** Inline rename — node + new name (already non-empty, post-validate). */
26
+ rename?: (node: TreeNode<T>, nextName: string) => Promise<void>;
27
+ /** Create a file under `parent` (null → root). Tree prompts for name. */
28
+ createFile?: (parent: TreeNode<T> | null, name: string) => Promise<void>;
29
+ /** Create a folder under `parent` (null → root). Tree prompts for name. */
30
+ createFolder?: (parent: TreeNode<T> | null, name: string) => Promise<void>;
31
+ /** Duplicate the given nodes in-place. */
32
+ duplicate?: (nodes: TreeNode<T>[]) => Promise<void>;
33
+ /** Move nodes (drag-and-drop, cut+paste). */
34
+ move?: (
35
+ nodes: TreeNode<T>[],
36
+ target: TreeNode<T> | null,
37
+ position: TreeMovePosition,
38
+ ) => Promise<void>;
39
+ /** Copy nodes (copy+paste, drop with modifier). */
40
+ copy?: (
41
+ nodes: TreeNode<T>[],
42
+ target: TreeNode<T> | null,
43
+ position: TreeMovePosition,
44
+ ) => Promise<void>;
45
+
46
+ /**
47
+ * Optional name validator. Return a non-empty string to surface as an
48
+ * error via `window.dialog.alert`. Return `null` to accept.
49
+ */
50
+ validateName?: (
51
+ name: string,
52
+ ctx: { node?: TreeNode<T>; parent?: TreeNode<T> | null },
53
+ ) => string | null;
54
+ }
55
+
56
+ /**
57
+ * Built-in action ids. Used by `defaultMenuItems` and the internal
58
+ * built-in action registry. Each id maps 1:1 to an adapter method
59
+ * (plus a few selection helpers).
60
+ */
61
+ export type TreeBuiltinAction =
62
+ | 'open'
63
+ | 'rename'
64
+ | 'duplicate'
65
+ | 'cut'
66
+ | 'copy'
67
+ | 'paste'
68
+ | 'delete'
69
+ | 'new-file'
70
+ | 'new-folder';
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+
3
+ // Public surface — re-exports each types module. External code imports
4
+ // from `'../types'` (or `'@djangocfg/ui-tools/tree'`) and never touches
5
+ // these files directly.
6
+
7
+ export type { TreeItemId, TreeNode, FlatRow } from './node';
8
+ export type { TreeSelectionMode } from './selection';
9
+ export type { TreeActivationMode, TreeActivateOptions } from './activation';
10
+ export type { TreeLoadChildren } from './loader';
11
+ export type { TreeLabels } from './labels';
12
+ export { DEFAULT_TREE_LABELS } from './labels';
13
+ export type {
14
+ TreeRowRenderProps,
15
+ TreeRowSlot,
16
+ TreeContextMenuSlot,
17
+ TreeContextMenuAction,
18
+ TreeContextMenuItem,
19
+ TreeContextMenuActionsContext,
20
+ TreeContextMenuActionsResolver,
21
+ } from './slots';
22
+ export type {
23
+ TreeMovePosition,
24
+ TreeAdapter,
25
+ TreeBuiltinAction,
26
+ } from './adapter';
27
+ export type { TreeRootProps } from './root-props';
@@ -0,0 +1,97 @@
1
+ 'use client';
2
+
3
+ export interface TreeLabels {
4
+ loading: string;
5
+ empty: string;
6
+ error: string;
7
+ searchPlaceholder: string;
8
+ searchMatches: (count: number) => string;
9
+ ariaLabel: string;
10
+
11
+ // ---- CRUD flow copy (used by built-in adapter actions + dialogs) ----
12
+
13
+ /** Default context-menu item labels. */
14
+ actionOpen: string;
15
+ actionRename: string;
16
+ actionDuplicate: string;
17
+ actionCut: string;
18
+ actionCopy: string;
19
+ actionPaste: string;
20
+ actionDelete: string;
21
+ actionNewFile: string;
22
+ actionNewFolder: string;
23
+
24
+ /** Delete confirmation dialog. */
25
+ confirmDeleteTitle: (count: number) => string;
26
+ confirmDeleteMessage: (names: string[]) => string;
27
+ confirmDeleteOk: string;
28
+ confirmDeleteCancel: string;
29
+
30
+ /** New file prompt. */
31
+ newFileTitle: string;
32
+ newFileMessage: string;
33
+ newFilePlaceholder: string;
34
+ newFileDefault: string;
35
+
36
+ /** New folder prompt. */
37
+ newFolderTitle: string;
38
+ newFolderMessage: string;
39
+ newFolderPlaceholder: string;
40
+ newFolderDefault: string;
41
+
42
+ /** Rename prompt (used when inline rename is unavailable / disabled). */
43
+ renameTitle: string;
44
+ renameMessage: string;
45
+
46
+ /** Name validation. */
47
+ invalidNameEmpty: string;
48
+
49
+ /** Suffix used by the default `duplicate` flow when the consumer's adapter
50
+ * needs a hint name. Receives the source name. */
51
+ duplicateSuffix: (name: string) => string;
52
+ }
53
+
54
+ export const DEFAULT_TREE_LABELS: TreeLabels = {
55
+ loading: 'Loading…',
56
+ empty: 'Nothing to show',
57
+ error: 'Failed to load',
58
+ searchPlaceholder: 'Search…',
59
+ searchMatches: (n) => `${n} match${n === 1 ? '' : 'es'}`,
60
+ ariaLabel: 'Tree',
61
+
62
+ actionOpen: 'Open',
63
+ actionRename: 'Rename',
64
+ actionDuplicate: 'Duplicate',
65
+ actionCut: 'Cut',
66
+ actionCopy: 'Copy',
67
+ actionPaste: 'Paste',
68
+ actionDelete: 'Delete',
69
+ actionNewFile: 'New file',
70
+ actionNewFolder: 'New folder',
71
+
72
+ confirmDeleteTitle: (n) =>
73
+ n === 1 ? 'Delete item?' : `Delete ${n} items?`,
74
+ confirmDeleteMessage: (names) =>
75
+ names.length === 1
76
+ ? `"${names[0]}" will be removed. This action cannot be undone.`
77
+ : `${names.length} items will be removed. This action cannot be undone.`,
78
+ confirmDeleteOk: 'Delete',
79
+ confirmDeleteCancel: 'Cancel',
80
+
81
+ newFileTitle: 'New file',
82
+ newFileMessage: 'File name',
83
+ newFilePlaceholder: 'untitled.txt',
84
+ newFileDefault: 'untitled.txt',
85
+
86
+ newFolderTitle: 'New folder',
87
+ newFolderMessage: 'Folder name',
88
+ newFolderPlaceholder: 'untitled folder',
89
+ newFolderDefault: 'untitled folder',
90
+
91
+ renameTitle: 'Rename',
92
+ renameMessage: 'New name',
93
+
94
+ invalidNameEmpty: 'Name cannot be empty',
95
+
96
+ duplicateSuffix: (name) => `${name} copy`,
97
+ };
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+
3
+ import type { TreeNode } from './node';
4
+
5
+ /**
6
+ * Async loader: called the first time a folder is expanded with no inline
7
+ * `children`. Result is cached; concurrent expansions are de-duplicated.
8
+ */
9
+ export type TreeLoadChildren<T> = (node: TreeNode<T>) => Promise<TreeNode<T>[]>;
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ export type TreeItemId = string;
4
+
5
+ /** A single node in the consumer's tree data. Generic over your payload. */
6
+ export interface TreeNode<T = unknown> {
7
+ id: TreeItemId;
8
+ data: T;
9
+ /** Inline children. Omit (and provide a `loadChildren`) for async loading. */
10
+ children?: TreeNode<T>[];
11
+ /**
12
+ * Set to `true` to mark a node as a folder even when its `children` array
13
+ * is empty (e.g. an unloaded async folder). Default: derived from
14
+ * `Array.isArray(children)`.
15
+ */
16
+ isFolder?: boolean;
17
+ /** Disable interaction. */
18
+ disabled?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Internal flat-row representation used by the renderer + keyboard nav.
23
+ * Produced by `flattenTree`; consumed by `TreeRow`, the keyboard hook,
24
+ * and the type-ahead hook.
25
+ */
26
+ export interface FlatRow<T> {
27
+ node: TreeNode<T>;
28
+ level: number;
29
+ parentId: TreeItemId | null;
30
+ isFolder: boolean;
31
+ isExpanded: boolean;
32
+ isLoading: boolean;
33
+ hasError: boolean;
34
+ /** 1-based position among visible siblings (for `aria-posinset`). */
35
+ posInSet: number;
36
+ /** Count of visible siblings sharing this row's parent (for `aria-setsize`). */
37
+ setSize: number;
38
+ }