@runtypelabs/persona 3.31.1 → 3.34.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.
- package/README.md +17 -10
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-quh7NmYD.d.cts → types-CthJFfNx.d.cts} +7 -0
- package/dist/animations/{types-quh7NmYD.d.ts → types-CthJFfNx.d.ts} +7 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/index.cjs +43 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +320 -8
- package/dist/index.d.ts +320 -8
- package/dist/index.global.js +193 -192
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +43 -43
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +117 -117
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +110 -2
- package/dist/smart-dom-reader.d.ts +110 -2
- package/dist/theme-editor.cjs +30 -30
- package/dist/theme-editor.d.cts +124 -5
- package/dist/theme-editor.d.ts +124 -5
- package/dist/theme-editor.js +30 -30
- package/package.json +3 -3
- package/src/ask-user-question-tool.test.ts +148 -0
- package/src/ask-user-question-tool.ts +138 -0
- package/src/client.ts +43 -9
- package/src/codegen.test.ts +0 -14
- package/src/components/messages.ts +10 -0
- package/src/components/suggestions.ts +49 -6
- package/src/index-core.ts +15 -0
- package/src/runtime/host-layout.test.ts +206 -9
- package/src/runtime/host-layout.ts +121 -8
- package/src/runtime/init.test.ts +2 -2
- package/src/session.ts +188 -32
- package/src/session.webmcp.test.ts +48 -0
- package/src/suggest-replies-tool.test.ts +445 -0
- package/src/suggest-replies-tool.ts +152 -0
- package/src/theme-editor/index.ts +2 -0
- package/src/theme-editor/webmcp/index.ts +2 -0
- package/src/theme-editor/webmcp/types.ts +16 -3
- package/src/types.ts +108 -2
- package/src/ui.docked.test.ts +2 -2
- package/src/ui.suggest-replies.test.ts +237 -0
- package/src/ui.ts +57 -13
- package/src/utils/dock.test.ts +23 -1
- package/src/utils/dock.ts +2 -0
- package/src/voice/voice.test.ts +0 -51
- package/src/install-config.test.ts +0 -38
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
|
|
5
5
|
import { createWidgetHostLayout } from "./host-layout";
|
|
6
6
|
|
|
@@ -108,7 +108,7 @@ describe("createWidgetHostLayout docked", () => {
|
|
|
108
108
|
layout.destroy();
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
-
it("push reveal
|
|
111
|
+
it("push reveal offsets a track with margin (no transform); main column width stays fixed in px", () => {
|
|
112
112
|
const parent = document.createElement("div");
|
|
113
113
|
parent.style.width = "800px";
|
|
114
114
|
document.body.appendChild(parent);
|
|
@@ -132,15 +132,20 @@ describe("createWidgetHostLayout docked", () => {
|
|
|
132
132
|
expect(shell.dataset.personaDockReveal).toBe("push");
|
|
133
133
|
expect(contentSlot?.style.width).toBe("800px");
|
|
134
134
|
expect(pushTrack?.style.width).toBe("1120px");
|
|
135
|
-
expect(pushTrack?.style.
|
|
135
|
+
expect(pushTrack?.style.marginLeft).toBe("0px");
|
|
136
|
+
// The track must NOT carry a transform: a transformed ancestor becomes the
|
|
137
|
+
// containing block for `position: fixed` host chrome and pushes it
|
|
138
|
+
// off-screen (regression guard, see host-layout.ts push branch).
|
|
139
|
+
expect(pushTrack?.style.transform).toBe("");
|
|
136
140
|
|
|
137
141
|
layout.syncWidgetState({ open: true, launcherEnabled: true });
|
|
138
|
-
expect(pushTrack?.style.
|
|
142
|
+
expect(pushTrack?.style.marginLeft).toBe("-320px");
|
|
143
|
+
expect(pushTrack?.style.transform).toBe("");
|
|
139
144
|
|
|
140
145
|
layout.destroy();
|
|
141
146
|
});
|
|
142
147
|
|
|
143
|
-
it("push reveal on the left uses negative
|
|
148
|
+
it("push reveal on the left uses negative margin when closed", () => {
|
|
144
149
|
const parent = document.createElement("div");
|
|
145
150
|
parent.style.width = "600px";
|
|
146
151
|
document.body.appendChild(parent);
|
|
@@ -159,10 +164,12 @@ describe("createWidgetHostLayout docked", () => {
|
|
|
159
164
|
layout.updateConfig({ launcher: dockConfig });
|
|
160
165
|
|
|
161
166
|
const pushTrack = shell.querySelector<HTMLElement>('[data-persona-dock-role="push-track"]');
|
|
162
|
-
expect(pushTrack?.style.
|
|
167
|
+
expect(pushTrack?.style.marginLeft).toBe("-200px");
|
|
168
|
+
expect(pushTrack?.style.transform).toBe("");
|
|
163
169
|
|
|
164
170
|
layout.syncWidgetState({ open: true, launcherEnabled: true });
|
|
165
|
-
expect(pushTrack?.style.
|
|
171
|
+
expect(pushTrack?.style.marginLeft).toBe("0px");
|
|
172
|
+
expect(pushTrack?.style.transform).toBe("");
|
|
166
173
|
|
|
167
174
|
layout.destroy();
|
|
168
175
|
});
|
|
@@ -194,6 +201,161 @@ describe("createWidgetHostLayout docked", () => {
|
|
|
194
201
|
layout.destroy();
|
|
195
202
|
});
|
|
196
203
|
|
|
204
|
+
it("clamps the dock slot to the viewport guard and pins resize/emerge sticky", () => {
|
|
205
|
+
for (const reveal of ["resize", "emerge"] as const) {
|
|
206
|
+
const parent = document.createElement("div");
|
|
207
|
+
document.body.appendChild(parent);
|
|
208
|
+
const target = document.createElement("div");
|
|
209
|
+
parent.appendChild(target);
|
|
210
|
+
|
|
211
|
+
const layout = createWidgetHostLayout(target, {
|
|
212
|
+
launcher: {
|
|
213
|
+
mountMode: "docked",
|
|
214
|
+
autoExpand: false,
|
|
215
|
+
dock: { width: "320px", reveal },
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
|
|
220
|
+
expect(dockSlot?.style.maxHeight, reveal).not.toBe("");
|
|
221
|
+
expect(dockSlot?.style.position, reveal).toBe("sticky");
|
|
222
|
+
expect(dockSlot?.style.top, reveal).toBe("0px");
|
|
223
|
+
|
|
224
|
+
layout.destroy();
|
|
225
|
+
document.body.innerHTML = "";
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("clamps push and overlay dock slots without sticky (in-flow/absolute contexts)", () => {
|
|
230
|
+
// push: the slot is an in-flow `position: relative` column — max-height cap
|
|
231
|
+
// only, no sticky. overlay: keeps absolute positioning.
|
|
232
|
+
const cases = [
|
|
233
|
+
{ reveal: "push" as const, position: "relative" },
|
|
234
|
+
{ reveal: "overlay" as const, position: "absolute" },
|
|
235
|
+
];
|
|
236
|
+
for (const { reveal, position } of cases) {
|
|
237
|
+
const parent = document.createElement("div");
|
|
238
|
+
document.body.appendChild(parent);
|
|
239
|
+
const target = document.createElement("div");
|
|
240
|
+
parent.appendChild(target);
|
|
241
|
+
|
|
242
|
+
const layout = createWidgetHostLayout(target, {
|
|
243
|
+
launcher: {
|
|
244
|
+
mountMode: "docked",
|
|
245
|
+
autoExpand: false,
|
|
246
|
+
dock: { width: "320px", reveal },
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
|
|
251
|
+
expect(dockSlot?.style.maxHeight, reveal).not.toBe("");
|
|
252
|
+
expect(dockSlot?.style.position, reveal).toBe(position);
|
|
253
|
+
|
|
254
|
+
layout.destroy();
|
|
255
|
+
document.body.innerHTML = "";
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("honors a custom dock.maxHeight and the false opt-out", () => {
|
|
260
|
+
const parent = document.createElement("div");
|
|
261
|
+
document.body.appendChild(parent);
|
|
262
|
+
const target = document.createElement("div");
|
|
263
|
+
parent.appendChild(target);
|
|
264
|
+
|
|
265
|
+
const dockConfig = {
|
|
266
|
+
mountMode: "docked" as const,
|
|
267
|
+
autoExpand: false,
|
|
268
|
+
dock: { width: "320px", maxHeight: "600px" },
|
|
269
|
+
};
|
|
270
|
+
const layout = createWidgetHostLayout(target, { launcher: dockConfig });
|
|
271
|
+
|
|
272
|
+
const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
|
|
273
|
+
expect(dockSlot?.style.maxHeight).toBe("600px");
|
|
274
|
+
|
|
275
|
+
layout.updateConfig({
|
|
276
|
+
launcher: { ...dockConfig, dock: { width: "320px", maxHeight: false } },
|
|
277
|
+
});
|
|
278
|
+
expect(dockSlot?.style.maxHeight).toBe("");
|
|
279
|
+
expect(dockSlot?.style.position).toBe("relative");
|
|
280
|
+
expect(dockSlot?.style.top).toBe("");
|
|
281
|
+
|
|
282
|
+
layout.destroy();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("height-chain warning", () => {
|
|
286
|
+
const withOffsetHeight = (impl: (el: HTMLElement) => number, fn: () => void): void => {
|
|
287
|
+
const original = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight");
|
|
288
|
+
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
|
|
289
|
+
configurable: true,
|
|
290
|
+
get(this: HTMLElement) {
|
|
291
|
+
return impl(this);
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
try {
|
|
295
|
+
fn();
|
|
296
|
+
} finally {
|
|
297
|
+
if (original) {
|
|
298
|
+
Object.defineProperty(HTMLElement.prototype, "offsetHeight", original);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const mountDocked = () => {
|
|
304
|
+
const parent = document.createElement("div");
|
|
305
|
+
document.body.appendChild(parent);
|
|
306
|
+
const target = document.createElement("div");
|
|
307
|
+
parent.appendChild(target);
|
|
308
|
+
return createWidgetHostLayout(target, {
|
|
309
|
+
launcher: {
|
|
310
|
+
mountMode: "docked",
|
|
311
|
+
autoExpand: true,
|
|
312
|
+
dock: { width: "320px" },
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
afterEach(() => {
|
|
318
|
+
vi.restoreAllMocks();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("warns once when a percentage height does not resolve against the shell's parent", () => {
|
|
322
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
323
|
+
// Fixed-height probe measures (environment works); 100% probe collapses
|
|
324
|
+
// to 0 (no ancestor provides a definite height).
|
|
325
|
+
withOffsetHeight((el) => (el.style.height === "1px" ? 1 : 0), () => {
|
|
326
|
+
const layout = mountDocked();
|
|
327
|
+
layout.syncWidgetState({ open: false, launcherEnabled: true });
|
|
328
|
+
layout.syncWidgetState({ open: true, launcherEnabled: true });
|
|
329
|
+
layout.destroy();
|
|
330
|
+
});
|
|
331
|
+
const heightWarnings = warn.mock.calls.filter((c) =>
|
|
332
|
+
String(c[0]).includes("definite height")
|
|
333
|
+
);
|
|
334
|
+
expect(heightWarnings).toHaveLength(1);
|
|
335
|
+
expect(String(heightWarnings[0][0])).toContain("100dvh");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("does not warn when the height chain resolves", () => {
|
|
339
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
340
|
+
withOffsetHeight(() => 1, () => {
|
|
341
|
+
const layout = mountDocked();
|
|
342
|
+
layout.destroy();
|
|
343
|
+
});
|
|
344
|
+
expect(
|
|
345
|
+
warn.mock.calls.filter((c) => String(c[0]).includes("definite height"))
|
|
346
|
+
).toHaveLength(0);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("does not warn when the environment cannot measure layout (jsdom default)", () => {
|
|
350
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
351
|
+
const layout = mountDocked();
|
|
352
|
+
layout.destroy();
|
|
353
|
+
expect(
|
|
354
|
+
warn.mock.calls.filter((c) => String(c[0]).includes("definite height"))
|
|
355
|
+
).toHaveLength(0);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
197
359
|
const withInnerWidth = (width: number, fn: () => void): void => {
|
|
198
360
|
const prev = window.innerWidth;
|
|
199
361
|
try {
|
|
@@ -235,6 +397,41 @@ describe("createWidgetHostLayout docked", () => {
|
|
|
235
397
|
});
|
|
236
398
|
});
|
|
237
399
|
|
|
400
|
+
it("resets the push track margin when a desktop push offset enters mobile fullscreen", () => {
|
|
401
|
+
const parent = document.createElement("div");
|
|
402
|
+
parent.style.width = "800px";
|
|
403
|
+
document.body.appendChild(parent);
|
|
404
|
+
const target = document.createElement("div");
|
|
405
|
+
parent.appendChild(target);
|
|
406
|
+
|
|
407
|
+
const layout = createWidgetHostLayout(target, {
|
|
408
|
+
launcher: {
|
|
409
|
+
mountMode: "docked",
|
|
410
|
+
autoExpand: false,
|
|
411
|
+
dock: { width: "320px", reveal: "push" },
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
const pushTrack = layout.shell?.querySelector<HTMLElement>(
|
|
415
|
+
'[data-persona-dock-role="push-track"]',
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
// Desktop, expanded: track carries a negative margin offset.
|
|
419
|
+
withInnerWidth(800, () => {
|
|
420
|
+
layout.syncWidgetState({ open: true, launcherEnabled: true });
|
|
421
|
+
});
|
|
422
|
+
expect(pushTrack?.style.marginLeft).toBe("-320px");
|
|
423
|
+
|
|
424
|
+
// Same layout falls into mobile fullscreen (viewport resize): the stale
|
|
425
|
+
// margin must be cleared or the width:100% track renders 320px off-screen.
|
|
426
|
+
withInnerWidth(500, () => {
|
|
427
|
+
window.dispatchEvent(new Event("resize"));
|
|
428
|
+
});
|
|
429
|
+
expect(layout.shell?.dataset.personaDockMobileFullscreen).toBe("true");
|
|
430
|
+
expect(pushTrack?.style.marginLeft).toBe("0px");
|
|
431
|
+
|
|
432
|
+
layout.destroy();
|
|
433
|
+
});
|
|
434
|
+
|
|
238
435
|
it("does not use fixed fullscreen above mobile breakpoint", () => {
|
|
239
436
|
withInnerWidth(800, () => {
|
|
240
437
|
const parent = document.createElement("div");
|
|
@@ -277,7 +474,7 @@ describe("createWidgetHostLayout docked", () => {
|
|
|
277
474
|
|
|
278
475
|
const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
|
|
279
476
|
layout.syncWidgetState({ open: true, launcherEnabled: true });
|
|
280
|
-
expect(dockSlot?.style.position).toBe("
|
|
477
|
+
expect(dockSlot?.style.position).toBe("sticky");
|
|
281
478
|
expect(layout.shell?.dataset.personaDockMobileFullscreen).toBeUndefined();
|
|
282
479
|
|
|
283
480
|
layout.destroy();
|
|
@@ -325,7 +522,7 @@ describe("createWidgetHostLayout docked", () => {
|
|
|
325
522
|
|
|
326
523
|
const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
|
|
327
524
|
layout.syncWidgetState({ open: false, launcherEnabled: true });
|
|
328
|
-
expect(dockSlot?.style.position).toBe("
|
|
525
|
+
expect(dockSlot?.style.position).toBe("sticky");
|
|
329
526
|
|
|
330
527
|
layout.destroy();
|
|
331
528
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentWidgetConfig,
|
|
3
|
+
AgentWidgetDockConfig,
|
|
3
4
|
AgentWidgetStateSnapshot,
|
|
4
5
|
} from "../types";
|
|
5
6
|
import { isDockedMountMode, resolveDockConfig } from "../utils/dock";
|
|
@@ -26,6 +27,84 @@ const parseDockWidthToPx = (width: string, shellClientWidth: number): number =>
|
|
|
26
27
|
return 420;
|
|
27
28
|
};
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Viewport-overflow guard. The docked shell sizes itself with `height: 100%`,
|
|
32
|
+
* which only resolves when an ancestor provides a definite height; without one
|
|
33
|
+
* the dock column would grow with the conversation and scroll off the page.
|
|
34
|
+
* Clamping the slot keeps the panel viewport-sized (messages scroll
|
|
35
|
+
* internally) even when the page's height chain is missing. `100vh` is set
|
|
36
|
+
* first as a fallback for engines without `dvh` support — an invalid value
|
|
37
|
+
* leaves the previous one in place.
|
|
38
|
+
*/
|
|
39
|
+
const applyDockSlotMaxHeight = (
|
|
40
|
+
dockSlot: HTMLElement,
|
|
41
|
+
maxHeight: string | false
|
|
42
|
+
): void => {
|
|
43
|
+
if (maxHeight === false) {
|
|
44
|
+
dockSlot.style.maxHeight = "";
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
dockSlot.style.maxHeight = "100vh";
|
|
48
|
+
dockSlot.style.maxHeight = maxHeight;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Sticky keeps the resize/emerge dock column pinned to the top of the viewport
|
|
53
|
+
* when the surrounding page is taller than the screen (e.g. a missing height
|
|
54
|
+
* chain or a deliberately scrolling page). With a properly sized shell it
|
|
55
|
+
* behaves exactly like the previous `position: relative`. Not used for push:
|
|
56
|
+
* push gets the max-height cap only, like overlay, since the dock slot is an
|
|
57
|
+
* in-flow `position: relative` column there rather than a viewport-pinned one.
|
|
58
|
+
* (The push track itself is offset with `margin-left`, not a transform, so it
|
|
59
|
+
* no longer establishes a containing block for fixed/sticky descendants.)
|
|
60
|
+
*/
|
|
61
|
+
const applyInFlowDockSlotPosition = (
|
|
62
|
+
dockSlot: HTMLElement,
|
|
63
|
+
maxHeight: string | false
|
|
64
|
+
): void => {
|
|
65
|
+
if (maxHeight === false) {
|
|
66
|
+
dockSlot.style.position = "relative";
|
|
67
|
+
dockSlot.style.top = "";
|
|
68
|
+
} else {
|
|
69
|
+
dockSlot.style.position = "sticky";
|
|
70
|
+
dockSlot.style.top = "0";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Warns once per docked mount when no ancestor of the dock target provides a
|
|
76
|
+
* definite height, which is the common misconfiguration behind a dock panel
|
|
77
|
+
* that grows past the viewport. Probes with a throwaway `height: 100%` child
|
|
78
|
+
* of the shell's parent; a fixed-height reference probe first checks the
|
|
79
|
+
* environment can measure at all (display: none subtrees and jsdom measure
|
|
80
|
+
* everything as 0, and must not produce a false warning).
|
|
81
|
+
*/
|
|
82
|
+
const warnIfDockHeightChainUnresolved = (
|
|
83
|
+
shell: HTMLElement,
|
|
84
|
+
dock: Required<AgentWidgetDockConfig>
|
|
85
|
+
): void => {
|
|
86
|
+
const parent = shell.parentElement;
|
|
87
|
+
if (!parent) return;
|
|
88
|
+
const probe = shell.ownerDocument.createElement("div");
|
|
89
|
+
probe.style.cssText =
|
|
90
|
+
"width:0;height:1px;margin:0;padding:0;border:0;visibility:hidden;";
|
|
91
|
+
parent.appendChild(probe);
|
|
92
|
+
const measurable = probe.offsetHeight > 0;
|
|
93
|
+
probe.style.height = "100%";
|
|
94
|
+
const resolved = probe.offsetHeight > 0;
|
|
95
|
+
probe.remove();
|
|
96
|
+
if (!measurable || resolved) return;
|
|
97
|
+
console.warn(
|
|
98
|
+
"[AgentWidget] Docked mode: no ancestor of the dock target provides a definite height, " +
|
|
99
|
+
"so the dock panel cannot size to your layout." +
|
|
100
|
+
(dock.maxHeight === false
|
|
101
|
+
? " The viewport guard is disabled (dock.maxHeight: false), so the panel will grow with the conversation and overflow the viewport."
|
|
102
|
+
: ` Falling back to clamping the panel to ${dock.maxHeight} (configurable via launcher.dock.maxHeight).`) +
|
|
103
|
+
" To size the panel from your layout instead, give the height chain a definite height " +
|
|
104
|
+
"(e.g. `html, body { height: 100% }`) down to the dock target's parent."
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
29
108
|
const setDirectHostStyles = (host: HTMLElement, config?: AgentWidgetConfig): void => {
|
|
30
109
|
const launcherEnabled = config?.launcher?.enabled ?? true;
|
|
31
110
|
host.className = "persona-host";
|
|
@@ -53,6 +132,7 @@ const clearMobileFullscreenDockSlotStyles = (dockSlot: HTMLElement): void => {
|
|
|
53
132
|
dockSlot.style.width = "";
|
|
54
133
|
dockSlot.style.height = "";
|
|
55
134
|
dockSlot.style.maxWidth = "";
|
|
135
|
+
dockSlot.style.maxHeight = "";
|
|
56
136
|
dockSlot.style.minWidth = "";
|
|
57
137
|
clearOverlayDockSlotStyles(dockSlot);
|
|
58
138
|
};
|
|
@@ -72,6 +152,7 @@ const clearPushTrackStyles = (pushTrack: HTMLElement): void => {
|
|
|
72
152
|
pushTrack.style.alignItems = "";
|
|
73
153
|
pushTrack.style.transition = "";
|
|
74
154
|
pushTrack.style.transform = "";
|
|
155
|
+
pushTrack.style.marginLeft = "";
|
|
75
156
|
};
|
|
76
157
|
|
|
77
158
|
const resetContentSlotFlexSizing = (contentSlot: HTMLElement): void => {
|
|
@@ -215,6 +296,11 @@ const applyDockStyles = (
|
|
|
215
296
|
pushTrack.style.flex = "1 1 auto";
|
|
216
297
|
pushTrack.style.alignItems = "stretch";
|
|
217
298
|
pushTrack.style.transform = "none";
|
|
299
|
+
// Reset the desktop push offset: this fullscreen path applies styles
|
|
300
|
+
// inline without going through clearPushTrackStyles, so a stale negative
|
|
301
|
+
// marginLeft from a prior expanded desktop render would shift the now
|
|
302
|
+
// width:100% track off-screen on mobile.
|
|
303
|
+
pushTrack.style.marginLeft = "0";
|
|
218
304
|
pushTrack.style.transition = "none";
|
|
219
305
|
contentSlot.style.flex = "1 1 auto";
|
|
220
306
|
contentSlot.style.width = "100%";
|
|
@@ -227,6 +313,7 @@ const applyDockStyles = (
|
|
|
227
313
|
|
|
228
314
|
shell.removeAttribute("data-persona-dock-mobile-fullscreen");
|
|
229
315
|
clearMobileFullscreenDockSlotStyles(dockSlot);
|
|
316
|
+
applyDockSlotMaxHeight(dockSlot, dock.maxHeight);
|
|
230
317
|
|
|
231
318
|
if (dock.reveal === "overlay") {
|
|
232
319
|
shell.style.display = "flex";
|
|
@@ -279,15 +366,24 @@ const applyDockStyles = (
|
|
|
279
366
|
|
|
280
367
|
const panelPx = parseDockWidthToPx(dock.width, shell.clientWidth);
|
|
281
368
|
const contentPx = Math.max(0, shell.clientWidth);
|
|
282
|
-
const dockTransition = dock.animate ? "
|
|
283
|
-
|
|
369
|
+
const dockTransition = dock.animate ? "margin-left 180ms ease" : "none";
|
|
370
|
+
// Slide the wide track with a negative left margin rather than a CSS
|
|
371
|
+
// transform. A `transform` (even `translateX(0)`) turns the push track into
|
|
372
|
+
// the containing block for any `position: fixed` descendant, so host pages
|
|
373
|
+
// that render viewport-fixed chrome inside the pushed content (e.g. the
|
|
374
|
+
// dashboard editor's `fixed top-0 right-0` toolbar) resolve `right: 0`
|
|
375
|
+
// against the track's right edge — `panelPx` past the viewport, off-screen.
|
|
376
|
+
// `margin-left` produces the identical visual offset (the track is clipped
|
|
377
|
+
// by the overflow:hidden shell) without establishing a containing block for
|
|
378
|
+
// fixed OR absolutely-positioned descendants.
|
|
379
|
+
const marginOffsetPx =
|
|
284
380
|
dock.side === "right"
|
|
285
381
|
? expanded
|
|
286
|
-
?
|
|
287
|
-
:
|
|
382
|
+
? -panelPx
|
|
383
|
+
: 0
|
|
288
384
|
: expanded
|
|
289
|
-
?
|
|
290
|
-
:
|
|
385
|
+
? 0
|
|
386
|
+
: -panelPx;
|
|
291
387
|
|
|
292
388
|
pushTrack.style.display = "flex";
|
|
293
389
|
pushTrack.style.flexDirection = "row";
|
|
@@ -298,7 +394,10 @@ const applyDockStyles = (
|
|
|
298
394
|
pushTrack.style.height = "100%";
|
|
299
395
|
pushTrack.style.width = `${contentPx + panelPx}px`;
|
|
300
396
|
pushTrack.style.transition = dockTransition;
|
|
301
|
-
pushTrack.style.
|
|
397
|
+
pushTrack.style.marginLeft = `${marginOffsetPx}px`;
|
|
398
|
+
// Defensively clear any transform a previous reveal mode may have set —
|
|
399
|
+
// leaving one would re-establish the fixed-position containing block.
|
|
400
|
+
pushTrack.style.transform = "";
|
|
302
401
|
|
|
303
402
|
contentSlot.style.flex = "0 0 auto";
|
|
304
403
|
contentSlot.style.flexGrow = "0";
|
|
@@ -315,6 +414,7 @@ const applyDockStyles = (
|
|
|
315
414
|
dockSlot.style.minWidth = dock.width;
|
|
316
415
|
dockSlot.style.maxWidth = dock.width;
|
|
317
416
|
dockSlot.style.position = "relative";
|
|
417
|
+
dockSlot.style.top = "";
|
|
318
418
|
dockSlot.style.overflow = "hidden";
|
|
319
419
|
dockSlot.style.transition = "none";
|
|
320
420
|
dockSlot.style.pointerEvents = expanded ? "auto" : "none";
|
|
@@ -348,7 +448,7 @@ const applyDockStyles = (
|
|
|
348
448
|
dockSlot.style.maxWidth = width;
|
|
349
449
|
dockSlot.style.minWidth = width;
|
|
350
450
|
dockSlot.style.minHeight = "0";
|
|
351
|
-
dockSlot.
|
|
451
|
+
applyInFlowDockSlotPosition(dockSlot, dock.maxHeight);
|
|
352
452
|
dockSlot.style.overflow =
|
|
353
453
|
isEmerge ? "hidden" : collapsedClosed ? "hidden" : "visible";
|
|
354
454
|
dockSlot.style.transition = dockTransition;
|
|
@@ -429,9 +529,22 @@ const createDockedLayout = (target: HTMLElement, config?: AgentWidgetConfig): Wi
|
|
|
429
529
|
resizeObserver.observe(shell);
|
|
430
530
|
};
|
|
431
531
|
|
|
532
|
+
let heightChainChecked = false;
|
|
533
|
+
|
|
432
534
|
const layout = (): void => {
|
|
433
535
|
applyDockStyles(shell, pushTrack, contentSlot, dockSlot, host, config, expanded);
|
|
434
536
|
syncPushResizeObserver();
|
|
537
|
+
// Check the height chain once, the first time the panel is actually shown
|
|
538
|
+
// in a layout that depends on it (mobile fullscreen is fixed-position and
|
|
539
|
+
// doesn't — keep checking until a dependent layout comes around).
|
|
540
|
+
if (
|
|
541
|
+
expanded &&
|
|
542
|
+
!heightChainChecked &&
|
|
543
|
+
shell.dataset.personaDockMobileFullscreen !== "true"
|
|
544
|
+
) {
|
|
545
|
+
heightChainChecked = true;
|
|
546
|
+
warnIfDockHeightChainUnresolved(shell, resolveDockConfig(config));
|
|
547
|
+
}
|
|
435
548
|
};
|
|
436
549
|
|
|
437
550
|
const ownerWindow = shell.ownerDocument.defaultView;
|
package/src/runtime/init.test.ts
CHANGED
|
@@ -424,11 +424,11 @@ describe("initAgentWidget docked mode", () => {
|
|
|
424
424
|
const panelSlot = shell.querySelector<HTMLElement>('[data-persona-dock-role="panel"]')!;
|
|
425
425
|
expect(pushTrack).not.toBeNull();
|
|
426
426
|
expect(panelSlot.style.width).toBe("400px");
|
|
427
|
-
expect(pushTrack?.style.
|
|
427
|
+
expect(pushTrack?.style.marginLeft).toBe("-400px");
|
|
428
428
|
|
|
429
429
|
handle.close();
|
|
430
430
|
expect(panelSlot.style.width).toBe("400px");
|
|
431
|
-
expect(pushTrack?.style.
|
|
431
|
+
expect(pushTrack?.style.marginLeft).toBe("0px");
|
|
432
432
|
|
|
433
433
|
handle.destroy();
|
|
434
434
|
wrapper.remove();
|