@runtypelabs/persona 3.7.0 → 3.8.1
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/dist/index.cjs +40 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -4
- package/dist/index.d.ts +43 -4
- package/dist/index.global.js +59 -59
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +40 -40
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +199 -24
- package/dist/theme-editor.d.cts +43 -4
- package/dist/theme-editor.d.ts +43 -4
- package/dist/theme-editor.js +199 -24
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +34 -0
- package/dist/theme-reference.d.ts +34 -0
- package/dist/widget.css +4 -0
- package/package.json +1 -1
- package/src/components/artifact-card.ts +1 -1
- package/src/components/header-builder.ts +3 -0
- package/src/components/launcher.ts +7 -2
- package/src/components/panel.ts +3 -1
- package/src/runtime/host-layout.test.ts +1 -1
- package/src/runtime/host-layout.ts +2 -1
- package/src/styles/widget.css +4 -0
- package/src/types/theme.ts +35 -0
- package/src/types.ts +9 -4
- package/src/ui.overlay-z-index.test.ts +34 -2
- package/src/ui.ts +88 -8
- package/src/utils/constants.ts +13 -0
- package/src/utils/dropdown.ts +2 -1
- package/src/utils/overlay-host-stacking.test.ts +61 -0
- package/src/utils/overlay-host-stacking.ts +38 -0
- package/src/utils/scroll-lock.test.ts +64 -0
- package/src/utils/scroll-lock.ts +62 -0
- package/src/utils/tokens.ts +63 -0
|
@@ -214,6 +214,17 @@ interface MessageTokens {
|
|
|
214
214
|
/** Assistant bubble box-shadow (token ref or raw CSS, e.g. `none`). */
|
|
215
215
|
shadow?: string;
|
|
216
216
|
};
|
|
217
|
+
/** Border color between messages in the thread. */
|
|
218
|
+
border?: TokenReference<'color'>;
|
|
219
|
+
}
|
|
220
|
+
/** Collapsible widget chrome (tool bubbles, reasoning bubbles, approval bubbles). */
|
|
221
|
+
interface CollapsibleWidgetTokens {
|
|
222
|
+
/** Background for content areas. */
|
|
223
|
+
container?: TokenReference<'color'>;
|
|
224
|
+
/** Background for code blocks inside collapsible sections. */
|
|
225
|
+
surface?: TokenReference<'color'>;
|
|
226
|
+
/** Border color for collapsible sections. */
|
|
227
|
+
border?: TokenReference<'color'>;
|
|
217
228
|
}
|
|
218
229
|
interface MarkdownTokens {
|
|
219
230
|
inlineCode: {
|
|
@@ -242,6 +253,27 @@ interface MarkdownTokens {
|
|
|
242
253
|
fontWeight?: string;
|
|
243
254
|
};
|
|
244
255
|
};
|
|
256
|
+
/** Fenced code block styling. */
|
|
257
|
+
codeBlock?: {
|
|
258
|
+
background?: TokenReference<'color'>;
|
|
259
|
+
borderColor?: TokenReference<'color'>;
|
|
260
|
+
textColor?: TokenReference<'color'>;
|
|
261
|
+
};
|
|
262
|
+
/** Table styling. */
|
|
263
|
+
table?: {
|
|
264
|
+
headerBackground?: TokenReference<'color'>;
|
|
265
|
+
borderColor?: TokenReference<'color'>;
|
|
266
|
+
};
|
|
267
|
+
/** Horizontal rule styling. */
|
|
268
|
+
hr?: {
|
|
269
|
+
color?: TokenReference<'color'>;
|
|
270
|
+
};
|
|
271
|
+
/** Blockquote styling. */
|
|
272
|
+
blockquote?: {
|
|
273
|
+
borderColor?: TokenReference<'color'>;
|
|
274
|
+
background?: TokenReference<'color'>;
|
|
275
|
+
textColor?: TokenReference<'color'>;
|
|
276
|
+
};
|
|
245
277
|
}
|
|
246
278
|
interface VoiceTokens {
|
|
247
279
|
recording: {
|
|
@@ -404,6 +436,8 @@ interface ComponentTokens {
|
|
|
404
436
|
tab?: ArtifactTabTokens;
|
|
405
437
|
pane?: ArtifactPaneTokens;
|
|
406
438
|
};
|
|
439
|
+
/** Collapsible widget chrome (tool/reasoning/approval bubbles). */
|
|
440
|
+
collapsibleWidget?: CollapsibleWidgetTokens;
|
|
407
441
|
}
|
|
408
442
|
interface PaletteExtras {
|
|
409
443
|
transitions?: Record<string, string>;
|
package/dist/widget.css
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/persona",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.1",
|
|
4
4
|
"description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -24,7 +24,7 @@ function renderDefaultArtifactCard(
|
|
|
24
24
|
root.className =
|
|
25
25
|
"persona-flex persona-w-full persona-max-w-full persona-items-center persona-gap-3 persona-rounded-xl persona-px-4 persona-py-3";
|
|
26
26
|
root.style.border = "1px solid var(--persona-border, #e5e7eb)";
|
|
27
|
-
root.style.backgroundColor = "var(--persona-
|
|
27
|
+
root.style.backgroundColor = "var(--persona-surface, #ffffff)";
|
|
28
28
|
root.style.cursor = "pointer";
|
|
29
29
|
root.tabIndex = 0;
|
|
30
30
|
root.setAttribute("role", "button");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createElement, createElementInDocument } from "../utils/dom";
|
|
2
2
|
import { renderLucideIcon } from "../utils/icons";
|
|
3
3
|
import { AgentWidgetConfig } from "../types";
|
|
4
|
+
import { PORTALED_OVERLAY_Z_INDEX } from "../utils/constants";
|
|
4
5
|
|
|
5
6
|
/** CSS `color` values; variables are set on `[data-persona-root]` from `theme.components.header`. */
|
|
6
7
|
export const HEADER_THEME_CSS = {
|
|
@@ -233,6 +234,7 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
|
|
|
233
234
|
|
|
234
235
|
// Position tooltip above button
|
|
235
236
|
portaledTooltip.style.position = "fixed";
|
|
237
|
+
portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
|
|
236
238
|
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
237
239
|
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
238
240
|
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
@@ -389,6 +391,7 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
|
|
|
389
391
|
|
|
390
392
|
// Position tooltip above button
|
|
391
393
|
portaledTooltip.style.position = "fixed";
|
|
394
|
+
portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
|
|
392
395
|
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
393
396
|
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
394
397
|
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
@@ -3,6 +3,7 @@ import { AgentWidgetConfig } from "../types";
|
|
|
3
3
|
import { positionMap } from "../utils/positioning";
|
|
4
4
|
import { isDockedMountMode } from "../utils/dock";
|
|
5
5
|
import { renderLucideIcon } from "../utils/icons";
|
|
6
|
+
import { DEFAULT_OVERLAY_Z_INDEX } from "../utils/constants";
|
|
6
7
|
|
|
7
8
|
export interface LauncherButton {
|
|
8
9
|
element: HTMLButtonElement;
|
|
@@ -174,12 +175,16 @@ export const createLauncherButton = (
|
|
|
174
175
|
: positionMap["bottom-right"];
|
|
175
176
|
|
|
176
177
|
const floatingBase =
|
|
177
|
-
"persona-fixed persona-flex persona-items-center persona-gap-3 persona-rounded-launcher persona-bg-persona-surface persona-py-2.5 persona-pl-3 persona-pr-3 persona-transition hover:persona-translate-y-[-2px] persona-cursor-pointer
|
|
178
|
+
"persona-fixed persona-flex persona-items-center persona-gap-3 persona-rounded-launcher persona-bg-persona-surface persona-py-2.5 persona-pl-3 persona-pr-3 persona-transition hover:persona-translate-y-[-2px] persona-cursor-pointer";
|
|
178
179
|
const dockedBase =
|
|
179
180
|
"persona-relative persona-mt-4 persona-mb-4 persona-mx-auto persona-flex persona-items-center persona-justify-center persona-rounded-launcher persona-bg-persona-surface persona-transition hover:persona-translate-y-[-2px] persona-cursor-pointer";
|
|
180
181
|
|
|
181
182
|
button.className = dockedMode ? dockedBase : `${floatingBase} ${positionClass}`;
|
|
182
|
-
|
|
183
|
+
|
|
184
|
+
if (!dockedMode) {
|
|
185
|
+
button.style.zIndex = String(launcher.zIndex ?? DEFAULT_OVERLAY_Z_INDEX);
|
|
186
|
+
}
|
|
187
|
+
|
|
183
188
|
// Apply launcher border and shadow from config (with defaults matching previous Tailwind classes)
|
|
184
189
|
const defaultBorder = "1px solid var(--persona-border, #e5e7eb)";
|
|
185
190
|
const defaultShadow = "var(--persona-shadow, 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1))";
|
package/src/components/panel.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createElement } from "../utils/dom";
|
|
|
2
2
|
import { AgentWidgetConfig } from "../types";
|
|
3
3
|
import { positionMap } from "../utils/positioning";
|
|
4
4
|
import { isDockedMountMode } from "../utils/dock";
|
|
5
|
+
import { DEFAULT_OVERLAY_Z_INDEX } from "../utils/constants";
|
|
5
6
|
import { buildHeader, attachHeaderToContainer, HeaderElements } from "./header-builder";
|
|
6
7
|
import { buildHeaderWithLayout } from "./header-layouts";
|
|
7
8
|
import { buildComposer, ComposerElements } from "./composer-builder";
|
|
@@ -58,8 +59,9 @@ export const createWrapper = (config?: AgentWidgetConfig): PanelWrapper => {
|
|
|
58
59
|
|
|
59
60
|
const wrapper = createElement(
|
|
60
61
|
"div",
|
|
61
|
-
`persona-widget-wrapper persona-fixed ${position} persona-
|
|
62
|
+
`persona-widget-wrapper persona-fixed ${position} persona-transition`
|
|
62
63
|
);
|
|
64
|
+
wrapper.style.zIndex = String(config?.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX);
|
|
63
65
|
|
|
64
66
|
const panel = createElement(
|
|
65
67
|
"div",
|
|
@@ -228,7 +228,7 @@ describe("createWidgetHostLayout docked", () => {
|
|
|
228
228
|
const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
|
|
229
229
|
layout.syncWidgetState({ open: true, launcherEnabled: true });
|
|
230
230
|
expect(dockSlot?.style.position).toBe("fixed");
|
|
231
|
-
expect(dockSlot?.style.zIndex).toBe("
|
|
231
|
+
expect(dockSlot?.style.zIndex).toBe("100000");
|
|
232
232
|
expect(layout.shell?.dataset.personaDockMobileFullscreen).toBe("true");
|
|
233
233
|
|
|
234
234
|
layout.destroy();
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
AgentWidgetStateSnapshot,
|
|
4
4
|
} from "../types";
|
|
5
5
|
import { isDockedMountMode, resolveDockConfig } from "../utils/dock";
|
|
6
|
+
import { DEFAULT_OVERLAY_Z_INDEX } from "../utils/constants";
|
|
6
7
|
|
|
7
8
|
export type WidgetHostLayoutMode = "direct" | "docked";
|
|
8
9
|
|
|
@@ -198,7 +199,7 @@ const applyDockStyles = (
|
|
|
198
199
|
dockSlot.style.minWidth = "0";
|
|
199
200
|
dockSlot.style.minHeight = "0";
|
|
200
201
|
dockSlot.style.overflow = "hidden";
|
|
201
|
-
dockSlot.style.zIndex = String(config?.launcher?.zIndex ??
|
|
202
|
+
dockSlot.style.zIndex = String(config?.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX);
|
|
202
203
|
dockSlot.style.transform = "none";
|
|
203
204
|
dockSlot.style.transition = "none";
|
|
204
205
|
dockSlot.style.pointerEvents = "auto";
|
package/src/styles/widget.css
CHANGED
package/src/types/theme.ts
CHANGED
|
@@ -233,6 +233,18 @@ export interface MessageTokens {
|
|
|
233
233
|
/** Assistant bubble box-shadow (token ref or raw CSS, e.g. `none`). */
|
|
234
234
|
shadow?: string;
|
|
235
235
|
};
|
|
236
|
+
/** Border color between messages in the thread. */
|
|
237
|
+
border?: TokenReference<'color'>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Collapsible widget chrome (tool bubbles, reasoning bubbles, approval bubbles). */
|
|
241
|
+
export interface CollapsibleWidgetTokens {
|
|
242
|
+
/** Background for content areas. */
|
|
243
|
+
container?: TokenReference<'color'>;
|
|
244
|
+
/** Background for code blocks inside collapsible sections. */
|
|
245
|
+
surface?: TokenReference<'color'>;
|
|
246
|
+
/** Border color for collapsible sections. */
|
|
247
|
+
border?: TokenReference<'color'>;
|
|
236
248
|
}
|
|
237
249
|
|
|
238
250
|
export interface MarkdownTokens {
|
|
@@ -262,6 +274,27 @@ export interface MarkdownTokens {
|
|
|
262
274
|
fontWeight?: string;
|
|
263
275
|
};
|
|
264
276
|
};
|
|
277
|
+
/** Fenced code block styling. */
|
|
278
|
+
codeBlock?: {
|
|
279
|
+
background?: TokenReference<'color'>;
|
|
280
|
+
borderColor?: TokenReference<'color'>;
|
|
281
|
+
textColor?: TokenReference<'color'>;
|
|
282
|
+
};
|
|
283
|
+
/** Table styling. */
|
|
284
|
+
table?: {
|
|
285
|
+
headerBackground?: TokenReference<'color'>;
|
|
286
|
+
borderColor?: TokenReference<'color'>;
|
|
287
|
+
};
|
|
288
|
+
/** Horizontal rule styling. */
|
|
289
|
+
hr?: {
|
|
290
|
+
color?: TokenReference<'color'>;
|
|
291
|
+
};
|
|
292
|
+
/** Blockquote styling. */
|
|
293
|
+
blockquote?: {
|
|
294
|
+
borderColor?: TokenReference<'color'>;
|
|
295
|
+
background?: TokenReference<'color'>;
|
|
296
|
+
textColor?: TokenReference<'color'>;
|
|
297
|
+
};
|
|
265
298
|
}
|
|
266
299
|
|
|
267
300
|
export interface VoiceTokens {
|
|
@@ -438,6 +471,8 @@ export interface ComponentTokens {
|
|
|
438
471
|
tab?: ArtifactTabTokens;
|
|
439
472
|
pane?: ArtifactPaneTokens;
|
|
440
473
|
};
|
|
474
|
+
/** Collapsible widget chrome (tool/reasoning/approval bubbles). */
|
|
475
|
+
collapsibleWidget?: CollapsibleWidgetTokens;
|
|
441
476
|
}
|
|
442
477
|
|
|
443
478
|
export interface PaletteExtras {
|
package/src/types.ts
CHANGED
|
@@ -813,11 +813,16 @@ export type AgentWidgetLauncherConfig = {
|
|
|
813
813
|
*/
|
|
814
814
|
mobileBreakpoint?: number;
|
|
815
815
|
/**
|
|
816
|
-
* CSS z-index applied to the widget wrapper
|
|
817
|
-
* (floating panel, mobile fullscreen,
|
|
818
|
-
*
|
|
816
|
+
* CSS z-index applied to the widget wrapper and launcher button in all
|
|
817
|
+
* positioned modes (floating panel, mobile fullscreen, sidebar, docked
|
|
818
|
+
* mobile fullscreen). Increase this value if other elements on the host
|
|
819
|
+
* page appear on top of the widget.
|
|
819
820
|
*
|
|
820
|
-
*
|
|
821
|
+
* In viewport-covering modes (sidebar, mobile fullscreen), the widget
|
|
822
|
+
* also elevates the host element's stacking context and locks
|
|
823
|
+
* document scroll to prevent background scrolling.
|
|
824
|
+
*
|
|
825
|
+
* @default 100000
|
|
821
826
|
*/
|
|
822
827
|
zIndex?: number;
|
|
823
828
|
callToActionIconText?: string;
|
|
@@ -39,7 +39,7 @@ describe("createAgentExperience overlay z-index", () => {
|
|
|
39
39
|
const wrapper = mount.firstElementChild as HTMLElement | null;
|
|
40
40
|
|
|
41
41
|
expect(wrapper).not.toBeNull();
|
|
42
|
-
expect(wrapper?.style.zIndex).toBe("
|
|
42
|
+
expect(wrapper?.style.zIndex).toBe("100000");
|
|
43
43
|
|
|
44
44
|
controller.destroy();
|
|
45
45
|
});
|
|
@@ -55,7 +55,39 @@ describe("createAgentExperience overlay z-index", () => {
|
|
|
55
55
|
const wrapper = mount.firstElementChild as HTMLElement | null;
|
|
56
56
|
|
|
57
57
|
expect(wrapper).not.toBeNull();
|
|
58
|
-
expect(wrapper?.style.zIndex).toBe("
|
|
58
|
+
expect(wrapper?.style.zIndex).toBe("100000");
|
|
59
|
+
|
|
60
|
+
controller.destroy();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("defaults floating panel wrapper to the overlay z-index", () => {
|
|
64
|
+
setInnerWidth(1024);
|
|
65
|
+
|
|
66
|
+
const mount = createMount();
|
|
67
|
+
const controller = createAgentExperience(mount, {
|
|
68
|
+
apiUrl: "https://api.example.com/chat",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const wrapper = mount.firstElementChild as HTMLElement | null;
|
|
72
|
+
|
|
73
|
+
expect(wrapper).not.toBeNull();
|
|
74
|
+
expect(wrapper?.style.zIndex).toBe("100000");
|
|
75
|
+
|
|
76
|
+
controller.destroy();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("respects a custom zIndex", () => {
|
|
80
|
+
const mount = createMount();
|
|
81
|
+
const controller = createAgentExperience(mount, {
|
|
82
|
+
apiUrl: "https://api.example.com/chat",
|
|
83
|
+
launcher: {
|
|
84
|
+
sidebarMode: true,
|
|
85
|
+
zIndex: 42,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const wrapper = mount.firstElementChild as HTMLElement | null;
|
|
90
|
+
expect(wrapper?.style.zIndex).toBe("42");
|
|
59
91
|
|
|
60
92
|
controller.destroy();
|
|
61
93
|
});
|
package/src/ui.ts
CHANGED
|
@@ -40,7 +40,9 @@ import {
|
|
|
40
40
|
resolveFollowStateFromScroll,
|
|
41
41
|
resolveFollowStateFromWheel
|
|
42
42
|
} from "./utils/auto-follow";
|
|
43
|
-
import { statusCopy } from "./utils/constants";
|
|
43
|
+
import { statusCopy, DEFAULT_OVERLAY_Z_INDEX, PORTALED_OVERLAY_Z_INDEX } from "./utils/constants";
|
|
44
|
+
import { syncOverlayHostStacking } from "./utils/overlay-host-stacking";
|
|
45
|
+
import { acquireScrollLock } from "./utils/scroll-lock";
|
|
44
46
|
import { isDockedMountMode, resolveDockConfig } from "./utils/dock";
|
|
45
47
|
import { createLauncherButton } from "./components/launcher";
|
|
46
48
|
import { createWrapper, buildPanel, buildHeader, buildComposer, attachHeaderToContainer } from "./components/panel";
|
|
@@ -1287,6 +1289,7 @@ export const createAgentExperience = (
|
|
|
1287
1289
|
if (openPrevented === true) return;
|
|
1288
1290
|
event.preventDefault();
|
|
1289
1291
|
event.stopPropagation();
|
|
1292
|
+
artifactsPaneUserHidden = false;
|
|
1290
1293
|
session.selectArtifact(artifactId);
|
|
1291
1294
|
syncArtifactPane();
|
|
1292
1295
|
});
|
|
@@ -1546,7 +1549,7 @@ export const createAgentExperience = (
|
|
|
1546
1549
|
// Determine panel styling based on mode, with theme overrides
|
|
1547
1550
|
const position = config.launcher?.position ?? 'bottom-left';
|
|
1548
1551
|
const isLeftSidebar = position === 'bottom-left' || position === 'top-left';
|
|
1549
|
-
const overlayZIndex = config.launcher?.zIndex ??
|
|
1552
|
+
const overlayZIndex = config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX;
|
|
1550
1553
|
|
|
1551
1554
|
// Default values based on mode
|
|
1552
1555
|
let defaultPanelBorder = (sidebarMode || shouldGoFullscreen) ? 'none' : '1px solid var(--persona-border)';
|
|
@@ -1819,9 +1822,8 @@ export const createAgentExperience = (
|
|
|
1819
1822
|
if (!isInlineEmbed && !dockedMode) {
|
|
1820
1823
|
const maxHeightStyles = 'max-height: -moz-available !important; max-height: stretch !important;';
|
|
1821
1824
|
const paddingStyles = sidebarMode ? '' : 'padding-top: 1.25em !important;';
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
? `z-index: ${config.launcher.zIndex} !important;`
|
|
1825
|
+
const zIndexStyles = !sidebarMode
|
|
1826
|
+
? `z-index: ${config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX} !important;`
|
|
1825
1827
|
: '';
|
|
1826
1828
|
wrapper.style.cssText += maxHeightStyles + paddingStyles + zIndexStyles;
|
|
1827
1829
|
}
|
|
@@ -1834,6 +1836,16 @@ export const createAgentExperience = (
|
|
|
1834
1836
|
|
|
1835
1837
|
const destroyCallbacks: Array<() => void> = [];
|
|
1836
1838
|
|
|
1839
|
+
let teardownHostStacking: (() => void) | null = null;
|
|
1840
|
+
let releaseScrollLock: (() => void) | null = null;
|
|
1841
|
+
|
|
1842
|
+
destroyCallbacks.push(() => {
|
|
1843
|
+
teardownHostStacking?.();
|
|
1844
|
+
teardownHostStacking = null;
|
|
1845
|
+
releaseScrollLock?.();
|
|
1846
|
+
releaseScrollLock = null;
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1837
1849
|
if (artifactPanelResizeObs) {
|
|
1838
1850
|
destroyCallbacks.push(() => {
|
|
1839
1851
|
artifactPanelResizeObs?.disconnect();
|
|
@@ -2033,7 +2045,8 @@ export const createAgentExperience = (
|
|
|
2033
2045
|
container.appendChild(scrollToBottomButton);
|
|
2034
2046
|
}
|
|
2035
2047
|
updateScrollToBottomButtonOffset();
|
|
2036
|
-
|
|
2048
|
+
const hasOverflow = getScrollBottomOffset(body) > 0;
|
|
2049
|
+
scrollToBottomButton.style.display = (autoFollow.isFollowing() || !hasOverflow) ? "none" : "";
|
|
2037
2050
|
};
|
|
2038
2051
|
|
|
2039
2052
|
const pauseAutoScroll = () => {
|
|
@@ -2620,12 +2633,46 @@ export const createAgentExperience = (
|
|
|
2620
2633
|
const prevOpen = open;
|
|
2621
2634
|
open = nextOpen;
|
|
2622
2635
|
updateOpenState();
|
|
2623
|
-
|
|
2636
|
+
|
|
2637
|
+
// Sync host stacking and scroll lock for viewport-covering modes
|
|
2638
|
+
const isViewportCovering = (() => {
|
|
2639
|
+
const sm = config.launcher?.sidebarMode ?? false;
|
|
2640
|
+
const ow = mount.ownerDocument.defaultView ?? window;
|
|
2641
|
+
const mf = config.launcher?.mobileFullscreen ?? true;
|
|
2642
|
+
const mb = config.launcher?.mobileBreakpoint ?? 640;
|
|
2643
|
+
const isMobile = ow.innerWidth <= mb;
|
|
2644
|
+
const dockedMF = isDockedMountMode(config) && mf && isMobile;
|
|
2645
|
+
return sm || (mf && isMobile && launcherEnabled) || dockedMF;
|
|
2646
|
+
})();
|
|
2647
|
+
|
|
2648
|
+
if (open && isViewportCovering) {
|
|
2649
|
+
if (!teardownHostStacking) {
|
|
2650
|
+
const root = mount.getRootNode();
|
|
2651
|
+
const hostEl = root instanceof ShadowRoot
|
|
2652
|
+
? (root.host as HTMLElement)
|
|
2653
|
+
: mount.closest<HTMLElement>(".persona-host");
|
|
2654
|
+
if (hostEl) {
|
|
2655
|
+
teardownHostStacking = syncOverlayHostStacking(
|
|
2656
|
+
hostEl,
|
|
2657
|
+
config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX
|
|
2658
|
+
);
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
if (!releaseScrollLock) {
|
|
2662
|
+
releaseScrollLock = acquireScrollLock(mount.ownerDocument);
|
|
2663
|
+
}
|
|
2664
|
+
} else if (!open) {
|
|
2665
|
+
teardownHostStacking?.();
|
|
2666
|
+
teardownHostStacking = null;
|
|
2667
|
+
releaseScrollLock?.();
|
|
2668
|
+
releaseScrollLock = null;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2624
2671
|
if (open) {
|
|
2625
2672
|
recalcPanelHeight();
|
|
2626
2673
|
scheduleAutoScroll(true);
|
|
2627
2674
|
}
|
|
2628
|
-
|
|
2675
|
+
|
|
2629
2676
|
// Emit widget state events
|
|
2630
2677
|
const stateEvent: AgentWidgetStateEvent = {
|
|
2631
2678
|
open,
|
|
@@ -3572,6 +3619,35 @@ export const createAgentExperience = (
|
|
|
3572
3619
|
// overwrites updateOpenState()'s display:none when docked+closed. Re-sync after every recalc.
|
|
3573
3620
|
updateScrollToBottomButtonOffset();
|
|
3574
3621
|
updateOpenState();
|
|
3622
|
+
|
|
3623
|
+
// Sync scroll lock and host stacking when viewport mode changes (e.g. orientation change)
|
|
3624
|
+
if (open && launcherEnabled) {
|
|
3625
|
+
const ow = mount.ownerDocument.defaultView ?? window;
|
|
3626
|
+
const isMobile = ow.innerWidth <= (config.launcher?.mobileBreakpoint ?? 640);
|
|
3627
|
+
const sm = config.launcher?.sidebarMode ?? false;
|
|
3628
|
+
const mf = config.launcher?.mobileFullscreen ?? true;
|
|
3629
|
+
const dockedMF = isDockedMountMode(config) && mf && isMobile;
|
|
3630
|
+
const isVC = sm || (mf && isMobile && launcherEnabled) || dockedMF;
|
|
3631
|
+
|
|
3632
|
+
if (isVC && !releaseScrollLock) {
|
|
3633
|
+
const root = mount.getRootNode();
|
|
3634
|
+
const hostEl = root instanceof ShadowRoot
|
|
3635
|
+
? (root.host as HTMLElement)
|
|
3636
|
+
: mount.closest<HTMLElement>(".persona-host");
|
|
3637
|
+
if (hostEl && !teardownHostStacking) {
|
|
3638
|
+
teardownHostStacking = syncOverlayHostStacking(
|
|
3639
|
+
hostEl,
|
|
3640
|
+
config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX
|
|
3641
|
+
);
|
|
3642
|
+
}
|
|
3643
|
+
releaseScrollLock = acquireScrollLock(mount.ownerDocument);
|
|
3644
|
+
} else if (!isVC) {
|
|
3645
|
+
teardownHostStacking?.();
|
|
3646
|
+
teardownHostStacking = null;
|
|
3647
|
+
releaseScrollLock?.();
|
|
3648
|
+
releaseScrollLock = null;
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3575
3651
|
}
|
|
3576
3652
|
};
|
|
3577
3653
|
|
|
@@ -3671,6 +3747,7 @@ export const createAgentExperience = (
|
|
|
3671
3747
|
// Clear messages in session (this will trigger onMessagesChanged which re-renders)
|
|
3672
3748
|
session.clearMessages();
|
|
3673
3749
|
messageCache.clear();
|
|
3750
|
+
resumeAutoScroll();
|
|
3674
3751
|
|
|
3675
3752
|
// Always clear the default localStorage key
|
|
3676
3753
|
try {
|
|
@@ -4250,6 +4327,7 @@ export const createAgentExperience = (
|
|
|
4250
4327
|
|
|
4251
4328
|
// Position tooltip above button
|
|
4252
4329
|
portaledTooltip.style.position = "fixed";
|
|
4330
|
+
portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
|
|
4253
4331
|
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
4254
4332
|
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
4255
4333
|
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
@@ -4464,6 +4542,7 @@ export const createAgentExperience = (
|
|
|
4464
4542
|
|
|
4465
4543
|
// Position tooltip above button
|
|
4466
4544
|
portaledTooltip.style.position = "fixed";
|
|
4545
|
+
portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
|
|
4467
4546
|
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
4468
4547
|
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
4469
4548
|
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
@@ -4994,6 +5073,7 @@ export const createAgentExperience = (
|
|
|
4994
5073
|
artifactsPaneUserHidden = false;
|
|
4995
5074
|
session.clearMessages();
|
|
4996
5075
|
messageCache.clear();
|
|
5076
|
+
resumeAutoScroll();
|
|
4997
5077
|
|
|
4998
5078
|
// Always clear the default localStorage key
|
|
4999
5079
|
try {
|
package/src/utils/constants.ts
CHANGED
|
@@ -7,6 +7,19 @@ export const statusCopy: Record<AgentWidgetSessionStatus, string> = {
|
|
|
7
7
|
error: "Offline"
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Default z-index for widget overlays. Used for the floating panel, launcher
|
|
12
|
+
* button, sidebar, mobile fullscreen, and docked mobile fullscreen modes.
|
|
13
|
+
* Integrators can override via `launcher.zIndex`.
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_OVERLAY_Z_INDEX = 100000;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Z-index for elements portaled to document.body (tooltips, dropdowns).
|
|
19
|
+
* Must be above the widget overlay so portaled UI is not clipped.
|
|
20
|
+
*/
|
|
21
|
+
export const PORTALED_OVERLAY_Z_INDEX = DEFAULT_OVERLAY_Z_INDEX + 1;
|
|
22
|
+
|
|
10
23
|
|
|
11
24
|
|
|
12
25
|
|
package/src/utils/dropdown.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createElement } from "./dom";
|
|
2
2
|
import { renderLucideIcon } from "./icons";
|
|
3
|
+
import { PORTALED_OVERLAY_Z_INDEX } from "./constants";
|
|
3
4
|
|
|
4
5
|
export interface DropdownMenuItem {
|
|
5
6
|
id: string;
|
|
@@ -73,7 +74,7 @@ export function createDropdownMenu(options: CreateDropdownOptions): DropdownMenu
|
|
|
73
74
|
if (portal) {
|
|
74
75
|
// Fixed positioning — menu is portaled outside the anchor's overflow context
|
|
75
76
|
menu.style.position = "fixed";
|
|
76
|
-
menu.style.zIndex =
|
|
77
|
+
menu.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
|
|
77
78
|
} else {
|
|
78
79
|
// Absolute positioning — menu lives inside the anchor
|
|
79
80
|
menu.style.position = "absolute";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { syncOverlayHostStacking } from "./overlay-host-stacking";
|
|
5
|
+
|
|
6
|
+
describe("syncOverlayHostStacking", () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
document.body.innerHTML = "";
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("sets position relative on a static element and restores on teardown", () => {
|
|
12
|
+
const host = document.createElement("div");
|
|
13
|
+
document.body.appendChild(host);
|
|
14
|
+
|
|
15
|
+
const teardown = syncOverlayHostStacking(host);
|
|
16
|
+
expect(host.style.position).toBe("relative");
|
|
17
|
+
expect(host.style.zIndex).toBe("100000");
|
|
18
|
+
expect(host.style.isolation).toBe("isolate");
|
|
19
|
+
|
|
20
|
+
teardown();
|
|
21
|
+
expect(host.style.position).toBe("");
|
|
22
|
+
expect(host.style.zIndex).toBe("");
|
|
23
|
+
expect(host.style.isolation).toBe("");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("preserves existing positioned value", () => {
|
|
27
|
+
const host = document.createElement("div");
|
|
28
|
+
host.style.position = "absolute";
|
|
29
|
+
document.body.appendChild(host);
|
|
30
|
+
|
|
31
|
+
const teardown = syncOverlayHostStacking(host);
|
|
32
|
+
expect(host.style.position).toBe("absolute");
|
|
33
|
+
expect(host.style.zIndex).toBe("100000");
|
|
34
|
+
|
|
35
|
+
teardown();
|
|
36
|
+
expect(host.style.position).toBe("absolute");
|
|
37
|
+
expect(host.style.zIndex).toBe("");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("accepts custom z-index", () => {
|
|
41
|
+
const host = document.createElement("div");
|
|
42
|
+
document.body.appendChild(host);
|
|
43
|
+
|
|
44
|
+
const teardown = syncOverlayHostStacking(host, 42);
|
|
45
|
+
expect(host.style.zIndex).toBe("42");
|
|
46
|
+
|
|
47
|
+
teardown();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("restores previous inline z-index on teardown", () => {
|
|
51
|
+
const host = document.createElement("div");
|
|
52
|
+
host.style.zIndex = "5";
|
|
53
|
+
document.body.appendChild(host);
|
|
54
|
+
|
|
55
|
+
const teardown = syncOverlayHostStacking(host);
|
|
56
|
+
expect(host.style.zIndex).toBe("100000");
|
|
57
|
+
|
|
58
|
+
teardown();
|
|
59
|
+
expect(host.style.zIndex).toBe("5");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { DEFAULT_OVERLAY_Z_INDEX } from "./constants";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Elevates the light-DOM host element's stacking context so viewport-covering
|
|
5
|
+
* overlays (sidebar, fullscreen) can escape parent stacking traps.
|
|
6
|
+
*
|
|
7
|
+
* - If the host has `position: static`, sets it to `relative` (required for
|
|
8
|
+
* `z-index` to take effect).
|
|
9
|
+
* - Applies `z-index` matching the overlay default.
|
|
10
|
+
* - Applies `isolation: isolate` to create a predictable stacking context.
|
|
11
|
+
*
|
|
12
|
+
* @returns A teardown function that restores only the properties that were changed.
|
|
13
|
+
*/
|
|
14
|
+
export function syncOverlayHostStacking(
|
|
15
|
+
host: HTMLElement,
|
|
16
|
+
zIndex: number = DEFAULT_OVERLAY_Z_INDEX
|
|
17
|
+
): () => void {
|
|
18
|
+
const originalPosition = host.style.position;
|
|
19
|
+
const originalZIndex = host.style.zIndex;
|
|
20
|
+
const originalIsolation = host.style.isolation;
|
|
21
|
+
|
|
22
|
+
const computed = getComputedStyle(host);
|
|
23
|
+
const positionWasSet = computed.position === "static" || computed.position === "";
|
|
24
|
+
if (positionWasSet) {
|
|
25
|
+
host.style.position = "relative";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
host.style.zIndex = String(zIndex);
|
|
29
|
+
host.style.isolation = "isolate";
|
|
30
|
+
|
|
31
|
+
return () => {
|
|
32
|
+
if (positionWasSet) {
|
|
33
|
+
host.style.position = originalPosition;
|
|
34
|
+
}
|
|
35
|
+
host.style.zIndex = originalZIndex;
|
|
36
|
+
host.style.isolation = originalIsolation;
|
|
37
|
+
};
|
|
38
|
+
}
|