@runtypelabs/persona 3.18.0 → 3.19.0
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/README.md +1 -1
- package/dist/index.cjs +47 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +281 -4
- package/dist/index.d.ts +281 -4
- package/dist/index.global.js +102 -1636
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +47 -47
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +1438 -619
- package/dist/theme-editor.d.cts +119 -1
- package/dist/theme-editor.d.ts +119 -1
- package/dist/theme-editor.js +1552 -619
- package/dist/widget.css +348 -0
- package/package.json +1 -1
- package/src/components/composer-builder.test.ts +52 -0
- package/src/components/composer-builder.ts +67 -490
- package/src/components/composer-parts.test.ts +152 -0
- package/src/components/composer-parts.ts +452 -0
- package/src/components/header-builder.ts +22 -299
- package/src/components/header-parts.ts +360 -0
- package/src/components/panel.test.ts +61 -0
- package/src/components/panel.ts +262 -5
- package/src/components/pill-composer-builder.test.ts +85 -0
- package/src/components/pill-composer-builder.ts +183 -0
- package/src/index.ts +4 -0
- package/src/runtime/init.ts +4 -2
- package/src/runtime/persist-state.test.ts +152 -0
- package/src/styles/widget.css +348 -0
- package/src/types.ts +121 -1
- package/src/ui.component-directive.test.ts +183 -0
- package/src/ui.composer-bar.test.ts +1009 -0
- package/src/ui.ts +809 -72
- package/src/utils/attachment-manager.ts +1 -1
- package/src/utils/dock.test.ts +45 -0
- package/src/utils/dock.ts +3 -0
- package/src/utils/icons.ts +314 -58
- package/src/utils/stream-animation.ts +7 -2
|
@@ -66,7 +66,7 @@ function getFileIconName(mimeType: string): string {
|
|
|
66
66
|
if (mimeType.startsWith('text/')) return 'file-text';
|
|
67
67
|
if (mimeType.includes('word')) return 'file-text';
|
|
68
68
|
if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return 'file-spreadsheet';
|
|
69
|
-
if (mimeType === 'application/json') return 'file-
|
|
69
|
+
if (mimeType === 'application/json') return 'file-code';
|
|
70
70
|
return 'file';
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isComposerBarMountMode, isDockedMountMode } from "./dock";
|
|
3
|
+
import type { AgentWidgetConfig } from "../types";
|
|
4
|
+
|
|
5
|
+
describe("isDockedMountMode", () => {
|
|
6
|
+
it("returns true for mountMode: 'docked'", () => {
|
|
7
|
+
const config: AgentWidgetConfig = { apiUrl: "/api", launcher: { mountMode: "docked" } };
|
|
8
|
+
expect(isDockedMountMode(config)).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns false for default and other mount modes", () => {
|
|
12
|
+
expect(isDockedMountMode(undefined)).toBe(false);
|
|
13
|
+
expect(isDockedMountMode({ apiUrl: "/api" } as AgentWidgetConfig)).toBe(false);
|
|
14
|
+
expect(
|
|
15
|
+
isDockedMountMode({ apiUrl: "/api", launcher: { mountMode: "composer-bar" } } as AgentWidgetConfig)
|
|
16
|
+
).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("isComposerBarMountMode", () => {
|
|
21
|
+
it("returns true for mountMode: 'composer-bar'", () => {
|
|
22
|
+
const config: AgentWidgetConfig = {
|
|
23
|
+
apiUrl: "/api",
|
|
24
|
+
launcher: { mountMode: "composer-bar" },
|
|
25
|
+
};
|
|
26
|
+
expect(isComposerBarMountMode(config)).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns false for default, floating, and docked modes", () => {
|
|
30
|
+
expect(isComposerBarMountMode(undefined)).toBe(false);
|
|
31
|
+
expect(isComposerBarMountMode({ apiUrl: "/api" } as AgentWidgetConfig)).toBe(false);
|
|
32
|
+
expect(
|
|
33
|
+
isComposerBarMountMode({
|
|
34
|
+
apiUrl: "/api",
|
|
35
|
+
launcher: { mountMode: "floating" },
|
|
36
|
+
} as AgentWidgetConfig)
|
|
37
|
+
).toBe(false);
|
|
38
|
+
expect(
|
|
39
|
+
isComposerBarMountMode({
|
|
40
|
+
apiUrl: "/api",
|
|
41
|
+
launcher: { mountMode: "docked" },
|
|
42
|
+
} as AgentWidgetConfig)
|
|
43
|
+
).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
package/src/utils/dock.ts
CHANGED
|
@@ -10,6 +10,9 @@ const DEFAULT_DOCK_CONFIG: Required<AgentWidgetDockConfig> = {
|
|
|
10
10
|
export const isDockedMountMode = (config?: AgentWidgetConfig): boolean =>
|
|
11
11
|
(config?.launcher?.mountMode ?? "floating") === "docked";
|
|
12
12
|
|
|
13
|
+
export const isComposerBarMountMode = (config?: AgentWidgetConfig): boolean =>
|
|
14
|
+
(config?.launcher?.mountMode ?? "floating") === "composer-bar";
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* Resolved dock layout. For `reveal: "resize"`, when the panel is closed the dock column is `0px`.
|
|
15
18
|
* For `reveal: "overlay"`, the panel overlays with `transform`. For `reveal: "push"`, a sliding track
|
package/src/utils/icons.ts
CHANGED
|
@@ -1,58 +1,324 @@
|
|
|
1
|
-
import * as icons from "lucide";
|
|
2
1
|
import type { IconNode } from "lucide";
|
|
2
|
+
import {
|
|
3
|
+
// ---------- Mandatory (referenced as string literals in widget source) ----------
|
|
4
|
+
Activity,
|
|
5
|
+
ArrowDown,
|
|
6
|
+
ArrowUp,
|
|
7
|
+
ArrowUpRight,
|
|
8
|
+
Bot,
|
|
9
|
+
ChevronDown,
|
|
10
|
+
ChevronUp,
|
|
11
|
+
ChevronRight,
|
|
12
|
+
ChevronLeft,
|
|
13
|
+
Check,
|
|
14
|
+
Clipboard,
|
|
15
|
+
ClipboardCopy,
|
|
16
|
+
Copy,
|
|
17
|
+
File as FileIcon,
|
|
18
|
+
FileCode,
|
|
19
|
+
FileSpreadsheet,
|
|
20
|
+
FileText,
|
|
21
|
+
ImagePlus,
|
|
22
|
+
Loader,
|
|
23
|
+
LoaderCircle,
|
|
24
|
+
Mic,
|
|
25
|
+
Paperclip,
|
|
26
|
+
RefreshCw,
|
|
27
|
+
Search,
|
|
28
|
+
Send,
|
|
29
|
+
ShieldAlert,
|
|
30
|
+
ShieldCheck,
|
|
31
|
+
ShieldX,
|
|
32
|
+
Square,
|
|
33
|
+
ThumbsDown,
|
|
34
|
+
ThumbsUp,
|
|
35
|
+
Upload,
|
|
36
|
+
Volume2,
|
|
37
|
+
X,
|
|
38
|
+
// ---------- Forms / inputs ----------
|
|
39
|
+
User,
|
|
40
|
+
Mail,
|
|
41
|
+
Phone,
|
|
42
|
+
Calendar,
|
|
43
|
+
Clock,
|
|
44
|
+
Building,
|
|
45
|
+
MapPin,
|
|
46
|
+
Lock,
|
|
47
|
+
Key,
|
|
48
|
+
CreditCard,
|
|
49
|
+
AtSign,
|
|
50
|
+
Hash,
|
|
51
|
+
Globe,
|
|
52
|
+
Link,
|
|
53
|
+
// ---------- Status / feedback ----------
|
|
54
|
+
CircleCheck,
|
|
55
|
+
CircleX,
|
|
56
|
+
TriangleAlert,
|
|
57
|
+
Info,
|
|
58
|
+
Ban,
|
|
59
|
+
Shield,
|
|
60
|
+
// ---------- Navigation ----------
|
|
61
|
+
ArrowLeft,
|
|
62
|
+
ArrowRight,
|
|
63
|
+
ExternalLink,
|
|
64
|
+
Ellipsis,
|
|
65
|
+
EllipsisVertical,
|
|
66
|
+
Menu,
|
|
67
|
+
House,
|
|
68
|
+
// ---------- Actions ----------
|
|
69
|
+
Plus,
|
|
70
|
+
Minus,
|
|
71
|
+
Pencil,
|
|
72
|
+
Trash,
|
|
73
|
+
Trash2,
|
|
74
|
+
Save,
|
|
75
|
+
Download,
|
|
76
|
+
Share,
|
|
77
|
+
Funnel,
|
|
78
|
+
Settings,
|
|
79
|
+
RotateCw,
|
|
80
|
+
Maximize,
|
|
81
|
+
Minimize,
|
|
82
|
+
// ---------- Commerce ----------
|
|
83
|
+
ShoppingCart,
|
|
84
|
+
ShoppingBag,
|
|
85
|
+
Package,
|
|
86
|
+
Truck,
|
|
87
|
+
Tag,
|
|
88
|
+
Gift,
|
|
89
|
+
Receipt,
|
|
90
|
+
Wallet,
|
|
91
|
+
Store,
|
|
92
|
+
DollarSign,
|
|
93
|
+
Percent,
|
|
94
|
+
// ---------- Media ----------
|
|
95
|
+
Play,
|
|
96
|
+
Pause,
|
|
97
|
+
VolumeX,
|
|
98
|
+
Camera,
|
|
99
|
+
Image as ImageIcon,
|
|
100
|
+
Film,
|
|
101
|
+
Headphones,
|
|
102
|
+
// ---------- Social / Comms ----------
|
|
103
|
+
MessageCircle,
|
|
104
|
+
MessageSquare,
|
|
105
|
+
Bell,
|
|
106
|
+
Heart,
|
|
107
|
+
Star,
|
|
108
|
+
Eye,
|
|
109
|
+
EyeOff,
|
|
110
|
+
Bookmark,
|
|
111
|
+
// ---------- Time ----------
|
|
112
|
+
CalendarDays,
|
|
113
|
+
History,
|
|
114
|
+
Timer,
|
|
115
|
+
// ---------- Files ----------
|
|
116
|
+
Folder,
|
|
117
|
+
FolderOpen,
|
|
118
|
+
Files,
|
|
119
|
+
// ---------- Decorative ----------
|
|
120
|
+
Sparkles,
|
|
121
|
+
Zap,
|
|
122
|
+
Sun,
|
|
123
|
+
Moon,
|
|
124
|
+
Flag,
|
|
125
|
+
// ---------- Devices ----------
|
|
126
|
+
Monitor,
|
|
127
|
+
Smartphone,
|
|
128
|
+
} from "lucide";
|
|
3
129
|
|
|
4
130
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
131
|
+
* Curated registry of lucide icons available to `renderLucideIcon`.
|
|
132
|
+
*
|
|
133
|
+
* The widget used to do `import * as icons from "lucide"` and look up
|
|
134
|
+
* icons dynamically by string. That defeated tree-shaking, so the IIFE
|
|
135
|
+
* (CDN/script-tag) bundle shipped all 1640 lucide icons (~400KB of icon
|
|
136
|
+
* data) regardless of which we actually used. This explicit registry
|
|
137
|
+
* lets the bundler drop any icon not listed here.
|
|
138
|
+
*
|
|
139
|
+
* Trade-off: `renderLucideIcon(name)` is now a *closed set*. Names not
|
|
140
|
+
* in this map return `null` and log a warning, exactly as a typo did
|
|
141
|
+
* before. The registry is intentionally generous (~110 icons) so that
|
|
142
|
+
* custom `ComponentRenderer` authors rarely hit a missing-icon dead end.
|
|
143
|
+
*
|
|
144
|
+
* To add icons: add a named import above and a row in `LUCIDE_ICONS`,
|
|
145
|
+
* keyed by the lucide kebab-case name (matches their filename and
|
|
146
|
+
* https://lucide.dev/icons).
|
|
147
|
+
*
|
|
148
|
+
* See `packages/widget/docs/icon-registry-shortlist.md` for the full
|
|
149
|
+
* curation rationale and which icons were considered but excluded.
|
|
150
|
+
*/
|
|
151
|
+
const LUCIDE_ICONS = {
|
|
152
|
+
// Mandatory
|
|
153
|
+
"activity": Activity,
|
|
154
|
+
"arrow-down": ArrowDown,
|
|
155
|
+
"arrow-up": ArrowUp,
|
|
156
|
+
"arrow-up-right": ArrowUpRight,
|
|
157
|
+
"bot": Bot,
|
|
158
|
+
"chevron-down": ChevronDown,
|
|
159
|
+
"chevron-up": ChevronUp,
|
|
160
|
+
"chevron-right": ChevronRight,
|
|
161
|
+
"chevron-left": ChevronLeft,
|
|
162
|
+
"check": Check,
|
|
163
|
+
"clipboard": Clipboard,
|
|
164
|
+
"clipboard-copy": ClipboardCopy,
|
|
165
|
+
"copy": Copy,
|
|
166
|
+
"file": FileIcon,
|
|
167
|
+
"file-code": FileCode,
|
|
168
|
+
"file-spreadsheet": FileSpreadsheet,
|
|
169
|
+
"file-text": FileText,
|
|
170
|
+
"image-plus": ImagePlus,
|
|
171
|
+
"loader": Loader,
|
|
172
|
+
"loader-circle": LoaderCircle,
|
|
173
|
+
"mic": Mic,
|
|
174
|
+
"paperclip": Paperclip,
|
|
175
|
+
"refresh-cw": RefreshCw,
|
|
176
|
+
"search": Search,
|
|
177
|
+
"send": Send,
|
|
178
|
+
"shield-alert": ShieldAlert,
|
|
179
|
+
"shield-check": ShieldCheck,
|
|
180
|
+
"shield-x": ShieldX,
|
|
181
|
+
"square": Square,
|
|
182
|
+
"thumbs-down": ThumbsDown,
|
|
183
|
+
"thumbs-up": ThumbsUp,
|
|
184
|
+
"upload": Upload,
|
|
185
|
+
"volume-2": Volume2,
|
|
186
|
+
"x": X,
|
|
187
|
+
// Forms / inputs
|
|
188
|
+
"user": User,
|
|
189
|
+
"mail": Mail,
|
|
190
|
+
"phone": Phone,
|
|
191
|
+
"calendar": Calendar,
|
|
192
|
+
"clock": Clock,
|
|
193
|
+
"building": Building,
|
|
194
|
+
"map-pin": MapPin,
|
|
195
|
+
"lock": Lock,
|
|
196
|
+
"key": Key,
|
|
197
|
+
"credit-card": CreditCard,
|
|
198
|
+
"at-sign": AtSign,
|
|
199
|
+
"hash": Hash,
|
|
200
|
+
"globe": Globe,
|
|
201
|
+
"link": Link,
|
|
202
|
+
// Status / feedback
|
|
203
|
+
"circle-check": CircleCheck,
|
|
204
|
+
"circle-x": CircleX,
|
|
205
|
+
"triangle-alert": TriangleAlert,
|
|
206
|
+
"info": Info,
|
|
207
|
+
"ban": Ban,
|
|
208
|
+
"shield": Shield,
|
|
209
|
+
// Navigation
|
|
210
|
+
"arrow-left": ArrowLeft,
|
|
211
|
+
"arrow-right": ArrowRight,
|
|
212
|
+
"external-link": ExternalLink,
|
|
213
|
+
"ellipsis": Ellipsis,
|
|
214
|
+
"ellipsis-vertical": EllipsisVertical,
|
|
215
|
+
"menu": Menu,
|
|
216
|
+
"house": House,
|
|
217
|
+
// Actions
|
|
218
|
+
"plus": Plus,
|
|
219
|
+
"minus": Minus,
|
|
220
|
+
"pencil": Pencil,
|
|
221
|
+
"trash": Trash,
|
|
222
|
+
"trash-2": Trash2,
|
|
223
|
+
"save": Save,
|
|
224
|
+
"download": Download,
|
|
225
|
+
"share": Share,
|
|
226
|
+
"funnel": Funnel,
|
|
227
|
+
"settings": Settings,
|
|
228
|
+
"rotate-cw": RotateCw,
|
|
229
|
+
"maximize": Maximize,
|
|
230
|
+
"minimize": Minimize,
|
|
231
|
+
// Commerce
|
|
232
|
+
"shopping-cart": ShoppingCart,
|
|
233
|
+
"shopping-bag": ShoppingBag,
|
|
234
|
+
"package": Package,
|
|
235
|
+
"truck": Truck,
|
|
236
|
+
"tag": Tag,
|
|
237
|
+
"gift": Gift,
|
|
238
|
+
"receipt": Receipt,
|
|
239
|
+
"wallet": Wallet,
|
|
240
|
+
"store": Store,
|
|
241
|
+
"dollar-sign": DollarSign,
|
|
242
|
+
"percent": Percent,
|
|
243
|
+
// Media
|
|
244
|
+
"play": Play,
|
|
245
|
+
"pause": Pause,
|
|
246
|
+
"volume-x": VolumeX,
|
|
247
|
+
"camera": Camera,
|
|
248
|
+
"image": ImageIcon,
|
|
249
|
+
"film": Film,
|
|
250
|
+
"headphones": Headphones,
|
|
251
|
+
// Social / Comms
|
|
252
|
+
"message-circle": MessageCircle,
|
|
253
|
+
"message-square": MessageSquare,
|
|
254
|
+
"bell": Bell,
|
|
255
|
+
"heart": Heart,
|
|
256
|
+
"star": Star,
|
|
257
|
+
"eye": Eye,
|
|
258
|
+
"eye-off": EyeOff,
|
|
259
|
+
"bookmark": Bookmark,
|
|
260
|
+
// Time
|
|
261
|
+
"calendar-days": CalendarDays,
|
|
262
|
+
"history": History,
|
|
263
|
+
"timer": Timer,
|
|
264
|
+
// Files
|
|
265
|
+
"folder": Folder,
|
|
266
|
+
"folder-open": FolderOpen,
|
|
267
|
+
"files": Files,
|
|
268
|
+
// Decorative
|
|
269
|
+
"sparkles": Sparkles,
|
|
270
|
+
"zap": Zap,
|
|
271
|
+
"sun": Sun,
|
|
272
|
+
"moon": Moon,
|
|
273
|
+
"flag": Flag,
|
|
274
|
+
// Devices
|
|
275
|
+
"monitor": Monitor,
|
|
276
|
+
"smartphone": Smartphone,
|
|
277
|
+
} as const satisfies Record<string, IconNode>;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Names of lucide icons that ship with the widget. Names not in this
|
|
281
|
+
* union return `null` from `renderLucideIcon` (with a console warning).
|
|
282
|
+
*/
|
|
283
|
+
export type IconName = keyof typeof LUCIDE_ICONS;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Renders a lucide icon as an inline SVG element. Works inside Shadow
|
|
287
|
+
* DOM and requires no CSS.
|
|
288
|
+
*
|
|
289
|
+
* @param iconName - A lucide kebab-case name from the registry. See
|
|
290
|
+
* `IconName` for the full list, or `docs/icon-registry-shortlist.md`
|
|
291
|
+
* for rationale.
|
|
292
|
+
* @param size - The size in pixels (number) or any CSS length string.
|
|
293
|
+
* @param color - Stroke color (default: "currentColor").
|
|
294
|
+
* @param strokeWidth - Stroke width (default: 2).
|
|
295
|
+
* @returns SVGElement, or null if the name is not in the registry.
|
|
13
296
|
*/
|
|
14
297
|
export const renderLucideIcon = (
|
|
15
|
-
iconName: string,
|
|
298
|
+
iconName: IconName | (string & {}),
|
|
16
299
|
size: number | string = 24,
|
|
17
300
|
color: string = "currentColor",
|
|
18
301
|
strokeWidth: number = 2
|
|
19
302
|
): SVGElement | null => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// Lucide's icons object contains IconNode data directly, not functions
|
|
28
|
-
const iconData = (icons as unknown as Record<string, IconNode>)[pascalName] as IconNode;
|
|
29
|
-
|
|
30
|
-
if (!iconData) {
|
|
31
|
-
console.warn(`Lucide icon "${iconName}" not found (tried "${pascalName}"). Available icons: https://lucide.dev/icons`);
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return createSvgFromIconData(iconData, size, color, strokeWidth);
|
|
36
|
-
} catch (error) {
|
|
37
|
-
console.warn(`Failed to render Lucide icon "${iconName}":`, error);
|
|
303
|
+
const iconData = (LUCIDE_ICONS as Record<string, IconNode | undefined>)[iconName];
|
|
304
|
+
if (!iconData) {
|
|
305
|
+
console.warn(
|
|
306
|
+
`Lucide icon "${iconName}" is not in the Persona registry. ` +
|
|
307
|
+
`Add it to packages/widget/src/utils/icons.ts (see docs/icon-registry-shortlist.md).`
|
|
308
|
+
);
|
|
38
309
|
return null;
|
|
39
310
|
}
|
|
311
|
+
return createSvgFromIconData(iconData, size, color, strokeWidth);
|
|
40
312
|
};
|
|
41
313
|
|
|
42
|
-
/**
|
|
43
|
-
* Helper function to create SVG from IconNode data
|
|
44
|
-
*/
|
|
45
314
|
function createSvgFromIconData(
|
|
46
315
|
iconData: IconNode,
|
|
47
316
|
size: number | string,
|
|
48
317
|
color: string,
|
|
49
318
|
strokeWidth: number
|
|
50
319
|
): SVGElement | null {
|
|
51
|
-
if (!
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
320
|
+
if (!Array.isArray(iconData)) return null;
|
|
54
321
|
|
|
55
|
-
// Create SVG element
|
|
56
322
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
57
323
|
svg.setAttribute("width", String(size));
|
|
58
324
|
svg.setAttribute("height", String(size));
|
|
@@ -63,30 +329,20 @@ function createSvgFromIconData(
|
|
|
63
329
|
svg.setAttribute("stroke-linecap", "round");
|
|
64
330
|
svg.setAttribute("stroke-linejoin", "round");
|
|
65
331
|
svg.setAttribute("aria-hidden", "true");
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
// IconNode format: [["path", {"d": "..."}], ["rect", {"x": "...", "y": "..."}], ...]
|
|
332
|
+
|
|
333
|
+
// IconNode shape: [["path", {"d": "..."}], ["circle", {"cx": "..."}], ...]
|
|
69
334
|
iconData.forEach((elementData) => {
|
|
70
|
-
if (Array.isArray(elementData)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (key !== "stroke") {
|
|
81
|
-
element.setAttribute(key, String(value));
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
svg.appendChild(element);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
335
|
+
if (!Array.isArray(elementData) || elementData.length < 2) return;
|
|
336
|
+
const tagName = elementData[0] as string;
|
|
337
|
+
const attrs = elementData[1] as Record<string, string> | undefined;
|
|
338
|
+
if (!attrs) return;
|
|
339
|
+
const element = document.createElementNS("http://www.w3.org/2000/svg", tagName);
|
|
340
|
+
Object.entries(attrs).forEach(([key, value]) => {
|
|
341
|
+
// Skip 'stroke' so the parent SVG's stroke attribute drives color uniformly
|
|
342
|
+
if (key !== "stroke") element.setAttribute(key, String(value));
|
|
343
|
+
});
|
|
344
|
+
svg.appendChild(element);
|
|
88
345
|
});
|
|
89
|
-
|
|
346
|
+
|
|
90
347
|
return svg;
|
|
91
348
|
}
|
|
92
|
-
|
|
@@ -275,7 +275,7 @@ export const wrapStreamAnimation = (
|
|
|
275
275
|
html: string,
|
|
276
276
|
mode: "char" | "word",
|
|
277
277
|
messageId: string,
|
|
278
|
-
options?: { skipTags?: string[] }
|
|
278
|
+
options?: { skipTags?: string[]; startIndex?: number }
|
|
279
279
|
): string => {
|
|
280
280
|
if (!html) return html;
|
|
281
281
|
if (typeof document === "undefined") return html;
|
|
@@ -294,7 +294,12 @@ export const wrapStreamAnimation = (
|
|
|
294
294
|
node = walker.nextNode();
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
|
|
297
|
+
// `startIndex` lets callers number spans by their absolute position in a
|
|
298
|
+
// larger string, even when only a slice is being wrapped. The peek banner
|
|
299
|
+
// uses this so per-char span IDs stay stable as the trailing-100-char
|
|
300
|
+
// window shifts each chunk — idiomorph then preserves animations on
|
|
301
|
+
// already-revealed chars instead of restarting them.
|
|
302
|
+
const counterRef = { value: options?.startIndex ?? 0 };
|
|
298
303
|
const wrap = mode === "char" ? wrapTextNodeChars : wrapTextNodeWords;
|
|
299
304
|
for (const textNode of textNodes) {
|
|
300
305
|
wrap(textNode, messageId, counterRef);
|