@runtypelabs/persona 3.17.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 +143 -1
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-HPZY7oAI.d.cts → types-cwY5HaFD.d.cts} +25 -0
- package/dist/animations/{types-HPZY7oAI.d.ts → types-cwY5HaFD.d.ts} +25 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/index.cjs +47 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +580 -4
- package/dist/index.d.ts +580 -4
- package/dist/index.global.js +102 -1636
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +45 -45
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +2844 -752
- package/dist/theme-editor.d.cts +337 -1
- package/dist/theme-editor.d.ts +337 -1
- package/dist/theme-editor.js +2958 -752
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +14 -0
- package/dist/theme-reference.d.ts +14 -0
- package/dist/widget.css +780 -0
- package/package.json +1 -1
- package/src/client.test.ts +134 -0
- package/src/client.ts +71 -0
- package/src/components/ask-user-question-bubble.test.ts +583 -0
- package/src/components/ask-user-question-bubble.ts +924 -0
- 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/messages.ts +33 -1
- package/src/components/panel.test.ts +61 -0
- package/src/components/panel.ts +303 -9
- package/src/components/pill-composer-builder.test.ts +85 -0
- package/src/components/pill-composer-builder.ts +183 -0
- package/src/defaults.ts +21 -0
- package/src/index.ts +20 -1
- package/src/plugins/types.ts +57 -0
- package/src/runtime/init.ts +4 -2
- package/src/runtime/persist-state.test.ts +152 -0
- package/src/session.test.ts +183 -0
- package/src/session.ts +242 -3
- package/src/styles/widget.css +780 -0
- package/src/types/theme.ts +15 -0
- package/src/types.ts +271 -1
- package/src/ui.ask-user-question-plugin.test.ts +649 -0
- package/src/ui.component-directive.test.ts +183 -0
- package/src/ui.composer-bar.test.ts +1009 -0
- package/src/ui.ts +1439 -76
- 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/storage.ts +10 -2
- package/src/utils/stream-animation.ts +7 -2
- package/src/utils/theme.test.ts +36 -0
- package/src/utils/tokens.ts +23 -0
|
@@ -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
|
-
|
package/src/utils/storage.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentWidgetMessage,
|
|
3
3
|
AgentWidgetStorageAdapter,
|
|
4
|
-
AgentWidgetStoredState
|
|
4
|
+
AgentWidgetStoredState,
|
|
5
|
+
PersonaArtifactRecord
|
|
5
6
|
} from "../types";
|
|
6
7
|
|
|
7
8
|
const safeJsonParse = (value: string | null) => {
|
|
@@ -23,6 +24,12 @@ const sanitizeMessages = (messages: AgentWidgetMessage[]) =>
|
|
|
23
24
|
streaming: false
|
|
24
25
|
}));
|
|
25
26
|
|
|
27
|
+
const sanitizeArtifacts = (artifacts: PersonaArtifactRecord[]) =>
|
|
28
|
+
artifacts.map((artifact) => ({
|
|
29
|
+
...artifact,
|
|
30
|
+
status: "complete" as const
|
|
31
|
+
}));
|
|
32
|
+
|
|
26
33
|
export const createLocalStorageAdapter = (
|
|
27
34
|
key = "persona-state"
|
|
28
35
|
): AgentWidgetStorageAdapter => {
|
|
@@ -45,7 +52,8 @@ export const createLocalStorageAdapter = (
|
|
|
45
52
|
try {
|
|
46
53
|
const payload: AgentWidgetStoredState = {
|
|
47
54
|
...state,
|
|
48
|
-
messages: state.messages ? sanitizeMessages(state.messages) : undefined
|
|
55
|
+
messages: state.messages ? sanitizeMessages(state.messages) : undefined,
|
|
56
|
+
artifacts: state.artifacts ? sanitizeArtifacts(state.artifacts) : undefined
|
|
49
57
|
};
|
|
50
58
|
storage.setItem(key, JSON.stringify(payload));
|
|
51
59
|
} catch (error) {
|
|
@@ -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);
|
package/src/utils/theme.test.ts
CHANGED
|
@@ -252,6 +252,42 @@ describe('theme utils', () => {
|
|
|
252
252
|
expect(cssVars['--persona-scroll-to-bottom-icon-size']).toBe('14px');
|
|
253
253
|
});
|
|
254
254
|
|
|
255
|
+
it('maps introCard component tokens to dedicated CSS variables', () => {
|
|
256
|
+
const theme = createTheme({
|
|
257
|
+
components: {
|
|
258
|
+
introCard: {
|
|
259
|
+
background: 'palette.colors.accent.50',
|
|
260
|
+
borderRadius: 'palette.radius.xl',
|
|
261
|
+
padding: 'semantic.spacing.lg',
|
|
262
|
+
shadow: '0 10px 30px rgba(53, 44, 131, 0.15)',
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
} as any);
|
|
266
|
+
|
|
267
|
+
const cssVars = themeToCssVariables(theme);
|
|
268
|
+
|
|
269
|
+
expect(cssVars['--persona-components-introCard-background']).toBe('#ecfeff');
|
|
270
|
+
expect(cssVars['--persona-components-introCard-borderRadius']).toBe('0.75rem');
|
|
271
|
+
expect(cssVars['--persona-components-introCard-padding']).toBe('1.5rem');
|
|
272
|
+
expect(cssVars['--persona-components-introCard-shadow']).toBe(
|
|
273
|
+
'0 10px 30px rgba(53, 44, 131, 0.15)'
|
|
274
|
+
);
|
|
275
|
+
expect(cssVars['--persona-intro-card-bg']).toBe('#ecfeff');
|
|
276
|
+
expect(cssVars['--persona-intro-card-radius']).toBe('0.75rem');
|
|
277
|
+
expect(cssVars['--persona-intro-card-padding']).toBe('1.5rem');
|
|
278
|
+
expect(cssVars['--persona-intro-card-shadow']).toBe(
|
|
279
|
+
'0 10px 30px rgba(53, 44, 131, 0.15)'
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('falls back to the legacy intro-card shadow when no token is set', () => {
|
|
284
|
+
const theme = createTheme({});
|
|
285
|
+
const cssVars = themeToCssVariables(theme);
|
|
286
|
+
expect(cssVars['--persona-intro-card-shadow']).toBe(
|
|
287
|
+
'0 5px 15px rgba(15, 23, 42, 0.08)'
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
255
291
|
it('lets config.toolCall.shadow override theme tool bubble shadow on the root element', () => {
|
|
256
292
|
const el = document.createElement('div');
|
|
257
293
|
applyThemeVariables(el, {
|
package/src/utils/tokens.ts
CHANGED
|
@@ -318,6 +318,14 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
318
318
|
},
|
|
319
319
|
border: 'semantic.colors.border',
|
|
320
320
|
},
|
|
321
|
+
introCard: {
|
|
322
|
+
// Defaults preserve the legacy `persona-shadow-sm` look exactly so existing
|
|
323
|
+
// pages render unchanged when no token is set.
|
|
324
|
+
background: 'semantic.colors.surface',
|
|
325
|
+
borderRadius: 'palette.radius.2xl',
|
|
326
|
+
padding: 'semantic.spacing.lg',
|
|
327
|
+
shadow: '0 5px 15px rgba(15, 23, 42, 0.08)',
|
|
328
|
+
},
|
|
321
329
|
toolBubble: {
|
|
322
330
|
shadow: 'palette.shadows.sm',
|
|
323
331
|
},
|
|
@@ -763,6 +771,21 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
763
771
|
if (headerTokens?.shadow) cssVars['--persona-header-shadow'] = headerTokens.shadow;
|
|
764
772
|
if (headerTokens?.borderBottom) cssVars['--persona-header-border-bottom'] = headerTokens.borderBottom;
|
|
765
773
|
|
|
774
|
+
// Intro card aliases — short names the panel inline-styles read directly.
|
|
775
|
+
// The full-path `--persona-components-introCard-*` variables auto-emit above;
|
|
776
|
+
// these mirror them with sensible fallbacks so existing pages keep their look.
|
|
777
|
+
const introCardTokens = theme.components?.introCard;
|
|
778
|
+
cssVars['--persona-intro-card-bg'] =
|
|
779
|
+
cssVars['--persona-components-introCard-background'] ?? cssVars['--persona-surface'];
|
|
780
|
+
cssVars['--persona-intro-card-radius'] =
|
|
781
|
+
cssVars['--persona-components-introCard-borderRadius'] ?? '1rem';
|
|
782
|
+
cssVars['--persona-intro-card-padding'] =
|
|
783
|
+
cssVars['--persona-components-introCard-padding'] ?? '1.5rem';
|
|
784
|
+
cssVars['--persona-intro-card-shadow'] =
|
|
785
|
+
introCardTokens?.shadow
|
|
786
|
+
?? cssVars['--persona-components-introCard-shadow']
|
|
787
|
+
?? '0 5px 15px rgba(15, 23, 42, 0.08)';
|
|
788
|
+
|
|
766
789
|
cssVars['--persona-input-background'] =
|
|
767
790
|
cssVars['--persona-components-input-background'] ?? cssVars['--persona-surface'];
|
|
768
791
|
cssVars['--persona-input-placeholder'] =
|