@runtypelabs/persona 2.3.1 → 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 (40) 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 +119 -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/theme.test.ts +90 -15
  38. package/src/utils/theme.ts +20 -46
  39. package/src/utils/tokens.ts +108 -11
  40. 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";
@@ -1402,6 +1404,7 @@ export const createAgentExperience = (
1402
1404
  if (!launcherEnabled || !artifactPaneApi) return;
1403
1405
  const sidebarMode = config.launcher?.sidebarMode ?? false;
1404
1406
  if (sidebarMode) return;
1407
+ if (isDockedMountMode(config) && resolveDockConfig(config).reveal === "emerge") return;
1405
1408
  const ownerWindow = mount.ownerDocument.defaultView ?? window;
1406
1409
  const mobileFullscreen = config.launcher?.mobileFullscreen ?? true;
1407
1410
  const mobileBreakpoint = config.launcher?.mobileBreakpoint ?? 640;
@@ -1442,7 +1445,12 @@ export const createAgentExperience = (
1442
1445
  const fullHeight = dockedMode || sidebarMode || (config.launcher?.fullHeight ?? false);
1443
1446
  /** Script-tag / div embed: launcher off, host supplies a sized mount. */
1444
1447
  const isInlineEmbed = config.launcher?.enabled === false;
1445
- 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
+ };
1446
1454
 
1447
1455
  // Mobile fullscreen detection
1448
1456
  // Use mount's ownerDocument window to get correct viewport width when widget is inside an iframe
@@ -1457,20 +1465,25 @@ export const createAgentExperience = (
1457
1465
  const isLeftSidebar = position === 'bottom-left' || position === 'top-left';
1458
1466
 
1459
1467
  // Default values based on mode
1460
- const defaultPanelBorder = (sidebarMode || shouldGoFullscreen) ? 'none' : '1px solid var(--persona-persona-border)';
1461
- const defaultPanelShadow = shouldGoFullscreen
1468
+ let defaultPanelBorder = (sidebarMode || shouldGoFullscreen) ? 'none' : '1px solid var(--persona-border)';
1469
+ let defaultPanelShadow = shouldGoFullscreen
1462
1470
  ? 'none'
1463
1471
  : sidebarMode
1464
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))')
1465
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
+ }
1466
1479
  const defaultPanelBorderRadius = (sidebarMode || shouldGoFullscreen)
1467
1480
  ? '0'
1468
1481
  : 'var(--persona-panel-radius, var(--persona-radius-xl, 0.75rem))';
1469
1482
 
1470
- // Apply theme overrides or defaults
1471
- const panelBorder = theme.panelBorder ?? defaultPanelBorder;
1472
- const panelShadow = theme.panelShadow ?? defaultPanelShadow;
1473
- 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);
1474
1487
 
1475
1488
  // Reset all inline styles first to handle mode toggling
1476
1489
  // This ensures styles don't persist when switching between modes
@@ -1557,8 +1570,15 @@ export const createAgentExperience = (
1557
1570
  panel.style.maxWidth = width;
1558
1571
  }
1559
1572
  } else if (dockedMode) {
1560
- panel.style.width = "100%";
1561
- 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
+ }
1562
1582
  }
1563
1583
  applyLauncherArtifactPanelWidth();
1564
1584
 
@@ -1571,6 +1591,16 @@ export const createAgentExperience = (
1571
1591
  container.style.border = panelBorder;
1572
1592
  container.style.borderRadius = panelBorderRadius;
1573
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
+
1574
1604
  if (fullHeight) {
1575
1605
  // Mount container
1576
1606
  mount.style.display = 'flex';
@@ -2432,7 +2462,19 @@ export const createAgentExperience = (
2432
2462
  const updateOpenState = () => {
2433
2463
  if (!launcherEnabled) return;
2434
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
+
2435
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");
2436
2478
  wrapper.style.display = dockedMode ? "flex" : "";
2437
2479
  wrapper.classList.remove("persona-pointer-events-none", "persona-opacity-0");
2438
2480
  panel.classList.remove("persona-scale-95", "persona-opacity-0");
@@ -2445,20 +2487,29 @@ export const createAgentExperience = (
2445
2487
  }
2446
2488
  } else {
2447
2489
  if (dockedMode) {
2448
- wrapper.style.display = "none";
2449
- wrapper.classList.remove("persona-pointer-events-none", "persona-opacity-0");
2450
- 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
+ }
2451
2502
  } else {
2452
2503
  wrapper.style.display = "";
2453
2504
  wrapper.classList.add("persona-pointer-events-none", "persona-opacity-0");
2454
2505
  panel.classList.remove("persona-scale-100", "persona-opacity-100");
2455
2506
  panel.classList.add("persona-scale-95", "persona-opacity-0");
2456
2507
  }
2457
- // Show launcher button when widget is closed
2508
+ // Show launcher when closed, except docked mode (0px column — use controller.open()).
2458
2509
  if (launcherButtonInstance) {
2459
- launcherButtonInstance.element.style.display = "";
2510
+ launcherButtonInstance.element.style.display = dockedMode ? "none" : "";
2460
2511
  } else if (customLauncherElement) {
2461
- customLauncherElement.style.display = "";
2512
+ customLauncherElement.style.display = dockedMode ? "none" : "";
2462
2513
  }
2463
2514
  }
2464
2515
  };
@@ -2544,24 +2595,9 @@ export const createAgentExperience = (
2544
2595
  sendButton.textContent = config.copy?.sendButtonLabel ?? "Send";
2545
2596
  }
2546
2597
 
2547
- // Update textarea font family and weight
2548
- const fontFamily = config.theme?.inputFontFamily ?? "sans-serif";
2549
- const fontWeight = config.theme?.inputFontWeight ?? "400";
2550
-
2551
- const getFontFamilyValue = (family: "sans-serif" | "serif" | "mono"): string => {
2552
- switch (family) {
2553
- case "serif":
2554
- return 'Georgia, "Times New Roman", Times, serif';
2555
- case "mono":
2556
- return '"Courier New", Courier, "Lucida Console", Monaco, monospace';
2557
- case "sans-serif":
2558
- default:
2559
- return '-apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif';
2560
- }
2561
- };
2562
-
2563
- textarea.style.fontFamily = getFontFamilyValue(fontFamily);
2564
- 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))";
2565
2601
  };
2566
2602
 
2567
2603
  // Add session ID persistence callbacks for client token mode
@@ -3383,43 +3419,49 @@ export const createAgentExperience = (
3383
3419
  const isMobileViewport = ownerWindow.innerWidth <= mobileBreakpoint;
3384
3420
  const shouldGoFullscreen = mobileFullscreen && isMobileViewport && launcherEnabled;
3385
3421
 
3386
- if (shouldGoFullscreen) {
3387
- applyFullHeightStyles();
3388
- applyThemeVariables(mount, config);
3389
- return;
3390
- }
3391
-
3392
- // Exiting mobile fullscreen (e.g., orientation change to landscape) — reset all styles
3393
- if (wasMobileFullscreen) {
3394
- wasMobileFullscreen = false;
3395
- applyFullHeightStyles();
3396
- applyThemeVariables(mount, config);
3397
- }
3422
+ try {
3423
+ if (shouldGoFullscreen) {
3424
+ applyFullHeightStyles();
3425
+ applyThemeVariables(mount, config);
3426
+ return;
3427
+ }
3398
3428
 
3399
- if (!launcherEnabled && !dockedMode) {
3400
- panel.style.height = "";
3401
- panel.style.width = "";
3402
- return;
3403
- }
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
+ }
3404
3435
 
3405
- // In sidebar/fullHeight mode, don't override the width - it's handled by applyFullHeightStyles
3406
- if (!sidebarMode && !dockedMode) {
3407
- const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
3408
- const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
3409
- panel.style.width = width;
3410
- panel.style.maxWidth = width;
3411
- }
3412
- applyLauncherArtifactPanelWidth();
3436
+ if (!launcherEnabled && !dockedMode) {
3437
+ panel.style.height = "";
3438
+ panel.style.width = "";
3439
+ return;
3440
+ }
3413
3441
 
3414
- // In fullHeight mode, don't set a fixed height
3415
- if (!fullHeight) {
3416
- const viewportHeight = ownerWindow.innerHeight;
3417
- const verticalMargin = 64; // leave space for launcher's offset
3418
- const heightOffset = config.launcher?.heightOffset ?? 0;
3419
- const available = Math.max(200, viewportHeight - verticalMargin);
3420
- const clamped = Math.min(640, available);
3421
- const finalHeight = Math.max(200, clamped - heightOffset);
3422
- 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();
3423
3465
  }
3424
3466
  };
3425
3467
 
@@ -3959,14 +4001,9 @@ export const createAgentExperience = (
3959
4001
  }
3960
4002
  }
3961
4003
 
3962
- // Apply close button styling from config
3963
- if (launcher.closeButtonColor) {
3964
- closeButton.style.color = launcher.closeButtonColor;
3965
- closeButton.classList.remove("persona-text-persona-muted");
3966
- } else {
3967
- closeButton.style.color = "";
3968
- closeButton.classList.add("persona-text-persona-muted");
3969
- }
4004
+ // Close icon: launcher color wins; else theme.components.header.actionIconForeground
4005
+ closeButton.style.color =
4006
+ launcher.closeButtonColor || HEADER_THEME_CSS.actionIconColor;
3970
4007
 
3971
4008
  if (launcher.closeButtonBackgroundColor) {
3972
4009
  closeButton.style.backgroundColor = launcher.closeButtonBackgroundColor;
@@ -4017,7 +4054,7 @@ export const createAgentExperience = (
4017
4054
 
4018
4055
  // Clear existing content and render new icon
4019
4056
  closeButton.innerHTML = "";
4020
- const iconSvg = renderLucideIcon(closeButtonIconName, "20px", launcher.closeButtonColor || "", 2);
4057
+ const iconSvg = renderLucideIcon(closeButtonIconName, "20px", "currentColor", 2);
4021
4058
  if (iconSvg) {
4022
4059
  closeButton.appendChild(iconSvg);
4023
4060
  } else {
@@ -4184,22 +4221,16 @@ export const createAgentExperience = (
4184
4221
  const clearChatIconName = clearChatConfig.iconName ?? "refresh-cw";
4185
4222
  const clearChatIconColor = clearChatConfig.iconColor ?? "";
4186
4223
 
4224
+ clearChatButton.style.color =
4225
+ clearChatIconColor || HEADER_THEME_CSS.actionIconColor;
4226
+
4187
4227
  // Clear existing icon and render new one
4188
4228
  clearChatButton.innerHTML = "";
4189
- const iconSvg = renderLucideIcon(clearChatIconName, "20px", clearChatIconColor || "", 2);
4229
+ const iconSvg = renderLucideIcon(clearChatIconName, "20px", "currentColor", 2);
4190
4230
  if (iconSvg) {
4191
4231
  clearChatButton.appendChild(iconSvg);
4192
4232
  }
4193
4233
 
4194
- // Update icon color
4195
- if (clearChatIconColor) {
4196
- clearChatButton.style.color = clearChatIconColor;
4197
- clearChatButton.classList.remove("persona-text-persona-muted");
4198
- } else {
4199
- clearChatButton.style.color = "";
4200
- clearChatButton.classList.add("persona-text-persona-muted");
4201
- }
4202
-
4203
4234
  // Update background color
4204
4235
  if (clearChatConfig.backgroundColor) {
4205
4236
  clearChatButton.style.backgroundColor = clearChatConfig.backgroundColor;