@runtypelabs/persona 4.10.1 → 4.11.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/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-Bx_rvjff.d.cts → types-DveIaNx6.d.cts} +2 -0
- package/dist/animations/{types-Bx_rvjff.d.ts → types-DveIaNx6.d.ts} +2 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-MMUPR2JW.js +1 -0
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/{context-mentions-7S5KVUTG.js → context-mentions-ONG7A7M6.js} +1 -1
- package/dist/context-mentions.d.cts +26 -0
- package/dist/context-mentions.d.ts +26 -0
- package/dist/index.cjs +67 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.global.js +55 -55
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +78 -78
- package/dist/index.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +26 -0
- package/dist/smart-dom-reader.d.ts +26 -0
- package/dist/theme-editor-preview.cjs +66 -66
- package/dist/theme-editor-preview.d.cts +26 -0
- package/dist/theme-editor-preview.d.ts +26 -0
- package/dist/theme-editor-preview.js +55 -55
- package/dist/theme-editor.cjs +5 -5
- package/dist/theme-editor.d.cts +26 -0
- package/dist/theme-editor.d.ts +26 -0
- package/dist/theme-editor.js +9 -9
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/components/approval-bubble.ts +0 -1
- package/src/components/composer-parts.ts +19 -19
- package/src/components/context-mention-button.test.ts +5 -1
- package/src/components/context-mention-button.ts +6 -4
- package/src/components/header-builder.test.ts +3 -3
- package/src/components/header-parts.ts +14 -108
- package/src/components/message-bubble.test.ts +49 -0
- package/src/components/message-bubble.ts +8 -7
- package/src/components/reasoning-bubble.ts +0 -2
- package/src/components/tool-bubble.ts +0 -2
- package/src/generated/runtype-openapi-contract.ts +2 -0
- package/src/index-core.ts +1 -0
- package/src/styles/widget.css +118 -58
- package/src/theme-editor/sections.test.ts +33 -0
- package/src/theme-editor/sections.ts +10 -1
- package/src/types.ts +25 -0
- package/src/ui.attachments-drop.test.ts +2 -1
- package/src/ui.message-width.test.ts +407 -0
- package/src/ui.ts +145 -238
- package/src/utils/table-scroll-fade.test.ts +123 -0
- package/src/utils/table-scroll-fade.ts +75 -0
- package/src/utils/tooltip.test.ts +226 -0
- package/src/utils/tooltip.ts +245 -0
- package/dist/chunk-IPVK3KOM.js +0 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
4
|
+
import { wrapScrollableTables, refreshTableScrollFades } from "./table-scroll-fade";
|
|
5
|
+
|
|
6
|
+
// Off-edge fade value: a themeable token reference, not a resolved px length.
|
|
7
|
+
const FADE_ON = "var(--persona-md-table-scroll-fade, 24px)";
|
|
8
|
+
|
|
9
|
+
// jsdom has no layout, so scroll metrics are stubbed per element.
|
|
10
|
+
function stubMetrics(
|
|
11
|
+
el: HTMLElement,
|
|
12
|
+
{ scrollWidth, clientWidth, scrollLeft = 0 }: { scrollWidth: number; clientWidth: number; scrollLeft?: number }
|
|
13
|
+
): void {
|
|
14
|
+
Object.defineProperty(el, "scrollWidth", { value: scrollWidth, configurable: true });
|
|
15
|
+
Object.defineProperty(el, "clientWidth", { value: clientWidth, configurable: true });
|
|
16
|
+
let sl = scrollLeft;
|
|
17
|
+
Object.defineProperty(el, "scrollLeft", {
|
|
18
|
+
get: () => sl,
|
|
19
|
+
set: (v: number) => {
|
|
20
|
+
sl = v;
|
|
21
|
+
},
|
|
22
|
+
configurable: true,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeContainerWithTable(): { container: HTMLElement; table: HTMLElement } {
|
|
27
|
+
const container = document.createElement("div");
|
|
28
|
+
const bubble = document.createElement("div");
|
|
29
|
+
bubble.className = "persona-message-content";
|
|
30
|
+
const table = document.createElement("table");
|
|
31
|
+
table.innerHTML = "<tbody><tr><td>a</td></tr></tbody>";
|
|
32
|
+
bubble.appendChild(table);
|
|
33
|
+
container.appendChild(bubble);
|
|
34
|
+
return { container, table };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("wrapScrollableTables", () => {
|
|
38
|
+
it("wraps a bare table in a .persona-table-scroll container", () => {
|
|
39
|
+
const { container, table } = makeContainerWithTable();
|
|
40
|
+
wrapScrollableTables(container);
|
|
41
|
+
const wrapper = table.parentElement!;
|
|
42
|
+
expect(wrapper.className).toBe("persona-table-scroll");
|
|
43
|
+
expect(wrapper.firstElementChild).toBe(table);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("is idempotent: an already-wrapped table is not double-wrapped", () => {
|
|
47
|
+
const { container, table } = makeContainerWithTable();
|
|
48
|
+
wrapScrollableTables(container);
|
|
49
|
+
wrapScrollableTables(container);
|
|
50
|
+
expect(container.querySelectorAll(".persona-table-scroll").length).toBe(1);
|
|
51
|
+
expect(table.parentElement!.parentElement!.className).toBe("persona-message-content");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("refreshTableScrollFades", () => {
|
|
56
|
+
let container: HTMLElement;
|
|
57
|
+
let wrapper: HTMLElement;
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
const built = makeContainerWithTable();
|
|
61
|
+
container = built.container;
|
|
62
|
+
wrapScrollableTables(container);
|
|
63
|
+
wrapper = built.table.parentElement!;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("marks an overflowing table scrollable and fades only the right edge at start", () => {
|
|
67
|
+
stubMetrics(wrapper, { scrollWidth: 600, clientWidth: 300, scrollLeft: 0 });
|
|
68
|
+
refreshTableScrollFades(container);
|
|
69
|
+
expect(wrapper.hasAttribute("data-persona-scroll-x")).toBe(true);
|
|
70
|
+
expect(wrapper.style.getPropertyValue("--persona-fade-l")).toBe("0px");
|
|
71
|
+
expect(wrapper.style.getPropertyValue("--persona-fade-r")).toBe(FADE_ON);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("fades only the left edge when scrolled to the end", () => {
|
|
75
|
+
stubMetrics(wrapper, { scrollWidth: 600, clientWidth: 300, scrollLeft: 300 });
|
|
76
|
+
refreshTableScrollFades(container);
|
|
77
|
+
expect(wrapper.style.getPropertyValue("--persona-fade-l")).toBe(FADE_ON);
|
|
78
|
+
expect(wrapper.style.getPropertyValue("--persona-fade-r")).toBe("0px");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("fades both edges mid-scroll", () => {
|
|
82
|
+
stubMetrics(wrapper, { scrollWidth: 600, clientWidth: 300, scrollLeft: 120 });
|
|
83
|
+
refreshTableScrollFades(container);
|
|
84
|
+
expect(wrapper.style.getPropertyValue("--persona-fade-l")).toBe(FADE_ON);
|
|
85
|
+
expect(wrapper.style.getPropertyValue("--persona-fade-r")).toBe(FADE_ON);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// RTL scrolls on the negative model: scrollLeft runs [-overflow, 0], starting
|
|
89
|
+
// at 0 with the hidden content to the physical left (mirror of LTR).
|
|
90
|
+
it("fades the left edge at an RTL table's start position", () => {
|
|
91
|
+
wrapper.style.direction = "rtl";
|
|
92
|
+
stubMetrics(wrapper, { scrollWidth: 600, clientWidth: 300, scrollLeft: 0 });
|
|
93
|
+
refreshTableScrollFades(container);
|
|
94
|
+
expect(wrapper.style.getPropertyValue("--persona-fade-l")).toBe(FADE_ON);
|
|
95
|
+
expect(wrapper.style.getPropertyValue("--persona-fade-r")).toBe("0px");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("fades the right edge when an RTL table is scrolled to its end", () => {
|
|
99
|
+
wrapper.style.direction = "rtl";
|
|
100
|
+
stubMetrics(wrapper, { scrollWidth: 600, clientWidth: 300, scrollLeft: -300 });
|
|
101
|
+
refreshTableScrollFades(container);
|
|
102
|
+
expect(wrapper.style.getPropertyValue("--persona-fade-l")).toBe("0px");
|
|
103
|
+
expect(wrapper.style.getPropertyValue("--persona-fade-r")).toBe(FADE_ON);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("leaves a table that fits unmarked (no fade)", () => {
|
|
107
|
+
stubMetrics(wrapper, { scrollWidth: 300, clientWidth: 300, scrollLeft: 0 });
|
|
108
|
+
refreshTableScrollFades(container);
|
|
109
|
+
expect(wrapper.hasAttribute("data-persona-scroll-x")).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("updates fades when the table is scrolled (delegated capture listener)", () => {
|
|
113
|
+
stubMetrics(wrapper, { scrollWidth: 600, clientWidth: 300, scrollLeft: 0 });
|
|
114
|
+
refreshTableScrollFades(container);
|
|
115
|
+
expect(wrapper.style.getPropertyValue("--persona-fade-r")).toBe(FADE_ON);
|
|
116
|
+
// Simulate a real scroll to the end: move position, fire the event the
|
|
117
|
+
// container's capture-phase listener is waiting for.
|
|
118
|
+
wrapper.scrollLeft = 300;
|
|
119
|
+
wrapper.dispatchEvent(new Event("scroll"));
|
|
120
|
+
expect(wrapper.style.getPropertyValue("--persona-fade-l")).toBe(FADE_ON);
|
|
121
|
+
expect(wrapper.style.getPropertyValue("--persona-fade-r")).toBe("0px");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Wide markdown tables must scroll inside their own container, never widen the
|
|
2
|
+
// chat column. Tables render from an HTML string with no wrapper, so we wrap
|
|
3
|
+
// them in the freshly-built morph container before idiomorph runs: the wrapper
|
|
4
|
+
// then exists on both sides of every subsequent diff, so it (and its scroll
|
|
5
|
+
// position) survives streaming re-renders.
|
|
6
|
+
|
|
7
|
+
// Off-edge fade width, themeable via --persona-md-table-scroll-fade (set it to
|
|
8
|
+
// 0 to disable the fade). Written as a var() reference, not a resolved px value,
|
|
9
|
+
// so a consumer's CSS token wins over this inline style.
|
|
10
|
+
const FADE = "var(--persona-md-table-scroll-fade, 24px)";
|
|
11
|
+
|
|
12
|
+
// Containers that already carry the delegated scroll listener.
|
|
13
|
+
const LISTENING = new WeakSet<HTMLElement>();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wrap every unwrapped `<table>` under `root` in a `.persona-table-scroll`
|
|
17
|
+
* horizontal-scroll container. Idempotent: tables already wrapped are skipped.
|
|
18
|
+
*/
|
|
19
|
+
export function wrapScrollableTables(root: HTMLElement): void {
|
|
20
|
+
const tables = root.querySelectorAll("table");
|
|
21
|
+
tables.forEach((table) => {
|
|
22
|
+
const parent = table.parentElement;
|
|
23
|
+
if (parent?.classList.contains("persona-table-scroll")) return;
|
|
24
|
+
const wrapper = document.createElement("div");
|
|
25
|
+
wrapper.className = "persona-table-scroll";
|
|
26
|
+
table.replaceWith(wrapper);
|
|
27
|
+
wrapper.appendChild(table);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Toggle the edge-fade mask for one scroll container based on its position.
|
|
32
|
+
function updateFade(el: HTMLElement): void {
|
|
33
|
+
const overflow = el.scrollWidth - el.clientWidth;
|
|
34
|
+
if (overflow <= 1) {
|
|
35
|
+
el.removeAttribute("data-persona-scroll-x");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
el.setAttribute("data-persona-scroll-x", "");
|
|
39
|
+
// Each edge fades only when content is hidden past it, so the amounts must be
|
|
40
|
+
// physical (left/right), not directional. RTL scrolls on the negative model:
|
|
41
|
+
// scrollLeft runs [-overflow, 0] and starts at 0 with content hidden to the
|
|
42
|
+
// left, the mirror of LTR. Magnitude alone can't tell them apart at 0.
|
|
43
|
+
const rtl = getComputedStyle(el).direction === "rtl";
|
|
44
|
+
const hiddenLeft = rtl ? overflow + el.scrollLeft : el.scrollLeft;
|
|
45
|
+
const hiddenRight = overflow - hiddenLeft;
|
|
46
|
+
el.style.setProperty("--persona-fade-l", hiddenLeft <= 1 ? "0px" : FADE);
|
|
47
|
+
el.style.setProperty("--persona-fade-r", hiddenRight <= 1 ? "0px" : FADE);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Refresh edge-fade state for every table scroll container under `container`,
|
|
52
|
+
* and (once per container) attach the delegated scroll listener that keeps the
|
|
53
|
+
* fades in sync as the user scrolls. Call after each transcript morph.
|
|
54
|
+
*/
|
|
55
|
+
export function refreshTableScrollFades(container: HTMLElement): void {
|
|
56
|
+
if (!LISTENING.has(container)) {
|
|
57
|
+
LISTENING.add(container);
|
|
58
|
+
// scroll does not bubble, but it does fire in the capture phase, so one
|
|
59
|
+
// ancestor listener catches every table without per-table wiring that
|
|
60
|
+
// idiomorph would strip.
|
|
61
|
+
container.addEventListener(
|
|
62
|
+
"scroll",
|
|
63
|
+
(event) => {
|
|
64
|
+
const target = event.target as HTMLElement | null;
|
|
65
|
+
if (target?.classList?.contains("persona-table-scroll")) {
|
|
66
|
+
updateFade(target);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
true
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
container
|
|
73
|
+
.querySelectorAll<HTMLElement>(".persona-table-scroll")
|
|
74
|
+
.forEach(updateFade);
|
|
75
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { attachTooltip } from "./tooltip";
|
|
5
|
+
|
|
6
|
+
const rect = (
|
|
7
|
+
left: number,
|
|
8
|
+
top: number,
|
|
9
|
+
width: number,
|
|
10
|
+
height: number
|
|
11
|
+
): DOMRect =>
|
|
12
|
+
({
|
|
13
|
+
left,
|
|
14
|
+
top,
|
|
15
|
+
width,
|
|
16
|
+
height,
|
|
17
|
+
right: left + width,
|
|
18
|
+
bottom: top + height,
|
|
19
|
+
x: left,
|
|
20
|
+
y: top,
|
|
21
|
+
toJSON: () => ({}),
|
|
22
|
+
}) as DOMRect;
|
|
23
|
+
|
|
24
|
+
describe("attachTooltip", () => {
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
document.body.innerHTML = "";
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
vi.unstubAllGlobals();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("portals outside a clipped wrapper and clamps a long tooltip to the viewport", () => {
|
|
32
|
+
const wrapper = document.createElement("div");
|
|
33
|
+
wrapper.style.overflow = "hidden";
|
|
34
|
+
const button = document.createElement("button");
|
|
35
|
+
button.setAttribute("aria-label", "Add context — a slide, element or comment");
|
|
36
|
+
wrapper.appendChild(button);
|
|
37
|
+
document.body.appendChild(wrapper);
|
|
38
|
+
|
|
39
|
+
Object.defineProperty(document.documentElement, "clientWidth", {
|
|
40
|
+
configurable: true,
|
|
41
|
+
value: 320,
|
|
42
|
+
});
|
|
43
|
+
Object.defineProperty(document.documentElement, "clientHeight", {
|
|
44
|
+
configurable: true,
|
|
45
|
+
value: 500,
|
|
46
|
+
});
|
|
47
|
+
vi.spyOn(button, "getBoundingClientRect").mockReturnValue(
|
|
48
|
+
rect(4, 400, 40, 40)
|
|
49
|
+
);
|
|
50
|
+
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
|
|
51
|
+
function (this: HTMLElement) {
|
|
52
|
+
return this.classList.contains("persona-control-tooltip")
|
|
53
|
+
? rect(0, 0, 300, 32)
|
|
54
|
+
: rect(0, 0, 0, 0);
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
attachTooltip({
|
|
59
|
+
anchor: button,
|
|
60
|
+
trigger: wrapper,
|
|
61
|
+
text: () => button.getAttribute("aria-label") ?? "",
|
|
62
|
+
});
|
|
63
|
+
wrapper.dispatchEvent(new MouseEvent("mouseenter"));
|
|
64
|
+
|
|
65
|
+
const tooltip = document.body.querySelector<HTMLElement>(
|
|
66
|
+
".persona-control-tooltip"
|
|
67
|
+
);
|
|
68
|
+
expect(tooltip).not.toBeNull();
|
|
69
|
+
expect(wrapper.contains(tooltip)).toBe(false);
|
|
70
|
+
expect(tooltip!.textContent).toContain("Add context");
|
|
71
|
+
expect(tooltip!.style.left).toBe("8px");
|
|
72
|
+
expect(tooltip!.style.getPropertyValue("--persona-tooltip-arrow-x")).toBe(
|
|
73
|
+
"16px"
|
|
74
|
+
);
|
|
75
|
+
expect(tooltip!.dataset.placement).toBe("top");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("flips below the control when there is not enough room above", () => {
|
|
79
|
+
const button = document.createElement("button");
|
|
80
|
+
document.body.appendChild(button);
|
|
81
|
+
Object.defineProperty(document.documentElement, "clientWidth", {
|
|
82
|
+
configurable: true,
|
|
83
|
+
value: 500,
|
|
84
|
+
});
|
|
85
|
+
Object.defineProperty(document.documentElement, "clientHeight", {
|
|
86
|
+
configurable: true,
|
|
87
|
+
value: 300,
|
|
88
|
+
});
|
|
89
|
+
vi.spyOn(button, "getBoundingClientRect").mockReturnValue(
|
|
90
|
+
rect(200, 4, 40, 40)
|
|
91
|
+
);
|
|
92
|
+
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
|
|
93
|
+
function (this: HTMLElement) {
|
|
94
|
+
return this.classList.contains("persona-control-tooltip")
|
|
95
|
+
? rect(0, 0, 120, 32)
|
|
96
|
+
: rect(0, 0, 0, 0);
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
attachTooltip({ anchor: button, text: "Close chat" });
|
|
101
|
+
button.focus();
|
|
102
|
+
|
|
103
|
+
const tooltip = document.body.querySelector<HTMLElement>(
|
|
104
|
+
".persona-control-tooltip"
|
|
105
|
+
);
|
|
106
|
+
expect(tooltip?.dataset.placement).toBe("bottom");
|
|
107
|
+
expect(tooltip?.style.top).toBe("52px");
|
|
108
|
+
|
|
109
|
+
button.blur();
|
|
110
|
+
expect(document.body.querySelector(".persona-control-tooltip")).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("dismisses an open tooltip with Escape", () => {
|
|
114
|
+
const button = document.createElement("button");
|
|
115
|
+
document.body.appendChild(button);
|
|
116
|
+
|
|
117
|
+
attachTooltip({ anchor: button, text: "Close chat" });
|
|
118
|
+
button.focus();
|
|
119
|
+
expect(document.body.querySelector(".persona-control-tooltip")).not.toBeNull();
|
|
120
|
+
|
|
121
|
+
button.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
|
122
|
+
|
|
123
|
+
expect(document.body.querySelector(".persona-control-tooltip")).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("suppresses hover tooltips when the device has no hover capability", () => {
|
|
127
|
+
vi.stubGlobal(
|
|
128
|
+
"matchMedia",
|
|
129
|
+
vi.fn().mockReturnValue({ matches: true })
|
|
130
|
+
);
|
|
131
|
+
const button = document.createElement("button");
|
|
132
|
+
button.setAttribute("aria-label", "Add context");
|
|
133
|
+
document.body.appendChild(button);
|
|
134
|
+
|
|
135
|
+
attachTooltip({
|
|
136
|
+
anchor: button,
|
|
137
|
+
text: () => button.getAttribute("aria-label") ?? "",
|
|
138
|
+
});
|
|
139
|
+
button.dispatchEvent(new MouseEvent("mouseenter"));
|
|
140
|
+
|
|
141
|
+
expect(document.body.querySelector(".persona-control-tooltip")).toBeNull();
|
|
142
|
+
expect(button.getAttribute("aria-label")).toBe("Add context");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("repositions an open tooltip after resize and scroll", () => {
|
|
146
|
+
const button = document.createElement("button");
|
|
147
|
+
document.body.appendChild(button);
|
|
148
|
+
Object.defineProperty(document.documentElement, "clientWidth", {
|
|
149
|
+
configurable: true,
|
|
150
|
+
value: 500,
|
|
151
|
+
});
|
|
152
|
+
Object.defineProperty(document.documentElement, "clientHeight", {
|
|
153
|
+
configurable: true,
|
|
154
|
+
value: 300,
|
|
155
|
+
});
|
|
156
|
+
let anchorLeft = 100;
|
|
157
|
+
vi.spyOn(button, "getBoundingClientRect").mockImplementation(() =>
|
|
158
|
+
rect(anchorLeft, 200, 40, 40)
|
|
159
|
+
);
|
|
160
|
+
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
|
|
161
|
+
function (this: HTMLElement) {
|
|
162
|
+
return this.classList.contains("persona-control-tooltip")
|
|
163
|
+
? rect(0, 0, 120, 32)
|
|
164
|
+
: rect(0, 0, 0, 0);
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
attachTooltip({ anchor: button, text: "Add context" });
|
|
169
|
+
button.focus();
|
|
170
|
+
const tooltip = document.body.querySelector<HTMLElement>(
|
|
171
|
+
".persona-control-tooltip"
|
|
172
|
+
);
|
|
173
|
+
expect(tooltip?.style.left).toBe("60px");
|
|
174
|
+
|
|
175
|
+
anchorLeft = 200;
|
|
176
|
+
window.dispatchEvent(new Event("resize"));
|
|
177
|
+
expect(tooltip?.style.left).toBe("160px");
|
|
178
|
+
|
|
179
|
+
anchorLeft = 300;
|
|
180
|
+
window.dispatchEvent(new Event("scroll"));
|
|
181
|
+
expect(tooltip?.style.left).toBe("260px");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("cleans up an open tooltip when its anchor is removed", async () => {
|
|
185
|
+
const button = document.createElement("button");
|
|
186
|
+
document.body.appendChild(button);
|
|
187
|
+
|
|
188
|
+
attachTooltip({ anchor: button, text: "Add context" });
|
|
189
|
+
button.focus();
|
|
190
|
+
expect(document.body.querySelector(".persona-control-tooltip")).not.toBeNull();
|
|
191
|
+
|
|
192
|
+
button.remove();
|
|
193
|
+
|
|
194
|
+
await vi.waitFor(() => {
|
|
195
|
+
expect(document.body.querySelector(".persona-control-tooltip")).toBeNull();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("re-attaching replaces old listeners and disabled tooltips stay closed", () => {
|
|
200
|
+
const button = document.createElement("button");
|
|
201
|
+
document.body.appendChild(button);
|
|
202
|
+
const old = attachTooltip({ anchor: button, text: "Old" });
|
|
203
|
+
const next = attachTooltip({ anchor: button, text: "New", enabled: false });
|
|
204
|
+
|
|
205
|
+
expect(old.isOpen).toBe(false);
|
|
206
|
+
button.focus();
|
|
207
|
+
expect(next.isOpen).toBe(false);
|
|
208
|
+
expect(document.body.querySelector(".persona-control-tooltip")).toBeNull();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("portals into the anchor's shadow root so widget styles still apply", () => {
|
|
212
|
+
const host = document.createElement("div");
|
|
213
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
214
|
+
const button = document.createElement("button");
|
|
215
|
+
shadow.appendChild(button);
|
|
216
|
+
document.body.appendChild(host);
|
|
217
|
+
|
|
218
|
+
attachTooltip({ anchor: button, text: "Shadow tooltip" });
|
|
219
|
+
button.focus();
|
|
220
|
+
|
|
221
|
+
expect(shadow.querySelector(".persona-control-tooltip")?.textContent).toContain(
|
|
222
|
+
"Shadow tooltip"
|
|
223
|
+
);
|
|
224
|
+
expect(document.body.querySelector(".persona-control-tooltip")).toBeNull();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { PORTALED_OVERLAY_Z_INDEX } from "./constants";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_GAP = 8;
|
|
4
|
+
const DEFAULT_VIEWPORT_PADDING = 8;
|
|
5
|
+
const MIN_ARROW_INSET = 10;
|
|
6
|
+
|
|
7
|
+
export interface TooltipOptions {
|
|
8
|
+
/** The control the tooltip describes and positions against. */
|
|
9
|
+
anchor: HTMLElement;
|
|
10
|
+
/** Hover target. Defaults to `anchor`; wrappers make small icon buttons easier to hit. */
|
|
11
|
+
trigger?: HTMLElement;
|
|
12
|
+
/** Tooltip copy. A callback keeps live aria-label updates in sync. */
|
|
13
|
+
text: string | (() => string);
|
|
14
|
+
/** Whether the visual tooltip may open. The anchor's accessible name is unaffected. */
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
/** Gap between the control and tooltip, in pixels. */
|
|
17
|
+
gap?: number;
|
|
18
|
+
/** Minimum distance from viewport edges, in pixels. */
|
|
19
|
+
viewportPadding?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TooltipHandle {
|
|
23
|
+
readonly isOpen: boolean;
|
|
24
|
+
show(): void;
|
|
25
|
+
hide(): void;
|
|
26
|
+
reposition(): void;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const handles = new WeakMap<HTMLElement, TooltipHandle>();
|
|
31
|
+
|
|
32
|
+
const isShadowRoot = (root: Node): root is ShadowRoot =>
|
|
33
|
+
root.nodeType === Node.DOCUMENT_FRAGMENT_NODE && "host" in root;
|
|
34
|
+
|
|
35
|
+
const tooltipContainer = (anchor: HTMLElement): HTMLElement | ShadowRoot | null => {
|
|
36
|
+
const root = anchor.getRootNode?.();
|
|
37
|
+
if (isShadowRoot(root)) return root;
|
|
38
|
+
return anchor.ownerDocument.body;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const clamp = (value: number, min: number, max: number): number =>
|
|
42
|
+
Math.min(Math.max(value, min), Math.max(min, max));
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Attach a portaled, viewport-aware tooltip to an icon control.
|
|
46
|
+
*
|
|
47
|
+
* The tooltip is created only while visible and is mounted outside the widget
|
|
48
|
+
* panel's clipping containers. Re-attaching to the same anchor replaces the
|
|
49
|
+
* previous behavior, which makes controller.update() safe and idempotent.
|
|
50
|
+
*/
|
|
51
|
+
export function attachTooltip(options: TooltipOptions): TooltipHandle {
|
|
52
|
+
handles.get(options.anchor)?.destroy();
|
|
53
|
+
|
|
54
|
+
const {
|
|
55
|
+
anchor,
|
|
56
|
+
trigger = anchor,
|
|
57
|
+
text,
|
|
58
|
+
enabled = true,
|
|
59
|
+
gap = DEFAULT_GAP,
|
|
60
|
+
viewportPadding = DEFAULT_VIEWPORT_PADDING,
|
|
61
|
+
} = options;
|
|
62
|
+
const currentDocument = (): Document => anchor.ownerDocument;
|
|
63
|
+
const currentWindow = (): Window =>
|
|
64
|
+
currentDocument().defaultView ?? window;
|
|
65
|
+
|
|
66
|
+
let tooltip: HTMLElement | null = null;
|
|
67
|
+
let label: HTMLElement | null = null;
|
|
68
|
+
let detachOpenListeners: (() => void) | null = null;
|
|
69
|
+
let hovered = false;
|
|
70
|
+
let focused = false;
|
|
71
|
+
let destroyed = false;
|
|
72
|
+
|
|
73
|
+
const resolvedText = (): string =>
|
|
74
|
+
(typeof text === "function" ? text() : text).trim();
|
|
75
|
+
|
|
76
|
+
const hide = (): void => {
|
|
77
|
+
detachOpenListeners?.();
|
|
78
|
+
detachOpenListeners = null;
|
|
79
|
+
tooltip?.remove();
|
|
80
|
+
tooltip = null;
|
|
81
|
+
label = null;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const reposition = (): void => {
|
|
85
|
+
if (!tooltip) return;
|
|
86
|
+
|
|
87
|
+
const ownerDocument = currentDocument();
|
|
88
|
+
const ownerWindow = currentWindow();
|
|
89
|
+
const anchorRect = anchor.getBoundingClientRect();
|
|
90
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
91
|
+
const viewportWidth =
|
|
92
|
+
ownerDocument.documentElement.clientWidth || ownerWindow.innerWidth;
|
|
93
|
+
const viewportHeight =
|
|
94
|
+
ownerDocument.documentElement.clientHeight || ownerWindow.innerHeight;
|
|
95
|
+
|
|
96
|
+
const maxLeft = viewportWidth - viewportPadding - tooltipRect.width;
|
|
97
|
+
const desiredLeft =
|
|
98
|
+
anchorRect.left + anchorRect.width / 2 - tooltipRect.width / 2;
|
|
99
|
+
const left = clamp(desiredLeft, viewportPadding, maxLeft);
|
|
100
|
+
|
|
101
|
+
const roomAbove = anchorRect.top - viewportPadding;
|
|
102
|
+
const roomBelow = viewportHeight - viewportPadding - anchorRect.bottom;
|
|
103
|
+
const requiredHeight = tooltipRect.height + gap;
|
|
104
|
+
const placement =
|
|
105
|
+
roomAbove >= requiredHeight || roomAbove >= roomBelow ? "top" : "bottom";
|
|
106
|
+
const desiredTop =
|
|
107
|
+
placement === "top"
|
|
108
|
+
? anchorRect.top - gap - tooltipRect.height
|
|
109
|
+
: anchorRect.bottom + gap;
|
|
110
|
+
const maxTop = viewportHeight - viewportPadding - tooltipRect.height;
|
|
111
|
+
const top = clamp(desiredTop, viewportPadding, maxTop);
|
|
112
|
+
|
|
113
|
+
const anchorCenter = anchorRect.left + anchorRect.width / 2;
|
|
114
|
+
const arrowMax = Math.max(MIN_ARROW_INSET, tooltipRect.width - MIN_ARROW_INSET);
|
|
115
|
+
const arrowX = clamp(
|
|
116
|
+
anchorCenter - left,
|
|
117
|
+
MIN_ARROW_INSET,
|
|
118
|
+
arrowMax
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
tooltip.dataset.placement = placement;
|
|
122
|
+
tooltip.style.left = `${left}px`;
|
|
123
|
+
tooltip.style.top = `${top}px`;
|
|
124
|
+
tooltip.style.setProperty("--persona-tooltip-arrow-x", `${arrowX}px`);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const show = (): void => {
|
|
128
|
+
if (destroyed || !enabled || tooltip || !anchor.isConnected) return;
|
|
129
|
+
const copy = resolvedText();
|
|
130
|
+
const container = tooltipContainer(anchor);
|
|
131
|
+
if (!copy || !container) return;
|
|
132
|
+
|
|
133
|
+
const ownerDocument = currentDocument();
|
|
134
|
+
const ownerWindow = currentWindow();
|
|
135
|
+
tooltip = ownerDocument.createElement("div");
|
|
136
|
+
tooltip.className = "persona-control-tooltip";
|
|
137
|
+
tooltip.setAttribute("role", "tooltip");
|
|
138
|
+
tooltip.dataset.state = "measuring";
|
|
139
|
+
tooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
|
|
140
|
+
|
|
141
|
+
label = ownerDocument.createElement("span");
|
|
142
|
+
label.className = "persona-control-tooltip__label";
|
|
143
|
+
label.textContent = copy;
|
|
144
|
+
tooltip.appendChild(label);
|
|
145
|
+
|
|
146
|
+
const arrow = ownerDocument.createElement("span");
|
|
147
|
+
arrow.className = "persona-control-tooltip__arrow";
|
|
148
|
+
tooltip.appendChild(arrow);
|
|
149
|
+
|
|
150
|
+
container.appendChild(tooltip);
|
|
151
|
+
reposition();
|
|
152
|
+
tooltip.dataset.state = "open";
|
|
153
|
+
|
|
154
|
+
const onReposition = (): void => {
|
|
155
|
+
if (!anchor.isConnected) {
|
|
156
|
+
hide();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (label) label.textContent = resolvedText();
|
|
160
|
+
reposition();
|
|
161
|
+
};
|
|
162
|
+
ownerWindow.addEventListener("scroll", onReposition, true);
|
|
163
|
+
ownerWindow.addEventListener("resize", onReposition);
|
|
164
|
+
|
|
165
|
+
const root = anchor.getRootNode();
|
|
166
|
+
const mutationTarget =
|
|
167
|
+
isShadowRoot(root)
|
|
168
|
+
? root
|
|
169
|
+
: ownerDocument.documentElement;
|
|
170
|
+
const observer =
|
|
171
|
+
typeof MutationObserver === "undefined"
|
|
172
|
+
? null
|
|
173
|
+
: new MutationObserver(() => {
|
|
174
|
+
if (!anchor.isConnected) hide();
|
|
175
|
+
});
|
|
176
|
+
observer?.observe(mutationTarget, { childList: true, subtree: true });
|
|
177
|
+
|
|
178
|
+
detachOpenListeners = () => {
|
|
179
|
+
ownerWindow.removeEventListener("scroll", onReposition, true);
|
|
180
|
+
ownerWindow.removeEventListener("resize", onReposition);
|
|
181
|
+
observer?.disconnect();
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const syncVisibility = (): void => {
|
|
186
|
+
if (hovered || focused) show();
|
|
187
|
+
else hide();
|
|
188
|
+
};
|
|
189
|
+
const onMouseEnter = (): void => {
|
|
190
|
+
const ownerWindow = currentWindow();
|
|
191
|
+
const hasHover =
|
|
192
|
+
!ownerWindow.matchMedia ||
|
|
193
|
+
!ownerWindow.matchMedia("(hover: none)").matches;
|
|
194
|
+
if (!hasHover) return;
|
|
195
|
+
hovered = true;
|
|
196
|
+
syncVisibility();
|
|
197
|
+
};
|
|
198
|
+
const onMouseLeave = (): void => {
|
|
199
|
+
hovered = false;
|
|
200
|
+
syncVisibility();
|
|
201
|
+
};
|
|
202
|
+
const onFocus = (): void => {
|
|
203
|
+
focused = true;
|
|
204
|
+
syncVisibility();
|
|
205
|
+
};
|
|
206
|
+
const onBlur = (): void => {
|
|
207
|
+
focused = false;
|
|
208
|
+
syncVisibility();
|
|
209
|
+
};
|
|
210
|
+
const onKeyDown = (event: KeyboardEvent): void => {
|
|
211
|
+
if (event.key !== "Escape") return;
|
|
212
|
+
hovered = false;
|
|
213
|
+
focused = false;
|
|
214
|
+
hide();
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
trigger.addEventListener("mouseenter", onMouseEnter);
|
|
218
|
+
trigger.addEventListener("mouseleave", onMouseLeave);
|
|
219
|
+
anchor.addEventListener("focus", onFocus);
|
|
220
|
+
anchor.addEventListener("blur", onBlur);
|
|
221
|
+
anchor.addEventListener("keydown", onKeyDown);
|
|
222
|
+
|
|
223
|
+
const handle: TooltipHandle = {
|
|
224
|
+
get isOpen() {
|
|
225
|
+
return tooltip !== null;
|
|
226
|
+
},
|
|
227
|
+
show,
|
|
228
|
+
hide,
|
|
229
|
+
reposition,
|
|
230
|
+
destroy: () => {
|
|
231
|
+
if (destroyed) return;
|
|
232
|
+
destroyed = true;
|
|
233
|
+
hide();
|
|
234
|
+
trigger.removeEventListener("mouseenter", onMouseEnter);
|
|
235
|
+
trigger.removeEventListener("mouseleave", onMouseLeave);
|
|
236
|
+
anchor.removeEventListener("focus", onFocus);
|
|
237
|
+
anchor.removeEventListener("blur", onBlur);
|
|
238
|
+
anchor.removeEventListener("keydown", onKeyDown);
|
|
239
|
+
handles.delete(anchor);
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
handles.set(anchor, handle);
|
|
244
|
+
return handle;
|
|
245
|
+
}
|
package/dist/chunk-IPVK3KOM.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
var zt=(e,t)=>{let n=document.createElement(e);return t&&(n.className=t),n},Bt=(e,t,n)=>{let o=e.createElement(t);return n&&(o.className=n),o},Ft=()=>document.createDocumentFragment(),Kt=(e,t={},...n)=>{let o=document.createElement(e);if(t.className&&(o.className=t.className),t.text!==void 0&&(o.textContent=t.text),t.attrs)for(let[s,a]of Object.entries(t.attrs))o.setAttribute(s,a);if(t.style){let s=o.style,a=t.style;for(let c of Object.keys(a)){let l=a[c];l!=null&&(s[c]=l)}}let r=n.filter(s=>s!=null);return r.length>0&&o.append(...r),o},Wt=(...e)=>e.filter(Boolean).join(" ");import{Activity as A,ArrowDown as k,ArrowUp as P,ArrowUpRight as L,Bot as I,ChevronDown as R,ChevronUp as D,ChevronRight as H,ChevronLeft as O,Check as z,Clipboard as B,ClipboardCopy as F,CodeXml as K,Copy as W,File as U,FileCode as j,FileSpreadsheet as $,FileText as q,ImagePlus as V,Loader as X,LoaderCircle as G,Mic as _,Paperclip as Y,RefreshCw as Z,Search as J,Send as Q,ShieldAlert as ee,ShieldCheck as te,ShieldX as ne,Square as oe,ThumbsDown as re,ThumbsUp as ie,Upload as se,Volume2 as ae,X as le,User as ce,Mail as de,Phone as ue,Calendar as me,Clock as pe,Building as ge,MapPin as he,Lock as fe,Key as we,CreditCard as ye,AtSign as be,Hash as ve,Globe as xe,Link as Ce,CircleCheck as Se,CircleX as Ee,TriangleAlert as Te,Info as Me,Ban as Ne,Shield as Ae,ArrowLeft as ke,ArrowRight as Pe,ExternalLink as Le,Ellipsis as Ie,EllipsisVertical as Re,Menu as De,House as He,Plus as Oe,Minus as ze,Pencil as Be,Trash as Fe,Trash2 as Ke,Save as We,Download as Ue,Share as je,Funnel as $e,Settings as qe,RotateCw as Ve,Maximize as Xe,Minimize as Ge,ShoppingCart as _e,ShoppingBag as Ye,Package as Ze,Truck as Je,Tag as Qe,Gift as et,Receipt as tt,Wallet as nt,Store as ot,DollarSign as rt,Percent as it,Play as st,Pause as at,VolumeX as lt,Camera as ct,Image as dt,Film as ut,Headphones as mt,MessageCircle as pt,MessageSquare as gt,Bell as ht,Heart as ft,Star as wt,Eye as yt,EyeOff as bt,Bookmark as vt,CalendarDays as xt,History as Ct,Timer as St,Folder as Et,FolderOpen as Tt,Files as Mt,Sparkles as Nt,Zap as At,Sun as kt,Moon as Pt,Flag as Lt,Monitor as It,Smartphone as Rt}from"lucide";var Dt={activity:A,"arrow-down":k,"arrow-up":P,"arrow-up-right":L,bot:I,"chevron-down":R,"chevron-up":D,"chevron-right":H,"chevron-left":O,check:z,clipboard:B,"clipboard-copy":F,"code-xml":K,copy:W,file:U,"file-code":j,"file-spreadsheet":$,"file-text":q,"image-plus":V,loader:X,"loader-circle":G,mic:_,paperclip:Y,"refresh-cw":Z,search:J,send:Q,"shield-alert":ee,"shield-check":te,"shield-x":ne,square:oe,"thumbs-down":re,"thumbs-up":ie,upload:se,"volume-2":ae,x:le,user:ce,mail:de,phone:ue,calendar:me,clock:pe,building:ge,"map-pin":he,lock:fe,key:we,"credit-card":ye,"at-sign":be,hash:ve,globe:xe,link:Ce,"circle-check":Se,"circle-x":Ee,"triangle-alert":Te,info:Me,ban:Ne,shield:Ae,"arrow-left":ke,"arrow-right":Pe,"external-link":Le,ellipsis:Ie,"ellipsis-vertical":Re,menu:De,house:He,plus:Oe,minus:ze,pencil:Be,trash:Fe,"trash-2":Ke,save:We,download:Ue,share:je,funnel:$e,settings:qe,"rotate-cw":Ve,maximize:Xe,minimize:Ge,"shopping-cart":_e,"shopping-bag":Ye,package:Ze,truck:Je,tag:Qe,gift:et,receipt:tt,wallet:nt,store:ot,"dollar-sign":rt,percent:it,play:st,pause:at,"volume-x":lt,camera:ct,image:dt,film:ut,headphones:mt,"message-circle":pt,"message-square":gt,bell:ht,heart:ft,star:wt,eye:yt,"eye-off":bt,bookmark:vt,"calendar-days":xt,history:Ct,timer:St,folder:Et,"folder-open":Tt,files:Mt,sparkles:Nt,zap:At,sun:kt,moon:Pt,flag:Lt,monitor:It,smartphone:Rt},$t=(e,t=24,n="currentColor",o=2)=>{let r=Dt[e];return r?Ht(r,t,n,o):(console.warn(`Lucide icon "${e}" is not in the Persona registry. Add it to packages/widget/src/utils/icons.ts (see docs/icon-registry-shortlist.md).`),null)};function Ht(e,t,n,o){if(!Array.isArray(e))return null;let r=document.createElementNS("http://www.w3.org/2000/svg","svg");return r.setAttribute("width",String(t)),r.setAttribute("height",String(t)),r.setAttribute("viewBox","0 0 24 24"),r.setAttribute("fill","none"),r.setAttribute("stroke",n),r.setAttribute("stroke-width",String(o)),r.setAttribute("stroke-linecap","round"),r.setAttribute("stroke-linejoin","round"),r.setAttribute("aria-hidden","true"),e.forEach(s=>{if(!Array.isArray(s)||s.length<2)return;let a=s[0],c=s[1];if(!c)return;let l=document.createElementNS("http://www.w3.org/2000/svg",a);Object.entries(c).forEach(([h,f])=>{h!=="stroke"&&l.setAttribute(h,String(f))}),r.appendChild(l)}),r}function Vt(e){let t={trigger:e.trigger??"@",position:e.triggerPosition??"anywhere",allowSpaces:!1,sources:Array.isArray(e.sources)?e.sources:[],searchPlaceholder:e.searchPlaceholder,showButton:e.showButton!==!1,buttonIconName:e.buttonIconName,buttonTooltipText:e.buttonTooltipText},n=(e.triggers??[]).map(o=>({trigger:o.trigger,position:o.triggerPosition??"anywhere",allowSpaces:o.allowSpaces??!1,sources:Array.isArray(o.sources)?o.sources:[],searchPlaceholder:o.searchPlaceholder,showButton:o.showButton===!0,buttonIconName:o.buttonIconName,buttonTooltipText:o.buttonTooltipText}));return[t,...n]}function T(e){let t=e.getRootNode?.();return t instanceof ShadowRoot||t instanceof Document?t:e.ownerDocument??document}function b(e,t,n){let o=e instanceof Document?e.head:e,r=t.replace(/["\\]/g,"\\$&");if(o.querySelector(`style[data-persona-plugin-style="${r}"]`))return;let a=(e instanceof Document?e:e.ownerDocument??document).createElement("style");a.setAttribute("data-persona-plugin-style",t),a.textContent=n,o.appendChild(a)}function Gt(e,t,n){if(e instanceof Document||e instanceof ShadowRoot){b(e,t,n);return}let o=e;if(o.isConnected){b(T(o),t,n);return}let r=o.ownerDocument??document;b(r,t,n),queueMicrotask(()=>{let s=T(o);s!==r&&b(s,t,n)})}function Ot(e){let t=e.getRootNode?.();return t instanceof ShadowRoot?t:(e.ownerDocument??document).body}function _t(e){let{anchor:t,content:n,placement:o="bottom-start",offset:r=6,matchAnchorWidth:s=!1,horizontalOffset:a,verticalOffset:c,zIndex:l=2147483e3,onOpen:h,onDismiss:f}=e,M=e.container??Ot(t),d=!1,w=null,v=()=>{if(!d)return;let i=t.getBoundingClientRect();n.style.position="fixed",s&&(n.style.minWidth=`${i.width}px`),a&&(n.style.maxWidth=`${i.width}px`);let m=n.getBoundingClientRect(),u=c?.()??null,y=u!=null?i.top+u:i.top,x=o==="top-start"||o==="top-end"?y-r-m.height:i.bottom+r,g=o==="bottom-end"||o==="top-end"?i.right-m.width:i.left,E=a?.()??null;if(E!=null){let N=Math.max(i.left,i.right-m.width);g=Math.min(Math.max(i.left+E,i.left),N)}n.style.top=`${x}px`,n.style.left=`${g}px`},p=()=>{d&&(d=!1,w&&(w(),w=null),n.remove())},S=()=>{if(d)return;d=!0,l!=null&&(n.style.zIndex=String(l)),M.appendChild(n),v();let i=(t.ownerDocument??document).defaultView??window,m=t.ownerDocument??document,u=()=>{if(!t.isConnected){p(),f?.("anchor-removed");return}v()},y=C=>{let g=typeof C.composedPath=="function"?C.composedPath():[];g.includes(n)||g.includes(t)||(p(),f?.("outside"))},x=i.setTimeout(()=>{m.addEventListener("pointerdown",y,!0)},0);i.addEventListener("scroll",u,!0),i.addEventListener("resize",u),w=()=>{i.clearTimeout(x),m.removeEventListener("pointerdown",y,!0),i.removeEventListener("scroll",u,!0),i.removeEventListener("resize",u)},h?.()};return{get isOpen(){return d},open:S,close:p,toggle:()=>d?p():S(),reposition:v,destroy:p}}function Yt(e){return(typeof e.composedPath=="function"?e.composedPath():[]).some(n=>n instanceof HTMLElement&&(n.tagName==="INPUT"||n.tagName==="TEXTAREA"||n.isContentEditable))}export{zt as a,Bt as b,Ft as c,Kt as d,Wt as e,$t as f,Vt as g,Gt as h,_t as i,Yt as j};
|