@runtypelabs/persona 2.1.0 → 2.2.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/dist/widget.css CHANGED
@@ -2064,9 +2064,9 @@
2064
2064
  display: inline-flex;
2065
2065
  align-items: center;
2066
2066
  justify-content: center;
2067
- padding: 0.25rem;
2068
- border-radius: var(--persona-radius-md, 0.375rem);
2069
- border: 1px solid var(--persona-border, #e5e7eb);
2067
+ padding: var(--persona-artifact-toolbar-icon-padding, 0.25rem);
2068
+ border-radius: var(--persona-artifact-toolbar-icon-radius, var(--persona-radius-md, 0.375rem));
2069
+ border: var(--persona-artifact-toolbar-icon-border, 1px solid var(--persona-border, #e5e7eb));
2070
2070
  background: var(--persona-surface, #ffffff);
2071
2071
  color: var(--persona-artifact-doc-toolbar-icon-color, var(--persona-text, #111827));
2072
2072
  cursor: pointer;
@@ -2074,7 +2074,8 @@
2074
2074
  }
2075
2075
 
2076
2076
  #persona-root .persona-artifact-toolbar-document .persona-artifact-doc-icon-btn:hover {
2077
- background: var(--persona-container, #f3f4f6);
2077
+ color: var(--persona-artifact-toolbar-icon-hover-color, inherit);
2078
+ background: var(--persona-artifact-toolbar-icon-hover-bg, var(--persona-container, #f3f4f6));
2078
2079
  }
2079
2080
 
2080
2081
  #persona-root .persona-artifact-toolbar-document .persona-artifact-doc-icon-btn[aria-pressed="true"] {
@@ -2086,24 +2087,53 @@
2086
2087
  display: inline-flex;
2087
2088
  align-items: center;
2088
2089
  gap: 0.35rem;
2089
- padding: 0.25rem 0.5rem;
2090
- border-radius: var(--persona-radius-md, 0.375rem);
2091
- border: 1px solid var(--persona-border, #e5e7eb);
2092
- background: var(--persona-surface, #ffffff);
2093
- color: var(--persona-artifact-doc-toolbar-icon-color, var(--persona-text, #111827));
2090
+ padding: var(--persona-artifact-toolbar-copy-padding, 0.25rem 0.5rem);
2091
+ border-radius: var(--persona-artifact-toolbar-copy-radius, var(--persona-radius-md, 0.375rem));
2092
+ border: var(--persona-artifact-toolbar-copy-border, 1px solid var(--persona-border, #e5e7eb));
2093
+ background: var(--persona-artifact-toolbar-copy-bg, var(--persona-surface, #ffffff));
2094
+ color: var(--persona-artifact-toolbar-copy-color, var(--persona-artifact-doc-toolbar-icon-color, var(--persona-text, #111827)));
2094
2095
  cursor: pointer;
2095
2096
  font-size: 0.75rem;
2096
2097
  line-height: 1.25;
2097
2098
  }
2098
2099
 
2099
2100
  #persona-root .persona-artifact-toolbar-document .persona-artifact-doc-copy-btn:hover {
2100
- background: var(--persona-container, #f3f4f6);
2101
+ background: var(--persona-artifact-toolbar-icon-hover-bg, var(--persona-container, #f3f4f6));
2101
2102
  }
2102
2103
 
2103
2104
  #persona-root .persona-artifact-toolbar-document .persona-artifact-doc-copy-label {
2104
2105
  font-weight: 500;
2105
2106
  }
2106
2107
 
2108
+ /* Copy menu dropdown theming */
2109
+ #persona-root .persona-artifact-doc-copy-menu {
2110
+ background: var(--persona-artifact-toolbar-copy-menu-bg, var(--persona-surface, #fff));
2111
+ border: var(--persona-artifact-toolbar-copy-menu-border, 1px solid var(--persona-border, #e5e7eb));
2112
+ box-shadow: var(--persona-artifact-toolbar-copy-menu-shadow, 0 4px 6px -1px rgba(0,0,0,.1));
2113
+ border-radius: var(--persona-artifact-toolbar-copy-menu-radius, 0.375rem);
2114
+ }
2115
+
2116
+ #persona-root .persona-artifact-doc-copy-menu button:hover {
2117
+ background: var(--persona-artifact-toolbar-copy-menu-item-hover-bg, var(--persona-container, #f3f4f6));
2118
+ }
2119
+
2120
+ /* Artifact tab theming */
2121
+ #persona-root .persona-artifact-tab {
2122
+ background: var(--persona-artifact-tab-bg, transparent);
2123
+ border-radius: var(--persona-artifact-tab-radius, 0.5rem);
2124
+ color: var(--persona-artifact-tab-color, inherit);
2125
+ }
2126
+
2127
+ #persona-root .persona-artifact-tab.persona-bg-persona-container {
2128
+ background: var(--persona-artifact-tab-active-bg, var(--persona-container, #f3f4f6));
2129
+ border-color: var(--persona-artifact-tab-active-border, var(--persona-border, #e5e7eb));
2130
+ }
2131
+
2132
+ /* Artifact toolbar background theming */
2133
+ #persona-root .persona-artifact-toolbar {
2134
+ background: var(--persona-artifact-toolbar-bg, var(--persona-surface, #fff));
2135
+ }
2136
+
2107
2137
  /* Draggable split handle (desktop split only; hidden in drawer / narrow host / small viewport) */
2108
2138
  #persona-root .persona-artifact-split-handle {
2109
2139
  width: 6px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -47,6 +47,7 @@
47
47
  "engines": {
48
48
  "node": ">=18.17.0"
49
49
  },
50
+ "author": "Runtype",
50
51
  "license": "MIT",
51
52
  "keywords": [
52
53
  "ai",
package/src/client.ts CHANGED
@@ -45,7 +45,6 @@ const DEFAULT_CLIENT_API_BASE = "https://api.runtype.com";
45
45
  * Check if a message has valid (non-empty) content for sending to the API.
46
46
  * Filters out messages with empty content that would cause validation errors.
47
47
  *
48
- * @see https://github.com/anthropics/claude-code/issues/XXX - Empty assistant messages from failed requests
49
48
  */
50
49
  const hasValidContent = (message: AgentWidgetMessage): boolean => {
51
50
  // Check contentParts (multi-modal content)
@@ -127,6 +127,7 @@ export function createArtifactPane(
127
127
  "aside",
128
128
  "persona-artifact-pane persona-flex persona-flex-col persona-min-h-0 persona-min-w-0 persona-bg-persona-surface persona-text-persona-primary persona-border-l persona-border-persona-border"
129
129
  );
130
+ shell.setAttribute("data-persona-theme-zone", "artifact-pane");
130
131
  if (documentChrome) {
131
132
  shell.classList.add("persona-artifact-pane-document");
132
133
  }
@@ -135,6 +136,7 @@ export function createArtifactPane(
135
136
  "div",
136
137
  "persona-artifact-toolbar persona-flex persona-items-center persona-justify-between persona-gap-2 persona-px-2 persona-py-2 persona-border-b persona-border-persona-border persona-shrink-0"
137
138
  );
139
+ toolbar.setAttribute("data-persona-theme-zone", "artifact-toolbar");
138
140
  if (documentChrome) {
139
141
  toolbar.classList.add("persona-artifact-toolbar-document");
140
142
  }
@@ -68,6 +68,7 @@ export const buildComposer = (context: ComposerBuildContext): ComposerElements =
68
68
  "div",
69
69
  "persona-widget-footer persona-border-t-persona-divider persona-bg-persona-surface persona-px-6 persona-py-4"
70
70
  );
71
+ footer.setAttribute("data-persona-theme-zone", "composer");
71
72
 
72
73
  const suggestions = createElement(
73
74
  "div",
@@ -31,6 +31,7 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
31
31
  "div",
32
32
  "persona-widget-header persona-flex persona-items-center persona-gap-3 persona-px-6 persona-py-5"
33
33
  );
34
+ header.setAttribute("data-persona-theme-zone", "header");
34
35
  header.style.backgroundColor = 'var(--persona-header-bg, var(--persona-surface, #ffffff))';
35
36
  header.style.borderBottomWidth = '1px';
36
37
  header.style.borderBottomStyle = 'solid';
@@ -24,12 +24,32 @@ export type HeaderLayoutRenderer = (context: HeaderLayoutContext) => HeaderEleme
24
24
  * Full header with icon, title, subtitle, clear chat, and close button
25
25
  */
26
26
  export const buildDefaultHeader: HeaderLayoutRenderer = (context) => {
27
- return buildHeader({
27
+ const elements = buildHeader({
28
28
  config: context.config,
29
29
  showClose: context.showClose,
30
30
  onClose: context.onClose,
31
31
  onClearChat: context.onClearChat
32
32
  });
33
+
34
+ // Make the title/subtitle area clickable when onTitleClick is provided
35
+ const onTitleClick = context.layoutHeaderConfig?.onTitleClick;
36
+ if (onTitleClick) {
37
+ const headerCopy = elements.headerTitle.parentElement;
38
+ if (headerCopy) {
39
+ headerCopy.style.cursor = "pointer";
40
+ headerCopy.setAttribute("role", "button");
41
+ headerCopy.setAttribute("tabindex", "0");
42
+ headerCopy.addEventListener("click", () => onTitleClick());
43
+ headerCopy.addEventListener("keydown", (e) => {
44
+ if (e.key === "Enter" || e.key === " ") {
45
+ e.preventDefault();
46
+ onTitleClick();
47
+ }
48
+ });
49
+ }
50
+ }
51
+
52
+ return elements;
33
53
  };
34
54
 
35
55
  /**
@@ -68,6 +88,7 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
68
88
  "div",
69
89
  "persona-flex persona-items-center persona-justify-between persona-bg-persona-surface persona-px-6 persona-py-4 persona-border-b-persona-divider"
70
90
  );
91
+ header.setAttribute("data-persona-theme-zone", "header");
71
92
 
72
93
  const titleRow = createElement(
73
94
  "div",
@@ -85,6 +106,25 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
85
106
  layoutHeaderConfig?.onAction ?? onHeaderAction
86
107
  );
87
108
 
109
+ // Make title row clickable when onTitleClick is provided
110
+ if (layoutHeaderConfig?.onTitleClick) {
111
+ titleRow.style.cursor = "pointer";
112
+ titleRow.setAttribute("role", "button");
113
+ titleRow.setAttribute("tabindex", "0");
114
+ const handleTitleClick = layoutHeaderConfig.onTitleClick;
115
+ titleRow.addEventListener("click", (e) => {
116
+ // Skip if the click was on a trailing action button
117
+ if ((e.target as HTMLElement).closest("button")) return;
118
+ handleTitleClick();
119
+ });
120
+ titleRow.addEventListener("keydown", (e) => {
121
+ if (e.key === "Enter" || e.key === " ") {
122
+ e.preventDefault();
123
+ handleTitleClick();
124
+ }
125
+ });
126
+ }
127
+
88
128
  header.appendChild(titleRow);
89
129
 
90
130
  // Close button
@@ -470,6 +470,8 @@ export const createStandardBubble = (
470
470
  bubble.id = `bubble-${message.id}`;
471
471
  bubble.setAttribute("data-message-id", message.id);
472
472
 
473
+ bubble.setAttribute("data-persona-theme-zone", message.role === "user" ? "user-message" : "assistant-message");
474
+
473
475
  // Apply component-level color overrides via CSS variables
474
476
  if (message.role === "user") {
475
477
  bubble.style.backgroundColor = 'var(--persona-message-user-bg, var(--persona-accent))';
@@ -116,6 +116,7 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
116
116
  "div",
117
117
  "persona-widget-container persona-flex persona-h-full persona-w-full persona-flex-1 persona-min-h-0 persona-flex-col persona-bg-persona-surface persona-text-persona-primary persona-rounded-2xl persona-overflow-hidden persona-border persona-border-persona-border"
118
118
  );
119
+ container.setAttribute("data-persona-theme-zone", "container");
119
120
 
120
121
  // Build header using layout config if available, otherwise use standard builder
121
122
  const headerLayoutConfig = config?.layout?.header;
@@ -130,6 +131,7 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
130
131
  "persona-widget-body persona-flex persona-flex-1 persona-min-h-0 persona-flex-col persona-gap-6 persona-overflow-y-auto persona-bg-persona-container persona-px-6 persona-py-6"
131
132
  );
132
133
  body.id = "persona-scroll-container";
134
+ body.setAttribute("data-persona-theme-zone", "messages");
133
135
 
134
136
  const introCard = createElement(
135
137
  "div",
package/src/index.ts CHANGED
@@ -189,8 +189,10 @@ export {
189
189
  DEFAULT_PALETTE,
190
190
  DEFAULT_SEMANTIC,
191
191
  DEFAULT_COMPONENTS,
192
- validateTheme
192
+ validateTheme,
193
+ THEME_ZONES
193
194
  } from "./utils/tokens";
195
+ export type { ThemeZone } from "./utils/tokens";
194
196
  export {
195
197
  accessibilityPlugin,
196
198
  animationsPlugin,
@@ -219,6 +221,9 @@ export type {
219
221
  SemanticSpacing,
220
222
  SemanticTypography,
221
223
  ComponentTokens,
224
+ ArtifactToolbarTokens,
225
+ ArtifactTabTokens,
226
+ ArtifactPaneTokens,
222
227
  ThemeValidationResult,
223
228
  ThemeValidationError
224
229
  } from "./types/theme";
@@ -245,6 +250,14 @@ export {
245
250
  DEFAULT_DARK_THEME,
246
251
  mergeWithDefaults
247
252
  } from "./defaults";
253
+ export {
254
+ PRESETS,
255
+ getPreset,
256
+ PRESET_SHOP,
257
+ PRESET_MINIMAL,
258
+ PRESET_FULLSCREEN
259
+ } from "./presets";
260
+ export type { WidgetPreset } from "./presets";
248
261
 
249
262
  // Layout system exports
250
263
  export {
package/src/presets.ts ADDED
@@ -0,0 +1,127 @@
1
+ import type { AgentWidgetConfig } from './types';
2
+
3
+ /**
4
+ * A named preset containing partial widget configuration.
5
+ * Apply with: `createAgentExperience(el, { ...PRESET_SHOP.config, apiUrl: '...' })`
6
+ * or via IIFE: `{ ...AgentWidget.PRESETS.shop.config, apiUrl: '...' }`
7
+ */
8
+ export interface WidgetPreset {
9
+ id: string;
10
+ label: string;
11
+ config: Partial<AgentWidgetConfig>;
12
+ }
13
+
14
+ /**
15
+ * Shopping / e-commerce preset.
16
+ * Dark header, rounded launchers, shopping-oriented copy.
17
+ */
18
+ export const PRESET_SHOP: WidgetPreset = {
19
+ id: 'shop',
20
+ label: 'Shopping Assistant',
21
+ config: {
22
+ theme: {
23
+ primary: '#111827',
24
+ accent: '#1d4ed8',
25
+ surface: '#ffffff',
26
+ muted: '#6b7280',
27
+ container: '#f8fafc',
28
+ border: '#f1f5f9',
29
+ divider: '#f1f5f9',
30
+ messageBorder: '#f1f5f9',
31
+ inputBackground: '#ffffff',
32
+ callToAction: '#000000',
33
+ callToActionBackground: '#ffffff',
34
+ sendButtonBackgroundColor: '#111827',
35
+ sendButtonTextColor: '#ffffff',
36
+ radiusSm: '0.75rem',
37
+ radiusMd: '1rem',
38
+ radiusLg: '1.5rem',
39
+ launcherRadius: '9999px',
40
+ buttonRadius: '9999px',
41
+ },
42
+ launcher: {
43
+ title: 'Shopping Assistant',
44
+ subtitle: 'Here to help you find what you need',
45
+ agentIconText: '🛍️',
46
+ position: 'bottom-right',
47
+ width: 'min(400px, calc(100vw - 24px))',
48
+ },
49
+ copy: {
50
+ welcomeTitle: 'Welcome to our shop!',
51
+ welcomeSubtitle: 'I can help you find products and answer questions',
52
+ inputPlaceholder: 'Ask me anything...',
53
+ sendButtonLabel: 'Send',
54
+ },
55
+ suggestionChips: [
56
+ 'What can you help me with?',
57
+ 'Tell me about your features',
58
+ 'How does this work?',
59
+ ],
60
+ },
61
+ };
62
+
63
+ /**
64
+ * Minimal preset.
65
+ * Stripped-down header, no launcher button, suitable for inline embeds.
66
+ */
67
+ export const PRESET_MINIMAL: WidgetPreset = {
68
+ id: 'minimal',
69
+ label: 'Minimal',
70
+ config: {
71
+ launcher: {
72
+ enabled: false,
73
+ fullHeight: true,
74
+ },
75
+ layout: {
76
+ header: {
77
+ layout: 'minimal',
78
+ showCloseButton: false,
79
+ },
80
+ messages: {
81
+ layout: 'minimal',
82
+ },
83
+ },
84
+ theme: {
85
+ panelBorderRadius: '0',
86
+ panelShadow: 'none',
87
+ },
88
+ },
89
+ };
90
+
91
+ /**
92
+ * Fullscreen assistant preset.
93
+ * No launcher, content-max-width constrained, minimal header.
94
+ */
95
+ export const PRESET_FULLSCREEN: WidgetPreset = {
96
+ id: 'fullscreen',
97
+ label: 'Fullscreen Assistant',
98
+ config: {
99
+ launcher: {
100
+ enabled: false,
101
+ fullHeight: true,
102
+ },
103
+ layout: {
104
+ header: {
105
+ layout: 'minimal',
106
+ showCloseButton: false,
107
+ },
108
+ contentMaxWidth: '72ch',
109
+ },
110
+ theme: {
111
+ panelBorderRadius: '0',
112
+ panelShadow: 'none',
113
+ },
114
+ },
115
+ };
116
+
117
+ /** All named presets keyed by ID. */
118
+ export const PRESETS: Record<string, WidgetPreset> = {
119
+ shop: PRESET_SHOP,
120
+ minimal: PRESET_MINIMAL,
121
+ fullscreen: PRESET_FULLSCREEN,
122
+ };
123
+
124
+ /** Look up a preset by ID. */
125
+ export function getPreset(id: string): WidgetPreset | undefined {
126
+ return PRESETS[id];
127
+ }
@@ -2064,9 +2064,9 @@
2064
2064
  display: inline-flex;
2065
2065
  align-items: center;
2066
2066
  justify-content: center;
2067
- padding: 0.25rem;
2068
- border-radius: var(--persona-radius-md, 0.375rem);
2069
- border: 1px solid var(--persona-border, #e5e7eb);
2067
+ padding: var(--persona-artifact-toolbar-icon-padding, 0.25rem);
2068
+ border-radius: var(--persona-artifact-toolbar-icon-radius, var(--persona-radius-md, 0.375rem));
2069
+ border: var(--persona-artifact-toolbar-icon-border, 1px solid var(--persona-border, #e5e7eb));
2070
2070
  background: var(--persona-surface, #ffffff);
2071
2071
  color: var(--persona-artifact-doc-toolbar-icon-color, var(--persona-text, #111827));
2072
2072
  cursor: pointer;
@@ -2074,7 +2074,8 @@
2074
2074
  }
2075
2075
 
2076
2076
  #persona-root .persona-artifact-toolbar-document .persona-artifact-doc-icon-btn:hover {
2077
- background: var(--persona-container, #f3f4f6);
2077
+ color: var(--persona-artifact-toolbar-icon-hover-color, inherit);
2078
+ background: var(--persona-artifact-toolbar-icon-hover-bg, var(--persona-container, #f3f4f6));
2078
2079
  }
2079
2080
 
2080
2081
  #persona-root .persona-artifact-toolbar-document .persona-artifact-doc-icon-btn[aria-pressed="true"] {
@@ -2086,24 +2087,53 @@
2086
2087
  display: inline-flex;
2087
2088
  align-items: center;
2088
2089
  gap: 0.35rem;
2089
- padding: 0.25rem 0.5rem;
2090
- border-radius: var(--persona-radius-md, 0.375rem);
2091
- border: 1px solid var(--persona-border, #e5e7eb);
2092
- background: var(--persona-surface, #ffffff);
2093
- color: var(--persona-artifact-doc-toolbar-icon-color, var(--persona-text, #111827));
2090
+ padding: var(--persona-artifact-toolbar-copy-padding, 0.25rem 0.5rem);
2091
+ border-radius: var(--persona-artifact-toolbar-copy-radius, var(--persona-radius-md, 0.375rem));
2092
+ border: var(--persona-artifact-toolbar-copy-border, 1px solid var(--persona-border, #e5e7eb));
2093
+ background: var(--persona-artifact-toolbar-copy-bg, var(--persona-surface, #ffffff));
2094
+ color: var(--persona-artifact-toolbar-copy-color, var(--persona-artifact-doc-toolbar-icon-color, var(--persona-text, #111827)));
2094
2095
  cursor: pointer;
2095
2096
  font-size: 0.75rem;
2096
2097
  line-height: 1.25;
2097
2098
  }
2098
2099
 
2099
2100
  #persona-root .persona-artifact-toolbar-document .persona-artifact-doc-copy-btn:hover {
2100
- background: var(--persona-container, #f3f4f6);
2101
+ background: var(--persona-artifact-toolbar-icon-hover-bg, var(--persona-container, #f3f4f6));
2101
2102
  }
2102
2103
 
2103
2104
  #persona-root .persona-artifact-toolbar-document .persona-artifact-doc-copy-label {
2104
2105
  font-weight: 500;
2105
2106
  }
2106
2107
 
2108
+ /* Copy menu dropdown theming */
2109
+ #persona-root .persona-artifact-doc-copy-menu {
2110
+ background: var(--persona-artifact-toolbar-copy-menu-bg, var(--persona-surface, #fff));
2111
+ border: var(--persona-artifact-toolbar-copy-menu-border, 1px solid var(--persona-border, #e5e7eb));
2112
+ box-shadow: var(--persona-artifact-toolbar-copy-menu-shadow, 0 4px 6px -1px rgba(0,0,0,.1));
2113
+ border-radius: var(--persona-artifact-toolbar-copy-menu-radius, 0.375rem);
2114
+ }
2115
+
2116
+ #persona-root .persona-artifact-doc-copy-menu button:hover {
2117
+ background: var(--persona-artifact-toolbar-copy-menu-item-hover-bg, var(--persona-container, #f3f4f6));
2118
+ }
2119
+
2120
+ /* Artifact tab theming */
2121
+ #persona-root .persona-artifact-tab {
2122
+ background: var(--persona-artifact-tab-bg, transparent);
2123
+ border-radius: var(--persona-artifact-tab-radius, 0.5rem);
2124
+ color: var(--persona-artifact-tab-color, inherit);
2125
+ }
2126
+
2127
+ #persona-root .persona-artifact-tab.persona-bg-persona-container {
2128
+ background: var(--persona-artifact-tab-active-bg, var(--persona-container, #f3f4f6));
2129
+ border-color: var(--persona-artifact-tab-active-border, var(--persona-border, #e5e7eb));
2130
+ }
2131
+
2132
+ /* Artifact toolbar background theming */
2133
+ #persona-root .persona-artifact-toolbar {
2134
+ background: var(--persona-artifact-toolbar-bg, var(--persona-surface, #fff));
2135
+ }
2136
+
2107
2137
  /* Draggable split handle (desktop split only; hidden in drawer / narrow host / small viewport) */
2108
2138
  #persona-root .persona-artifact-split-handle {
2109
2139
  width: 6px;
@@ -298,6 +298,41 @@ export interface ComposerChromeTokens {
298
298
  shadow: string;
299
299
  }
300
300
 
301
+ /** Artifact toolbar chrome. */
302
+ export interface ArtifactToolbarTokens {
303
+ iconHoverColor?: string;
304
+ iconHoverBackground?: string;
305
+ iconPadding?: string;
306
+ iconBorderRadius?: string;
307
+ iconBorder?: string;
308
+ toggleGroupGap?: string;
309
+ toggleBorderRadius?: string;
310
+ copyBackground?: string;
311
+ copyBorder?: string;
312
+ copyColor?: string;
313
+ copyBorderRadius?: string;
314
+ copyPadding?: string;
315
+ copyMenuBackground?: string;
316
+ copyMenuBorder?: string;
317
+ copyMenuShadow?: string;
318
+ copyMenuBorderRadius?: string;
319
+ copyMenuItemHoverBackground?: string;
320
+ }
321
+
322
+ /** Artifact tab strip chrome. */
323
+ export interface ArtifactTabTokens {
324
+ background?: string;
325
+ activeBackground?: string;
326
+ activeBorder?: string;
327
+ borderRadius?: string;
328
+ textColor?: string;
329
+ }
330
+
331
+ /** Artifact pane chrome. */
332
+ export interface ArtifactPaneTokens {
333
+ toolbarBackground?: string;
334
+ }
335
+
301
336
  export interface ComponentTokens {
302
337
  button: ButtonTokens;
303
338
  input: InputTokens;
@@ -313,6 +348,12 @@ export interface ComponentTokens {
313
348
  toolBubble: ToolBubbleTokens;
314
349
  reasoningBubble: ReasoningBubbleTokens;
315
350
  composer: ComposerChromeTokens;
351
+ /** Artifact toolbar, tab strip, and pane chrome. */
352
+ artifact?: {
353
+ toolbar?: ArtifactToolbarTokens;
354
+ tab?: ArtifactTabTokens;
355
+ pane?: ArtifactPaneTokens;
356
+ };
316
357
  }
317
358
 
318
359
  export interface PaletteExtras {
package/src/types.ts CHANGED
@@ -525,6 +525,14 @@ export type AgentWidgetArtifactsFeature = {
525
525
  allowedTypes?: PersonaArtifactKind[];
526
526
  /** Split / drawer dimensions and launcher widen behavior */
527
527
  layout?: AgentWidgetArtifactsLayoutConfig;
528
+ /**
529
+ * Called when an artifact card action is triggered (open, download).
530
+ * Return `true` to prevent the default behavior.
531
+ */
532
+ onArtifactAction?: (action: {
533
+ type: 'open' | 'download';
534
+ artifactId: string;
535
+ }) => boolean | void;
528
536
  };
529
537
 
530
538
  export type AgentWidgetFeatureFlags = {
@@ -1452,6 +1460,12 @@ export type AgentWidgetHeaderLayoutConfig = {
1452
1460
  trailingActions?: AgentWidgetHeaderTrailingAction[];
1453
1461
  /** Called when a `trailingActions` button is clicked. */
1454
1462
  onAction?: (actionId: string) => void;
1463
+ /**
1464
+ * Called when the header title row is clicked.
1465
+ * Useful for dropdown menus or navigation triggered from the header.
1466
+ * When set, the title row becomes visually interactive (cursor: pointer).
1467
+ */
1468
+ onTitleClick?: () => void;
1455
1469
  };
1456
1470
 
1457
1471
  /**
package/src/ui.ts CHANGED
@@ -1142,6 +1142,9 @@ export const createAgentExperience = (
1142
1142
  event.stopPropagation();
1143
1143
  const artifactId = dlBtn.getAttribute('data-download-artifact');
1144
1144
  if (!artifactId) return;
1145
+ // Let integrator intercept
1146
+ const dlPrevented = config.features?.artifacts?.onArtifactAction?.({ type: 'download', artifactId });
1147
+ if (dlPrevented === true) return;
1145
1148
  // Try session state first, fall back to content stored in the card's rawContent props
1146
1149
  const artifact = session.getArtifactById(artifactId);
1147
1150
  let markdown = artifact?.markdown;
@@ -1180,6 +1183,9 @@ export const createAgentExperience = (
1180
1183
  if (!card) return;
1181
1184
  const artifactId = card.getAttribute('data-open-artifact');
1182
1185
  if (!artifactId) return;
1186
+ // Let integrator intercept
1187
+ const openPrevented = config.features?.artifacts?.onArtifactAction?.({ type: 'open', artifactId });
1188
+ if (openPrevented === true) return;
1183
1189
  event.preventDefault();
1184
1190
  event.stopPropagation();
1185
1191
  session.selectArtifact(artifactId);
@@ -3560,6 +3566,8 @@ export const createAgentExperience = (
3560
3566
  const previousMessageActions = config.messageActions;
3561
3567
  const previousLayoutMessages = config.layout?.messages;
3562
3568
  const previousColorScheme = config.colorScheme;
3569
+ const previousLoadingIndicator = config.loadingIndicator;
3570
+ const previousIterationDisplay = config.iterationDisplay;
3563
3571
  config = { ...config, ...nextConfig };
3564
3572
  // applyFullHeightStyles resets mount.style.cssText, so call it before applyThemeVariables
3565
3573
  applyFullHeightStyles();
@@ -3790,7 +3798,12 @@ export const createAgentExperience = (
3790
3798
  const toolCallConfigChanged = JSON.stringify(nextConfig.toolCall) !== JSON.stringify(previousToolCallConfig);
3791
3799
  const messageActionsChanged = JSON.stringify(config.messageActions) !== JSON.stringify(previousMessageActions);
3792
3800
  const layoutMessagesChanged = JSON.stringify(config.layout?.messages) !== JSON.stringify(previousLayoutMessages);
3793
- const messagesConfigChanged = toolCallConfigChanged || messageActionsChanged || layoutMessagesChanged;
3801
+ const loadingIndicatorChanged = config.loadingIndicator?.render !== previousLoadingIndicator?.render
3802
+ || config.loadingIndicator?.renderIdle !== previousLoadingIndicator?.renderIdle
3803
+ || config.loadingIndicator?.showBubble !== previousLoadingIndicator?.showBubble;
3804
+ const iterationDisplayChanged = config.iterationDisplay !== previousIterationDisplay;
3805
+ const messagesConfigChanged = toolCallConfigChanged || messageActionsChanged || layoutMessagesChanged
3806
+ || loadingIndicatorChanged || iterationDisplayChanged;
3794
3807
  if (messagesConfigChanged && session) {
3795
3808
  configVersion++;
3796
3809
  renderMessagesWithPlugins(messagesWrapper, session.getMessages(), postprocess);
@@ -689,6 +689,42 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
689
689
  cssVars['--persona-md-prose-font-family'] = mdProseFont;
690
690
  }
691
691
 
692
+ // Artifact tokens
693
+ const components = theme.components;
694
+ const artifact = components?.artifact;
695
+ if (artifact?.toolbar) {
696
+ const t = artifact.toolbar;
697
+ if (t.iconHoverColor) cssVars['--persona-artifact-toolbar-icon-hover-color'] = t.iconHoverColor;
698
+ if (t.iconHoverBackground) cssVars['--persona-artifact-toolbar-icon-hover-bg'] = t.iconHoverBackground;
699
+ if (t.iconPadding) cssVars['--persona-artifact-toolbar-icon-padding'] = t.iconPadding;
700
+ if (t.iconBorderRadius) cssVars['--persona-artifact-toolbar-icon-radius'] = t.iconBorderRadius;
701
+ if (t.iconBorder) cssVars['--persona-artifact-toolbar-icon-border'] = t.iconBorder;
702
+ if (t.toggleGroupGap) cssVars['--persona-artifact-toolbar-toggle-group-gap'] = t.toggleGroupGap;
703
+ if (t.toggleBorderRadius) cssVars['--persona-artifact-toolbar-toggle-radius'] = t.toggleBorderRadius;
704
+ if (t.copyBackground) cssVars['--persona-artifact-toolbar-copy-bg'] = t.copyBackground;
705
+ if (t.copyBorder) cssVars['--persona-artifact-toolbar-copy-border'] = t.copyBorder;
706
+ if (t.copyColor) cssVars['--persona-artifact-toolbar-copy-color'] = t.copyColor;
707
+ if (t.copyBorderRadius) cssVars['--persona-artifact-toolbar-copy-radius'] = t.copyBorderRadius;
708
+ if (t.copyPadding) cssVars['--persona-artifact-toolbar-copy-padding'] = t.copyPadding;
709
+ if (t.copyMenuBackground) cssVars['--persona-artifact-toolbar-copy-menu-bg'] = t.copyMenuBackground;
710
+ if (t.copyMenuBorder) cssVars['--persona-artifact-toolbar-copy-menu-border'] = t.copyMenuBorder;
711
+ if (t.copyMenuShadow) cssVars['--persona-artifact-toolbar-copy-menu-shadow'] = t.copyMenuShadow;
712
+ if (t.copyMenuBorderRadius) cssVars['--persona-artifact-toolbar-copy-menu-radius'] = t.copyMenuBorderRadius;
713
+ if (t.copyMenuItemHoverBackground) cssVars['--persona-artifact-toolbar-copy-menu-item-hover-bg'] = t.copyMenuItemHoverBackground;
714
+ }
715
+ if (artifact?.tab) {
716
+ const t = artifact.tab;
717
+ if (t.background) cssVars['--persona-artifact-tab-bg'] = t.background;
718
+ if (t.activeBackground) cssVars['--persona-artifact-tab-active-bg'] = t.activeBackground;
719
+ if (t.activeBorder) cssVars['--persona-artifact-tab-active-border'] = t.activeBorder;
720
+ if (t.borderRadius) cssVars['--persona-artifact-tab-radius'] = t.borderRadius;
721
+ if (t.textColor) cssVars['--persona-artifact-tab-color'] = t.textColor;
722
+ }
723
+ if (artifact?.pane) {
724
+ const t = artifact.pane;
725
+ if (t.toolbarBackground) cssVars['--persona-artifact-toolbar-bg'] = t.toolbarBackground;
726
+ }
727
+
692
728
  return cssVars;
693
729
  }
694
730
 
@@ -699,3 +735,21 @@ export function applyThemeVariables(element: HTMLElement, theme: PersonaTheme):
699
735
  element.style.setProperty(name, value);
700
736
  }
701
737
  }
738
+
739
+ /**
740
+ * Stable `data-persona-theme-zone` values applied to key widget regions.
741
+ * Visual editors should use `[data-persona-theme-zone="header"]` selectors
742
+ * rather than internal class names.
743
+ */
744
+ export const THEME_ZONES = {
745
+ header: 'Widget header bar',
746
+ messages: 'Message list area',
747
+ 'user-message': 'User message bubble',
748
+ 'assistant-message': 'Assistant message bubble',
749
+ composer: 'Footer / composer area',
750
+ container: 'Main widget container',
751
+ 'artifact-pane': 'Artifact sidebar',
752
+ 'artifact-toolbar': 'Artifact toolbar',
753
+ } as const;
754
+
755
+ export type ThemeZone = keyof typeof THEME_ZONES;