@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,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SlashCommandNode` — TipTap inline atom that renders a chosen slash
|
|
3
|
+
* verb as a styled chip inside the editor, analogous to how
|
|
4
|
+
* `@tiptap/extension-mention` renders an `@user` pill.
|
|
5
|
+
*
|
|
6
|
+
* Why a node (and not a mark / decoration):
|
|
7
|
+
*
|
|
8
|
+
* - The chip must read as one indivisible unit — pressing Backspace
|
|
9
|
+
* removes the whole `/verb`, not the trailing letter. That's
|
|
10
|
+
* exactly what `atom: true` + `inline: true` give us.
|
|
11
|
+
* - The chip's text content (`/verb`) participates in `editor.getText()`
|
|
12
|
+
* and the markdown serializer's text output so the host's
|
|
13
|
+
* `composer.value` round-trips to the same plain string the plain
|
|
14
|
+
* `<SlashHighlightTextarea>` produces. The slash hook (which lives on
|
|
15
|
+
* the string buffer) stays oblivious to the node form.
|
|
16
|
+
*
|
|
17
|
+
* Render parity with the plain mirror:
|
|
18
|
+
*
|
|
19
|
+
* The chip class `markdown-slash-command` mirrors the plain textarea's
|
|
20
|
+
* `bg-primary/15 text-primary` look (see `SlashHighlightTextarea.tsx`).
|
|
21
|
+
* Styles live in `../styles.css`. The TipTap chip can carry a touch of
|
|
22
|
+
* horizontal padding without breaking the caret — the node is atomic
|
|
23
|
+
* so the caret never lands inside it — which the plain mirror cannot.
|
|
24
|
+
*/
|
|
25
|
+
import { Node, mergeAttributes } from '@tiptap/core';
|
|
26
|
+
|
|
27
|
+
declare module '@tiptap/core' {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
29
|
+
interface Commands<ReturnType> {
|
|
30
|
+
slashCommand: {
|
|
31
|
+
/**
|
|
32
|
+
* Insert a slash-command atom at the given absolute document
|
|
33
|
+
* position. The host normally calls this once per `pick()`
|
|
34
|
+
* after the slash hook has produced the new string buffer.
|
|
35
|
+
*/
|
|
36
|
+
insertSlashCommandAt: (
|
|
37
|
+
pos: number,
|
|
38
|
+
from: number,
|
|
39
|
+
to: number,
|
|
40
|
+
attrs: { id: string; token: string },
|
|
41
|
+
) => ReturnType;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SlashCommandNodeOptions {
|
|
47
|
+
/** Extra HTML attrs spread onto the rendered `<span>`. */
|
|
48
|
+
HTMLAttributes: Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Inline atom node that paints a `/verb` chip inside the TipTap editor.
|
|
53
|
+
*
|
|
54
|
+
* - `inline: true` — lives next to text in a paragraph.
|
|
55
|
+
* - `atom: true` — caret cannot land inside it; Backspace removes it
|
|
56
|
+
* wholesale, matching the plain mirror's "the verb is one token"
|
|
57
|
+
* feel.
|
|
58
|
+
* - `selectable: false` — clicking the chip does not stick a node
|
|
59
|
+
* selection on it (mention does the same).
|
|
60
|
+
*
|
|
61
|
+
* `renderText` returns the bare `/verb` so `editor.getText()` (and
|
|
62
|
+
* therefore the host `composer.value`) reads the same string the plain
|
|
63
|
+
* mirror produces. This is what keeps the slash hook — which only
|
|
64
|
+
* understands strings — driving the menu correctly when the editor is
|
|
65
|
+
* TipTap-backed.
|
|
66
|
+
*/
|
|
67
|
+
export const SlashCommandNode = Node.create<SlashCommandNodeOptions>({
|
|
68
|
+
name: 'slashCommand',
|
|
69
|
+
group: 'inline',
|
|
70
|
+
inline: true,
|
|
71
|
+
atom: true,
|
|
72
|
+
selectable: false,
|
|
73
|
+
// Higher than StarterKit text so paste rules etc. don't try to
|
|
74
|
+
// re-parse our chip text as plain.
|
|
75
|
+
priority: 101,
|
|
76
|
+
|
|
77
|
+
addOptions() {
|
|
78
|
+
return {
|
|
79
|
+
HTMLAttributes: {},
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
addAttributes() {
|
|
84
|
+
return {
|
|
85
|
+
id: {
|
|
86
|
+
default: null,
|
|
87
|
+
parseHTML: (element) => element.getAttribute('data-id'),
|
|
88
|
+
renderHTML: (attrs) =>
|
|
89
|
+
attrs.id ? { 'data-id': attrs.id as string } : {},
|
|
90
|
+
},
|
|
91
|
+
token: {
|
|
92
|
+
default: null,
|
|
93
|
+
parseHTML: (element) => element.getAttribute('data-token'),
|
|
94
|
+
renderHTML: (attrs) =>
|
|
95
|
+
attrs.token ? { 'data-token': attrs.token as string } : {},
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
parseHTML() {
|
|
101
|
+
return [{ tag: `span[data-type="${this.name}"]` }];
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
105
|
+
const token =
|
|
106
|
+
(node.attrs.token as string | null) ??
|
|
107
|
+
(node.attrs.id ? `/${node.attrs.id as string}` : '/');
|
|
108
|
+
return [
|
|
109
|
+
'span',
|
|
110
|
+
mergeAttributes(
|
|
111
|
+
{ 'data-type': this.name, class: 'markdown-slash-command' },
|
|
112
|
+
this.options.HTMLAttributes,
|
|
113
|
+
HTMLAttributes,
|
|
114
|
+
),
|
|
115
|
+
token,
|
|
116
|
+
];
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// `editor.getText()` and the markdown text-serializer fallback read
|
|
120
|
+
// this — the chip flattens to its raw `/verb` glyph so consumers
|
|
121
|
+
// (slash hook, transport, backends) see plain text.
|
|
122
|
+
renderText({ node }) {
|
|
123
|
+
const token =
|
|
124
|
+
(node.attrs.token as string | null) ??
|
|
125
|
+
(node.attrs.id ? `/${node.attrs.id as string}` : '/');
|
|
126
|
+
return token;
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// `@tiptap/markdown` reads `renderMarkdown` off the extension at
|
|
130
|
+
// registration time. We emit the bare token, NOT the default
|
|
131
|
+
// shortcode `[slashCommand id="..."]`, so a round-trip through
|
|
132
|
+
// `getMarkdown()` yields the same `/verb argument` string the plain
|
|
133
|
+
// mirror produces. Slash commands never need re-parsed back from
|
|
134
|
+
// markdown — fresh value sync handles that via
|
|
135
|
+
// `syncLeadingSlashNode` after each `setContent`.
|
|
136
|
+
renderMarkdown(node) {
|
|
137
|
+
const attrs = node.attrs as { token?: string | null; id?: string | null };
|
|
138
|
+
const token = attrs.token ?? (attrs.id ? `/${attrs.id}` : '');
|
|
139
|
+
return token;
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
addCommands() {
|
|
143
|
+
return {
|
|
144
|
+
insertSlashCommandAt:
|
|
145
|
+
(pos, from, to, attrs) =>
|
|
146
|
+
({ chain }) => {
|
|
147
|
+
// Replace the [from, to) range (typically the leading `/verb`
|
|
148
|
+
// text the user just had the menu narrow down) with the atom
|
|
149
|
+
// node. `pos` is unused here but kept in the signature for
|
|
150
|
+
// future use (e.g. cursor-driven insertion not anchored at
|
|
151
|
+
// the start of the doc).
|
|
152
|
+
void pos;
|
|
153
|
+
return chain()
|
|
154
|
+
.insertContentAt(
|
|
155
|
+
{ from, to },
|
|
156
|
+
{ type: 'slashCommand', attrs },
|
|
157
|
+
)
|
|
158
|
+
.run();
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `syncSlashNode` — bridge between the slash hook's string buffer and
|
|
3
|
+
* the TipTap document's node form.
|
|
4
|
+
*
|
|
5
|
+
* The slash hook lives entirely on the editor value string — it has no
|
|
6
|
+
* concept of nodes. When the host wires a TipTap-backed
|
|
7
|
+
* `<ComposerRichTextarea>` with `slashCommands`, the document needs to
|
|
8
|
+
* pick up two things on its own:
|
|
9
|
+
*
|
|
10
|
+
* 1. After `setContent(newValue)` runs (e.g. the user picked `/verb`
|
|
11
|
+
* from the menu so `composer.value` became `"/verb "`), the
|
|
12
|
+
* leading `/verb` text must turn into a `SlashCommandNode` atom
|
|
13
|
+
* so the chip shows up.
|
|
14
|
+
* 2. If the user backspaces past the chip or types non-slash text,
|
|
15
|
+
* the buffer is no longer a slash command — there's nothing to
|
|
16
|
+
* do because the atom would have been removed wholesale by the
|
|
17
|
+
* backspace.
|
|
18
|
+
*
|
|
19
|
+
* This module owns the post-`setContent` "absorb the leading slash"
|
|
20
|
+
* pass. It is the only piece of TipTap-aware code the slash node needs
|
|
21
|
+
* beyond the node definition itself.
|
|
22
|
+
*/
|
|
23
|
+
import type { Editor } from '@tiptap/react';
|
|
24
|
+
|
|
25
|
+
import type {
|
|
26
|
+
SlashCommandInfo,
|
|
27
|
+
} from './types';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Find the leading `/verb` in the document — if and only if it appears
|
|
31
|
+
* as the very first inline text of the first paragraph. Returns
|
|
32
|
+
* `{ verb, from, to }` (`from` / `to` are absolute document positions
|
|
33
|
+
* usable with `insertContentAt`) or `null` when no leading slash is
|
|
34
|
+
* present.
|
|
35
|
+
*/
|
|
36
|
+
function findLeadingSlashText(
|
|
37
|
+
editor: Editor,
|
|
38
|
+
commands: readonly SlashCommandInfo[],
|
|
39
|
+
): { verb: SlashCommandInfo; from: number; to: number } | null {
|
|
40
|
+
const doc = editor.state.doc;
|
|
41
|
+
if (doc.childCount === 0) return null;
|
|
42
|
+
// Walk the document's first inline block. The buffer must START with
|
|
43
|
+
// the slash — verbs in the middle of the message are not slash
|
|
44
|
+
// commands (matches `parseSlashState` semantics).
|
|
45
|
+
const firstBlock = doc.firstChild;
|
|
46
|
+
if (!firstBlock || firstBlock.childCount === 0) return null;
|
|
47
|
+
const firstInline = firstBlock.firstChild;
|
|
48
|
+
if (!firstInline) return null;
|
|
49
|
+
// If the first inline is already a slash atom we're done.
|
|
50
|
+
if (firstInline.type.name === 'slashCommand') return null;
|
|
51
|
+
if (!firstInline.isText) return null;
|
|
52
|
+
const text = firstInline.text ?? '';
|
|
53
|
+
// Mirrors SLASH_RE from ../../../chat/composer/slash/state.ts. Kept
|
|
54
|
+
// inline to avoid pulling the chat package into the editor module.
|
|
55
|
+
const m = /^\/([a-zA-Z][\w-]*)(?:[ \n]|$)/.exec(text);
|
|
56
|
+
if (!m) return null;
|
|
57
|
+
const verb = m[1]?.toLowerCase() ?? '';
|
|
58
|
+
const cmd = commands.find((c) => c.id === verb);
|
|
59
|
+
if (!cmd) return null;
|
|
60
|
+
const slashLen = 1 + (m[1]?.length ?? 0); // `/verb` length, no trailing
|
|
61
|
+
// First text child of the first block starts at position 1 in the
|
|
62
|
+
// document (the block boundary takes position 0).
|
|
63
|
+
const from = 1;
|
|
64
|
+
const to = from + slashLen;
|
|
65
|
+
return { verb: cmd, from, to };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* If the document starts with a `/verb` text node whose verb is in
|
|
70
|
+
* `commands`, replace that text with a `SlashCommandNode` atom. No-op
|
|
71
|
+
* otherwise. Idempotent — calling it on a document that already starts
|
|
72
|
+
* with the atom does nothing.
|
|
73
|
+
*
|
|
74
|
+
* Returns `true` when a replacement happened (so callers can suppress
|
|
75
|
+
* a feedback `onUpdate` if needed).
|
|
76
|
+
*/
|
|
77
|
+
export function syncLeadingSlashNode(
|
|
78
|
+
editor: Editor,
|
|
79
|
+
commands: readonly SlashCommandInfo[],
|
|
80
|
+
): boolean {
|
|
81
|
+
if (!editor || editor.isDestroyed) return false;
|
|
82
|
+
if (commands.length === 0) return false;
|
|
83
|
+
const found = findLeadingSlashText(editor, commands);
|
|
84
|
+
if (!found) return false;
|
|
85
|
+
const { verb, from, to } = found;
|
|
86
|
+
editor
|
|
87
|
+
.chain()
|
|
88
|
+
.insertContentAt(
|
|
89
|
+
{ from, to },
|
|
90
|
+
{
|
|
91
|
+
type: 'slashCommand',
|
|
92
|
+
attrs: { id: verb.id, token: verb.token },
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
.run();
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor-side slash command info — a deliberately tiny subset of the
|
|
3
|
+
* chat package's `SlashCommand` so the editor module does not depend
|
|
4
|
+
* on `@djangocfg/ui-tools/chat`. Hosts pass either the chat package's
|
|
5
|
+
* `SlashCommand[]` directly (structural compatibility) or this minimal
|
|
6
|
+
* shape.
|
|
7
|
+
*/
|
|
8
|
+
export interface SlashCommandInfo {
|
|
9
|
+
/** Verb without the leading slash (e.g. `"clear"`). */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Display token with the slash (e.g. `"/clear"`). */
|
|
12
|
+
token: string;
|
|
13
|
+
}
|
|
@@ -116,6 +116,24 @@
|
|
|
116
116
|
box-shadow: none;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/* Slash-command inline chip — TipTap atom analogue of the plain
|
|
120
|
+
`<SlashHighlightTextarea>` mirror. Mirrors `bg-primary/15 text-primary`
|
|
121
|
+
so the two paths look identical. Atom nodes don't admit a caret, so
|
|
122
|
+
light horizontal padding is safe here (unlike the plain mirror,
|
|
123
|
+
which has to be padding-less to keep caret alignment). */
|
|
124
|
+
.markdown-slash-command {
|
|
125
|
+
background: color-mix(in oklab, var(--color-primary, var(--primary)) 15%, transparent);
|
|
126
|
+
color: var(--color-primary, var(--primary));
|
|
127
|
+
padding: 0 2px;
|
|
128
|
+
border-radius: 3px;
|
|
129
|
+
font-weight: 500;
|
|
130
|
+
/* Atom nodes can't be entered; reflect that visually so the user does
|
|
131
|
+
not try to click into the chip to edit it. */
|
|
132
|
+
cursor: default;
|
|
133
|
+
user-select: none;
|
|
134
|
+
display: inline;
|
|
135
|
+
}
|
|
136
|
+
|
|
119
137
|
/* Mention inline chip */
|
|
120
138
|
.markdown-mention {
|
|
121
139
|
background: var(--color-primary, var(--primary));
|
|
@@ -6,8 +6,7 @@ import { AlertCircle, Loader2, Mic } from 'lucide-react';
|
|
|
6
6
|
|
|
7
7
|
import { useCountdownFromSeconds, useNotificationSounds } from '@djangocfg/ui-core/hooks';
|
|
8
8
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
9
|
-
|
|
10
|
-
import { useChatContextOptional } from '../../../chat/context';
|
|
9
|
+
import { useActiveComposer } from '@djangocfg/ui-tools/composer-registry';
|
|
11
10
|
import { useSpeechRecognition } from '../hooks/useSpeechRecognition';
|
|
12
11
|
import { useVoiceSupport } from '../hooks/useVoiceSupport';
|
|
13
12
|
import { getSpeechLogger } from '../core/logger';
|
|
@@ -105,24 +104,24 @@ export function VoiceComposerSlot({
|
|
|
105
104
|
}: VoiceComposerSlotProps): React.ReactElement | null {
|
|
106
105
|
const support = useVoiceSupport(engine);
|
|
107
106
|
|
|
108
|
-
// Read the composer handle from
|
|
109
|
-
//
|
|
110
|
-
// hosts
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
const
|
|
114
|
-
const composerHandleRef = useRef(
|
|
115
|
-
composerHandleRef.current =
|
|
107
|
+
// Read the active composer handle from the cross-tool registry
|
|
108
|
+
// (`@djangocfg/ui-tools/composer-registry`). The built-in
|
|
109
|
+
// `<Composer>` (and TipTap hosts via `useRegisterComposer`) publish
|
|
110
|
+
// their handle to this registry on mount. Falls back to a no-op
|
|
111
|
+
// when nothing is registered (no composer in the tree).
|
|
112
|
+
const activeComposer = useActiveComposer();
|
|
113
|
+
const composerHandleRef = useRef(activeComposer);
|
|
114
|
+
composerHandleRef.current = activeComposer;
|
|
116
115
|
|
|
117
116
|
useEffect(() => {
|
|
118
117
|
log.slot.debug('mount', {
|
|
119
118
|
supported: support.supported,
|
|
120
119
|
reason: support.reason,
|
|
121
|
-
hasComposerHandle: !!
|
|
120
|
+
hasComposerHandle: !!activeComposer,
|
|
122
121
|
hasExplicitValue: value !== undefined,
|
|
123
122
|
hasOnChange: !!onChange,
|
|
124
123
|
});
|
|
125
|
-
}, [support.supported, support.reason,
|
|
124
|
+
}, [support.supported, support.reason, activeComposer, value, onChange]);
|
|
126
125
|
|
|
127
126
|
// Resolve value/onChange: prop wins; otherwise pull from the
|
|
128
127
|
// registered composer handle. The slot can therefore be dropped into
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal imperative handle every text-editor surface implements so
|
|
7
|
+
* an external tool (voice dictation, command palette, AI suggestion)
|
|
8
|
+
* can read/write its text content without traversing React.
|
|
9
|
+
*
|
|
10
|
+
* Methods are optional so a host can register a partial handle
|
|
11
|
+
* (e.g. only `getValue` + `setValue`), and the caller checks before use.
|
|
12
|
+
*/
|
|
13
|
+
export interface ComposerHandle {
|
|
14
|
+
/** Move keyboard focus into the composer's editable surface. */
|
|
15
|
+
focus: () => void;
|
|
16
|
+
/** Move the caret to the very end of the input. */
|
|
17
|
+
moveCursorToEnd?: () => void;
|
|
18
|
+
/** Read the current draft text. Voice dictation anchors partial
|
|
19
|
+
* transcripts onto the user's already-typed prefix via this. */
|
|
20
|
+
getValue?: () => string;
|
|
21
|
+
/** Replace the current draft text. Voice dictation pushes interim
|
|
22
|
+
* and final transcripts through this without owning a controlled
|
|
23
|
+
* binding. */
|
|
24
|
+
setValue?: (value: string) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `@djangocfg/ui-tools/composer-registry`
|
|
29
|
+
*
|
|
30
|
+
* Cross-tool bridge: the currently-active text composer's handle.
|
|
31
|
+
*
|
|
32
|
+
* Producer side (`@djangocfg/ui-tools/chat` and TipTap hosts):
|
|
33
|
+
* register their composer's imperative handle via `attachComposer`.
|
|
34
|
+
*
|
|
35
|
+
* Consumer side (`@djangocfg/ui-tools/speech-recognition`):
|
|
36
|
+
* reads the active handle via `useActiveComposer`/`getActiveComposer`
|
|
37
|
+
* and pipes voice transcripts into it.
|
|
38
|
+
*
|
|
39
|
+
* Why this lives in its own subpath (not inside `chat`)
|
|
40
|
+
* ----------------------------------------------------
|
|
41
|
+
* `chat` and `speech-recognition` are sibling subpath exports. If the
|
|
42
|
+
* registry lived inside `chat`, then `speech-recognition` would have
|
|
43
|
+
* to reach into it via a cross-tool relative import — and under Vite
|
|
44
|
+
* dev's dependency optimizer that file ends up loaded TWICE (once via
|
|
45
|
+
* the `./chat` URL, once via the `./speech-recognition` relative-up
|
|
46
|
+
* URL), giving the producer and the consumer two separate `let active`
|
|
47
|
+
* slots. The active handle registered by chat would be invisible to
|
|
48
|
+
* speech-recognition (and vice versa).
|
|
49
|
+
*
|
|
50
|
+
* Putting the registry in its own dedicated subpath (a single tool
|
|
51
|
+
* that NEITHER chat nor speech-recognition cross-import — they both
|
|
52
|
+
* import this one as their dependency) means Vite resolves it from a
|
|
53
|
+
* single URL across the whole graph. One module instance, one shared
|
|
54
|
+
* `active` slot.
|
|
55
|
+
*
|
|
56
|
+
* Semantics: one active composer per realm. The most recent
|
|
57
|
+
* `registerComposer(handle)` wins; `registerComposer(null)` clears it.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
type Listener = (handle: ComposerHandle | null) => void;
|
|
61
|
+
|
|
62
|
+
let active: ComposerHandle | null = null;
|
|
63
|
+
const listeners = new Set<Listener>();
|
|
64
|
+
|
|
65
|
+
/** Set or replace the active composer handle. Pass `null` to clear. */
|
|
66
|
+
export function registerComposer(handle: ComposerHandle | null): void {
|
|
67
|
+
active = handle;
|
|
68
|
+
for (const fn of listeners) fn(active);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Convenience for components: register on mount, unregister on
|
|
73
|
+
* unmount. Returns a cleanup function suitable for `useEffect`.
|
|
74
|
+
*/
|
|
75
|
+
export function attachComposer(handle: ComposerHandle): () => void {
|
|
76
|
+
registerComposer(handle);
|
|
77
|
+
return () => {
|
|
78
|
+
if (active === handle) registerComposer(null);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Read the current active handle (no subscription). */
|
|
83
|
+
export function getActiveComposer(): ComposerHandle | null {
|
|
84
|
+
return active;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Subscribe to handle changes; returns an unsubscribe fn. */
|
|
88
|
+
export function subscribeComposer(listener: Listener): () => void {
|
|
89
|
+
listeners.add(listener);
|
|
90
|
+
return () => {
|
|
91
|
+
listeners.delete(listener);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* React hook: re-renders the caller whenever the active composer
|
|
97
|
+
* changes. Built on `useSyncExternalStore` so concurrent rendering,
|
|
98
|
+
* SSR, and dev-mode strict-effects all behave correctly.
|
|
99
|
+
*/
|
|
100
|
+
export function useActiveComposer(): ComposerHandle | null {
|
|
101
|
+
const subscribe = useCallback((onChange: () => void) => {
|
|
102
|
+
return subscribeComposer(onChange);
|
|
103
|
+
}, []);
|
|
104
|
+
return useSyncExternalStore(subscribe, getActiveComposer, () => null);
|
|
105
|
+
}
|
|
@@ -34,6 +34,7 @@ export const Player = forwardRef<PlayerHandle, PlayerProps>(function Player(prop
|
|
|
34
34
|
ariaLabel,
|
|
35
35
|
enableKeyboardShortcuts,
|
|
36
36
|
seekStartsPlayback,
|
|
37
|
+
autoFocus,
|
|
37
38
|
} = props;
|
|
38
39
|
|
|
39
40
|
// onTimeUpdate is intentionally not wired in the provider — we expose it via
|
|
@@ -71,6 +72,7 @@ export const Player = forwardRef<PlayerHandle, PlayerProps>(function Player(prop
|
|
|
71
72
|
enableKeyboardShortcuts={enableKeyboardShortcuts}
|
|
72
73
|
ariaLabel={ariaLabel}
|
|
73
74
|
seekStartsPlayback={seekStartsPlayback}
|
|
75
|
+
autoFocus={autoFocus}
|
|
74
76
|
handleRef={ref}
|
|
75
77
|
/>
|
|
76
78
|
</PlayerProvider>
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
// keyboard shortcuts and MediaSession wiring; renders the picked layout.
|
|
5
5
|
|
|
6
6
|
import { useCallback, useEffect, useImperativeHandle, useState } from 'react';
|
|
7
|
-
import { TooltipProvider } from '@djangocfg/ui-core/components';
|
|
8
7
|
import { useIsPhone } from '@djangocfg/ui-core/hooks';
|
|
9
8
|
import { usePlayerAudio, usePlayerControls, usePlayerMeta } from './context/selectors';
|
|
10
9
|
import { useElementWidth } from './hooks/useResizeObserver';
|
|
@@ -26,6 +25,7 @@ type Props = Pick<
|
|
|
26
25
|
| 'enableKeyboardShortcuts'
|
|
27
26
|
| 'ariaLabel'
|
|
28
27
|
| 'seekStartsPlayback'
|
|
28
|
+
| 'autoFocus'
|
|
29
29
|
> & {
|
|
30
30
|
handleRef?: React.Ref<PlayerHandle>;
|
|
31
31
|
};
|
|
@@ -40,6 +40,7 @@ export function PlayerShell({
|
|
|
40
40
|
enableKeyboardShortcuts = true,
|
|
41
41
|
ariaLabel,
|
|
42
42
|
seekStartsPlayback = true,
|
|
43
|
+
autoFocus = false,
|
|
43
44
|
handleRef,
|
|
44
45
|
}: Props) {
|
|
45
46
|
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
|
@@ -87,8 +88,9 @@ export function PlayerShell({
|
|
|
87
88
|
seek: (s: number) => controls.seek(s),
|
|
88
89
|
getCurrentTime: () => audio.currentTime,
|
|
89
90
|
getDuration: () => (Number.isFinite(audio.duration) ? audio.duration : 0),
|
|
91
|
+
focus: () => container?.focus(),
|
|
90
92
|
}),
|
|
91
|
-
[audio, controls],
|
|
93
|
+
[audio, controls, container],
|
|
92
94
|
);
|
|
93
95
|
|
|
94
96
|
// Keyboard shortcuts work only when the container can take focus.
|
|
@@ -97,26 +99,39 @@ export function PlayerShell({
|
|
|
97
99
|
container.setAttribute('tabindex', '0');
|
|
98
100
|
}, [container]);
|
|
99
101
|
|
|
102
|
+
// `autoFocus` opts the player into pulling keyboard focus on mount
|
|
103
|
+
// (and whenever the container ref is established). Once focused,
|
|
104
|
+
// the hotkey scope (Space=play/pause, ←→=seek, ↑↓=volume, M=mute)
|
|
105
|
+
// is immediately live.
|
|
106
|
+
//
|
|
107
|
+
// Deferred via a 0-timeout so the tree row's native focus event
|
|
108
|
+
// (fired by the click that triggered the mount) lands first; we
|
|
109
|
+
// then steal focus here. rAF was racy — the row's focus event can
|
|
110
|
+
// fire AFTER the next animation frame.
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!autoFocus || !container) return;
|
|
113
|
+
const id = setTimeout(() => container.focus(), 0);
|
|
114
|
+
return () => clearTimeout(id);
|
|
115
|
+
}, [autoFocus, container]);
|
|
116
|
+
|
|
100
117
|
return (
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
{
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
</div>
|
|
120
|
-
</TooltipProvider>
|
|
118
|
+
<div
|
|
119
|
+
ref={setRootRef}
|
|
120
|
+
role="group"
|
|
121
|
+
aria-label={ariaLabel ?? 'Audio player'}
|
|
122
|
+
className={`audioplayer @container/player rounded-lg border border-border/60 bg-card text-foreground shadow-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 ${className}`}
|
|
123
|
+
>
|
|
124
|
+
{resolvedVariant === 'compact' ? (
|
|
125
|
+
<CompactLayout waveform={waveform} seekStartsPlayback={seekStartsPlayback} />
|
|
126
|
+
) : (
|
|
127
|
+
<DefaultLayout
|
|
128
|
+
waveform={waveform}
|
|
129
|
+
reactiveCover={reactiveCover}
|
|
130
|
+
onPrev={onPrev}
|
|
131
|
+
onNext={onNext}
|
|
132
|
+
seekStartsPlayback={seekStartsPlayback}
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
121
136
|
);
|
|
122
137
|
}
|