@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
@@ -15,6 +15,16 @@ export type WidgetHostLayout = {
15
15
  destroy: () => void;
16
16
  };
17
17
 
18
+ /** Parse `dock.width` for push layout math (px or % of shell). Fallback: 420. */
19
+ const parseDockWidthToPx = (width: string, shellClientWidth: number): number => {
20
+ const w = width.trim();
21
+ const px = /^(\d+(?:\.\d+)?)px$/i.exec(w);
22
+ if (px) return Math.max(0, parseFloat(px[1]));
23
+ const pct = /^(\d+(?:\.\d+)?)%$/i.exec(w);
24
+ if (pct) return Math.max(0, (shellClientWidth * parseFloat(pct[1])) / 100);
25
+ return 420;
26
+ };
27
+
18
28
  const setDirectHostStyles = (host: HTMLElement, config?: AgentWidgetConfig): void => {
19
29
  const launcherEnabled = config?.launcher?.enabled ?? true;
20
30
  host.className = "persona-host";
@@ -25,8 +35,100 @@ const setDirectHostStyles = (host: HTMLElement, config?: AgentWidgetConfig): voi
25
35
  host.style.minHeight = launcherEnabled ? "" : "0";
26
36
  };
27
37
 
38
+ const clearOverlayDockSlotStyles = (dockSlot: HTMLElement): void => {
39
+ dockSlot.style.position = "";
40
+ dockSlot.style.top = "";
41
+ dockSlot.style.bottom = "";
42
+ dockSlot.style.left = "";
43
+ dockSlot.style.right = "";
44
+ dockSlot.style.zIndex = "";
45
+ dockSlot.style.transform = "";
46
+ dockSlot.style.pointerEvents = "";
47
+ };
48
+
49
+ /** Clears viewport-escape fullscreen styles so reveal modes can re-apply dock layout. */
50
+ const clearMobileFullscreenDockSlotStyles = (dockSlot: HTMLElement): void => {
51
+ dockSlot.style.inset = "";
52
+ dockSlot.style.width = "";
53
+ dockSlot.style.height = "";
54
+ dockSlot.style.maxWidth = "";
55
+ dockSlot.style.minWidth = "";
56
+ clearOverlayDockSlotStyles(dockSlot);
57
+ };
58
+
59
+ const clearResizeDockSlotTransition = (dockSlot: HTMLElement): void => {
60
+ dockSlot.style.transition = "";
61
+ };
62
+
63
+ const clearPushTrackStyles = (pushTrack: HTMLElement): void => {
64
+ pushTrack.style.display = "";
65
+ pushTrack.style.flexDirection = "";
66
+ pushTrack.style.flex = "";
67
+ pushTrack.style.minHeight = "";
68
+ pushTrack.style.minWidth = "";
69
+ pushTrack.style.width = "";
70
+ pushTrack.style.height = "";
71
+ pushTrack.style.alignItems = "";
72
+ pushTrack.style.transition = "";
73
+ pushTrack.style.transform = "";
74
+ };
75
+
76
+ const resetContentSlotFlexSizing = (contentSlot: HTMLElement): void => {
77
+ contentSlot.style.width = "";
78
+ contentSlot.style.maxWidth = "";
79
+ contentSlot.style.minWidth = "";
80
+ contentSlot.style.flex = "1 1 auto";
81
+ };
82
+
83
+ const clearEmergeDockStyles = (host: HTMLElement, dockSlot: HTMLElement): void => {
84
+ host.style.width = "";
85
+ host.style.minWidth = "";
86
+ host.style.maxWidth = "";
87
+ host.style.boxSizing = "";
88
+ dockSlot.style.alignItems = "";
89
+ };
90
+
91
+ const migrateDockChildren = (
92
+ shell: HTMLElement,
93
+ pushTrack: HTMLElement,
94
+ contentSlot: HTMLElement,
95
+ dockSlot: HTMLElement,
96
+ usePush: boolean
97
+ ): void => {
98
+ if (usePush) {
99
+ if (contentSlot.parentElement !== pushTrack) {
100
+ shell.replaceChildren();
101
+ pushTrack.replaceChildren(contentSlot, dockSlot);
102
+ shell.appendChild(pushTrack);
103
+ }
104
+ } else if (contentSlot.parentElement === pushTrack) {
105
+ pushTrack.replaceChildren();
106
+ shell.appendChild(contentSlot);
107
+ shell.appendChild(dockSlot);
108
+ }
109
+ };
110
+
111
+ const orderDockChildren = (
112
+ shell: HTMLElement,
113
+ pushTrack: HTMLElement,
114
+ contentSlot: HTMLElement,
115
+ dockSlot: HTMLElement,
116
+ side: "left" | "right",
117
+ usePush: boolean
118
+ ): void => {
119
+ const parent = usePush ? pushTrack : shell;
120
+ if (side === "left") {
121
+ if (parent.firstElementChild !== dockSlot) {
122
+ parent.replaceChildren(dockSlot, contentSlot);
123
+ }
124
+ } else if (parent.lastElementChild !== dockSlot) {
125
+ parent.replaceChildren(contentSlot, dockSlot);
126
+ }
127
+ };
128
+
28
129
  const applyDockStyles = (
29
130
  shell: HTMLElement,
131
+ pushTrack: HTMLElement,
30
132
  contentSlot: HTMLElement,
31
133
  dockSlot: HTMLElement,
32
134
  host: HTMLElement,
@@ -34,14 +136,14 @@ const applyDockStyles = (
34
136
  expanded: boolean
35
137
  ): void => {
36
138
  const dock = resolveDockConfig(config);
37
- const width = expanded ? dock.width : dock.collapsedWidth;
139
+ const usePush = dock.reveal === "push";
140
+
141
+ migrateDockChildren(shell, pushTrack, contentSlot, dockSlot, usePush);
142
+ orderDockChildren(shell, pushTrack, contentSlot, dockSlot, dock.side, usePush);
38
143
 
39
144
  shell.dataset.personaHostLayout = "docked";
40
145
  shell.dataset.personaDockSide = dock.side;
41
146
  shell.dataset.personaDockOpen = expanded ? "true" : "false";
42
- shell.style.display = "flex";
43
- shell.style.flexDirection = "row";
44
- shell.style.alignItems = "stretch";
45
147
  shell.style.width = "100%";
46
148
  shell.style.maxWidth = "100%";
47
149
  shell.style.minWidth = "0";
@@ -51,22 +153,9 @@ const applyDockStyles = (
51
153
 
52
154
  contentSlot.style.display = "flex";
53
155
  contentSlot.style.flexDirection = "column";
54
- contentSlot.style.flex = "1 1 auto";
55
- contentSlot.style.minWidth = "0";
56
156
  contentSlot.style.minHeight = "0";
57
157
  contentSlot.style.position = "relative";
58
158
 
59
- dockSlot.style.display = "flex";
60
- dockSlot.style.flexDirection = "column";
61
- dockSlot.style.flex = `0 0 ${width}`;
62
- dockSlot.style.width = width;
63
- dockSlot.style.maxWidth = width;
64
- dockSlot.style.minWidth = width;
65
- dockSlot.style.minHeight = "0";
66
- dockSlot.style.position = "relative";
67
- dockSlot.style.overflow = "visible";
68
- dockSlot.style.transition = "width 180ms ease, min-width 180ms ease, max-width 180ms ease, flex-basis 180ms ease";
69
-
70
159
  host.className = "persona-host";
71
160
  host.style.height = "100%";
72
161
  host.style.minHeight = "0";
@@ -74,12 +163,202 @@ const applyDockStyles = (
74
163
  host.style.flexDirection = "column";
75
164
  host.style.flex = "1 1 auto";
76
165
 
77
- if (dock.side === "left") {
78
- if (shell.firstElementChild !== dockSlot) {
79
- shell.replaceChildren(dockSlot, contentSlot);
166
+ const ownerWindow = shell.ownerDocument.defaultView;
167
+ const mobileFullscreenEnabled = config?.launcher?.mobileFullscreen ?? true;
168
+ const mobileBreakpoint = config?.launcher?.mobileBreakpoint ?? 640;
169
+ const isMobileViewport =
170
+ ownerWindow != null ? ownerWindow.innerWidth <= mobileBreakpoint : false;
171
+ const useMobileFullscreen = mobileFullscreenEnabled && isMobileViewport && expanded;
172
+
173
+ if (useMobileFullscreen) {
174
+ shell.dataset.personaDockMobileFullscreen = "true";
175
+ shell.removeAttribute("data-persona-dock-reveal");
176
+ clearPushTrackStyles(pushTrack);
177
+ clearResizeDockSlotTransition(dockSlot);
178
+ clearMobileFullscreenDockSlotStyles(dockSlot);
179
+ resetContentSlotFlexSizing(contentSlot);
180
+ clearEmergeDockStyles(host, dockSlot);
181
+
182
+ shell.style.display = "flex";
183
+ shell.style.flexDirection = "column";
184
+ shell.style.alignItems = "stretch";
185
+ shell.style.overflow = "hidden";
186
+
187
+ contentSlot.style.flex = "1 1 auto";
188
+ contentSlot.style.width = "100%";
189
+ contentSlot.style.minWidth = "0";
190
+
191
+ dockSlot.style.display = "flex";
192
+ dockSlot.style.flexDirection = "column";
193
+ dockSlot.style.position = "fixed";
194
+ dockSlot.style.inset = "0";
195
+ dockSlot.style.width = "100%";
196
+ dockSlot.style.height = "100%";
197
+ dockSlot.style.maxWidth = "100%";
198
+ dockSlot.style.minWidth = "0";
199
+ dockSlot.style.minHeight = "0";
200
+ dockSlot.style.overflow = "hidden";
201
+ dockSlot.style.zIndex = "9999";
202
+ dockSlot.style.transform = "none";
203
+ dockSlot.style.transition = "none";
204
+ dockSlot.style.pointerEvents = "auto";
205
+ dockSlot.style.flex = "none";
206
+
207
+ if (usePush) {
208
+ pushTrack.style.display = "flex";
209
+ pushTrack.style.flexDirection = "column";
210
+ pushTrack.style.width = "100%";
211
+ pushTrack.style.height = "100%";
212
+ pushTrack.style.minHeight = "0";
213
+ pushTrack.style.minWidth = "0";
214
+ pushTrack.style.flex = "1 1 auto";
215
+ pushTrack.style.alignItems = "stretch";
216
+ pushTrack.style.transform = "none";
217
+ pushTrack.style.transition = "none";
218
+ contentSlot.style.flex = "1 1 auto";
219
+ contentSlot.style.width = "100%";
220
+ contentSlot.style.maxWidth = "100%";
221
+ contentSlot.style.minWidth = "0";
222
+ }
223
+
224
+ return;
225
+ }
226
+
227
+ shell.removeAttribute("data-persona-dock-mobile-fullscreen");
228
+ clearMobileFullscreenDockSlotStyles(dockSlot);
229
+
230
+ if (dock.reveal === "overlay") {
231
+ shell.style.display = "flex";
232
+ shell.style.flexDirection = "row";
233
+ shell.style.alignItems = "stretch";
234
+ shell.style.overflow = "hidden";
235
+ shell.dataset.personaDockReveal = "overlay";
236
+ clearPushTrackStyles(pushTrack);
237
+ clearResizeDockSlotTransition(dockSlot);
238
+ resetContentSlotFlexSizing(contentSlot);
239
+ clearEmergeDockStyles(host, dockSlot);
240
+
241
+ const dockTransition = dock.animate ? "transform 180ms ease" : "none";
242
+ const translateClosed = dock.side === "right" ? "translateX(100%)" : "translateX(-100%)";
243
+ const translate = expanded ? "translateX(0)" : translateClosed;
244
+
245
+ dockSlot.style.display = "flex";
246
+ dockSlot.style.flexDirection = "column";
247
+ dockSlot.style.flex = "none";
248
+ dockSlot.style.position = "absolute";
249
+ dockSlot.style.top = "0";
250
+ dockSlot.style.bottom = "0";
251
+ dockSlot.style.width = dock.width;
252
+ dockSlot.style.maxWidth = dock.width;
253
+ dockSlot.style.minWidth = dock.width;
254
+ dockSlot.style.minHeight = "0";
255
+ dockSlot.style.overflow = "hidden";
256
+ dockSlot.style.transition = dockTransition;
257
+ dockSlot.style.transform = translate;
258
+ dockSlot.style.pointerEvents = expanded ? "auto" : "none";
259
+ dockSlot.style.zIndex = "2";
260
+ if (dock.side === "right") {
261
+ dockSlot.style.right = "0";
262
+ dockSlot.style.left = "";
263
+ } else {
264
+ dockSlot.style.left = "0";
265
+ dockSlot.style.right = "";
266
+ }
267
+ } else if (dock.reveal === "push") {
268
+ // Row flex so the wide push track is laid out on the horizontal axis; column was stretching
269
+ // the track to the shell width and fighting explicit width, which could confuse overflow.
270
+ shell.style.display = "flex";
271
+ shell.style.flexDirection = "row";
272
+ shell.style.alignItems = "stretch";
273
+ shell.style.overflow = "hidden";
274
+ shell.dataset.personaDockReveal = "push";
275
+ clearResizeDockSlotTransition(dockSlot);
276
+ clearOverlayDockSlotStyles(dockSlot);
277
+ clearEmergeDockStyles(host, dockSlot);
278
+
279
+ const panelPx = parseDockWidthToPx(dock.width, shell.clientWidth);
280
+ const contentPx = Math.max(0, shell.clientWidth);
281
+ const dockTransition = dock.animate ? "transform 180ms ease" : "none";
282
+ const translate =
283
+ dock.side === "right"
284
+ ? expanded
285
+ ? `translateX(-${panelPx}px)`
286
+ : "translateX(0)"
287
+ : expanded
288
+ ? "translateX(0)"
289
+ : `translateX(-${panelPx}px)`;
290
+
291
+ pushTrack.style.display = "flex";
292
+ pushTrack.style.flexDirection = "row";
293
+ pushTrack.style.flex = "0 0 auto";
294
+ pushTrack.style.minHeight = "0";
295
+ pushTrack.style.minWidth = "0";
296
+ pushTrack.style.alignItems = "stretch";
297
+ pushTrack.style.height = "100%";
298
+ pushTrack.style.width = `${contentPx + panelPx}px`;
299
+ pushTrack.style.transition = dockTransition;
300
+ pushTrack.style.transform = translate;
301
+
302
+ contentSlot.style.flex = "0 0 auto";
303
+ contentSlot.style.flexGrow = "0";
304
+ contentSlot.style.flexShrink = "0";
305
+ contentSlot.style.width = `${contentPx}px`;
306
+ contentSlot.style.maxWidth = `${contentPx}px`;
307
+ contentSlot.style.minWidth = `${contentPx}px`;
308
+
309
+ dockSlot.style.display = "flex";
310
+ dockSlot.style.flexDirection = "column";
311
+ dockSlot.style.flex = "0 0 auto";
312
+ dockSlot.style.flexShrink = "0";
313
+ dockSlot.style.width = dock.width;
314
+ dockSlot.style.minWidth = dock.width;
315
+ dockSlot.style.maxWidth = dock.width;
316
+ dockSlot.style.position = "relative";
317
+ dockSlot.style.overflow = "hidden";
318
+ dockSlot.style.transition = "none";
319
+ dockSlot.style.pointerEvents = expanded ? "auto" : "none";
320
+ } else {
321
+ shell.style.display = "flex";
322
+ shell.style.flexDirection = "row";
323
+ shell.style.alignItems = "stretch";
324
+ shell.style.overflow = "";
325
+ clearPushTrackStyles(pushTrack);
326
+ clearOverlayDockSlotStyles(dockSlot);
327
+ resetContentSlotFlexSizing(contentSlot);
328
+ clearEmergeDockStyles(host, dockSlot);
329
+
330
+ const isEmerge = dock.reveal === "emerge";
331
+ if (isEmerge) {
332
+ shell.dataset.personaDockReveal = "emerge";
333
+ } else {
334
+ shell.removeAttribute("data-persona-dock-reveal");
335
+ }
336
+
337
+ const width = expanded ? dock.width : "0px";
338
+ const dockTransition = dock.animate
339
+ ? "width 180ms ease, min-width 180ms ease, max-width 180ms ease, flex-basis 180ms ease"
340
+ : "none";
341
+ const collapsedClosed = !expanded;
342
+
343
+ dockSlot.style.display = "flex";
344
+ dockSlot.style.flexDirection = "column";
345
+ dockSlot.style.flex = `0 0 ${width}`;
346
+ dockSlot.style.width = width;
347
+ dockSlot.style.maxWidth = width;
348
+ dockSlot.style.minWidth = width;
349
+ dockSlot.style.minHeight = "0";
350
+ dockSlot.style.position = "relative";
351
+ dockSlot.style.overflow =
352
+ isEmerge ? "hidden" : collapsedClosed ? "hidden" : "visible";
353
+ dockSlot.style.transition = dockTransition;
354
+
355
+ if (isEmerge) {
356
+ dockSlot.style.alignItems = dock.side === "right" ? "flex-start" : "flex-end";
357
+ host.style.width = dock.width;
358
+ host.style.minWidth = dock.width;
359
+ host.style.maxWidth = dock.width;
360
+ host.style.boxSizing = "border-box";
80
361
  }
81
- } else if (shell.lastElementChild !== dockSlot) {
82
- shell.replaceChildren(contentSlot, dockSlot);
83
362
  }
84
363
  };
85
364
 
@@ -117,11 +396,13 @@ const createDockedLayout = (target: HTMLElement, config?: AgentWidgetConfig): Wi
117
396
 
118
397
  const originalNextSibling = target.nextSibling;
119
398
  const shell = ownerDocument.createElement("div");
399
+ const pushTrack = ownerDocument.createElement("div");
120
400
  const contentSlot = ownerDocument.createElement("div");
121
401
  const dockSlot = ownerDocument.createElement("aside");
122
402
  const host = ownerDocument.createElement("div");
123
403
  let expanded = (config?.launcher?.enabled ?? true) ? (config?.launcher?.autoExpand ?? false) : true;
124
404
 
405
+ pushTrack.dataset.personaDockRole = "push-track";
125
406
  contentSlot.dataset.personaDockRole = "content";
126
407
  dockSlot.dataset.personaDockRole = "panel";
127
408
  host.dataset.personaDockRole = "host";
@@ -129,9 +410,45 @@ const createDockedLayout = (target: HTMLElement, config?: AgentWidgetConfig): Wi
129
410
  dockSlot.appendChild(host);
130
411
  originalParent.insertBefore(shell, target);
131
412
  contentSlot.appendChild(target);
132
- shell.appendChild(contentSlot);
133
- shell.appendChild(dockSlot);
134
- applyDockStyles(shell, contentSlot, dockSlot, host, config, expanded);
413
+
414
+ let resizeObserver: ResizeObserver | null = null;
415
+
416
+ const disconnectResizeObserver = (): void => {
417
+ resizeObserver?.disconnect();
418
+ resizeObserver = null;
419
+ };
420
+
421
+ const syncPushResizeObserver = (): void => {
422
+ disconnectResizeObserver();
423
+ if (resolveDockConfig(config).reveal !== "push") return;
424
+ if (typeof ResizeObserver === "undefined") return;
425
+ resizeObserver = new ResizeObserver(() => {
426
+ applyDockStyles(shell, pushTrack, contentSlot, dockSlot, host, config, expanded);
427
+ });
428
+ resizeObserver.observe(shell);
429
+ };
430
+
431
+ const layout = (): void => {
432
+ applyDockStyles(shell, pushTrack, contentSlot, dockSlot, host, config, expanded);
433
+ syncPushResizeObserver();
434
+ };
435
+
436
+ const ownerWindow = shell.ownerDocument.defaultView;
437
+ const onViewportResize = (): void => {
438
+ layout();
439
+ };
440
+ ownerWindow?.addEventListener("resize", onViewportResize);
441
+
442
+ if (resolveDockConfig(config).reveal === "push") {
443
+ pushTrack.appendChild(contentSlot);
444
+ pushTrack.appendChild(dockSlot);
445
+ shell.appendChild(pushTrack);
446
+ } else {
447
+ shell.appendChild(contentSlot);
448
+ shell.appendChild(dockSlot);
449
+ }
450
+
451
+ layout();
135
452
 
136
453
  return {
137
454
  mode: "docked",
@@ -141,16 +458,18 @@ const createDockedLayout = (target: HTMLElement, config?: AgentWidgetConfig): Wi
141
458
  const nextExpanded = state.launcherEnabled ? state.open : true;
142
459
  if (expanded === nextExpanded) return;
143
460
  expanded = nextExpanded;
144
- applyDockStyles(shell, contentSlot, dockSlot, host, config, expanded);
461
+ layout();
145
462
  },
146
463
  updateConfig(nextConfig?: AgentWidgetConfig) {
147
464
  config = nextConfig;
148
465
  if ((config?.launcher?.enabled ?? true) === false) {
149
466
  expanded = true;
150
467
  }
151
- applyDockStyles(shell, contentSlot, dockSlot, host, config, expanded);
468
+ layout();
152
469
  },
153
470
  destroy() {
471
+ ownerWindow?.removeEventListener("resize", onViewportResize);
472
+ disconnectResizeObserver();
154
473
  if (originalParent.isConnected) {
155
474
  if (originalNextSibling && originalNextSibling.parentNode === originalParent) {
156
475
  originalParent.insertBefore(target, originalNextSibling);
@@ -104,7 +104,7 @@ describe("initAgentWidget docked mode", () => {
104
104
  config: {
105
105
  launcher: {
106
106
  mountMode: "docked",
107
- dock: { width: "420px", collapsedWidth: "72px" },
107
+ dock: { width: "420px" },
108
108
  },
109
109
  },
110
110
  });
@@ -146,7 +146,7 @@ describe("initAgentWidget docked mode", () => {
146
146
  launcher: {
147
147
  mountMode: "docked",
148
148
  autoExpand: true,
149
- dock: { width: "400px", collapsedWidth: "80px" },
149
+ dock: { width: "400px" },
150
150
  },
151
151
  },
152
152
  });
@@ -155,12 +155,82 @@ describe("initAgentWidget docked mode", () => {
155
155
  expect(panelSlot.style.width).toBe("400px");
156
156
 
157
157
  handle.close();
158
- expect(panelSlot.style.width).toBe("80px");
158
+ expect(panelSlot.style.width).toBe("0px");
159
159
 
160
160
  handle.open();
161
161
  expect(panelSlot.style.width).toBe("400px");
162
162
  });
163
163
 
164
+ it("overlay dock reveal keeps width and uses transform when closing", async () => {
165
+ const { initAgentWidget } = await import("./init");
166
+ document.body.innerHTML = `<div id="content">Workspace</div>`;
167
+
168
+ const handle = initAgentWidget({
169
+ target: "#content",
170
+ config: {
171
+ launcher: {
172
+ mountMode: "docked",
173
+ autoExpand: true,
174
+ dock: { width: "400px", reveal: "overlay" },
175
+ },
176
+ },
177
+ });
178
+
179
+ const panelSlot = document.querySelector<HTMLElement>('[data-persona-dock-role="panel"]')!;
180
+ expect(panelSlot.style.width).toBe("400px");
181
+
182
+ handle.close();
183
+ expect(panelSlot.style.width).toBe("400px");
184
+ expect(panelSlot.style.transform).toBe("translateX(100%)");
185
+
186
+ handle.open();
187
+ expect(panelSlot.style.transform).toBe("translateX(0)");
188
+
189
+ handle.destroy();
190
+ });
191
+
192
+ it("push dock reveal translates the push-track; panel keeps width when closing", async () => {
193
+ const { initAgentWidget } = await import("./init");
194
+ const wrapper = document.createElement("div");
195
+ wrapper.style.width = "900px";
196
+ document.body.appendChild(wrapper);
197
+ wrapper.innerHTML = `<div id="content">Workspace</div>`;
198
+
199
+ const handle = initAgentWidget({
200
+ target: "#content",
201
+ config: {
202
+ launcher: {
203
+ mountMode: "docked",
204
+ autoExpand: true,
205
+ dock: { width: "400px", reveal: "push" },
206
+ },
207
+ },
208
+ });
209
+
210
+ const shell = document.querySelector<HTMLElement>('[data-persona-host-layout="docked"]')!;
211
+ Object.defineProperty(shell, "clientWidth", { get: () => 900, configurable: true });
212
+ handle.update({
213
+ launcher: {
214
+ mountMode: "docked",
215
+ autoExpand: true,
216
+ dock: { width: "400px", reveal: "push" },
217
+ },
218
+ });
219
+
220
+ const pushTrack = shell.querySelector<HTMLElement>('[data-persona-dock-role="push-track"]');
221
+ const panelSlot = shell.querySelector<HTMLElement>('[data-persona-dock-role="panel"]')!;
222
+ expect(pushTrack).not.toBeNull();
223
+ expect(panelSlot.style.width).toBe("400px");
224
+ expect(pushTrack?.style.transform).toBe("translateX(-400px)");
225
+
226
+ handle.close();
227
+ expect(panelSlot.style.width).toBe("400px");
228
+ expect(pushTrack?.style.transform).toBe("translateX(0)");
229
+
230
+ handle.destroy();
231
+ wrapper.remove();
232
+ });
233
+
164
234
  it("rebuilds when mount mode changes from floating to docked", async () => {
165
235
  const { initAgentWidget } = await import("./init");
166
236
  document.body.innerHTML = `<div id="content">Workspace</div>`;
@@ -178,7 +248,7 @@ describe("initAgentWidget docked mode", () => {
178
248
  handle.update({
179
249
  launcher: {
180
250
  mountMode: "docked",
181
- dock: { side: "left", width: "460px", collapsedWidth: "88px" },
251
+ dock: { side: "left", width: "460px" },
182
252
  },
183
253
  });
184
254
 
@@ -197,7 +267,7 @@ describe("initAgentWidget docked mode", () => {
197
267
  config: {
198
268
  launcher: {
199
269
  mountMode: "docked",
200
- dock: { side: "right", width: "420px", collapsedWidth: "72px" },
270
+ dock: { side: "right", width: "420px" },
201
271
  },
202
272
  },
203
273
  });
@@ -205,7 +275,7 @@ describe("initAgentWidget docked mode", () => {
205
275
  expect(createAgentExperienceMock).toHaveBeenCalledTimes(1);
206
276
  handle.update({
207
277
  launcher: {
208
- dock: { side: "left", width: "500px", collapsedWidth: "96px" },
278
+ dock: { side: "left", width: "500px" },
209
279
  },
210
280
  });
211
281
 
@@ -213,7 +283,7 @@ describe("initAgentWidget docked mode", () => {
213
283
  const shell = document.querySelector<HTMLElement>('[data-persona-host-layout="docked"]');
214
284
  const panelSlot = document.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
215
285
  expect(shell?.firstElementChild?.getAttribute("data-persona-dock-role")).toBe("panel");
216
- expect(panelSlot?.style.width).toBe("96px");
286
+ expect(panelSlot?.style.width).toBe("0px");
217
287
  });
218
288
 
219
289
  it("supports shadow DOM hosts in docked mode", async () => {
@@ -231,6 +301,41 @@ describe("initAgentWidget docked mode", () => {
231
301
  });
232
302
 
233
303
  expect(handle.host.shadowRoot).not.toBeNull();
234
- expect(handle.host.shadowRoot?.querySelector("#persona-root")).not.toBeNull();
304
+ expect(handle.host.shadowRoot?.querySelector("[data-persona-root]")).not.toBeNull();
305
+ });
306
+
307
+ it("mounts two widgets with independent roots in light DOM", async () => {
308
+ const { initAgentWidget } = await import("./init");
309
+ document.body.innerHTML = `
310
+ <div id="widget-a"></div>
311
+ <div id="widget-b"></div>
312
+ `;
313
+
314
+ const handleA = initAgentWidget({
315
+ target: "#widget-a",
316
+ config: {
317
+ launcher: { enabled: false },
318
+ },
319
+ });
320
+
321
+ const handleB = initAgentWidget({
322
+ target: "#widget-b",
323
+ config: {
324
+ launcher: { enabled: false },
325
+ },
326
+ });
327
+
328
+ const roots = document.querySelectorAll("[data-persona-root]");
329
+ expect(roots.length).toBe(2);
330
+
331
+ // Each root should be inside its respective target
332
+ const rootA = document.querySelector("#widget-a [data-persona-root]");
333
+ const rootB = document.querySelector("#widget-b [data-persona-root]");
334
+ expect(rootA).not.toBeNull();
335
+ expect(rootB).not.toBeNull();
336
+ expect(rootA).not.toBe(rootB);
337
+
338
+ handleA.destroy();
339
+ handleB.destroy();
235
340
  });
236
341
  });
@@ -103,7 +103,7 @@ export const initAgentWidget = (
103
103
  const launcherEnabled = nextConfig?.launcher?.enabled ?? true;
104
104
  const shouldFillHost = !launcherEnabled || isDockedMountMode(nextConfig);
105
105
  const mount = ownerDocument.createElement("div");
106
- mount.id = "persona-root";
106
+ mount.setAttribute("data-persona-root", "true");
107
107
 
108
108
  if (shouldFillHost) {
109
109
  mount.style.height = "100%";