@runtypelabs/persona 2.3.1 → 3.1.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 (43) hide show
  1. package/README.md +222 -5
  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 +88 -88
  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 +257 -67
  11. package/package.json +2 -4
  12. package/src/components/artifact-card.ts +39 -5
  13. package/src/components/artifact-pane.ts +68 -127
  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 +333 -0
  24. package/src/runtime/host-layout.ts +346 -27
  25. package/src/runtime/init.test.ts +113 -8
  26. package/src/runtime/init.ts +1 -1
  27. package/src/styles/widget.css +257 -67
  28. package/src/types/theme.ts +76 -0
  29. package/src/types.ts +86 -97
  30. package/src/ui.docked.test.ts +203 -7
  31. package/src/ui.ts +125 -92
  32. package/src/utils/artifact-gate.ts +1 -1
  33. package/src/utils/buttons.ts +417 -0
  34. package/src/utils/code-generators.test.ts +43 -7
  35. package/src/utils/code-generators.ts +9 -25
  36. package/src/utils/deep-merge.ts +26 -0
  37. package/src/utils/dock.ts +18 -5
  38. package/src/utils/dropdown.ts +178 -0
  39. package/src/utils/theme.test.ts +90 -15
  40. package/src/utils/theme.ts +20 -46
  41. package/src/utils/tokens.ts +108 -11
  42. package/src/styles/tailwind.css +0 -20
  43. package/src/utils/migration.ts +0 -220
@@ -199,6 +199,20 @@ export interface HeaderTokens extends ComponentTokenSet {
199
199
  background: TokenReference<'color'>;
200
200
  border: TokenReference<'color'>;
201
201
  borderRadius: TokenReference<'radius'>;
202
+ /** Background of the rounded avatar tile next to the title (Lucide / emoji / image). */
203
+ iconBackground: TokenReference<'color'>;
204
+ /** Foreground (glyph stroke or emoji text) on the header avatar tile. */
205
+ iconForeground: TokenReference<'color'>;
206
+ /** Header title line (next to the icon, or minimal layout title). */
207
+ titleForeground: TokenReference<'color'>;
208
+ /** Header subtitle line under the title. */
209
+ subtitleForeground: TokenReference<'color'>;
210
+ /** Default color for clear / close icon buttons when launcher overrides are unset. */
211
+ actionIconForeground: TokenReference<'color'>;
212
+ /** Box-shadow on the header (e.g., a fade shadow to replace the default border). */
213
+ shadow?: string;
214
+ /** Override the header bottom border (e.g., `none`). */
215
+ borderBottom?: string;
202
216
  }
203
217
 
204
218
  export interface MessageTokens {
@@ -317,6 +331,10 @@ export interface ArtifactToolbarTokens {
317
331
  copyMenuShadow?: string;
318
332
  copyMenuBorderRadius?: string;
319
333
  copyMenuItemHoverBackground?: string;
334
+ /** Base background of icon buttons (defaults to --persona-surface). */
335
+ iconBackground?: string;
336
+ /** Border on the toolbar (e.g., `none` to remove the bottom border). */
337
+ toolbarBorder?: string;
320
338
  }
321
339
 
322
340
  /** Artifact tab strip chrome. */
@@ -326,13 +344,62 @@ export interface ArtifactTabTokens {
326
344
  activeBorder?: string;
327
345
  borderRadius?: string;
328
346
  textColor?: string;
347
+ /** Hover background for inactive tabs. */
348
+ hoverBackground?: string;
349
+ /** Tab list container background. */
350
+ listBackground?: string;
351
+ /** Tab list container border color. */
352
+ listBorderColor?: string;
353
+ /** Tab list container padding (CSS shorthand). */
354
+ listPadding?: string;
329
355
  }
330
356
 
331
357
  /** Artifact pane chrome. */
332
358
  export interface ArtifactPaneTokens {
359
+ /**
360
+ * Background for the artifact column (toolbar + content), resolved from the theme.
361
+ * Defaults to `semantic.colors.container` so the pane matches assistant message surfaces.
362
+ * `features.artifacts.layout.paneBackground` still wins when set (layout escape hatch).
363
+ */
364
+ background?: string;
333
365
  toolbarBackground?: string;
334
366
  }
335
367
 
368
+ /** Icon button chrome (used by createIconButton). */
369
+ export interface IconButtonTokens {
370
+ background?: string;
371
+ border?: string;
372
+ color?: string;
373
+ padding?: string;
374
+ borderRadius?: string;
375
+ hoverBackground?: string;
376
+ hoverColor?: string;
377
+ /** Background when aria-pressed="true". */
378
+ activeBackground?: string;
379
+ /** Border color when aria-pressed="true". */
380
+ activeBorder?: string;
381
+ }
382
+
383
+ /** Label button chrome (used by createLabelButton). */
384
+ export interface LabelButtonTokens {
385
+ background?: string;
386
+ border?: string;
387
+ color?: string;
388
+ padding?: string;
389
+ borderRadius?: string;
390
+ hoverBackground?: string;
391
+ fontSize?: string;
392
+ gap?: string;
393
+ }
394
+
395
+ /** Toggle group chrome (used by createToggleGroup). */
396
+ export interface ToggleGroupTokens {
397
+ /** Gap between toggle buttons. Default: 0 (connected). */
398
+ gap?: string;
399
+ /** Border radius for first/last buttons. */
400
+ borderRadius?: string;
401
+ }
402
+
336
403
  export interface ComponentTokens {
337
404
  button: ButtonTokens;
338
405
  input: InputTokens;
@@ -348,6 +415,12 @@ export interface ComponentTokens {
348
415
  toolBubble: ToolBubbleTokens;
349
416
  reasoningBubble: ReasoningBubbleTokens;
350
417
  composer: ComposerChromeTokens;
418
+ /** Icon button styling tokens. */
419
+ iconButton?: IconButtonTokens;
420
+ /** Label button styling tokens. */
421
+ labelButton?: LabelButtonTokens;
422
+ /** Toggle group styling tokens. */
423
+ toggleGroup?: ToggleGroupTokens;
351
424
  /** Artifact toolbar, tab strip, and pane chrome. */
352
425
  artifact?: {
353
426
  toolbar?: ArtifactToolbarTokens;
@@ -384,6 +457,9 @@ export type PersonaTheme = PersonaThemeBase &
384
457
  PersonaThemeSemantic &
385
458
  PersonaThemeComponents;
386
459
 
460
+ /** Recursive partial for `config.theme` / `config.darkTheme` overrides. */
461
+ export type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;
462
+
387
463
  export interface ResolvedToken {
388
464
  path: string;
389
465
  value: string;
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { AgentWidgetPlugin } from "./plugins/types";
2
+ import type { DeepPartial, PersonaTheme } from "./types/theme";
2
3
 
3
4
  // ============================================================================
4
5
  // Multi-Modal Content Types
@@ -469,7 +470,9 @@ export type AgentWidgetArtifactsLayoutConfig = {
469
470
  */
470
471
  unifiedSplitOuterRadius?: string;
471
472
  /**
472
- * Background color for the artifact column (CSS color). Sets `--persona-artifact-pane-bg` on the widget root.
473
+ * Strongest override: solid background for the artifact column (CSS color). Sets `--persona-artifact-pane-bg`
474
+ * on the widget root. Leave unset to use theme `components.artifact.pane.background` (defaults to semantic
475
+ * container) so light/dark stays consistent.
473
476
  */
474
477
  paneBackground?: string;
475
478
  /**
@@ -533,6 +536,20 @@ export type AgentWidgetArtifactsFeature = {
533
536
  type: 'open' | 'download';
534
537
  artifactId: string;
535
538
  }) => boolean | void;
539
+ /**
540
+ * Custom renderer for artifact reference cards shown in the message thread.
541
+ * Return an HTMLElement to replace the default card, or `null` to use the default.
542
+ */
543
+ renderCard?: (context: {
544
+ artifact: {
545
+ artifactId: string;
546
+ title: string;
547
+ artifactType: string;
548
+ status: string;
549
+ };
550
+ config: AgentWidgetConfig;
551
+ defaultRenderer: () => HTMLElement;
552
+ }) => HTMLElement | null;
536
553
  };
537
554
 
538
555
  export type AgentWidgetFeatureFlags = {
@@ -667,85 +684,6 @@ export type EventStreamPayloadRenderContext = {
667
684
  parsedPayload: unknown;
668
685
  };
669
686
 
670
- export type AgentWidgetTheme = {
671
- primary?: string;
672
- secondary?: string;
673
- surface?: string;
674
- muted?: string;
675
- accent?: string;
676
- container?: string;
677
- border?: string;
678
- divider?: string;
679
- messageBorder?: string;
680
- inputBackground?: string;
681
- callToAction?: string;
682
- callToActionBackground?: string;
683
- sendButtonBackgroundColor?: string;
684
- sendButtonTextColor?: string;
685
- sendButtonBorderColor?: string;
686
- closeButtonColor?: string;
687
- closeButtonBackgroundColor?: string;
688
- closeButtonBorderColor?: string;
689
- clearChatIconColor?: string;
690
- clearChatBackgroundColor?: string;
691
- clearChatBorderColor?: string;
692
- tooltipBackground?: string;
693
- tooltipForeground?: string;
694
- micIconColor?: string;
695
- micBackgroundColor?: string;
696
- micBorderColor?: string;
697
- recordingIconColor?: string;
698
- recordingBackgroundColor?: string;
699
- recordingBorderColor?: string;
700
- inputFontFamily?: "sans-serif" | "serif" | "mono";
701
- inputFontWeight?: string;
702
- radiusSm?: string;
703
- radiusMd?: string;
704
- radiusLg?: string;
705
- launcherRadius?: string;
706
- buttonRadius?: string;
707
- /**
708
- * Border style for the chat panel container.
709
- * @example "1px solid #e5e7eb" | "none"
710
- * @default "1px solid var(--persona-border)"
711
- */
712
- panelBorder?: string;
713
- /**
714
- * Box shadow for the chat panel container.
715
- * @example "0 25px 50px -12px rgba(0,0,0,0.25)" | "none"
716
- * @default "0 25px 50px -12px rgba(0,0,0,0.25)"
717
- */
718
- panelShadow?: string;
719
- /**
720
- * Border radius for the chat panel container.
721
- * @example "16px" | "0"
722
- * @default "16px"
723
- */
724
- panelBorderRadius?: string;
725
- /**
726
- * Box-shadow for user message bubbles (bubble message layout).
727
- * @example "none" | "0 1px 2px rgba(0,0,0,0.05)"
728
- */
729
- messageUserShadow?: string;
730
- /**
731
- * Box-shadow for assistant message bubbles (bubble message layout).
732
- * Overrides the default subtle assistant shadow when set.
733
- */
734
- messageAssistantShadow?: string;
735
- /**
736
- * Box-shadow for tool-call / function-call rows.
737
- */
738
- toolBubbleShadow?: string;
739
- /**
740
- * Box-shadow for reasoning (“thinking”) rows.
741
- */
742
- reasoningBubbleShadow?: string;
743
- /**
744
- * Box-shadow on the composer (input) container.
745
- */
746
- composerShadow?: string;
747
- };
748
-
749
687
  export type AgentWidgetDockConfig = {
750
688
  /**
751
689
  * Side of the wrapped container where the docked panel should render.
@@ -758,10 +696,22 @@ export type AgentWidgetDockConfig = {
758
696
  */
759
697
  width?: string;
760
698
  /**
761
- * Width of the collapsed launcher rail when the docked panel is closed.
762
- * @default "72px"
699
+ * When false, the dock column snaps between `0` and `width` with no CSS transition so main
700
+ * content does not reflow during the open/close animation.
701
+ * @default true
702
+ */
703
+ animate?: boolean;
704
+ /**
705
+ * How the dock panel is shown.
706
+ * - `"resize"` (default): a flex column grows/shrinks between `0` and `width` (main content reflows).
707
+ * - `"overlay"`: panel is absolutely positioned and translates in/out **over** full-width content.
708
+ * - `"push"`: a wide inner track `[content at shell width][panel]` translates horizontally so the panel
709
+ * appears to push the workspace aside **without** animating the content column width (Shopify-style).
710
+ * - `"emerge"`: like `"resize"`, the flex column animates so **page content reflows**; the chat
711
+ * panel keeps a **fixed** `dock.width` (not squeezed while the column grows), clipped by the slot so
712
+ * it appears to emerge at full width like a floating widget.
763
713
  */
764
- collapsedWidth?: string;
714
+ reveal?: "resize" | "overlay" | "push" | "emerge";
765
715
  };
766
716
 
767
717
  export type AgentWidgetLauncherConfig = {
@@ -1396,6 +1346,17 @@ export type AgentWidgetHeaderTrailingAction = {
1396
1346
  icon?: string;
1397
1347
  label?: string;
1398
1348
  ariaLabel?: string;
1349
+ /**
1350
+ * When set, clicking this action opens a dropdown menu.
1351
+ * Menu item selections fire `onAction(menuItemId)`.
1352
+ */
1353
+ menuItems?: Array<{
1354
+ id: string;
1355
+ label: string;
1356
+ icon?: string;
1357
+ destructive?: boolean;
1358
+ dividerBefore?: boolean;
1359
+ }>;
1399
1360
  };
1400
1361
 
1401
1362
  /**
@@ -1466,6 +1427,41 @@ export type AgentWidgetHeaderLayoutConfig = {
1466
1427
  * When set, the title row becomes visually interactive (cursor: pointer).
1467
1428
  */
1468
1429
  onTitleClick?: () => void;
1430
+ /** Style config for the title row hover effect (minimal layout). */
1431
+ titleRowHover?: {
1432
+ /** Hover background color. */
1433
+ background?: string;
1434
+ /** Hover border color. */
1435
+ border?: string;
1436
+ /** Border radius for the pill shape. */
1437
+ borderRadius?: string;
1438
+ /** Padding inside the pill. */
1439
+ padding?: string;
1440
+ };
1441
+ /**
1442
+ * Replaces the title with a combo button (label + chevron + dropdown menu).
1443
+ * When set, `trailingActions`, `onTitleClick`, and `titleRowHover` are ignored
1444
+ * since the combo button handles all of these internally.
1445
+ */
1446
+ titleMenu?: {
1447
+ /** Dropdown menu items. */
1448
+ menuItems: Array<{
1449
+ id: string;
1450
+ label: string;
1451
+ icon?: string;
1452
+ destructive?: boolean;
1453
+ dividerBefore?: boolean;
1454
+ }>;
1455
+ /** Called when a menu item is selected. */
1456
+ onSelect: (id: string) => void;
1457
+ /** Hover pill style. */
1458
+ hover?: {
1459
+ background?: string;
1460
+ border?: string;
1461
+ borderRadius?: string;
1462
+ padding?: string;
1463
+ };
1464
+ };
1469
1465
  };
1470
1466
 
1471
1467
  /**
@@ -2256,22 +2252,15 @@ export type AgentWidgetConfig = {
2256
2252
  */
2257
2253
  showWelcomeCard?: boolean;
2258
2254
  };
2259
- theme?: AgentWidgetTheme;
2260
2255
  /**
2261
- * Theme colors for dark mode. Applied when dark mode is detected
2262
- * (when colorScheme is 'dark' or 'auto' with dark mode active).
2263
- * If not provided, falls back to `theme` colors.
2264
- *
2265
- * @example
2266
- * ```typescript
2267
- * config: {
2268
- * theme: { primary: '#111827', surface: '#ffffff' },
2269
- * darkTheme: { primary: '#f9fafb', surface: '#1f2937' },
2270
- * colorScheme: 'auto'
2271
- * }
2272
- * ```
2256
+ * Semantic design tokens (`palette`, `semantic`, `components`).
2257
+ * Omit for library defaults.
2258
+ */
2259
+ theme?: DeepPartial<PersonaTheme>;
2260
+ /**
2261
+ * Dark-mode token overrides. Merged over `theme` when the active scheme is dark.
2273
2262
  */
2274
- darkTheme?: AgentWidgetTheme;
2263
+ darkTheme?: DeepPartial<PersonaTheme>;
2275
2264
  /**
2276
2265
  * Color scheme mode for the widget.
2277
2266
  * - 'light': Always use light theme (default)
@@ -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();