@runtypelabs/persona 3.0.0 → 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.
@@ -193,4 +193,141 @@ describe("createWidgetHostLayout docked", () => {
193
193
 
194
194
  layout.destroy();
195
195
  });
196
+
197
+ const withInnerWidth = (width: number, fn: () => void): void => {
198
+ const prev = window.innerWidth;
199
+ try {
200
+ Object.defineProperty(window, "innerWidth", {
201
+ configurable: true,
202
+ value: width,
203
+ });
204
+ fn();
205
+ } finally {
206
+ Object.defineProperty(window, "innerWidth", {
207
+ configurable: true,
208
+ value: prev,
209
+ });
210
+ }
211
+ };
212
+
213
+ it("uses fixed fullscreen dock slot on mobile viewport when open", () => {
214
+ withInnerWidth(500, () => {
215
+ const parent = document.createElement("div");
216
+ document.body.appendChild(parent);
217
+ const target = document.createElement("div");
218
+ parent.appendChild(target);
219
+
220
+ const layout = createWidgetHostLayout(target, {
221
+ launcher: {
222
+ mountMode: "docked",
223
+ autoExpand: false,
224
+ dock: { width: "320px" },
225
+ },
226
+ });
227
+
228
+ const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
229
+ layout.syncWidgetState({ open: true, launcherEnabled: true });
230
+ expect(dockSlot?.style.position).toBe("fixed");
231
+ expect(dockSlot?.style.zIndex).toBe("9999");
232
+ expect(layout.shell?.dataset.personaDockMobileFullscreen).toBe("true");
233
+
234
+ layout.destroy();
235
+ });
236
+ });
237
+
238
+ it("does not use fixed fullscreen above mobile breakpoint", () => {
239
+ withInnerWidth(800, () => {
240
+ const parent = document.createElement("div");
241
+ document.body.appendChild(parent);
242
+ const target = document.createElement("div");
243
+ parent.appendChild(target);
244
+
245
+ const layout = createWidgetHostLayout(target, {
246
+ launcher: {
247
+ mountMode: "docked",
248
+ autoExpand: false,
249
+ dock: { width: "320px", reveal: "overlay" },
250
+ },
251
+ });
252
+
253
+ const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
254
+ layout.syncWidgetState({ open: true, launcherEnabled: true });
255
+ expect(dockSlot?.style.position).toBe("absolute");
256
+ expect(layout.shell?.dataset.personaDockMobileFullscreen).toBeUndefined();
257
+
258
+ layout.destroy();
259
+ });
260
+ });
261
+
262
+ it("respects mobileFullscreen: false on narrow viewport", () => {
263
+ withInnerWidth(500, () => {
264
+ const parent = document.createElement("div");
265
+ document.body.appendChild(parent);
266
+ const target = document.createElement("div");
267
+ parent.appendChild(target);
268
+
269
+ const layout = createWidgetHostLayout(target, {
270
+ launcher: {
271
+ mountMode: "docked",
272
+ autoExpand: false,
273
+ mobileFullscreen: false,
274
+ dock: { width: "320px" },
275
+ },
276
+ });
277
+
278
+ const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
279
+ layout.syncWidgetState({ open: true, launcherEnabled: true });
280
+ expect(dockSlot?.style.position).toBe("relative");
281
+ expect(layout.shell?.dataset.personaDockMobileFullscreen).toBeUndefined();
282
+
283
+ layout.destroy();
284
+ });
285
+ });
286
+
287
+ it("respects custom mobileBreakpoint", () => {
288
+ withInnerWidth(900, () => {
289
+ const parent = document.createElement("div");
290
+ document.body.appendChild(parent);
291
+ const target = document.createElement("div");
292
+ parent.appendChild(target);
293
+
294
+ const layout = createWidgetHostLayout(target, {
295
+ launcher: {
296
+ mountMode: "docked",
297
+ autoExpand: false,
298
+ mobileBreakpoint: 1024,
299
+ dock: { width: "320px" },
300
+ },
301
+ });
302
+
303
+ const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
304
+ layout.syncWidgetState({ open: true, launcherEnabled: true });
305
+ expect(dockSlot?.style.position).toBe("fixed");
306
+
307
+ layout.destroy();
308
+ });
309
+ });
310
+
311
+ it("does not use fixed fullscreen when panel is closed on mobile", () => {
312
+ withInnerWidth(500, () => {
313
+ const parent = document.createElement("div");
314
+ document.body.appendChild(parent);
315
+ const target = document.createElement("div");
316
+ parent.appendChild(target);
317
+
318
+ const layout = createWidgetHostLayout(target, {
319
+ launcher: {
320
+ mountMode: "docked",
321
+ autoExpand: false,
322
+ dock: { width: "320px" },
323
+ },
324
+ });
325
+
326
+ const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
327
+ layout.syncWidgetState({ open: false, launcherEnabled: true });
328
+ expect(dockSlot?.style.position).toBe("relative");
329
+
330
+ layout.destroy();
331
+ });
332
+ });
196
333
  });
@@ -46,6 +46,16 @@ const clearOverlayDockSlotStyles = (dockSlot: HTMLElement): void => {
46
46
  dockSlot.style.pointerEvents = "";
47
47
  };
48
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
+
49
59
  const clearResizeDockSlotTransition = (dockSlot: HTMLElement): void => {
50
60
  dockSlot.style.transition = "";
51
61
  };
@@ -153,6 +163,70 @@ const applyDockStyles = (
153
163
  host.style.flexDirection = "column";
154
164
  host.style.flex = "1 1 auto";
155
165
 
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
+
156
230
  if (dock.reveal === "overlay") {
157
231
  shell.style.display = "flex";
158
232
  shell.style.flexDirection = "row";
@@ -359,6 +433,12 @@ const createDockedLayout = (target: HTMLElement, config?: AgentWidgetConfig): Wi
359
433
  syncPushResizeObserver();
360
434
  };
361
435
 
436
+ const ownerWindow = shell.ownerDocument.defaultView;
437
+ const onViewportResize = (): void => {
438
+ layout();
439
+ };
440
+ ownerWindow?.addEventListener("resize", onViewportResize);
441
+
362
442
  if (resolveDockConfig(config).reveal === "push") {
363
443
  pushTrack.appendChild(contentSlot);
364
444
  pushTrack.appendChild(dockSlot);
@@ -388,6 +468,7 @@ const createDockedLayout = (target: HTMLElement, config?: AgentWidgetConfig): Wi
388
468
  layout();
389
469
  },
390
470
  destroy() {
471
+ ownerWindow?.removeEventListener("resize", onViewportResize);
391
472
  disconnectResizeObserver();
392
473
  if (originalParent.isConnected) {
393
474
  if (originalNextSibling && originalNextSibling.parentNode === originalParent) {
@@ -301,6 +301,41 @@ describe("initAgentWidget docked mode", () => {
301
301
  });
302
302
 
303
303
  expect(handle.host.shadowRoot).not.toBeNull();
304
- 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();
305
340
  });
306
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%";