@djangocfg/ui-tools 2.1.397 → 2.1.400
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/components/ChatRoot.tsx +43 -10
- package/src/tools/Chat/components/index.ts +1 -1
- package/src/tools/Chat/hooks/useChatComposer.ts +2 -2
- package/src/tools/Chat/index.ts +1 -0
- package/src/tools/JsonTree/components/JsonContent.tsx +17 -4
- package/src/tools/MarkdownEditor/styles.css +20 -5
- package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +1 -0
- package/src/tools/Mermaid/hooks/useMermaidValidation.ts +1 -1
- package/src/tools/Mermaid/utils/mermaid-helpers.ts +1 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +42 -21
- package/src/tools/SpeechRecognition/core/engine/webspeech.ts +2 -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.400",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -154,8 +154,8 @@
|
|
|
154
154
|
"test:watch": "vitest"
|
|
155
155
|
},
|
|
156
156
|
"peerDependencies": {
|
|
157
|
-
"@djangocfg/i18n": "^2.1.
|
|
158
|
-
"@djangocfg/ui-core": "^2.1.
|
|
157
|
+
"@djangocfg/i18n": "^2.1.400",
|
|
158
|
+
"@djangocfg/ui-core": "^2.1.400",
|
|
159
159
|
"consola": "^3.4.2",
|
|
160
160
|
"lodash-es": "^4.18.1",
|
|
161
161
|
"lucide-react": "^0.545.0",
|
|
@@ -209,9 +209,9 @@
|
|
|
209
209
|
"material-file-icons": "^2.4.0"
|
|
210
210
|
},
|
|
211
211
|
"devDependencies": {
|
|
212
|
-
"@djangocfg/i18n": "^2.1.
|
|
213
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
214
|
-
"@djangocfg/ui-core": "^2.1.
|
|
212
|
+
"@djangocfg/i18n": "^2.1.400",
|
|
213
|
+
"@djangocfg/typescript-config": "^2.1.400",
|
|
214
|
+
"@djangocfg/ui-core": "^2.1.400",
|
|
215
215
|
"@types/lodash-es": "^4.17.12",
|
|
216
216
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
217
217
|
"@types/node": "^24.7.2",
|
|
@@ -80,6 +80,26 @@ export interface ChatRootProps {
|
|
|
80
80
|
renderHeader?: (ctx: ChatContextValue) => ReactNode;
|
|
81
81
|
/** Render the empty-state lazily — receives a `setValue` to seed the composer. */
|
|
82
82
|
renderEmpty?: (api: { setValue: (v: string) => void; focus: () => void }) => ReactNode;
|
|
83
|
+
/**
|
|
84
|
+
* Replace the default `<Composer>` entirely. Receives the live
|
|
85
|
+
* `useChatComposer` return and the composer-related slot props
|
|
86
|
+
* (placeholder, size, attach button, toolbar slots) — host can wire
|
|
87
|
+
* them into a custom widget (e.g. a MarkdownEditor with mentions).
|
|
88
|
+
*
|
|
89
|
+
* When set, `composerToolbarStart/End`, `composerAttachmentTray`,
|
|
90
|
+
* `composerSize`, `showAttachmentButton`, `onPickFiles` are passed
|
|
91
|
+
* to the render-prop but are no longer auto-rendered.
|
|
92
|
+
*/
|
|
93
|
+
renderComposer?: (api: {
|
|
94
|
+
composer: UseChatComposerReturn;
|
|
95
|
+
placeholder?: string;
|
|
96
|
+
size?: ComposerSize;
|
|
97
|
+
showAttachmentButton?: boolean;
|
|
98
|
+
onPickFiles?: () => void;
|
|
99
|
+
toolbarStart?: ReactNode;
|
|
100
|
+
toolbarEnd?: ReactNode;
|
|
101
|
+
attachmentTray?: ReactNode;
|
|
102
|
+
}) => ReactNode;
|
|
83
103
|
/** Forwarded into `<MessageBubble toolCallsProps>` so hosts can swap payload renderers. */
|
|
84
104
|
toolCallsProps?: Omit<ToolCallsProps, 'calls'>;
|
|
85
105
|
/** Per-type attachment renderers — `{ image, audio, video, file, default }`. */
|
|
@@ -246,16 +266,29 @@ function ChatRootShell({ className, listClassName, slots }: ChatRootShellProps)
|
|
|
246
266
|
</div>
|
|
247
267
|
</div>
|
|
248
268
|
{!slots.hideComposer && (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
269
|
+
slots.renderComposer
|
|
270
|
+
? slots.renderComposer({
|
|
271
|
+
composer,
|
|
272
|
+
placeholder: chat.config.placeholder,
|
|
273
|
+
size: slots.composerSize,
|
|
274
|
+
showAttachmentButton: slots.showAttachmentButton,
|
|
275
|
+
onPickFiles: slots.onPickFiles,
|
|
276
|
+
toolbarStart: slots.composerToolbarStart,
|
|
277
|
+
toolbarEnd: slots.composerToolbarEnd,
|
|
278
|
+
attachmentTray: slots.composerAttachmentTray,
|
|
279
|
+
})
|
|
280
|
+
: (
|
|
281
|
+
<Composer
|
|
282
|
+
composer={composer}
|
|
283
|
+
placeholder={chat.config.placeholder}
|
|
284
|
+
showAttachmentButton={slots.showAttachmentButton}
|
|
285
|
+
onPickFiles={slots.onPickFiles}
|
|
286
|
+
toolbarStart={slots.composerToolbarStart}
|
|
287
|
+
toolbarEnd={slots.composerToolbarEnd}
|
|
288
|
+
attachmentTray={slots.composerAttachmentTray}
|
|
289
|
+
size={slots.composerSize}
|
|
290
|
+
/>
|
|
291
|
+
)
|
|
259
292
|
)}
|
|
260
293
|
{slots.footer ?? null}
|
|
261
294
|
</div>
|
|
@@ -8,7 +8,7 @@ export {
|
|
|
8
8
|
} from './MessageList';
|
|
9
9
|
export { MessageBubble, type MessageBubbleProps } from './MessageBubble';
|
|
10
10
|
export { MessageActions, type MessageActionsProps } from './MessageActions';
|
|
11
|
-
export { Composer, type ComposerProps } from './Composer';
|
|
11
|
+
export { Composer, type ComposerProps, type ComposerSize } from './Composer';
|
|
12
12
|
export { Sources, type SourcesProps } from './Sources';
|
|
13
13
|
export { ToolCalls, type ToolCallsProps, type ToolPayloadKind } from './ToolCalls';
|
|
14
14
|
export {
|
|
@@ -201,7 +201,7 @@ export function useChatComposer(options: UseChatComposerOptions): UseChatCompose
|
|
|
201
201
|
if (!items.length) return;
|
|
202
202
|
const next = historyRef.current.index < 0 ? items.length - 1 : Math.max(0, historyRef.current.index - 1);
|
|
203
203
|
historyRef.current.index = next;
|
|
204
|
-
setValueState(items[next]);
|
|
204
|
+
setValueState(items[next] ?? '');
|
|
205
205
|
}, []);
|
|
206
206
|
|
|
207
207
|
const recallNext = useCallback(() => {
|
|
@@ -214,7 +214,7 @@ export function useChatComposer(options: UseChatComposerOptions): UseChatCompose
|
|
|
214
214
|
return;
|
|
215
215
|
}
|
|
216
216
|
historyRef.current.index = next;
|
|
217
|
-
setValueState(items[next]);
|
|
217
|
+
setValueState(items[next] ?? '');
|
|
218
218
|
}, []);
|
|
219
219
|
|
|
220
220
|
const onChange = useCallback(
|
package/src/tools/Chat/index.ts
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
import React, { memo } from 'react';
|
|
4
4
|
import { CommonExternalProps, JSONTree } from 'react-json-tree';
|
|
5
5
|
|
|
6
|
+
// JSON inspector uses a fixed dark palette regardless of the host
|
|
7
|
+
// theme — same convention as PrettyCode and devtool-style data
|
|
8
|
+
// renderers (Chrome DevTools, Insomnia, Bruno). Syntax highlighting
|
|
9
|
+
// has its own contrast model; mixing it with semantic UI tokens on
|
|
10
|
+
// a light surface flattens keys/values into low-contrast pastels.
|
|
11
|
+
const JSON_TREE_SURFACE_BG = '#0d1117';
|
|
6
12
|
const JSON_TREE_THEME = {
|
|
7
13
|
scheme: 'djangocfg-dark',
|
|
8
14
|
base00: 'transparent',
|
|
@@ -62,11 +68,11 @@ const JsonContent = memo(({
|
|
|
62
68
|
? (nodeType: string, nodeData: unknown) => {
|
|
63
69
|
if (nodeType === 'Array') {
|
|
64
70
|
const length = Array.isArray(nodeData) ? nodeData.length : 0;
|
|
65
|
-
return length > 0 ? <span className="text-
|
|
71
|
+
return length > 0 ? <span className="text-xs" style={{ color: '#9ca3af' }}>({length})</span> : null;
|
|
66
72
|
}
|
|
67
73
|
if (nodeType === 'Object') {
|
|
68
74
|
const keys = nodeData && typeof nodeData === 'object' ? Object.keys(nodeData) : [];
|
|
69
|
-
return keys.length > 0 ? <span className="text-
|
|
75
|
+
return keys.length > 0 ? <span className="text-xs" style={{ color: '#9ca3af' }}>{`{${keys.length}}`}</span> : null;
|
|
70
76
|
}
|
|
71
77
|
return null;
|
|
72
78
|
}
|
|
@@ -83,9 +89,16 @@ const JsonContent = memo(({
|
|
|
83
89
|
typeof value === 'string' && (value.startsWith('http://') || value.startsWith('https://'));
|
|
84
90
|
|
|
85
91
|
return (
|
|
86
|
-
<div
|
|
92
|
+
<div
|
|
93
|
+
className={`overflow-auto h-full rounded-md ${padding}`}
|
|
94
|
+
style={{
|
|
95
|
+
backgroundColor: JSON_TREE_SURFACE_BG,
|
|
96
|
+
color: '#e5e7eb',
|
|
97
|
+
...(fontSize ? { fontSize } : null),
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
87
100
|
{showTitle && title && (
|
|
88
|
-
<h6 className="text-sm font-semibold
|
|
101
|
+
<h6 className="text-sm font-semibold mb-2" style={{ color: '#f3f4f6' }}>{title}</h6>
|
|
89
102
|
)}
|
|
90
103
|
<JSONTree
|
|
91
104
|
key={renderKey}
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
outline: none;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
/* Inherit semantic foreground so the editor renders correctly in both
|
|
6
|
+
light + dark themes (and under any active preset). Without this the
|
|
7
|
+
browser falls back to UA black on a token-driven background. */
|
|
8
|
+
.markdown-editor .tiptap,
|
|
9
|
+
.markdown-editor .ProseMirror {
|
|
10
|
+
color: var(--color-foreground, var(--foreground));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.markdown-editor .tiptap a,
|
|
14
|
+
.markdown-editor .ProseMirror a {
|
|
15
|
+
color: var(--color-primary, var(--primary));
|
|
16
|
+
text-decoration: underline;
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
.markdown-editor .tiptap h1 {
|
|
6
20
|
font-size: 1.5em;
|
|
7
21
|
font-weight: 700;
|
|
@@ -46,7 +60,7 @@
|
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
.markdown-editor .tiptap blockquote {
|
|
49
|
-
border-left: 3px solid var(--border,
|
|
63
|
+
border-left: 3px solid var(--color-border, var(--border));
|
|
50
64
|
padding-left: 1em;
|
|
51
65
|
margin: 0.5em 0;
|
|
52
66
|
opacity: 0.8;
|
|
@@ -54,12 +68,13 @@
|
|
|
54
68
|
|
|
55
69
|
.markdown-editor .tiptap hr {
|
|
56
70
|
border: none;
|
|
57
|
-
border-top: 1px solid var(--border,
|
|
71
|
+
border-top: 1px solid var(--color-border, var(--border));
|
|
58
72
|
margin: 0.75em 0;
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
.markdown-editor .tiptap code {
|
|
62
|
-
background: var(--muted,
|
|
76
|
+
background: var(--color-muted, var(--muted));
|
|
77
|
+
color: var(--color-muted-foreground, var(--muted-foreground));
|
|
63
78
|
padding: 0.15em 0.3em;
|
|
64
79
|
border-radius: 0.25em;
|
|
65
80
|
font-size: 0.9em;
|
|
@@ -182,10 +197,10 @@
|
|
|
182
197
|
|
|
183
198
|
.markdown-toolbar-btn:hover {
|
|
184
199
|
opacity: 1;
|
|
185
|
-
background: var(--muted,
|
|
200
|
+
background: var(--color-muted, var(--muted));
|
|
186
201
|
}
|
|
187
202
|
|
|
188
203
|
.markdown-toolbar-btn.active {
|
|
189
204
|
opacity: 1;
|
|
190
|
-
background: var(--muted,
|
|
205
|
+
background: var(--color-muted, var(--muted));
|
|
191
206
|
}
|
|
@@ -42,6 +42,7 @@ const isVerticalDiagram = (svgElement: SVGSVGElement): boolean => {
|
|
|
42
42
|
const viewBox = svgElement.getAttribute('viewBox');
|
|
43
43
|
if (viewBox) {
|
|
44
44
|
const [, , width, height] = viewBox.split(' ').map(Number);
|
|
45
|
+
if (width === undefined || height === undefined) return false;
|
|
45
46
|
return height > width * 1.5;
|
|
46
47
|
}
|
|
47
48
|
const bbox = svgElement.getBBox?.();
|
|
@@ -15,7 +15,7 @@ export function useMermaidValidation() {
|
|
|
15
15
|
if (lines.length < 2) return false; // Need at least diagram type + one element
|
|
16
16
|
|
|
17
17
|
// Check for common incomplete patterns
|
|
18
|
-
const lastLine = lines[lines.length - 1].trim();
|
|
18
|
+
const lastLine = (lines[lines.length - 1] ?? '').trim();
|
|
19
19
|
|
|
20
20
|
// Incomplete if last line ends with arrow without destination
|
|
21
21
|
if (lastLine.match(/-->?\s*$/)) return false;
|
|
@@ -23,6 +23,7 @@ export const isVerticalDiagram = (svgElement: SVGSVGElement): boolean => {
|
|
|
23
23
|
const viewBox = svgElement.getAttribute('viewBox');
|
|
24
24
|
if (viewBox) {
|
|
25
25
|
const [, , width, height] = viewBox.split(' ').map(Number);
|
|
26
|
+
if (width === undefined || height === undefined) return false;
|
|
26
27
|
return height > width * 1.5;
|
|
27
28
|
}
|
|
28
29
|
const bbox = svgElement.getBBox?.();
|
|
@@ -4,10 +4,19 @@ import { Highlight, Language, themes } from 'prism-react-renderer';
|
|
|
4
4
|
import React, { useMemo, useRef } from 'react';
|
|
5
5
|
|
|
6
6
|
import { useAppT } from '@djangocfg/i18n';
|
|
7
|
-
import { useResolvedTheme } from '@djangocfg/ui-core/hooks';
|
|
8
7
|
import { FloatingToolbar } from '../../components/FloatingToolbar';
|
|
9
8
|
import { CopyAction } from '../../components/FloatingToolbar/actions';
|
|
10
9
|
|
|
10
|
+
// Surface palette — fixed dark regardless of host theme. Code blocks
|
|
11
|
+
// follow the IDE / GitHub / ChatGPT convention: syntax highlighting
|
|
12
|
+
// ships its own contrast model, and mixing it with light UI surfaces
|
|
13
|
+
// produces low-contrast pastel renders that hurt readability. The
|
|
14
|
+
// surface is hard-coded (not a semantic token) so the palette stays
|
|
15
|
+
// stable when the user flips themes.
|
|
16
|
+
const CODE_SURFACE_BG = '#0d1117';
|
|
17
|
+
const CODE_SURFACE_BORDER = '#1f2937';
|
|
18
|
+
const CODE_INLINE_BG = '#1f2937';
|
|
19
|
+
|
|
11
20
|
// Load extra Prism grammars (``bash``, ``ruby``, ``java``, ``php``)
|
|
12
21
|
// that aren't in ``prism-react-renderer``'s default bundle. The hook
|
|
13
22
|
// below subscribes to its ready state and re-renders this component
|
|
@@ -19,6 +28,13 @@ interface PrettyCodeProps {
|
|
|
19
28
|
data: string | object;
|
|
20
29
|
language: Language;
|
|
21
30
|
className?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Force a specific Prism palette. **Default is always `dark`** —
|
|
33
|
+
* code blocks ship their own contrast model and we render them on
|
|
34
|
+
* a fixed dark surface regardless of the surrounding UI theme
|
|
35
|
+
* (GitHub / VSCode / ChatGPT convention). Override only if you
|
|
36
|
+
* have a deliberate reason — e.g. printing a PDF on a light page.
|
|
37
|
+
*/
|
|
22
38
|
mode?: 'dark' | 'light';
|
|
23
39
|
inline?: boolean;
|
|
24
40
|
customBg?: string; // Custom background class
|
|
@@ -38,10 +54,9 @@ interface PrettyCodeProps {
|
|
|
38
54
|
variant?: 'card' | 'plain';
|
|
39
55
|
}
|
|
40
56
|
|
|
41
|
-
const PrettyCode = ({ data, language, className, mode, inline = false, customBg, isCompact = false, scrollIsolation, maxLines, variant = 'card' }: PrettyCodeProps) => {
|
|
57
|
+
const PrettyCode = ({ data, language, className, mode = 'dark', inline = false, customBg, isCompact = false, scrollIsolation, maxLines, variant = 'card' }: PrettyCodeProps) => {
|
|
42
58
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
43
59
|
const t = useAppT();
|
|
44
|
-
const detectedTheme = useResolvedTheme();
|
|
45
60
|
|
|
46
61
|
// Subscribe to the extra-grammars ready state. When ``bash`` /
|
|
47
62
|
// ``ruby`` / ``java`` / ``php`` finish loading, this hook triggers a
|
|
@@ -56,12 +71,12 @@ const PrettyCode = ({ data, language, className, mode, inline = false, customBg,
|
|
|
56
71
|
// Font size based on compact mode
|
|
57
72
|
const fontSize = isCompact ? '0.75rem' : '0.875rem'; // 12px vs 14px
|
|
58
73
|
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
74
|
+
const isDarkMode = mode !== 'light';
|
|
75
|
+
const prismTheme = isDarkMode ? themes.vsDark : themes.github;
|
|
76
|
+
const surfaceStyle = isDarkMode
|
|
77
|
+
? { backgroundColor: CODE_SURFACE_BG, borderColor: CODE_SURFACE_BORDER }
|
|
78
|
+
: undefined;
|
|
79
|
+
const inlineSurfaceStyle = isDarkMode ? { backgroundColor: CODE_INLINE_BG } : undefined;
|
|
65
80
|
|
|
66
81
|
// Convert form object to JSON string with proper formatting
|
|
67
82
|
const contentJson = typeof data === 'string' ? data : JSON.stringify(data || {}, null, 2);
|
|
@@ -81,12 +96,16 @@ const PrettyCode = ({ data, language, className, mode, inline = false, customBg,
|
|
|
81
96
|
|
|
82
97
|
// Handle empty content
|
|
83
98
|
if (!contentJson || contentJson.trim() === '') {
|
|
84
|
-
const emptyBgClass = customBg || (isDarkMode ? '
|
|
85
|
-
const emptyBorderClass = isDarkMode ? 'border-zinc-700' : 'border-border';
|
|
99
|
+
const emptyBgClass = customBg || (isDarkMode ? '' : 'bg-card');
|
|
86
100
|
return (
|
|
87
|
-
<div
|
|
101
|
+
<div
|
|
102
|
+
className={`relative h-full ${emptyBgClass} rounded-sm border ${isDarkMode ? '' : 'border-border'} ${className || ''}`}
|
|
103
|
+
style={customBg ? undefined : surfaceStyle}
|
|
104
|
+
>
|
|
88
105
|
<div className="h-full overflow-auto p-4">
|
|
89
|
-
<p className="text-
|
|
106
|
+
<p className="text-sm italic" style={{ color: isDarkMode ? '#9ca3af' : undefined }}>
|
|
107
|
+
{labels.noContent}
|
|
108
|
+
</p>
|
|
90
109
|
</div>
|
|
91
110
|
</div>
|
|
92
111
|
);
|
|
@@ -248,7 +267,7 @@ const PrettyCode = ({ data, language, className, mode, inline = false, customBg,
|
|
|
248
267
|
}
|
|
249
268
|
|
|
250
269
|
if (inline) {
|
|
251
|
-
const inlineBgClass = customBg || (isDarkMode ? '
|
|
270
|
+
const inlineBgClass = customBg || (isDarkMode ? '' : 'bg-muted');
|
|
252
271
|
return (
|
|
253
272
|
<Highlight theme={prismTheme} code={contentJson} language={normalizedLanguage as Language}>
|
|
254
273
|
{({ className, style, tokens, getTokenProps }) => (
|
|
@@ -256,6 +275,7 @@ const PrettyCode = ({ data, language, className, mode, inline = false, customBg,
|
|
|
256
275
|
className={`${className} ${inlineBgClass} px-2 py-1 rounded ${isCompact ? 'text-xs' : 'text-sm'} font-mono inline-block`}
|
|
257
276
|
style={{
|
|
258
277
|
...style,
|
|
278
|
+
...(customBg ? undefined : inlineSurfaceStyle),
|
|
259
279
|
fontSize,
|
|
260
280
|
fontFamily: 'monospace',
|
|
261
281
|
}}
|
|
@@ -271,19 +291,20 @@ const PrettyCode = ({ data, language, className, mode, inline = false, customBg,
|
|
|
271
291
|
);
|
|
272
292
|
}
|
|
273
293
|
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
const
|
|
294
|
+
// Code surface is fixed (dark by default). Falls back to semantic
|
|
295
|
+
// `bg-card` only when the caller explicitly opts into light mode.
|
|
296
|
+
const bgClass = customBg || (isDarkMode ? '' : 'bg-card');
|
|
277
297
|
|
|
278
298
|
return (
|
|
279
299
|
<div
|
|
280
300
|
ref={containerRef}
|
|
281
|
-
className={`group relative ${bgClass} rounded-lg border ${
|
|
282
|
-
style={
|
|
301
|
+
className={`group relative ${bgClass} rounded-lg border ${isDarkMode ? '' : 'border-border'} ${className || ''}`}
|
|
302
|
+
style={{
|
|
303
|
+
...(customBg ? undefined : surfaceStyle),
|
|
283
304
|
// maxHeight caps growth at ``maxLines`` rows; without maxLines we
|
|
284
305
|
// let the block grow to fit its content (no scroll).
|
|
285
|
-
maxHeightPx ? { maxHeight: `${maxHeightPx}px` } :
|
|
286
|
-
}
|
|
306
|
+
...(maxHeightPx ? { maxHeight: `${maxHeightPx}px` } : null),
|
|
307
|
+
}}
|
|
287
308
|
>
|
|
288
309
|
{/* Toolbar: hidden by default, appears on hover. Absolute overlay so it doesn't shift layout.
|
|
289
310
|
scrollIsolation is force-disabled when content fits without scrolling —
|
|
@@ -143,7 +143,9 @@ export function createWebSpeechEngine(
|
|
|
143
143
|
rec.onresult = (e) => {
|
|
144
144
|
for (let i = e.resultIndex; i < e.results.length; i += 1) {
|
|
145
145
|
const res = e.results[i];
|
|
146
|
+
if (!res) continue;
|
|
146
147
|
const alt = res[0];
|
|
148
|
+
if (!alt) continue;
|
|
147
149
|
const text = alt.transcript;
|
|
148
150
|
if (!currentSegmentId) currentSegmentId = newSegmentId();
|
|
149
151
|
if (res.isFinal) {
|