@runtypelabs/persona 2.3.0 → 3.0.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.
Files changed (41) hide show
  1. package/README.md +221 -4
  2. package/dist/index.cjs +42 -42
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +832 -571
  5. package/dist/index.d.ts +832 -571
  6. package/dist/index.global.js +87 -87
  7. package/dist/index.global.js.map +1 -1
  8. package/dist/index.js +42 -42
  9. package/dist/index.js.map +1 -1
  10. package/dist/widget.css +205 -15
  11. package/package.json +2 -2
  12. package/src/components/artifact-card.ts +39 -5
  13. package/src/components/artifact-pane.ts +67 -126
  14. package/src/components/composer-builder.ts +3 -23
  15. package/src/components/header-builder.ts +29 -34
  16. package/src/components/header-layouts.ts +109 -41
  17. package/src/components/launcher.ts +10 -7
  18. package/src/components/message-bubble.ts +7 -11
  19. package/src/components/panel.ts +4 -4
  20. package/src/defaults.ts +22 -93
  21. package/src/index.ts +20 -7
  22. package/src/presets.ts +66 -51
  23. package/src/runtime/host-layout.test.ts +196 -0
  24. package/src/runtime/host-layout.ts +265 -27
  25. package/src/runtime/init.test.ts +77 -7
  26. package/src/styles/widget.css +205 -15
  27. package/src/types/theme.ts +76 -0
  28. package/src/types.ts +86 -97
  29. package/src/ui.docked.test.ts +203 -7
  30. package/src/ui.ts +129 -88
  31. package/src/utils/buttons.ts +417 -0
  32. package/src/utils/code-generators.test.ts +43 -7
  33. package/src/utils/code-generators.ts +9 -25
  34. package/src/utils/deep-merge.ts +26 -0
  35. package/src/utils/dock.ts +18 -5
  36. package/src/utils/dropdown.ts +178 -0
  37. package/src/utils/sanitize.ts +1 -1
  38. package/src/utils/theme.test.ts +90 -15
  39. package/src/utils/theme.ts +20 -46
  40. package/src/utils/tokens.ts +108 -11
  41. package/src/utils/migration.ts +0 -220
@@ -2,6 +2,16 @@ import { createElement, createElementInDocument } from "../utils/dom";
2
2
  import { renderLucideIcon } from "../utils/icons";
3
3
  import { AgentWidgetConfig } from "../types";
4
4
 
5
+ /** CSS `color` values; variables are set on `#persona-root` from `theme.components.header`. */
6
+ export const HEADER_THEME_CSS = {
7
+ titleColor:
8
+ "var(--persona-header-title-fg, var(--persona-primary, #2563eb))",
9
+ subtitleColor:
10
+ "var(--persona-header-subtitle-fg, var(--persona-text-muted, var(--persona-muted, #9ca3af)))",
11
+ actionIconColor:
12
+ "var(--persona-header-action-icon-fg, var(--persona-muted, #9ca3af))",
13
+ } as const;
14
+
5
15
  export interface HeaderElements {
6
16
  header: HTMLElement;
7
17
  iconHolder: HTMLElement;
@@ -33,9 +43,9 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
33
43
  );
34
44
  header.setAttribute("data-persona-theme-zone", "header");
35
45
  header.style.backgroundColor = 'var(--persona-header-bg, var(--persona-surface, #ffffff))';
36
- header.style.borderBottomWidth = '1px';
37
- header.style.borderBottomStyle = 'solid';
38
46
  header.style.borderBottomColor = 'var(--persona-header-border, var(--persona-divider, #f1f5f9))';
47
+ header.style.boxShadow = 'var(--persona-header-shadow, none)';
48
+ header.style.borderBottom = 'var(--persona-header-border-bottom, 1px solid var(--persona-header-border, var(--persona-divider, #f1f5f9)))';
39
49
 
40
50
  const launcher = config?.launcher ?? {};
41
51
  const headerIconSize = launcher.headerIconSize ?? "48px";
@@ -46,17 +56,21 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
46
56
 
47
57
  const iconHolder = createElement(
48
58
  "div",
49
- "persona-flex persona-items-center persona-justify-center persona-rounded-xl persona-bg-persona-primary persona-text-white persona-text-xl"
59
+ "persona-flex persona-items-center persona-justify-center persona-rounded-xl persona-text-xl"
50
60
  );
51
61
  iconHolder.style.height = headerIconSize;
52
62
  iconHolder.style.width = headerIconSize;
63
+ iconHolder.style.backgroundColor =
64
+ "var(--persona-header-icon-bg, var(--persona-primary, #2563eb))";
65
+ iconHolder.style.color =
66
+ "var(--persona-header-icon-fg, var(--persona-text-inverse, #ffffff))";
53
67
 
54
68
  // Render icon based on priority: Lucide icon > iconUrl > agentIconText
55
69
  if (!headerIconHidden) {
56
70
  if (headerIconName) {
57
71
  // Use Lucide icon
58
72
  const iconSize = parseFloat(headerIconSize) || 24;
59
- const iconSvg = renderLucideIcon(headerIconName, iconSize * 0.6, "var(--persona-text-inverse, #ffffff)", 1);
73
+ const iconSvg = renderLucideIcon(headerIconName, iconSize * 0.6, "currentColor", 1);
60
74
  if (iconSvg) {
61
75
  iconHolder.replaceChildren(iconSvg);
62
76
  } else {
@@ -80,8 +94,10 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
80
94
 
81
95
  const headerCopy = createElement("div", "persona-flex persona-flex-col");
82
96
  const title = createElement("span", "persona-text-base persona-font-semibold");
97
+ title.style.color = HEADER_THEME_CSS.titleColor;
83
98
  title.textContent = config?.launcher?.title ?? "Chat Assistant";
84
- const subtitle = createElement("span", "persona-text-xs persona-text-persona-muted");
99
+ const subtitle = createElement("span", "persona-text-xs");
100
+ subtitle.style.color = HEADER_THEME_CSS.subtitleColor;
85
101
  subtitle.textContent =
86
102
  config?.launcher?.subtitle ?? "Here to help you get answers fast";
87
103
 
@@ -132,31 +148,22 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
132
148
 
133
149
  clearChatButton = createElement(
134
150
  "button",
135
- "persona-inline-flex persona-items-center persona-justify-center persona-rounded-full persona-text-persona-muted hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none"
151
+ "persona-inline-flex persona-items-center persona-justify-center persona-rounded-full hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none"
136
152
  ) as HTMLButtonElement;
137
153
 
138
154
  clearChatButton.style.height = clearChatSize;
139
155
  clearChatButton.style.width = clearChatSize;
140
156
  clearChatButton.type = "button";
141
157
  clearChatButton.setAttribute("aria-label", clearChatTooltipText);
158
+ clearChatButton.style.color =
159
+ clearChatIconColor || HEADER_THEME_CSS.actionIconColor;
142
160
 
143
161
  // Add icon
144
- const iconSvg = renderLucideIcon(
145
- clearChatIconName,
146
- "20px",
147
- clearChatIconColor || "",
148
- 1
149
- );
162
+ const iconSvg = renderLucideIcon(clearChatIconName, "20px", "currentColor", 1);
150
163
  if (iconSvg) {
151
164
  clearChatButton.appendChild(iconSvg);
152
165
  }
153
166
 
154
- // Apply styling from config
155
- if (clearChatIconColor) {
156
- clearChatButton.style.color = clearChatIconColor;
157
- clearChatButton.classList.remove("persona-text-persona-muted");
158
- }
159
-
160
167
  if (clearChatBgColor) {
161
168
  clearChatButton.style.backgroundColor = clearChatBgColor;
162
169
  clearChatButton.classList.remove("hover:persona-bg-gray-100");
@@ -281,7 +288,7 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
281
288
  // Create close button with base classes
282
289
  const closeButton = createElement(
283
290
  "button",
284
- "persona-inline-flex persona-items-center persona-justify-center persona-rounded-full persona-text-persona-muted hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none"
291
+ "persona-inline-flex persona-items-center persona-justify-center persona-rounded-full hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none"
285
292
  ) as HTMLButtonElement;
286
293
  closeButton.style.height = closeButtonSize;
287
294
  closeButton.style.width = closeButtonSize;
@@ -297,29 +304,17 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
297
304
  // Add icon or fallback text
298
305
  const closeButtonIconName = launcher.closeButtonIconName ?? "x";
299
306
  const closeButtonIconText = launcher.closeButtonIconText ?? "×";
307
+ closeButton.style.color =
308
+ launcher.closeButtonColor || HEADER_THEME_CSS.actionIconColor;
300
309
 
301
310
  // Try to render Lucide icon, fallback to text if not provided or fails
302
- const closeIconSvg = renderLucideIcon(
303
- closeButtonIconName,
304
- "20px",
305
- launcher.closeButtonColor || "",
306
- 1
307
- );
311
+ const closeIconSvg = renderLucideIcon(closeButtonIconName, "20px", "currentColor", 1);
308
312
  if (closeIconSvg) {
309
313
  closeButton.appendChild(closeIconSvg);
310
314
  } else {
311
315
  closeButton.textContent = closeButtonIconText;
312
316
  }
313
317
 
314
- // Apply close button styling from config
315
- if (launcher.closeButtonColor) {
316
- closeButton.style.color = launcher.closeButtonColor;
317
- closeButton.classList.remove("persona-text-persona-muted");
318
- } else {
319
- closeButton.style.color = "";
320
- closeButton.classList.add("persona-text-persona-muted");
321
- }
322
-
323
318
  if (launcher.closeButtonBackgroundColor) {
324
319
  closeButton.style.backgroundColor = launcher.closeButtonBackgroundColor;
325
320
  closeButton.classList.remove("hover:persona-bg-gray-100");
@@ -1,11 +1,18 @@
1
1
  import { createElement } from "../utils/dom";
2
2
  import { renderLucideIcon } from "../utils/icons";
3
+ import { createDropdownMenu } from "../utils/dropdown";
4
+ import { createComboButton } from "../utils/buttons";
3
5
  import {
4
6
  AgentWidgetConfig,
5
7
  AgentWidgetHeaderLayoutConfig,
6
8
  AgentWidgetHeaderTrailingAction
7
9
  } from "../types";
8
- import { buildHeader, HeaderElements, attachHeaderToContainer as _attachHeaderToContainer } from "./header-builder";
10
+ import {
11
+ buildHeader,
12
+ HEADER_THEME_CSS,
13
+ HeaderElements,
14
+ attachHeaderToContainer as _attachHeaderToContainer,
15
+ } from "./header-builder";
9
16
 
10
17
  export interface HeaderLayoutContext {
11
18
  config: AgentWidgetConfig;
@@ -75,8 +82,27 @@ function appendTrailingHeaderActions(
75
82
  } else if (a.label) {
76
83
  btn.textContent = a.label;
77
84
  }
78
- btn.addEventListener("click", () => onAction?.(a.id));
79
- container.appendChild(btn);
85
+
86
+ if (a.menuItems?.length) {
87
+ // Wrap in a relative container for dropdown positioning
88
+ const wrapper = createElement("div", "persona-relative");
89
+ wrapper.appendChild(btn);
90
+ const dropdown = createDropdownMenu({
91
+ items: a.menuItems,
92
+ onSelect: (itemId) => onAction?.(itemId),
93
+ anchor: wrapper,
94
+ position: 'bottom-left',
95
+ });
96
+ wrapper.appendChild(dropdown.element);
97
+ btn.addEventListener("click", (e) => {
98
+ e.stopPropagation();
99
+ dropdown.toggle();
100
+ });
101
+ container.appendChild(wrapper);
102
+ } else {
103
+ btn.addEventListener("click", () => onAction?.(a.id));
104
+ container.appendChild(btn);
105
+ }
80
106
  }
81
107
  }
82
108
 
@@ -86,43 +112,88 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
86
112
 
87
113
  const header = createElement(
88
114
  "div",
89
- "persona-flex persona-items-center persona-justify-between persona-bg-persona-surface persona-px-6 persona-py-4 persona-border-b-persona-divider"
115
+ "persona-flex persona-items-center persona-justify-between persona-px-6 persona-py-4"
90
116
  );
91
117
  header.setAttribute("data-persona-theme-zone", "header");
118
+ header.style.backgroundColor = 'var(--persona-header-bg, var(--persona-surface, #ffffff))';
119
+ header.style.borderBottomColor = 'var(--persona-header-border, var(--persona-divider, #f1f5f9))';
120
+ header.style.boxShadow = 'var(--persona-header-shadow, none)';
121
+ header.style.borderBottom =
122
+ 'var(--persona-header-border-bottom, 1px solid var(--persona-header-border, var(--persona-divider, #f1f5f9)))';
92
123
 
93
- const titleRow = createElement(
94
- "div",
95
- "persona-flex persona-min-w-0 persona-flex-1 persona-items-center persona-gap-1"
96
- );
124
+ // Build the title area — either a combo button (titleMenu) or standard title row
125
+ const titleMenuConfig = layoutHeaderConfig?.titleMenu;
126
+ let titleRow: HTMLElement;
127
+ let headerTitle: HTMLElement;
128
+
129
+ if (titleMenuConfig) {
130
+ // Combo button replaces title + trailing actions + hover
131
+ const combo = createComboButton({
132
+ label: launcher.title ?? "Chat Assistant",
133
+ menuItems: titleMenuConfig.menuItems,
134
+ onSelect: titleMenuConfig.onSelect,
135
+ hover: titleMenuConfig.hover,
136
+ className: "",
137
+ });
138
+ titleRow = combo.element;
139
+ titleRow.style.color = HEADER_THEME_CSS.titleColor;
140
+ // The combo button's label span acts as headerTitle for update()
141
+ headerTitle = titleRow.querySelector(".persona-combo-btn-label") ?? titleRow;
142
+ } else {
143
+ titleRow = createElement(
144
+ "div",
145
+ "persona-flex persona-min-w-0 persona-flex-1 persona-items-center persona-gap-1"
146
+ );
97
147
 
98
- // Title only (no icon, no subtitle)
99
- const title = createElement("span", "persona-text-base persona-font-semibold persona-truncate");
100
- title.textContent = launcher.title ?? "Chat Assistant";
148
+ // Title only (no icon, no subtitle)
149
+ headerTitle = createElement("span", "persona-text-base persona-font-semibold persona-truncate");
150
+ headerTitle.style.color = HEADER_THEME_CSS.titleColor;
151
+ headerTitle.textContent = launcher.title ?? "Chat Assistant";
101
152
 
102
- titleRow.appendChild(title);
103
- appendTrailingHeaderActions(
104
- titleRow,
105
- layoutHeaderConfig?.trailingActions,
106
- layoutHeaderConfig?.onAction ?? onHeaderAction
107
- );
153
+ titleRow.appendChild(headerTitle);
154
+ appendTrailingHeaderActions(
155
+ titleRow,
156
+ layoutHeaderConfig?.trailingActions,
157
+ layoutHeaderConfig?.onAction ?? onHeaderAction
158
+ );
108
159
 
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();
160
+ // Make title row clickable when onTitleClick is provided
161
+ if (layoutHeaderConfig?.onTitleClick) {
162
+ titleRow.style.cursor = "pointer";
163
+ titleRow.setAttribute("role", "button");
164
+ titleRow.setAttribute("tabindex", "0");
165
+ const handleTitleClick = layoutHeaderConfig.onTitleClick;
166
+ titleRow.addEventListener("click", (e) => {
167
+ if ((e.target as HTMLElement).closest("button")) return;
123
168
  handleTitleClick();
124
- }
125
- });
169
+ });
170
+ titleRow.addEventListener("keydown", (e) => {
171
+ if (e.key === "Enter" || e.key === " ") {
172
+ e.preventDefault();
173
+ handleTitleClick();
174
+ }
175
+ });
176
+ }
177
+
178
+ // Title row hover pill effect
179
+ const hoverCfg = layoutHeaderConfig?.titleRowHover;
180
+ if (hoverCfg) {
181
+ titleRow.style.borderRadius = hoverCfg.borderRadius ?? '10px';
182
+ titleRow.style.padding = hoverCfg.padding ?? '6px 4px 6px 12px';
183
+ titleRow.style.margin = '-6px 0 -6px -12px';
184
+ titleRow.style.border = '1px solid transparent';
185
+ titleRow.style.transition = 'background-color 0.15s ease, border-color 0.15s ease';
186
+ titleRow.style.width = 'fit-content';
187
+ titleRow.style.flex = 'none';
188
+ titleRow.addEventListener('mouseenter', () => {
189
+ titleRow.style.backgroundColor = hoverCfg.background ?? '';
190
+ titleRow.style.borderColor = hoverCfg.border ?? '';
191
+ });
192
+ titleRow.addEventListener('mouseleave', () => {
193
+ titleRow.style.backgroundColor = '';
194
+ titleRow.style.borderColor = 'transparent';
195
+ });
196
+ }
126
197
  }
127
198
 
128
199
  header.appendChild(titleRow);
@@ -133,21 +204,18 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
133
204
 
134
205
  const closeButton = createElement(
135
206
  "button",
136
- "persona-inline-flex persona-items-center persona-justify-center persona-rounded-full persona-text-persona-muted hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none"
207
+ "persona-inline-flex persona-items-center persona-justify-center persona-rounded-full hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none"
137
208
  ) as HTMLButtonElement;
138
209
  closeButton.style.height = closeButtonSize;
139
210
  closeButton.style.width = closeButtonSize;
140
211
  closeButton.type = "button";
141
212
  closeButton.setAttribute("aria-label", "Close chat");
142
213
  closeButton.style.display = showClose ? "" : "none";
214
+ closeButton.style.color =
215
+ launcher.closeButtonColor || HEADER_THEME_CSS.actionIconColor;
143
216
 
144
217
  const closeButtonIconName = launcher.closeButtonIconName ?? "x";
145
- const closeIconSvg = renderLucideIcon(
146
- closeButtonIconName,
147
- "20px",
148
- launcher.closeButtonColor || "",
149
- 2
150
- );
218
+ const closeIconSvg = renderLucideIcon(closeButtonIconName, "20px", "currentColor", 2);
151
219
  if (closeIconSvg) {
152
220
  closeButton.appendChild(closeIconSvg);
153
221
  } else {
@@ -172,7 +240,7 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
172
240
  return {
173
241
  header,
174
242
  iconHolder,
175
- headerTitle: title,
243
+ headerTitle,
176
244
  headerSubtitle,
177
245
  closeButton,
178
246
  closeButtonWrapper,
@@ -1,7 +1,7 @@
1
1
  import { createElement } from "../utils/dom";
2
2
  import { AgentWidgetConfig } from "../types";
3
3
  import { positionMap } from "../utils/positioning";
4
- import { isDockedMountMode, resolveDockConfig } from "../utils/dock";
4
+ import { isDockedMountMode } from "../utils/dock";
5
5
  import { renderLucideIcon } from "../utils/icons";
6
6
 
7
7
  export interface LauncherButton {
@@ -175,18 +175,21 @@ export const createLauncherButton = (
175
175
  button.style.boxShadow = launcher.shadow ?? defaultShadow;
176
176
 
177
177
  if (dockedMode) {
178
- const dock = resolveDockConfig(newConfig);
179
- button.style.width = `calc(${dock.collapsedWidth} - 16px)`;
180
- button.style.minWidth = "40px";
181
- button.style.maxWidth = `calc(${dock.collapsedWidth} - 16px)`;
182
- button.style.justifyContent = "center";
183
- button.style.padding = "12px 0";
178
+ // Docked mode uses a 0px column when closed and hides this button; keep no hit target.
179
+ button.style.width = "0";
180
+ button.style.minWidth = "0";
181
+ button.style.maxWidth = "0";
182
+ button.style.padding = "0";
183
+ button.style.overflow = "hidden";
184
+ button.style.border = "none";
185
+ button.style.boxShadow = "none";
184
186
  } else {
185
187
  button.style.width = "";
186
188
  button.style.minWidth = "";
187
189
  button.style.maxWidth = "";
188
190
  button.style.justifyContent = "";
189
191
  button.style.padding = "";
192
+ button.style.overflow = "";
190
193
  }
191
194
  };
192
195
 
@@ -9,7 +9,7 @@ import {
9
9
  LoadingIndicatorRenderContext,
10
10
  ImageContentPart
11
11
  } from "../types";
12
- import { renderLucideIcon } from "../utils/icons";
12
+ import { createIconButton } from "../utils/buttons";
13
13
  import { IMAGE_ONLY_MESSAGE_FALLBACK_TEXT } from "../utils/content";
14
14
 
15
15
  /** Validate that an image src URL uses a safe scheme (blocks javascript: and SVG data URIs). */
@@ -414,17 +414,13 @@ export const createMessageActions = (
414
414
  label: string,
415
415
  dataAction: string
416
416
  ): HTMLButtonElement => {
417
- const button = document.createElement("button");
418
- button.className = "persona-message-action-btn";
419
- button.setAttribute("aria-label", label);
420
- button.setAttribute("title", label);
417
+ const button = createIconButton({
418
+ icon: iconName,
419
+ label,
420
+ size: 14,
421
+ className: "persona-message-action-btn",
422
+ });
421
423
  button.setAttribute("data-action", dataAction);
422
-
423
- const icon = renderLucideIcon(iconName, 14, "currentColor", 2);
424
- if (icon) {
425
- button.appendChild(icon);
426
- }
427
-
428
424
  return button;
429
425
  };
430
426
 
@@ -133,10 +133,10 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
133
133
  body.id = "persona-scroll-container";
134
134
  body.setAttribute("data-persona-theme-zone", "messages");
135
135
 
136
- const introCard = createElement(
137
- "div",
138
- "persona-rounded-2xl persona-bg-persona-surface persona-p-6 persona-shadow-sm"
139
- );
136
+ const introCardClasses = isDockedMountMode(config)
137
+ ? "persona-rounded-2xl persona-bg-persona-surface persona-p-6"
138
+ : "persona-rounded-2xl persona-bg-persona-surface persona-p-6 persona-shadow-sm";
139
+ const introCard = createElement("div", introCardClasses);
140
140
  const introTitle = createElement(
141
141
  "h2",
142
142
  "persona-text-lg persona-font-semibold persona-text-persona-primary"
package/src/defaults.ts CHANGED
@@ -1,82 +1,6 @@
1
- import type { AgentWidgetConfig, AgentWidgetTheme } from "./types";
2
-
3
- /**
4
- * Default light theme colors
5
- */
6
- export const DEFAULT_LIGHT_THEME: AgentWidgetTheme = {
7
- primary: "#111827",
8
- accent: "#1d4ed8",
9
- surface: "#ffffff",
10
- muted: "#6b7280",
11
- container: "#f8fafc",
12
- border: "#f1f5f9",
13
- divider: "#f1f5f9",
14
- messageBorder: "#f1f5f9",
15
- inputBackground: "#ffffff",
16
- callToAction: "#000000",
17
- callToActionBackground: "#ffffff",
18
- sendButtonBackgroundColor: "#111827",
19
- sendButtonTextColor: "#ffffff",
20
- sendButtonBorderColor: "#60a5fa",
21
- closeButtonColor: "#6b7280",
22
- closeButtonBackgroundColor: "transparent",
23
- closeButtonBorderColor: "",
24
- clearChatIconColor: "#6b7280",
25
- clearChatBackgroundColor: "transparent",
26
- clearChatBorderColor: "transparent",
27
- micIconColor: "#111827",
28
- micBackgroundColor: "transparent",
29
- micBorderColor: "transparent",
30
- recordingIconColor: "#ffffff",
31
- recordingBackgroundColor: "#ef4444",
32
- recordingBorderColor: "transparent",
33
- inputFontFamily: "sans-serif",
34
- inputFontWeight: "400",
35
- radiusSm: "0.75rem",
36
- radiusMd: "1rem",
37
- radiusLg: "1.5rem",
38
- launcherRadius: "9999px",
39
- buttonRadius: "9999px",
40
- };
41
-
42
- /**
43
- * Default dark theme colors
44
- */
45
- export const DEFAULT_DARK_THEME: AgentWidgetTheme = {
46
- primary: "#f9fafb",
47
- accent: "#3b82f6",
48
- surface: "#1f2937",
49
- muted: "#9ca3af",
50
- container: "#111827",
51
- border: "#374151",
52
- divider: "#374151",
53
- messageBorder: "#374151",
54
- inputBackground: "#111827",
55
- callToAction: "#ffffff",
56
- callToActionBackground: "#374151",
57
- sendButtonBackgroundColor: "#3b82f6",
58
- sendButtonTextColor: "#ffffff",
59
- sendButtonBorderColor: "#60a5fa",
60
- closeButtonColor: "#9ca3af",
61
- closeButtonBackgroundColor: "transparent",
62
- closeButtonBorderColor: "",
63
- clearChatIconColor: "#9ca3af",
64
- clearChatBackgroundColor: "transparent",
65
- clearChatBorderColor: "transparent",
66
- micIconColor: "#f9fafb",
67
- micBackgroundColor: "transparent",
68
- micBorderColor: "transparent",
69
- recordingIconColor: "#ffffff",
70
- recordingBackgroundColor: "#ef4444",
71
- recordingBorderColor: "transparent",
72
- inputFontFamily: "sans-serif",
73
- inputFontWeight: "400",
74
- radiusSm: "0.75rem",
75
- radiusMd: "1rem",
76
- radiusLg: "1.5rem",
77
- launcherRadius: "9999px",
78
- buttonRadius: "9999px",
79
- };
1
+ import type { AgentWidgetConfig } from "./types";
2
+ import type { DeepPartial, PersonaTheme } from "./types/theme";
3
+ import { deepMerge } from "./utils/deep-merge";
80
4
 
81
5
  /**
82
6
  * Default widget configuration
@@ -86,8 +10,8 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
86
10
  apiUrl: "http://localhost:43111/api/chat/dispatch",
87
11
  // Client token mode defaults (optional, only used when clientToken is set)
88
12
  clientToken: undefined,
89
- theme: DEFAULT_LIGHT_THEME,
90
- darkTheme: DEFAULT_DARK_THEME,
13
+ theme: undefined,
14
+ darkTheme: undefined,
91
15
  colorScheme: "light",
92
16
  launcher: {
93
17
  enabled: true,
@@ -95,7 +19,6 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
95
19
  dock: {
96
20
  side: "right",
97
21
  width: "420px",
98
- collapsedWidth: "72px",
99
22
  },
100
23
  title: "Chat Assistant",
101
24
  subtitle: "Here to help you get answers fast",
@@ -114,10 +37,9 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
114
37
  callToActionIconPadding: "5px",
115
38
  callToActionIconColor: "#000000",
116
39
  callToActionIconBackgroundColor: "#ffffff",
117
- closeButtonColor: "#6b7280",
40
+ // closeButtonColor / clearChat.iconColor omitted so theme.components.header.actionIconForeground applies.
118
41
  closeButtonBackgroundColor: "transparent",
119
42
  clearChat: {
120
- iconColor: "#6b7280",
121
43
  backgroundColor: "transparent",
122
44
  borderColor: "transparent",
123
45
  enabled: true,
@@ -225,7 +147,7 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
225
147
  messageActions: {
226
148
  enabled: true,
227
149
  showCopy: true,
228
- showUpvote: false, // Requires backend - disabled by default
150
+ showUpvote: false, // Requires backend - disabled by default
229
151
  showDownvote: false, // Requires backend - disabled by default
230
152
  visibility: "hover",
231
153
  align: "right",
@@ -234,6 +156,19 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
234
156
  debug: false,
235
157
  };
236
158
 
159
+ function mergeThemePartials(
160
+ base: DeepPartial<PersonaTheme> | undefined,
161
+ override: DeepPartial<PersonaTheme> | undefined
162
+ ): DeepPartial<PersonaTheme> | undefined {
163
+ if (!base && !override) return undefined;
164
+ if (!base) return override;
165
+ if (!override) return base;
166
+ return deepMerge(
167
+ base as Record<string, unknown>,
168
+ override as Record<string, unknown>
169
+ ) as DeepPartial<PersonaTheme>;
170
+ }
171
+
237
172
  /**
238
173
  * Helper to deep merge user config with defaults
239
174
  * This ensures all default values are present while allowing selective overrides
@@ -246,14 +181,8 @@ export function mergeWithDefaults(
246
181
  return {
247
182
  ...DEFAULT_WIDGET_CONFIG,
248
183
  ...config,
249
- theme: {
250
- ...DEFAULT_WIDGET_CONFIG.theme,
251
- ...config.theme,
252
- },
253
- darkTheme: {
254
- ...DEFAULT_WIDGET_CONFIG.darkTheme,
255
- ...config.darkTheme,
256
- },
184
+ theme: mergeThemePartials(DEFAULT_WIDGET_CONFIG.theme, config.theme),
185
+ darkTheme: mergeThemePartials(DEFAULT_WIDGET_CONFIG.darkTheme, config.darkTheme),
257
186
  launcher: {
258
187
  ...DEFAULT_WIDGET_CONFIG.launcher,
259
188
  ...config.launcher,
package/src/index.ts CHANGED
@@ -5,7 +5,6 @@ import {
5
5
 
6
6
  export type {
7
7
  AgentWidgetConfig,
8
- AgentWidgetTheme,
9
8
  AgentWidgetFeatureFlags,
10
9
  AgentWidgetArtifactsFeature,
11
10
  AgentWidgetArtifactsLayoutConfig,
@@ -179,6 +178,22 @@ export type { AgentWidgetInitHandle };
179
178
  export type { AgentWidgetPlugin } from "./plugins/types";
180
179
  export { pluginRegistry } from "./plugins/registry";
181
180
 
181
+ // Dropdown utility exports
182
+ export { createDropdownMenu } from "./utils/dropdown";
183
+ export type { DropdownMenuItem, CreateDropdownOptions, DropdownMenuHandle } from "./utils/dropdown";
184
+
185
+ // Button utility exports
186
+ export { createIconButton, createLabelButton, createToggleGroup, createComboButton } from "./utils/buttons";
187
+ export type {
188
+ CreateIconButtonOptions,
189
+ CreateLabelButtonOptions,
190
+ CreateToggleGroupOptions,
191
+ ToggleGroupItem,
192
+ ToggleGroupHandle,
193
+ CreateComboButtonOptions,
194
+ ComboButtonHandle
195
+ } from "./utils/buttons";
196
+
182
197
  // Theme system exports
183
198
  export {
184
199
  createTheme,
@@ -206,11 +221,8 @@ export {
206
221
  highContrastPlugin,
207
222
  createPlugin
208
223
  } from "./utils/plugins";
209
- export {
210
- migrateV1Theme,
211
- validateV1Theme
212
- } from "./utils/migration";
213
224
  export type {
225
+ DeepPartial,
214
226
  PersonaTheme,
215
227
  PersonaThemePlugin,
216
228
  CreateThemeOptions,
@@ -229,6 +241,9 @@ export type {
229
241
  ArtifactToolbarTokens,
230
242
  ArtifactTabTokens,
231
243
  ArtifactPaneTokens,
244
+ IconButtonTokens,
245
+ LabelButtonTokens,
246
+ ToggleGroupTokens,
232
247
  ThemeValidationResult,
233
248
  ThemeValidationError
234
249
  } from "./types/theme";
@@ -251,8 +266,6 @@ export {
251
266
  // Default configuration exports
252
267
  export {
253
268
  DEFAULT_WIDGET_CONFIG,
254
- DEFAULT_LIGHT_THEME,
255
- DEFAULT_DARK_THEME,
256
269
  mergeWithDefaults
257
270
  } from "./defaults";
258
271
  export {