@djangocfg/ui-tools 2.1.415 → 2.1.417

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 (140) hide show
  1. package/dist/audio-player/index.cjs +2099 -0
  2. package/dist/audio-player/index.cjs.map +1 -0
  3. package/dist/audio-player/index.css +65 -0
  4. package/dist/audio-player/index.css.map +1 -0
  5. package/dist/audio-player/index.d.cts +174 -0
  6. package/dist/audio-player/index.d.ts +174 -0
  7. package/dist/audio-player/index.mjs +2076 -0
  8. package/dist/audio-player/index.mjs.map +1 -0
  9. package/dist/composer-registry/index.cjs +45 -0
  10. package/dist/composer-registry/index.cjs.map +1 -0
  11. package/dist/composer-registry/index.d.cts +73 -0
  12. package/dist/composer-registry/index.d.ts +73 -0
  13. package/dist/composer-registry/index.mjs +39 -0
  14. package/dist/composer-registry/index.mjs.map +1 -0
  15. package/dist/file-icon/index.d.cts +1 -1
  16. package/dist/file-icon/index.d.ts +1 -1
  17. package/dist/slots-ClRpIzoh.d.cts +88 -0
  18. package/dist/slots-ClRpIzoh.d.ts +88 -0
  19. package/dist/tree/index.cjs +2019 -279
  20. package/dist/tree/index.cjs.map +1 -1
  21. package/dist/tree/index.d.cts +731 -72
  22. package/dist/tree/index.d.ts +731 -72
  23. package/dist/tree/index.mjs +2009 -282
  24. package/dist/tree/index.mjs.map +1 -1
  25. package/package.json +18 -9
  26. package/src/tools/chat/README.md +111 -1
  27. package/src/tools/chat/composer/Composer.tsx +146 -25
  28. package/src/tools/chat/composer/ComposerRichTextarea.tsx +25 -0
  29. package/src/tools/chat/composer/index.ts +22 -0
  30. package/src/tools/chat/composer/slash/README.md +187 -0
  31. package/src/tools/chat/composer/slash/SlashHighlightTextarea.tsx +144 -0
  32. package/src/tools/chat/composer/slash/SlashMenu.tsx +142 -0
  33. package/src/tools/chat/composer/slash/SlashToken.tsx +57 -0
  34. package/src/tools/chat/composer/slash/index.ts +44 -0
  35. package/src/tools/chat/composer/slash/labels.ts +19 -0
  36. package/src/tools/chat/composer/slash/state.ts +168 -0
  37. package/src/tools/chat/composer/slash/types.ts +64 -0
  38. package/src/tools/chat/composer/slash/useSlashCommands.ts +204 -0
  39. package/src/tools/chat/composer/types.ts +8 -0
  40. package/src/tools/chat/context/ChatProvider.tsx +13 -78
  41. package/src/tools/chat/hooks/useAutoFocusOnStreamEnd.ts +12 -15
  42. package/src/tools/chat/hooks/useFocusOnEmptyClick.ts +4 -5
  43. package/src/tools/chat/launcher/header/ChatHeader.tsx +14 -19
  44. package/src/tools/chat/launcher/header/ChatHeaderActionButton.tsx +8 -12
  45. package/src/tools/chat/shell/SuggestedPrompts.tsx +194 -0
  46. package/src/tools/chat/shell/index.ts +6 -0
  47. package/src/tools/data/Listbox/lazy.tsx +1 -1
  48. package/src/tools/data/Masonry/lazy.tsx +1 -1
  49. package/src/tools/data/Timeline/lazy.tsx +1 -1
  50. package/src/tools/data/Tree/FinderTree.tsx +42 -0
  51. package/src/tools/data/Tree/README.md +337 -208
  52. package/src/tools/data/Tree/TreeDndProvider.tsx +137 -0
  53. package/src/tools/data/Tree/TreeRoot.tsx +111 -72
  54. package/src/tools/data/Tree/__tests__/dnd.test.ts +160 -0
  55. package/src/tools/data/Tree/__tests__/keyboard.test.ts +137 -0
  56. package/src/tools/data/Tree/__tests__/renameUtils.test.ts +52 -0
  57. package/src/tools/data/Tree/__tests__/selection.test.ts +227 -0
  58. package/src/tools/data/Tree/components/TreeDropIndicator.tsx +65 -0
  59. package/src/tools/data/Tree/components/TreeEmptyArea.tsx +160 -0
  60. package/src/tools/data/Tree/components/TreeRenameInput.tsx +114 -0
  61. package/src/tools/data/Tree/components/TreeRow.tsx +103 -8
  62. package/src/tools/data/Tree/components/index.ts +6 -0
  63. package/src/tools/data/Tree/context/TreeContext.tsx +223 -363
  64. package/src/tools/data/Tree/context/TreeContextValue.ts +139 -0
  65. package/src/tools/data/Tree/context/async-children/collect-ids.ts +27 -0
  66. package/src/tools/data/Tree/context/async-children/index.ts +8 -0
  67. package/src/tools/data/Tree/context/async-children/use-async-children.ts +157 -0
  68. package/src/tools/data/Tree/context/clipboard/index.ts +4 -0
  69. package/src/tools/data/Tree/context/clipboard/use-clipboard.ts +115 -0
  70. package/src/tools/data/Tree/context/dnd/index.ts +8 -0
  71. package/src/tools/data/Tree/context/dnd/use-dnd.ts +194 -0
  72. package/src/tools/data/Tree/context/expansion/index.ts +4 -0
  73. package/src/tools/data/Tree/context/expansion/use-expansion.ts +55 -0
  74. package/src/tools/data/Tree/context/hooks.ts +68 -1
  75. package/src/tools/data/Tree/context/index.ts +3 -0
  76. package/src/tools/data/Tree/context/menu/builtin-actions.ts +357 -0
  77. package/src/tools/data/Tree/context/menu/index.ts +11 -0
  78. package/src/tools/data/Tree/context/menu/render.tsx +75 -0
  79. package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +141 -0
  80. package/src/tools/data/Tree/context/persist/index.ts +4 -0
  81. package/src/tools/data/Tree/context/persist/use-persist-sync.ts +74 -0
  82. package/src/tools/data/Tree/context/rename/index.ts +4 -0
  83. package/src/tools/data/Tree/context/rename/use-rename.ts +113 -0
  84. package/src/tools/data/Tree/context/selection/index.ts +4 -0
  85. package/src/tools/data/Tree/context/selection/use-selection.ts +146 -0
  86. package/src/tools/data/Tree/context/state/index.ts +6 -0
  87. package/src/tools/data/Tree/context/state/initial.ts +41 -0
  88. package/src/tools/data/Tree/context/state/reducer.ts +76 -0
  89. package/src/tools/data/Tree/context/state/types.ts +46 -0
  90. package/src/tools/data/Tree/data/clipboard.ts +33 -0
  91. package/src/tools/data/Tree/data/dnd.ts +123 -0
  92. package/src/tools/data/Tree/data/finderShortcuts.ts +67 -0
  93. package/src/tools/data/Tree/data/index.ts +19 -0
  94. package/src/tools/data/Tree/data/renameUtils.ts +51 -0
  95. package/src/tools/data/Tree/data/selection.ts +157 -0
  96. package/src/tools/data/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
  97. package/src/tools/data/Tree/hooks/finder-hotkeys/index.ts +8 -0
  98. package/src/tools/data/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
  99. package/src/tools/data/Tree/hooks/index.ts +23 -4
  100. package/src/tools/data/Tree/hooks/keyboard/activation.ts +27 -0
  101. package/src/tools/data/Tree/hooks/keyboard/arrow-nav.ts +26 -0
  102. package/src/tools/data/Tree/hooks/keyboard/expand-collapse.ts +54 -0
  103. package/src/tools/data/Tree/hooks/keyboard/index.ts +10 -0
  104. package/src/tools/data/Tree/hooks/keyboard/types.ts +39 -0
  105. package/src/tools/data/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
  106. package/src/tools/data/Tree/hooks/type-ahead/index.ts +5 -0
  107. package/src/tools/data/Tree/hooks/type-ahead/match-prefix.ts +42 -0
  108. package/src/tools/data/Tree/hooks/{useTreeTypeAhead.ts → type-ahead/use-tree-type-ahead.ts} +8 -19
  109. package/src/tools/data/Tree/index.tsx +26 -2
  110. package/src/tools/data/Tree/types/activation.ts +30 -0
  111. package/src/tools/data/Tree/types/adapter.ts +70 -0
  112. package/src/tools/data/Tree/types/index.ts +27 -0
  113. package/src/tools/data/Tree/types/labels.ts +97 -0
  114. package/src/tools/data/Tree/types/loader.ts +9 -0
  115. package/src/tools/data/Tree/types/node.ts +38 -0
  116. package/src/tools/data/Tree/types/root-props.ts +158 -0
  117. package/src/tools/data/Tree/types/selection.ts +3 -0
  118. package/src/tools/data/Tree/types/slots.ts +64 -0
  119. package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +6 -9
  120. package/src/tools/dev/OpenapiViewer/components/DocsLayout/index.tsx +2 -4
  121. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +85 -0
  122. package/src/tools/forms/MarkdownEditor/index.ts +1 -0
  123. package/src/tools/forms/MarkdownEditor/lazy.tsx +6 -0
  124. package/src/tools/forms/MarkdownEditor/slash/SlashCommandNode.ts +162 -0
  125. package/src/tools/forms/MarkdownEditor/slash/index.ts +4 -0
  126. package/src/tools/forms/MarkdownEditor/slash/syncSlashNode.ts +97 -0
  127. package/src/tools/forms/MarkdownEditor/slash/types.ts +13 -0
  128. package/src/tools/forms/MarkdownEditor/styles.css +18 -0
  129. package/src/tools/input/SpeechRecognition/widgets/VoiceComposerSlot.tsx +11 -12
  130. package/src/tools/integration/ComposerRegistry/index.ts +105 -0
  131. package/src/tools/media/AudioPlayer/Player.tsx +2 -0
  132. package/src/tools/media/AudioPlayer/PlayerShell.tsx +37 -22
  133. package/src/tools/media/AudioPlayer/lazy.tsx +30 -42
  134. package/src/tools/media/AudioPlayer/parts/Controls/IconButton.tsx +10 -11
  135. package/src/tools/media/AudioPlayer/parts/Controls/VolumeControl.tsx +52 -115
  136. package/src/tools/media/AudioPlayer/types.ts +15 -0
  137. package/dist/types-j2vhn4Kv.d.cts +0 -241
  138. package/dist/types-j2vhn4Kv.d.ts +0 -241
  139. package/src/tools/data/Tree/hooks/useTreeKeyboard.ts +0 -171
  140. package/src/tools/data/Tree/types.ts +0 -217
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+
3
+ import type { FlatRow } from '../../types';
4
+
5
+ /**
6
+ * Enter / Space on the current row:
7
+ * - folder: toggle (collapse if expanded, else expand)
8
+ * - leaf: activate
9
+ *
10
+ * Always selects (single-select semantics for keyboard input).
11
+ */
12
+ export type ActivateOutcome<T> =
13
+ | { kind: 'toggle-folder'; id: string; willExpand: boolean }
14
+ | { kind: 'activate-leaf'; id: string }
15
+ | { kind: 'noop' };
16
+
17
+ export function resolveActivate<T>(current: FlatRow<T> | null): ActivateOutcome<T> {
18
+ if (!current) return { kind: 'noop' };
19
+ if (current.isFolder) {
20
+ return {
21
+ kind: 'toggle-folder',
22
+ id: current.node.id,
23
+ willExpand: !current.isExpanded,
24
+ };
25
+ }
26
+ return { kind: 'activate-leaf', id: current.node.id };
27
+ }
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+
3
+ import type { FlatRow, TreeItemId } from '../../types';
4
+
5
+ /** Pick the id one row below the current focused row (clamped at the end). */
6
+ export function nextRowId<T>(rows: readonly FlatRow<T>[], idx: number): TreeItemId | null {
7
+ if (rows.length === 0) return null;
8
+ const next = rows[Math.min(idx + 1, rows.length - 1)] ?? rows[0];
9
+ return next.node.id;
10
+ }
11
+
12
+ /** Pick the id one row above the current focused row (clamped at the start). */
13
+ export function prevRowId<T>(rows: readonly FlatRow<T>[], idx: number): TreeItemId | null {
14
+ if (rows.length === 0) return null;
15
+ const prev = rows[Math.max(idx - 1, 0)] ?? rows[0];
16
+ return prev.node.id;
17
+ }
18
+
19
+ /** First / last visible row id, or null if the list is empty. */
20
+ export function edgeRowId<T>(
21
+ rows: readonly FlatRow<T>[],
22
+ edge: 'first' | 'last',
23
+ ): TreeItemId | null {
24
+ if (rows.length === 0) return null;
25
+ return edge === 'first' ? rows[0].node.id : rows[rows.length - 1].node.id;
26
+ }
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+
3
+ import type { FlatRow } from '../../types';
4
+
5
+ /**
6
+ * What should `→` do on the current row?
7
+ *
8
+ * - collapsed folder → expand it
9
+ * - expanded folder → jump to first visible child
10
+ * - leaf → no-op
11
+ */
12
+ export type RightArrowOutcome<T> =
13
+ | { kind: 'expand'; id: string }
14
+ | { kind: 'focus'; id: string }
15
+ | { kind: 'noop' };
16
+
17
+ export function resolveRightArrow<T>(
18
+ current: FlatRow<T> | null,
19
+ rows: readonly FlatRow<T>[],
20
+ idx: number,
21
+ ): RightArrowOutcome<T> {
22
+ if (!current) return { kind: 'noop' };
23
+ if (current.isFolder && !current.isExpanded) {
24
+ return { kind: 'expand', id: current.node.id };
25
+ }
26
+ if (current.isFolder && current.isExpanded) {
27
+ const next = rows[idx + 1];
28
+ return next ? { kind: 'focus', id: next.node.id } : { kind: 'noop' };
29
+ }
30
+ return { kind: 'noop' };
31
+ }
32
+
33
+ /**
34
+ * What should `←` do on the current row?
35
+ *
36
+ * - expanded folder → collapse it
37
+ * - leaf / collapsed w/ parent → focus parent
38
+ * - root leaf → no-op
39
+ */
40
+ export type LeftArrowOutcome<T> =
41
+ | { kind: 'collapse'; id: string }
42
+ | { kind: 'focus'; id: string }
43
+ | { kind: 'noop' };
44
+
45
+ export function resolveLeftArrow<T>(current: FlatRow<T> | null): LeftArrowOutcome<T> {
46
+ if (!current) return { kind: 'noop' };
47
+ if (current.isFolder && current.isExpanded) {
48
+ return { kind: 'collapse', id: current.node.id };
49
+ }
50
+ if (current.parentId) {
51
+ return { kind: 'focus', id: current.parentId };
52
+ }
53
+ return { kind: 'noop' };
54
+ }
@@ -0,0 +1,10 @@
1
+ 'use client';
2
+
3
+ export { useTreeKeyboard } from './use-tree-keyboard';
4
+ export type {
5
+ UseTreeKeyboardOptions,
6
+ UseTreeKeyboardReturn,
7
+ } from './types';
8
+ export { nextRowId, prevRowId, edgeRowId } from './arrow-nav';
9
+ export { resolveLeftArrow, resolveRightArrow } from './expand-collapse';
10
+ export { resolveActivate } from './activation';
@@ -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,
@@ -86,6 +105,7 @@ export type {
86
105
 
87
106
  export { DEFAULT_TREE_LABELS } from './types';
88
107
  export type {
108
+ TreeActionsHandle,
89
109
  TreeRootProps,
90
110
  TreeNode,
91
111
  TreeItemId,
@@ -98,7 +118,11 @@ export type {
98
118
  TreeContextMenuSlot,
99
119
  TreeContextMenuAction,
100
120
  TreeContextMenuItem,
121
+ TreeContextMenuActionsContext,
101
122
  TreeContextMenuActionsResolver,
102
123
  TreeLoadChildren,
124
+ TreeAdapter,
125
+ TreeBuiltinAction,
126
+ TreeMovePosition,
103
127
  FlatRow,
104
128
  } 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, TreeActionsHandle } from './root-props';