@runtypelabs/persona 3.7.0 → 3.8.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/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 +198 -24
- package/dist/theme-editor.d.cts +43 -4
- package/dist/theme-editor.d.ts +43 -4
- package/dist/theme-editor.js +198 -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 +87 -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
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.0",
|
|
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";
|
|
@@ -1546,7 +1548,7 @@ export const createAgentExperience = (
|
|
|
1546
1548
|
// Determine panel styling based on mode, with theme overrides
|
|
1547
1549
|
const position = config.launcher?.position ?? 'bottom-left';
|
|
1548
1550
|
const isLeftSidebar = position === 'bottom-left' || position === 'top-left';
|
|
1549
|
-
const overlayZIndex = config.launcher?.zIndex ??
|
|
1551
|
+
const overlayZIndex = config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX;
|
|
1550
1552
|
|
|
1551
1553
|
// Default values based on mode
|
|
1552
1554
|
let defaultPanelBorder = (sidebarMode || shouldGoFullscreen) ? 'none' : '1px solid var(--persona-border)';
|
|
@@ -1819,9 +1821,8 @@ export const createAgentExperience = (
|
|
|
1819
1821
|
if (!isInlineEmbed && !dockedMode) {
|
|
1820
1822
|
const maxHeightStyles = 'max-height: -moz-available !important; max-height: stretch !important;';
|
|
1821
1823
|
const paddingStyles = sidebarMode ? '' : 'padding-top: 1.25em !important;';
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
? `z-index: ${config.launcher.zIndex} !important;`
|
|
1824
|
+
const zIndexStyles = !sidebarMode
|
|
1825
|
+
? `z-index: ${config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX} !important;`
|
|
1825
1826
|
: '';
|
|
1826
1827
|
wrapper.style.cssText += maxHeightStyles + paddingStyles + zIndexStyles;
|
|
1827
1828
|
}
|
|
@@ -1834,6 +1835,16 @@ export const createAgentExperience = (
|
|
|
1834
1835
|
|
|
1835
1836
|
const destroyCallbacks: Array<() => void> = [];
|
|
1836
1837
|
|
|
1838
|
+
let teardownHostStacking: (() => void) | null = null;
|
|
1839
|
+
let releaseScrollLock: (() => void) | null = null;
|
|
1840
|
+
|
|
1841
|
+
destroyCallbacks.push(() => {
|
|
1842
|
+
teardownHostStacking?.();
|
|
1843
|
+
teardownHostStacking = null;
|
|
1844
|
+
releaseScrollLock?.();
|
|
1845
|
+
releaseScrollLock = null;
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1837
1848
|
if (artifactPanelResizeObs) {
|
|
1838
1849
|
destroyCallbacks.push(() => {
|
|
1839
1850
|
artifactPanelResizeObs?.disconnect();
|
|
@@ -2033,7 +2044,8 @@ export const createAgentExperience = (
|
|
|
2033
2044
|
container.appendChild(scrollToBottomButton);
|
|
2034
2045
|
}
|
|
2035
2046
|
updateScrollToBottomButtonOffset();
|
|
2036
|
-
|
|
2047
|
+
const hasOverflow = getScrollBottomOffset(body) > 0;
|
|
2048
|
+
scrollToBottomButton.style.display = (autoFollow.isFollowing() || !hasOverflow) ? "none" : "";
|
|
2037
2049
|
};
|
|
2038
2050
|
|
|
2039
2051
|
const pauseAutoScroll = () => {
|
|
@@ -2620,12 +2632,46 @@ export const createAgentExperience = (
|
|
|
2620
2632
|
const prevOpen = open;
|
|
2621
2633
|
open = nextOpen;
|
|
2622
2634
|
updateOpenState();
|
|
2623
|
-
|
|
2635
|
+
|
|
2636
|
+
// Sync host stacking and scroll lock for viewport-covering modes
|
|
2637
|
+
const isViewportCovering = (() => {
|
|
2638
|
+
const sm = config.launcher?.sidebarMode ?? false;
|
|
2639
|
+
const ow = mount.ownerDocument.defaultView ?? window;
|
|
2640
|
+
const mf = config.launcher?.mobileFullscreen ?? true;
|
|
2641
|
+
const mb = config.launcher?.mobileBreakpoint ?? 640;
|
|
2642
|
+
const isMobile = ow.innerWidth <= mb;
|
|
2643
|
+
const dockedMF = isDockedMountMode(config) && mf && isMobile;
|
|
2644
|
+
return sm || (mf && isMobile && launcherEnabled) || dockedMF;
|
|
2645
|
+
})();
|
|
2646
|
+
|
|
2647
|
+
if (open && isViewportCovering) {
|
|
2648
|
+
if (!teardownHostStacking) {
|
|
2649
|
+
const root = mount.getRootNode();
|
|
2650
|
+
const hostEl = root instanceof ShadowRoot
|
|
2651
|
+
? (root.host as HTMLElement)
|
|
2652
|
+
: mount.closest<HTMLElement>(".persona-host");
|
|
2653
|
+
if (hostEl) {
|
|
2654
|
+
teardownHostStacking = syncOverlayHostStacking(
|
|
2655
|
+
hostEl,
|
|
2656
|
+
config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
if (!releaseScrollLock) {
|
|
2661
|
+
releaseScrollLock = acquireScrollLock(mount.ownerDocument);
|
|
2662
|
+
}
|
|
2663
|
+
} else if (!open) {
|
|
2664
|
+
teardownHostStacking?.();
|
|
2665
|
+
teardownHostStacking = null;
|
|
2666
|
+
releaseScrollLock?.();
|
|
2667
|
+
releaseScrollLock = null;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2624
2670
|
if (open) {
|
|
2625
2671
|
recalcPanelHeight();
|
|
2626
2672
|
scheduleAutoScroll(true);
|
|
2627
2673
|
}
|
|
2628
|
-
|
|
2674
|
+
|
|
2629
2675
|
// Emit widget state events
|
|
2630
2676
|
const stateEvent: AgentWidgetStateEvent = {
|
|
2631
2677
|
open,
|
|
@@ -3572,6 +3618,35 @@ export const createAgentExperience = (
|
|
|
3572
3618
|
// overwrites updateOpenState()'s display:none when docked+closed. Re-sync after every recalc.
|
|
3573
3619
|
updateScrollToBottomButtonOffset();
|
|
3574
3620
|
updateOpenState();
|
|
3621
|
+
|
|
3622
|
+
// Sync scroll lock and host stacking when viewport mode changes (e.g. orientation change)
|
|
3623
|
+
if (open && launcherEnabled) {
|
|
3624
|
+
const ow = mount.ownerDocument.defaultView ?? window;
|
|
3625
|
+
const isMobile = ow.innerWidth <= (config.launcher?.mobileBreakpoint ?? 640);
|
|
3626
|
+
const sm = config.launcher?.sidebarMode ?? false;
|
|
3627
|
+
const mf = config.launcher?.mobileFullscreen ?? true;
|
|
3628
|
+
const dockedMF = isDockedMountMode(config) && mf && isMobile;
|
|
3629
|
+
const isVC = sm || (mf && isMobile && launcherEnabled) || dockedMF;
|
|
3630
|
+
|
|
3631
|
+
if (isVC && !releaseScrollLock) {
|
|
3632
|
+
const root = mount.getRootNode();
|
|
3633
|
+
const hostEl = root instanceof ShadowRoot
|
|
3634
|
+
? (root.host as HTMLElement)
|
|
3635
|
+
: mount.closest<HTMLElement>(".persona-host");
|
|
3636
|
+
if (hostEl && !teardownHostStacking) {
|
|
3637
|
+
teardownHostStacking = syncOverlayHostStacking(
|
|
3638
|
+
hostEl,
|
|
3639
|
+
config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX
|
|
3640
|
+
);
|
|
3641
|
+
}
|
|
3642
|
+
releaseScrollLock = acquireScrollLock(mount.ownerDocument);
|
|
3643
|
+
} else if (!isVC) {
|
|
3644
|
+
teardownHostStacking?.();
|
|
3645
|
+
teardownHostStacking = null;
|
|
3646
|
+
releaseScrollLock?.();
|
|
3647
|
+
releaseScrollLock = null;
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3575
3650
|
}
|
|
3576
3651
|
};
|
|
3577
3652
|
|
|
@@ -3671,6 +3746,7 @@ export const createAgentExperience = (
|
|
|
3671
3746
|
// Clear messages in session (this will trigger onMessagesChanged which re-renders)
|
|
3672
3747
|
session.clearMessages();
|
|
3673
3748
|
messageCache.clear();
|
|
3749
|
+
resumeAutoScroll();
|
|
3674
3750
|
|
|
3675
3751
|
// Always clear the default localStorage key
|
|
3676
3752
|
try {
|
|
@@ -4250,6 +4326,7 @@ export const createAgentExperience = (
|
|
|
4250
4326
|
|
|
4251
4327
|
// Position tooltip above button
|
|
4252
4328
|
portaledTooltip.style.position = "fixed";
|
|
4329
|
+
portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
|
|
4253
4330
|
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
4254
4331
|
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
4255
4332
|
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
@@ -4464,6 +4541,7 @@ export const createAgentExperience = (
|
|
|
4464
4541
|
|
|
4465
4542
|
// Position tooltip above button
|
|
4466
4543
|
portaledTooltip.style.position = "fixed";
|
|
4544
|
+
portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
|
|
4467
4545
|
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
4468
4546
|
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
4469
4547
|
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
@@ -4994,6 +5072,7 @@ export const createAgentExperience = (
|
|
|
4994
5072
|
artifactsPaneUserHidden = false;
|
|
4995
5073
|
session.clearMessages();
|
|
4996
5074
|
messageCache.clear();
|
|
5075
|
+
resumeAutoScroll();
|
|
4997
5076
|
|
|
4998
5077
|
// Always clear the default localStorage key
|
|
4999
5078
|
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
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { acquireScrollLock } from "./scroll-lock";
|
|
5
|
+
|
|
6
|
+
describe("acquireScrollLock", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
document.body.style.overflow = "";
|
|
9
|
+
document.body.style.position = "";
|
|
10
|
+
document.body.style.top = "";
|
|
11
|
+
document.body.style.width = "";
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
document.body.style.overflow = "";
|
|
16
|
+
document.body.style.position = "";
|
|
17
|
+
document.body.style.top = "";
|
|
18
|
+
document.body.style.width = "";
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("sets overflow hidden and position fixed on body", () => {
|
|
22
|
+
const release = acquireScrollLock();
|
|
23
|
+
expect(document.body.style.overflow).toBe("hidden");
|
|
24
|
+
expect(document.body.style.position).toBe("fixed");
|
|
25
|
+
expect(document.body.style.width).toBe("100%");
|
|
26
|
+
|
|
27
|
+
release();
|
|
28
|
+
expect(document.body.style.overflow).toBe("");
|
|
29
|
+
expect(document.body.style.position).toBe("");
|
|
30
|
+
expect(document.body.style.width).toBe("");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("is ref-counted: first release does not unlock when second acquire is active", () => {
|
|
34
|
+
const release1 = acquireScrollLock();
|
|
35
|
+
const release2 = acquireScrollLock();
|
|
36
|
+
expect(document.body.style.overflow).toBe("hidden");
|
|
37
|
+
|
|
38
|
+
release1();
|
|
39
|
+
expect(document.body.style.overflow).toBe("hidden");
|
|
40
|
+
|
|
41
|
+
release2();
|
|
42
|
+
expect(document.body.style.overflow).toBe("");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("release is idempotent", () => {
|
|
46
|
+
const release = acquireScrollLock();
|
|
47
|
+
release();
|
|
48
|
+
release();
|
|
49
|
+
expect(document.body.style.overflow).toBe("");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("restores original body styles on release", () => {
|
|
53
|
+
document.body.style.overflow = "scroll";
|
|
54
|
+
document.body.style.position = "relative";
|
|
55
|
+
|
|
56
|
+
const release = acquireScrollLock();
|
|
57
|
+
expect(document.body.style.overflow).toBe("hidden");
|
|
58
|
+
expect(document.body.style.position).toBe("fixed");
|
|
59
|
+
|
|
60
|
+
release();
|
|
61
|
+
expect(document.body.style.overflow).toBe("scroll");
|
|
62
|
+
expect(document.body.style.position).toBe("relative");
|
|
63
|
+
});
|
|
64
|
+
});
|