@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
@@ -10,7 +10,7 @@ describe("createAgentExperience docked mode", () => {
10
10
  document.body.innerHTML = "";
11
11
  });
12
12
 
13
- it("collapses the docked panel wrapper and keeps the rail trigger visible when closed", () => {
13
+ it("toggles docked panel open/closed; built-in launcher stays hidden (open via controller.open)", () => {
14
14
  const mount = document.createElement("div");
15
15
  document.body.appendChild(mount);
16
16
 
@@ -22,7 +22,6 @@ describe("createAgentExperience docked mode", () => {
22
22
  dock: {
23
23
  side: "right",
24
24
  width: "420px",
25
- collapsedWidth: "72px",
26
25
  },
27
26
  },
28
27
  });
@@ -40,7 +39,7 @@ describe("createAgentExperience docked mode", () => {
40
39
  controller.close();
41
40
 
42
41
  expect(wrapper?.style.display).toBe("none");
43
- expect(launcherButton?.style.display).toBe("");
42
+ expect(launcherButton?.style.display).toBe("none");
44
43
 
45
44
  controller.open();
46
45
 
@@ -50,7 +49,141 @@ describe("createAgentExperience docked mode", () => {
50
49
  controller.destroy();
51
50
  });
52
51
 
53
- it("collapses the dock width when the header close button is clicked", () => {
52
+ it("keeps docked panel hidden when closed under mobile fullscreen breakpoint", () => {
53
+ const prevWidth = window.innerWidth;
54
+ try {
55
+ Object.defineProperty(window, "innerWidth", {
56
+ configurable: true,
57
+ value: 480,
58
+ });
59
+
60
+ const mount = document.createElement("div");
61
+ document.body.appendChild(mount);
62
+
63
+ const controller = createAgentExperience(mount, {
64
+ apiUrl: "https://api.example.com/chat",
65
+ launcher: {
66
+ mountMode: "docked",
67
+ autoExpand: false,
68
+ dock: {
69
+ side: "right",
70
+ width: "420px",
71
+ },
72
+ },
73
+ });
74
+
75
+ const wrapper = mount.firstElementChild as HTMLElement | null;
76
+ expect(wrapper?.style.display).toBe("none");
77
+ expect(getComputedStyle(wrapper!).display).toBe("none");
78
+
79
+ controller.open();
80
+ expect(wrapper?.style.display).toBe("flex");
81
+
82
+ controller.destroy();
83
+ } finally {
84
+ Object.defineProperty(window, "innerWidth", {
85
+ configurable: true,
86
+ writable: true,
87
+ value: prevWidth,
88
+ });
89
+ }
90
+ });
91
+
92
+ it("keeps docked panel hidden after resize into mobile when closed", () => {
93
+ const prevWidth = window.innerWidth;
94
+ try {
95
+ Object.defineProperty(window, "innerWidth", {
96
+ configurable: true,
97
+ value: 900,
98
+ });
99
+
100
+ const mount = document.createElement("div");
101
+ document.body.appendChild(mount);
102
+
103
+ const controller = createAgentExperience(mount, {
104
+ apiUrl: "https://api.example.com/chat",
105
+ launcher: {
106
+ mountMode: "docked",
107
+ autoExpand: false,
108
+ dock: {
109
+ side: "right",
110
+ width: "420px",
111
+ },
112
+ },
113
+ });
114
+
115
+ const wrapper = mount.firstElementChild as HTMLElement | null;
116
+ expect(wrapper?.style.display).toBe("none");
117
+
118
+ Object.defineProperty(window, "innerWidth", {
119
+ configurable: true,
120
+ value: 480,
121
+ });
122
+ window.dispatchEvent(new Event("resize"));
123
+
124
+ expect(wrapper?.style.display).toBe("none");
125
+
126
+ controller.destroy();
127
+ } finally {
128
+ Object.defineProperty(window, "innerWidth", {
129
+ configurable: true,
130
+ writable: true,
131
+ value: prevWidth,
132
+ });
133
+ }
134
+ });
135
+
136
+ it("collapses the dock width to 0 when the header close button is clicked", () => {
137
+ const target = document.createElement("div");
138
+ document.body.appendChild(target);
139
+
140
+ const hostLayout = createWidgetHostLayout(target, {
141
+ launcher: {
142
+ mountMode: "docked",
143
+ autoExpand: true,
144
+ dock: {
145
+ side: "right",
146
+ width: "420px",
147
+ },
148
+ },
149
+ });
150
+ const mount = document.createElement("div");
151
+ hostLayout.host.appendChild(mount);
152
+
153
+ const controller = createAgentExperience(mount, {
154
+ apiUrl: "https://api.example.com/chat",
155
+ launcher: {
156
+ mountMode: "docked",
157
+ autoExpand: true,
158
+ dock: {
159
+ side: "right",
160
+ width: "420px",
161
+ },
162
+ },
163
+ });
164
+
165
+ const syncDockState = () => hostLayout.syncWidgetState(controller.getState());
166
+ const openUnsub = controller.on("widget:opened", syncDockState);
167
+ const closeUnsub = controller.on("widget:closed", syncDockState);
168
+ syncDockState();
169
+
170
+ const dockSlot = hostLayout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
171
+ const closeButton = mount.querySelector<HTMLButtonElement>('[aria-label="Close chat"]');
172
+
173
+ expect(dockSlot?.style.width).toBe("420px");
174
+ expect(closeButton).not.toBeNull();
175
+
176
+ closeButton!.click();
177
+
178
+ expect(dockSlot?.style.width).toBe("0px");
179
+
180
+ openUnsub();
181
+ closeUnsub();
182
+ controller.destroy();
183
+ hostLayout.destroy();
184
+ });
185
+
186
+ it("overlay reveal keeps width when closed; host-layout uses transform", () => {
54
187
  const target = document.createElement("div");
55
188
  document.body.appendChild(target);
56
189
 
@@ -61,7 +194,7 @@ describe("createAgentExperience docked mode", () => {
61
194
  dock: {
62
195
  side: "right",
63
196
  width: "420px",
64
- collapsedWidth: "72px",
197
+ reveal: "overlay",
65
198
  },
66
199
  },
67
200
  });
@@ -76,7 +209,7 @@ describe("createAgentExperience docked mode", () => {
76
209
  dock: {
77
210
  side: "right",
78
211
  width: "420px",
79
- collapsedWidth: "72px",
212
+ reveal: "overlay",
80
213
  },
81
214
  },
82
215
  });
@@ -94,7 +227,70 @@ describe("createAgentExperience docked mode", () => {
94
227
 
95
228
  closeButton!.click();
96
229
 
97
- expect(dockSlot?.style.width).toBe("72px");
230
+ expect(dockSlot?.style.width).toBe("420px");
231
+ expect(dockSlot?.style.transform).toBe("translateX(100%)");
232
+
233
+ openUnsub();
234
+ closeUnsub();
235
+ controller.destroy();
236
+ hostLayout.destroy();
237
+ });
238
+
239
+ it("push reveal translates the push-track when the header close button is clicked", () => {
240
+ const target = document.createElement("div");
241
+ document.body.appendChild(target);
242
+
243
+ const hostLayout = createWidgetHostLayout(target, {
244
+ launcher: {
245
+ mountMode: "docked",
246
+ autoExpand: true,
247
+ dock: {
248
+ side: "right",
249
+ width: "420px",
250
+ reveal: "push",
251
+ },
252
+ },
253
+ });
254
+ const shell = hostLayout.shell!;
255
+ Object.defineProperty(shell, "clientWidth", { get: () => 1000, configurable: true });
256
+ hostLayout.updateConfig({
257
+ launcher: {
258
+ mountMode: "docked",
259
+ autoExpand: true,
260
+ dock: { side: "right", width: "420px", reveal: "push" },
261
+ },
262
+ });
263
+
264
+ const mount = document.createElement("div");
265
+ hostLayout.host.appendChild(mount);
266
+
267
+ const controller = createAgentExperience(mount, {
268
+ apiUrl: "https://api.example.com/chat",
269
+ launcher: {
270
+ mountMode: "docked",
271
+ autoExpand: true,
272
+ dock: {
273
+ side: "right",
274
+ width: "420px",
275
+ reveal: "push",
276
+ },
277
+ },
278
+ });
279
+
280
+ const syncDockState = () => hostLayout.syncWidgetState(controller.getState());
281
+ const openUnsub = controller.on("widget:opened", syncDockState);
282
+ const closeUnsub = controller.on("widget:closed", syncDockState);
283
+ syncDockState();
284
+
285
+ const pushTrack = shell.querySelector<HTMLElement>('[data-persona-dock-role="push-track"]');
286
+ const closeButton = mount.querySelector<HTMLButtonElement>('[aria-label="Close chat"]');
287
+
288
+ expect(pushTrack).not.toBeNull();
289
+ expect(pushTrack?.style.transform).toBe("translateX(-420px)");
290
+
291
+ closeButton!.click();
292
+
293
+ expect(pushTrack?.style.transform).toBe("translateX(0)");
98
294
 
99
295
  openUnsub();
100
296
  closeUnsub();
package/src/ui.ts CHANGED
@@ -27,15 +27,17 @@ import {
27
27
  } from "./types";
28
28
  import { AttachmentManager } from "./utils/attachment-manager";
29
29
  import { createTextPart, ALL_SUPPORTED_MIME_TYPES } from "./utils/content";
30
- import { applyThemeVariables, createThemeObserver } from "./utils/theme";
30
+ import { applyThemeVariables, createThemeObserver, getActiveTheme } from "./utils/theme";
31
+ import { resolveTokenValue } from "./utils/tokens";
31
32
  import { renderLucideIcon } from "./utils/icons";
32
33
  import { createElement, createElementInDocument } from "./utils/dom";
33
34
  import { morphMessages } from "./utils/morph";
34
35
  import { computeMessageFingerprint, createMessageCache, getCachedWrapper, setCachedWrapper, pruneCache } from "./utils/message-fingerprint";
35
36
  import { statusCopy } from "./utils/constants";
36
- import { isDockedMountMode } from "./utils/dock";
37
+ import { isDockedMountMode, resolveDockConfig } from "./utils/dock";
37
38
  import { createLauncherButton } from "./components/launcher";
38
39
  import { createWrapper, buildPanel, buildHeader, buildComposer, attachHeaderToContainer } from "./components/panel";
40
+ import { HEADER_THEME_CSS } from "./components/header-builder";
39
41
  import { buildHeaderWithLayout } from "./components/header-layouts";
40
42
  import { positionMap } from "./utils/positioning";
41
43
  import type { HeaderElements as _HeaderElements, ComposerElements as _ComposerElements } from "./components/panel";
@@ -325,6 +327,16 @@ const buildPostprocessor = (
325
327
  // Resolve sanitizer: enabled by default, can be disabled or replaced
326
328
  const sanitize = resolveSanitizer(cfg?.sanitize);
327
329
 
330
+ // Warn developers when a custom postprocessor is used with the default sanitizer,
331
+ // since DOMPurify will strip any tags/attributes not in the allowlist.
332
+ if (cfg?.postprocessMessage && sanitize && cfg?.sanitize === undefined) {
333
+ console.warn(
334
+ "[Persona] A custom postprocessMessage is active with the default HTML sanitizer. " +
335
+ "Tags or attributes not in the built-in allowlist will be stripped. " +
336
+ "To keep custom HTML, set `sanitize: false` or provide a custom sanitize function."
337
+ );
338
+ }
339
+
328
340
  return (context) => {
329
341
  let nextText = context.text ?? "";
330
342
  const rawPayload = context.message.rawContent ?? null;
@@ -1392,6 +1404,7 @@ export const createAgentExperience = (
1392
1404
  if (!launcherEnabled || !artifactPaneApi) return;
1393
1405
  const sidebarMode = config.launcher?.sidebarMode ?? false;
1394
1406
  if (sidebarMode) return;
1407
+ if (isDockedMountMode(config) && resolveDockConfig(config).reveal === "emerge") return;
1395
1408
  const ownerWindow = mount.ownerDocument.defaultView ?? window;
1396
1409
  const mobileFullscreen = config.launcher?.mobileFullscreen ?? true;
1397
1410
  const mobileBreakpoint = config.launcher?.mobileBreakpoint ?? 640;
@@ -1432,7 +1445,12 @@ export const createAgentExperience = (
1432
1445
  const fullHeight = dockedMode || sidebarMode || (config.launcher?.fullHeight ?? false);
1433
1446
  /** Script-tag / div embed: launcher off, host supplies a sized mount. */
1434
1447
  const isInlineEmbed = config.launcher?.enabled === false;
1435
- const theme = config.theme ?? {};
1448
+ const panelPartial = config.theme?.components?.panel;
1449
+ const activeTheme = getActiveTheme(config);
1450
+ const resolvePanelChrome = (raw: string | undefined, fallback: string): string => {
1451
+ if (raw == null || raw === "") return fallback;
1452
+ return resolveTokenValue(activeTheme, raw) ?? raw;
1453
+ };
1436
1454
 
1437
1455
  // Mobile fullscreen detection
1438
1456
  // Use mount's ownerDocument window to get correct viewport width when widget is inside an iframe
@@ -1447,20 +1465,25 @@ export const createAgentExperience = (
1447
1465
  const isLeftSidebar = position === 'bottom-left' || position === 'top-left';
1448
1466
 
1449
1467
  // Default values based on mode
1450
- const defaultPanelBorder = (sidebarMode || shouldGoFullscreen) ? 'none' : '1px solid var(--persona-persona-border)';
1451
- const defaultPanelShadow = shouldGoFullscreen
1468
+ let defaultPanelBorder = (sidebarMode || shouldGoFullscreen) ? 'none' : '1px solid var(--persona-border)';
1469
+ let defaultPanelShadow = shouldGoFullscreen
1452
1470
  ? 'none'
1453
1471
  : sidebarMode
1454
1472
  ? (isLeftSidebar ? 'var(--persona-palette-shadows-sidebar-left, 2px 0 12px rgba(0, 0, 0, 0.08))' : 'var(--persona-palette-shadows-sidebar-right, -2px 0 12px rgba(0, 0, 0, 0.08))')
1455
1473
  : 'var(--persona-palette-shadows-xl, 0 25px 50px -12px rgba(0, 0, 0, 0.25))';
1474
+
1475
+ if (dockedMode && !shouldGoFullscreen) {
1476
+ defaultPanelShadow = 'none';
1477
+ defaultPanelBorder = 'none';
1478
+ }
1456
1479
  const defaultPanelBorderRadius = (sidebarMode || shouldGoFullscreen)
1457
1480
  ? '0'
1458
1481
  : 'var(--persona-panel-radius, var(--persona-radius-xl, 0.75rem))';
1459
1482
 
1460
- // Apply theme overrides or defaults
1461
- const panelBorder = theme.panelBorder ?? defaultPanelBorder;
1462
- const panelShadow = theme.panelShadow ?? defaultPanelShadow;
1463
- const panelBorderRadius = theme.panelBorderRadius ?? defaultPanelBorderRadius;
1483
+ // Apply theme overrides or defaults (components.panel.*)
1484
+ const panelBorder = resolvePanelChrome(panelPartial?.border, defaultPanelBorder);
1485
+ const panelShadow = resolvePanelChrome(panelPartial?.shadow, defaultPanelShadow);
1486
+ const panelBorderRadius = resolvePanelChrome(panelPartial?.borderRadius, defaultPanelBorderRadius);
1464
1487
 
1465
1488
  // Reset all inline styles first to handle mode toggling
1466
1489
  // This ensures styles don't persist when switching between modes
@@ -1547,8 +1570,15 @@ export const createAgentExperience = (
1547
1570
  panel.style.maxWidth = width;
1548
1571
  }
1549
1572
  } else if (dockedMode) {
1550
- panel.style.width = "100%";
1551
- panel.style.maxWidth = "100%";
1573
+ const dockReveal = resolveDockConfig(config).reveal;
1574
+ if (dockReveal === "emerge") {
1575
+ const dw = resolveDockConfig(config).width;
1576
+ panel.style.width = dw;
1577
+ panel.style.maxWidth = dw;
1578
+ } else {
1579
+ panel.style.width = "100%";
1580
+ panel.style.maxWidth = "100%";
1581
+ }
1552
1582
  }
1553
1583
  applyLauncherArtifactPanelWidth();
1554
1584
 
@@ -1561,6 +1591,16 @@ export const createAgentExperience = (
1561
1591
  container.style.border = panelBorder;
1562
1592
  container.style.borderRadius = panelBorderRadius;
1563
1593
 
1594
+ if (dockedMode && !shouldGoFullscreen && panelPartial?.border === undefined) {
1595
+ container.style.border = 'none';
1596
+ const dockSide = resolveDockConfig(config).side;
1597
+ if (dockSide === 'right') {
1598
+ container.style.borderLeft = '1px solid var(--persona-border)';
1599
+ } else {
1600
+ container.style.borderRight = '1px solid var(--persona-border)';
1601
+ }
1602
+ }
1603
+
1564
1604
  if (fullHeight) {
1565
1605
  // Mount container
1566
1606
  mount.style.display = 'flex';
@@ -2422,7 +2462,19 @@ export const createAgentExperience = (
2422
2462
  const updateOpenState = () => {
2423
2463
  if (!launcherEnabled) return;
2424
2464
  const dockedMode = isDockedMountMode(config);
2465
+ const ownerWindow = mount.ownerDocument.defaultView ?? window;
2466
+ const mobileBreakpoint = config.launcher?.mobileBreakpoint ?? 640;
2467
+ const mobileFullscreen = config.launcher?.mobileFullscreen ?? true;
2468
+ const isMobileViewport = ownerWindow.innerWidth <= mobileBreakpoint;
2469
+ const shouldGoFullscreen = mobileFullscreen && isMobileViewport && launcherEnabled;
2470
+ const dockReveal = resolveDockConfig(config).reveal;
2471
+ const dockRevealUsesTransform =
2472
+ dockedMode && (dockReveal === "overlay" || dockReveal === "push") && !shouldGoFullscreen;
2473
+
2425
2474
  if (open) {
2475
+ // Clear any display:none !important from a closed docked state so mobile fullscreen
2476
+ // (display:flex !important) and dock layout can apply in recalcPanelHeight.
2477
+ wrapper.style.removeProperty("display");
2426
2478
  wrapper.style.display = dockedMode ? "flex" : "";
2427
2479
  wrapper.classList.remove("persona-pointer-events-none", "persona-opacity-0");
2428
2480
  panel.classList.remove("persona-scale-95", "persona-opacity-0");
@@ -2435,20 +2487,29 @@ export const createAgentExperience = (
2435
2487
  }
2436
2488
  } else {
2437
2489
  if (dockedMode) {
2438
- wrapper.style.display = "none";
2439
- wrapper.classList.remove("persona-pointer-events-none", "persona-opacity-0");
2440
- panel.classList.remove("persona-scale-100", "persona-opacity-100", "persona-scale-95", "persona-opacity-0");
2490
+ if (dockRevealUsesTransform) {
2491
+ // Slide/push reveal: keep the panel painted so host-layout `transform` can animate.
2492
+ wrapper.style.removeProperty("display");
2493
+ wrapper.style.display = "flex";
2494
+ wrapper.classList.remove("persona-pointer-events-none", "persona-opacity-0");
2495
+ panel.classList.remove("persona-scale-100", "persona-opacity-100", "persona-scale-95", "persona-opacity-0");
2496
+ } else {
2497
+ // Must beat applyFullHeightStyles() mobile shell: display:flex !important on wrapper
2498
+ wrapper.style.setProperty("display", "none", "important");
2499
+ wrapper.classList.remove("persona-pointer-events-none", "persona-opacity-0");
2500
+ panel.classList.remove("persona-scale-100", "persona-opacity-100", "persona-scale-95", "persona-opacity-0");
2501
+ }
2441
2502
  } else {
2442
2503
  wrapper.style.display = "";
2443
2504
  wrapper.classList.add("persona-pointer-events-none", "persona-opacity-0");
2444
2505
  panel.classList.remove("persona-scale-100", "persona-opacity-100");
2445
2506
  panel.classList.add("persona-scale-95", "persona-opacity-0");
2446
2507
  }
2447
- // Show launcher button when widget is closed
2508
+ // Show launcher when closed, except docked mode (0px column — use controller.open()).
2448
2509
  if (launcherButtonInstance) {
2449
- launcherButtonInstance.element.style.display = "";
2510
+ launcherButtonInstance.element.style.display = dockedMode ? "none" : "";
2450
2511
  } else if (customLauncherElement) {
2451
- customLauncherElement.style.display = "";
2512
+ customLauncherElement.style.display = dockedMode ? "none" : "";
2452
2513
  }
2453
2514
  }
2454
2515
  };
@@ -2534,24 +2595,9 @@ export const createAgentExperience = (
2534
2595
  sendButton.textContent = config.copy?.sendButtonLabel ?? "Send";
2535
2596
  }
2536
2597
 
2537
- // Update textarea font family and weight
2538
- const fontFamily = config.theme?.inputFontFamily ?? "sans-serif";
2539
- const fontWeight = config.theme?.inputFontWeight ?? "400";
2540
-
2541
- const getFontFamilyValue = (family: "sans-serif" | "serif" | "mono"): string => {
2542
- switch (family) {
2543
- case "serif":
2544
- return 'Georgia, "Times New Roman", Times, serif';
2545
- case "mono":
2546
- return '"Courier New", Courier, "Lucida Console", Monaco, monospace';
2547
- case "sans-serif":
2548
- default:
2549
- return '-apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif';
2550
- }
2551
- };
2552
-
2553
- textarea.style.fontFamily = getFontFamilyValue(fontFamily);
2554
- textarea.style.fontWeight = fontWeight;
2598
+ textarea.style.fontFamily =
2599
+ 'var(--persona-input-font-family, var(--persona-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif))';
2600
+ textarea.style.fontWeight = "var(--persona-input-font-weight, var(--persona-font-weight, 400))";
2555
2601
  };
2556
2602
 
2557
2603
  // Add session ID persistence callbacks for client token mode
@@ -3373,43 +3419,49 @@ export const createAgentExperience = (
3373
3419
  const isMobileViewport = ownerWindow.innerWidth <= mobileBreakpoint;
3374
3420
  const shouldGoFullscreen = mobileFullscreen && isMobileViewport && launcherEnabled;
3375
3421
 
3376
- if (shouldGoFullscreen) {
3377
- applyFullHeightStyles();
3378
- applyThemeVariables(mount, config);
3379
- return;
3380
- }
3381
-
3382
- // Exiting mobile fullscreen (e.g., orientation change to landscape) — reset all styles
3383
- if (wasMobileFullscreen) {
3384
- wasMobileFullscreen = false;
3385
- applyFullHeightStyles();
3386
- applyThemeVariables(mount, config);
3387
- }
3422
+ try {
3423
+ if (shouldGoFullscreen) {
3424
+ applyFullHeightStyles();
3425
+ applyThemeVariables(mount, config);
3426
+ return;
3427
+ }
3388
3428
 
3389
- if (!launcherEnabled && !dockedMode) {
3390
- panel.style.height = "";
3391
- panel.style.width = "";
3392
- return;
3393
- }
3429
+ // Exiting mobile fullscreen (e.g., orientation change to landscape) — reset all styles
3430
+ if (wasMobileFullscreen) {
3431
+ wasMobileFullscreen = false;
3432
+ applyFullHeightStyles();
3433
+ applyThemeVariables(mount, config);
3434
+ }
3394
3435
 
3395
- // In sidebar/fullHeight mode, don't override the width - it's handled by applyFullHeightStyles
3396
- if (!sidebarMode && !dockedMode) {
3397
- const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
3398
- const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
3399
- panel.style.width = width;
3400
- panel.style.maxWidth = width;
3401
- }
3402
- applyLauncherArtifactPanelWidth();
3436
+ if (!launcherEnabled && !dockedMode) {
3437
+ panel.style.height = "";
3438
+ panel.style.width = "";
3439
+ return;
3440
+ }
3403
3441
 
3404
- // In fullHeight mode, don't set a fixed height
3405
- if (!fullHeight) {
3406
- const viewportHeight = ownerWindow.innerHeight;
3407
- const verticalMargin = 64; // leave space for launcher's offset
3408
- const heightOffset = config.launcher?.heightOffset ?? 0;
3409
- const available = Math.max(200, viewportHeight - verticalMargin);
3410
- const clamped = Math.min(640, available);
3411
- const finalHeight = Math.max(200, clamped - heightOffset);
3412
- panel.style.height = `${finalHeight}px`;
3442
+ // In sidebar/fullHeight mode, don't override the width - it's handled by applyFullHeightStyles
3443
+ if (!sidebarMode && !dockedMode) {
3444
+ const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
3445
+ const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
3446
+ panel.style.width = width;
3447
+ panel.style.maxWidth = width;
3448
+ }
3449
+ applyLauncherArtifactPanelWidth();
3450
+
3451
+ // In fullHeight mode, don't set a fixed height
3452
+ if (!fullHeight) {
3453
+ const viewportHeight = ownerWindow.innerHeight;
3454
+ const verticalMargin = 64; // leave space for launcher's offset
3455
+ const heightOffset = config.launcher?.heightOffset ?? 0;
3456
+ const available = Math.max(200, viewportHeight - verticalMargin);
3457
+ const clamped = Math.min(640, available);
3458
+ const finalHeight = Math.max(200, clamped - heightOffset);
3459
+ panel.style.height = `${finalHeight}px`;
3460
+ }
3461
+ } finally {
3462
+ // applyFullHeightStyles() assigns wrapper.style.cssText (e.g. display:flex !important), which
3463
+ // overwrites updateOpenState()'s display:none when docked+closed. Re-sync after every recalc.
3464
+ updateOpenState();
3413
3465
  }
3414
3466
  };
3415
3467
 
@@ -3949,14 +4001,9 @@ export const createAgentExperience = (
3949
4001
  }
3950
4002
  }
3951
4003
 
3952
- // Apply close button styling from config
3953
- if (launcher.closeButtonColor) {
3954
- closeButton.style.color = launcher.closeButtonColor;
3955
- closeButton.classList.remove("persona-text-persona-muted");
3956
- } else {
3957
- closeButton.style.color = "";
3958
- closeButton.classList.add("persona-text-persona-muted");
3959
- }
4004
+ // Close icon: launcher color wins; else theme.components.header.actionIconForeground
4005
+ closeButton.style.color =
4006
+ launcher.closeButtonColor || HEADER_THEME_CSS.actionIconColor;
3960
4007
 
3961
4008
  if (launcher.closeButtonBackgroundColor) {
3962
4009
  closeButton.style.backgroundColor = launcher.closeButtonBackgroundColor;
@@ -4007,7 +4054,7 @@ export const createAgentExperience = (
4007
4054
 
4008
4055
  // Clear existing content and render new icon
4009
4056
  closeButton.innerHTML = "";
4010
- const iconSvg = renderLucideIcon(closeButtonIconName, "20px", launcher.closeButtonColor || "", 2);
4057
+ const iconSvg = renderLucideIcon(closeButtonIconName, "20px", "currentColor", 2);
4011
4058
  if (iconSvg) {
4012
4059
  closeButton.appendChild(iconSvg);
4013
4060
  } else {
@@ -4174,22 +4221,16 @@ export const createAgentExperience = (
4174
4221
  const clearChatIconName = clearChatConfig.iconName ?? "refresh-cw";
4175
4222
  const clearChatIconColor = clearChatConfig.iconColor ?? "";
4176
4223
 
4224
+ clearChatButton.style.color =
4225
+ clearChatIconColor || HEADER_THEME_CSS.actionIconColor;
4226
+
4177
4227
  // Clear existing icon and render new one
4178
4228
  clearChatButton.innerHTML = "";
4179
- const iconSvg = renderLucideIcon(clearChatIconName, "20px", clearChatIconColor || "", 2);
4229
+ const iconSvg = renderLucideIcon(clearChatIconName, "20px", "currentColor", 2);
4180
4230
  if (iconSvg) {
4181
4231
  clearChatButton.appendChild(iconSvg);
4182
4232
  }
4183
4233
 
4184
- // Update icon color
4185
- if (clearChatIconColor) {
4186
- clearChatButton.style.color = clearChatIconColor;
4187
- clearChatButton.classList.remove("persona-text-persona-muted");
4188
- } else {
4189
- clearChatButton.style.color = "";
4190
- clearChatButton.classList.add("persona-text-persona-muted");
4191
- }
4192
-
4193
4234
  // Update background color
4194
4235
  if (clearChatConfig.backgroundColor) {
4195
4236
  clearChatButton.style.backgroundColor = clearChatConfig.backgroundColor;