@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.
- package/dist/audio-player/index.cjs +2099 -0
- package/dist/audio-player/index.cjs.map +1 -0
- package/dist/audio-player/index.css +65 -0
- package/dist/audio-player/index.css.map +1 -0
- package/dist/audio-player/index.d.cts +174 -0
- package/dist/audio-player/index.d.ts +174 -0
- package/dist/audio-player/index.mjs +2076 -0
- package/dist/audio-player/index.mjs.map +1 -0
- package/dist/composer-registry/index.cjs +45 -0
- package/dist/composer-registry/index.cjs.map +1 -0
- package/dist/composer-registry/index.d.cts +73 -0
- package/dist/composer-registry/index.d.ts +73 -0
- package/dist/composer-registry/index.mjs +39 -0
- package/dist/composer-registry/index.mjs.map +1 -0
- package/dist/file-icon/index.d.cts +1 -1
- package/dist/file-icon/index.d.ts +1 -1
- package/dist/slots-ClRpIzoh.d.cts +88 -0
- package/dist/slots-ClRpIzoh.d.ts +88 -0
- package/dist/tree/index.cjs +2019 -279
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.d.cts +731 -72
- package/dist/tree/index.d.ts +731 -72
- package/dist/tree/index.mjs +2009 -282
- package/dist/tree/index.mjs.map +1 -1
- package/package.json +18 -9
- package/src/tools/chat/README.md +111 -1
- package/src/tools/chat/composer/Composer.tsx +146 -25
- package/src/tools/chat/composer/ComposerRichTextarea.tsx +25 -0
- package/src/tools/chat/composer/index.ts +22 -0
- package/src/tools/chat/composer/slash/README.md +187 -0
- package/src/tools/chat/composer/slash/SlashHighlightTextarea.tsx +144 -0
- package/src/tools/chat/composer/slash/SlashMenu.tsx +142 -0
- package/src/tools/chat/composer/slash/SlashToken.tsx +57 -0
- package/src/tools/chat/composer/slash/index.ts +44 -0
- package/src/tools/chat/composer/slash/labels.ts +19 -0
- package/src/tools/chat/composer/slash/state.ts +168 -0
- package/src/tools/chat/composer/slash/types.ts +64 -0
- package/src/tools/chat/composer/slash/useSlashCommands.ts +204 -0
- package/src/tools/chat/composer/types.ts +8 -0
- package/src/tools/chat/context/ChatProvider.tsx +13 -78
- package/src/tools/chat/hooks/useAutoFocusOnStreamEnd.ts +12 -15
- package/src/tools/chat/hooks/useFocusOnEmptyClick.ts +4 -5
- package/src/tools/chat/launcher/header/ChatHeader.tsx +14 -19
- package/src/tools/chat/launcher/header/ChatHeaderActionButton.tsx +8 -12
- package/src/tools/chat/shell/SuggestedPrompts.tsx +194 -0
- package/src/tools/chat/shell/index.ts +6 -0
- package/src/tools/data/Listbox/lazy.tsx +1 -1
- package/src/tools/data/Masonry/lazy.tsx +1 -1
- package/src/tools/data/Timeline/lazy.tsx +1 -1
- package/src/tools/data/Tree/FinderTree.tsx +42 -0
- package/src/tools/data/Tree/README.md +337 -208
- package/src/tools/data/Tree/TreeDndProvider.tsx +137 -0
- package/src/tools/data/Tree/TreeRoot.tsx +111 -72
- package/src/tools/data/Tree/__tests__/dnd.test.ts +160 -0
- package/src/tools/data/Tree/__tests__/keyboard.test.ts +137 -0
- package/src/tools/data/Tree/__tests__/renameUtils.test.ts +52 -0
- package/src/tools/data/Tree/__tests__/selection.test.ts +227 -0
- package/src/tools/data/Tree/components/TreeDropIndicator.tsx +65 -0
- package/src/tools/data/Tree/components/TreeEmptyArea.tsx +160 -0
- package/src/tools/data/Tree/components/TreeRenameInput.tsx +114 -0
- package/src/tools/data/Tree/components/TreeRow.tsx +103 -8
- package/src/tools/data/Tree/components/index.ts +6 -0
- package/src/tools/data/Tree/context/TreeContext.tsx +223 -363
- package/src/tools/data/Tree/context/TreeContextValue.ts +139 -0
- package/src/tools/data/Tree/context/async-children/collect-ids.ts +27 -0
- package/src/tools/data/Tree/context/async-children/index.ts +8 -0
- package/src/tools/data/Tree/context/async-children/use-async-children.ts +157 -0
- package/src/tools/data/Tree/context/clipboard/index.ts +4 -0
- package/src/tools/data/Tree/context/clipboard/use-clipboard.ts +115 -0
- package/src/tools/data/Tree/context/dnd/index.ts +8 -0
- package/src/tools/data/Tree/context/dnd/use-dnd.ts +194 -0
- package/src/tools/data/Tree/context/expansion/index.ts +4 -0
- package/src/tools/data/Tree/context/expansion/use-expansion.ts +55 -0
- package/src/tools/data/Tree/context/hooks.ts +68 -1
- package/src/tools/data/Tree/context/index.ts +3 -0
- package/src/tools/data/Tree/context/menu/builtin-actions.ts +357 -0
- package/src/tools/data/Tree/context/menu/index.ts +11 -0
- package/src/tools/data/Tree/context/menu/render.tsx +75 -0
- package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +141 -0
- package/src/tools/data/Tree/context/persist/index.ts +4 -0
- package/src/tools/data/Tree/context/persist/use-persist-sync.ts +74 -0
- package/src/tools/data/Tree/context/rename/index.ts +4 -0
- package/src/tools/data/Tree/context/rename/use-rename.ts +113 -0
- package/src/tools/data/Tree/context/selection/index.ts +4 -0
- package/src/tools/data/Tree/context/selection/use-selection.ts +146 -0
- package/src/tools/data/Tree/context/state/index.ts +6 -0
- package/src/tools/data/Tree/context/state/initial.ts +41 -0
- package/src/tools/data/Tree/context/state/reducer.ts +76 -0
- package/src/tools/data/Tree/context/state/types.ts +46 -0
- package/src/tools/data/Tree/data/clipboard.ts +33 -0
- package/src/tools/data/Tree/data/dnd.ts +123 -0
- package/src/tools/data/Tree/data/finderShortcuts.ts +67 -0
- package/src/tools/data/Tree/data/index.ts +19 -0
- package/src/tools/data/Tree/data/renameUtils.ts +51 -0
- package/src/tools/data/Tree/data/selection.ts +157 -0
- package/src/tools/data/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
- package/src/tools/data/Tree/hooks/finder-hotkeys/index.ts +8 -0
- package/src/tools/data/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
- package/src/tools/data/Tree/hooks/index.ts +23 -4
- package/src/tools/data/Tree/hooks/keyboard/activation.ts +27 -0
- package/src/tools/data/Tree/hooks/keyboard/arrow-nav.ts +26 -0
- package/src/tools/data/Tree/hooks/keyboard/expand-collapse.ts +54 -0
- package/src/tools/data/Tree/hooks/keyboard/index.ts +10 -0
- package/src/tools/data/Tree/hooks/keyboard/types.ts +39 -0
- package/src/tools/data/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
- package/src/tools/data/Tree/hooks/type-ahead/index.ts +5 -0
- package/src/tools/data/Tree/hooks/type-ahead/match-prefix.ts +42 -0
- package/src/tools/data/Tree/hooks/{useTreeTypeAhead.ts → type-ahead/use-tree-type-ahead.ts} +8 -19
- package/src/tools/data/Tree/index.tsx +26 -2
- package/src/tools/data/Tree/types/activation.ts +30 -0
- package/src/tools/data/Tree/types/adapter.ts +70 -0
- package/src/tools/data/Tree/types/index.ts +27 -0
- package/src/tools/data/Tree/types/labels.ts +97 -0
- package/src/tools/data/Tree/types/loader.ts +9 -0
- package/src/tools/data/Tree/types/node.ts +38 -0
- package/src/tools/data/Tree/types/root-props.ts +158 -0
- package/src/tools/data/Tree/types/selection.ts +3 -0
- package/src/tools/data/Tree/types/slots.ts +64 -0
- package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +6 -9
- package/src/tools/dev/OpenapiViewer/components/DocsLayout/index.tsx +2 -4
- package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +85 -0
- package/src/tools/forms/MarkdownEditor/index.ts +1 -0
- package/src/tools/forms/MarkdownEditor/lazy.tsx +6 -0
- package/src/tools/forms/MarkdownEditor/slash/SlashCommandNode.ts +162 -0
- package/src/tools/forms/MarkdownEditor/slash/index.ts +4 -0
- package/src/tools/forms/MarkdownEditor/slash/syncSlashNode.ts +97 -0
- package/src/tools/forms/MarkdownEditor/slash/types.ts +13 -0
- package/src/tools/forms/MarkdownEditor/styles.css +18 -0
- package/src/tools/input/SpeechRecognition/widgets/VoiceComposerSlot.tsx +11 -12
- package/src/tools/integration/ComposerRegistry/index.ts +105 -0
- package/src/tools/media/AudioPlayer/Player.tsx +2 -0
- package/src/tools/media/AudioPlayer/PlayerShell.tsx +37 -22
- package/src/tools/media/AudioPlayer/lazy.tsx +30 -42
- package/src/tools/media/AudioPlayer/parts/Controls/IconButton.tsx +10 -11
- package/src/tools/media/AudioPlayer/parts/Controls/VolumeControl.tsx +52 -115
- package/src/tools/media/AudioPlayer/types.ts +15 -0
- package/dist/types-j2vhn4Kv.d.cts +0 -241
- package/dist/types-j2vhn4Kv.d.ts +0 -241
- package/src/tools/data/Tree/hooks/useTreeKeyboard.ts +0 -171
- 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,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 '
|
|
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
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
getNameRef.current
|
|
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 {
|
|
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';
|