@djangocfg/ui-tools 2.1.469 → 2.1.470
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/package.json +6 -6
- package/src/tools/chat/README.md +8 -1
- package/src/tools/chat/context/ChatProvider.tsx +5 -1
- package/src/tools/chat/hooks/useAutoFocusOnStreamEnd.ts +7 -26
- package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +20 -0
- package/src/tools/forms/MarkdownEditor/README.md +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.470",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -334,8 +334,8 @@
|
|
|
334
334
|
"test:watch": "vitest"
|
|
335
335
|
},
|
|
336
336
|
"peerDependencies": {
|
|
337
|
-
"@djangocfg/i18n": "^2.1.
|
|
338
|
-
"@djangocfg/ui-core": "^2.1.
|
|
337
|
+
"@djangocfg/i18n": "^2.1.470",
|
|
338
|
+
"@djangocfg/ui-core": "^2.1.470",
|
|
339
339
|
"consola": "^3.4.2",
|
|
340
340
|
"lodash-es": "^4.18.1",
|
|
341
341
|
"lucide-react": "^0.545.0",
|
|
@@ -418,9 +418,9 @@
|
|
|
418
418
|
"@maplibre/maplibre-gl-geocoder": "^1.9.4"
|
|
419
419
|
},
|
|
420
420
|
"devDependencies": {
|
|
421
|
-
"@djangocfg/i18n": "^2.1.
|
|
422
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
423
|
-
"@djangocfg/ui-core": "^2.1.
|
|
421
|
+
"@djangocfg/i18n": "^2.1.470",
|
|
422
|
+
"@djangocfg/typescript-config": "^2.1.470",
|
|
423
|
+
"@djangocfg/ui-core": "^2.1.470",
|
|
424
424
|
"@types/lodash-es": "^4.17.12",
|
|
425
425
|
"@types/mapbox__mapbox-gl-draw": "^1.4.9",
|
|
426
426
|
"@types/node": "^25.9.5",
|
package/src/tools/chat/README.md
CHANGED
|
@@ -90,7 +90,14 @@ ChatRoot (one-line preset) ◄── optionally wrapped by ──► ChatLau
|
|
|
90
90
|
|
|
91
91
|
`ChatLauncher` mounts the `ChatProvider` itself — pass `transport` / `config` / `audio` / `initialSessionId` / `autoCreateSession` / `streaming` / `debug` to the launcher and use `<ChatRoot />` without props as the child. `ChatRoot` detects the ambient provider and reuses it; standalone `<ChatRoot transport={…}>` still works for non-launcher embeds. This is what makes declarative `headerSlots` (which render in the dock header) able to call `useChatContext()` and read `sessionId` / `clearMessages` / etc.
|
|
92
92
|
|
|
93
|
-
`ChatProvider`
|
|
93
|
+
`ChatProvider` is the single owner of automatic composer focus: one settled
|
|
94
|
+
focus after a chat mounts or switches session, and one after a streamed reply
|
|
95
|
+
ends (`autoFocusOnMount` / `autoFocusOnStreamEnd`, both default `true`). A
|
|
96
|
+
normal message, presence, telemetry, or prop update never takes focus. Direct
|
|
97
|
+
user intent remains local: clicking the empty message area or the composer
|
|
98
|
+
surface focuses the editor. This keeps a hand-rolled `ChatProvider` +
|
|
99
|
+
`MessageList` + `Composer` layout identical to `ChatRoot` without competing
|
|
100
|
+
focus effects.
|
|
94
101
|
|
|
95
102
|
Module boundaries:
|
|
96
103
|
|
|
@@ -242,7 +242,11 @@ export function ChatProvider({
|
|
|
242
242
|
if (prevSessionRef.current === chat.sessionId) return;
|
|
243
243
|
prevSessionRef.current = chat.sessionId;
|
|
244
244
|
if (!autoFocusOnMount) return;
|
|
245
|
-
|
|
245
|
+
// Session switch is the provider's second and final automatic focus
|
|
246
|
+
// intent (after mount). Wait one frame for a closing dialog / editor DOM
|
|
247
|
+
// update, rather than competing with consumers through retry loops.
|
|
248
|
+
const raf = requestAnimationFrame(() => getActiveComposer()?.focus());
|
|
249
|
+
return () => cancelAnimationFrame(raf);
|
|
246
250
|
}, [chat.sessionId, autoFocusOnMount]);
|
|
247
251
|
|
|
248
252
|
const value = useMemo<ChatContextValue>(
|
|
@@ -111,35 +111,16 @@ export function useAutoFocusOnMount(
|
|
|
111
111
|
const { enabled = true, targetRef } = options;
|
|
112
112
|
useEffect(() => {
|
|
113
113
|
if (!enabled) return;
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
// a handful of frames and stop as soon as the editor actually holds
|
|
121
|
-
// focus, so the caret reliably lands in the composer without a
|
|
122
|
-
// guessed fixed delay.
|
|
123
|
-
let raf = 0;
|
|
124
|
-
let tries = 0;
|
|
125
|
-
const MAX_TRIES = 8; // ~8 frames ≈ 130ms @60fps — enough post-remount.
|
|
126
|
-
const tick = () => {
|
|
114
|
+
// One settled frame is enough: child effects register the composer before
|
|
115
|
+
// the provider's effect runs. Repeated focus polling was hostile to rich
|
|
116
|
+
// editors — an unrelated render could keep reclaiming their selection.
|
|
117
|
+
// A chat has one focus owner (ChatProvider); mounting is one deliberate
|
|
118
|
+
// focus intent, not an 8-frame retry loop.
|
|
119
|
+
const raf = requestAnimationFrame(() => {
|
|
127
120
|
const target =
|
|
128
121
|
(targetRef?.current as Focusable | null) ?? getActiveComposer();
|
|
129
122
|
target?.focus?.();
|
|
130
|
-
|
|
131
|
-
// Stop once the composer owns focus, or we run out of tries. The
|
|
132
|
-
// active-element check keeps us from stealing focus the user just
|
|
133
|
-
// moved elsewhere (e.g. opened the slash menu) after the first frame.
|
|
134
|
-
const editor = document.querySelector<HTMLElement>(
|
|
135
|
-
'.ProseMirror, textarea',
|
|
136
|
-
);
|
|
137
|
-
const settled = !!editor && document.activeElement === editor;
|
|
138
|
-
if (!settled && tries < MAX_TRIES) {
|
|
139
|
-
raf = requestAnimationFrame(tick);
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
raf = requestAnimationFrame(tick);
|
|
123
|
+
});
|
|
143
124
|
return () => cancelAnimationFrame(raf);
|
|
144
125
|
// Mount-only by design: the empty dep array means a host that wants
|
|
145
126
|
// re-focus on chat-switch must remount the tree (the `key={id}`
|
|
@@ -510,6 +510,14 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
|
|
510
510
|
if (!editor) return;
|
|
511
511
|
const current = getMarkdown(editor);
|
|
512
512
|
if (current !== value) {
|
|
513
|
+
// `setContent` replaces the ProseMirror document and therefore creates a
|
|
514
|
+
// fresh selection. In a controlled chat editor an unrelated parent
|
|
515
|
+
// render can arrive while the user is typing; without preserving this
|
|
516
|
+
// selection Chromium may release focus to <body> or place the caret at
|
|
517
|
+
// document start. External content still wins, but it must never erase
|
|
518
|
+
// the user's active editing position.
|
|
519
|
+
const hadFocus = editor.isFocused;
|
|
520
|
+
const previousSelection = editor.state.selection;
|
|
513
521
|
isExternalUpdate.current = true;
|
|
514
522
|
editor.commands.setContent(value, { contentType: 'markdown', emitUpdate: false });
|
|
515
523
|
// After the new content is written, convert a leading `/verb` text
|
|
@@ -535,6 +543,18 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
|
|
535
543
|
});
|
|
536
544
|
}
|
|
537
545
|
isExternalUpdate.current = false;
|
|
546
|
+
|
|
547
|
+
if (hadFocus) {
|
|
548
|
+
// SetContent / chip normalization can shorten the document. Clamp the
|
|
549
|
+
// old range to the new valid bounds, then restore it after focusing so
|
|
550
|
+
// both a collapsed caret and a text selection remain where the user
|
|
551
|
+
// left them. We intentionally do not focus an editor that was not
|
|
552
|
+
// active — a background update must not steal focus from another UI.
|
|
553
|
+
const maxPosition = editor.state.doc.content.size;
|
|
554
|
+
const from = Math.max(1, Math.min(previousSelection.from, maxPosition));
|
|
555
|
+
const to = Math.max(1, Math.min(previousSelection.to, maxPosition));
|
|
556
|
+
editor.chain().focus().setTextSelection({ from, to }).run();
|
|
557
|
+
}
|
|
538
558
|
}
|
|
539
559
|
}, [value, editor]);
|
|
540
560
|
|
|
@@ -204,6 +204,10 @@ A bare URL serialises as the bare URL; a file path as the literal path.
|
|
|
204
204
|
pasted text, keeping the surrounding prose.
|
|
205
205
|
- **Initial `value`**: paths / URLs in the controlled markdown are
|
|
206
206
|
converted after `setContent` (the same way leading `/`-commands are).
|
|
207
|
+
- **External controlled update while editing**: the editor preserves the
|
|
208
|
+
current focused selection, clamped to the new document. A background parent
|
|
209
|
+
render must never send the caret to document start or steal focus from
|
|
210
|
+
another control.
|
|
207
211
|
|
|
208
212
|
### Caret / editing
|
|
209
213
|
|