@runtypelabs/persona 4.17.1 → 4.18.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/chunk-3I2S3I5P.js +1 -0
- package/dist/chunk-76IELU7F.js +1 -0
- package/dist/chunk-EFJBUH5F.js +1 -0
- package/dist/chunk-PDPVORUF.js +1 -0
- package/dist/chunk-Q735PSZL.js +1 -0
- package/dist/chunk-VK6JOIYJ.js +1 -0
- package/dist/codegen.cjs +4 -4
- package/dist/codegen.js +4 -4
- package/dist/{context-mentions-BTSTIBFC.js → context-mentions-MJL6S6P2.js} +1 -1
- package/dist/context-mentions.cjs +9 -8
- package/dist/context-mentions.d.cts +1212 -14
- package/dist/context-mentions.d.ts +1212 -14
- package/dist/context-mentions.js +9 -8
- package/dist/event-stream-view-4EZ7BGFK.js +1 -0
- package/dist/event-stream-view.cjs +129 -0
- package/dist/event-stream-view.d.cts +8384 -0
- package/dist/event-stream-view.d.ts +8384 -0
- package/dist/event-stream-view.js +129 -0
- package/dist/history-view-BD6OXLNA.js +898 -0
- package/dist/history-view.cjs +898 -0
- package/dist/history-view.d.cts +1461 -0
- package/dist/history-view.d.ts +1461 -0
- package/dist/history-view.js +898 -0
- package/dist/index.cjs +77 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1987 -17
- package/dist/index.d.ts +1987 -17
- package/dist/index.global.js +67 -66
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +77 -77
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +5 -4
- package/dist/launcher.global.js.map +1 -1
- package/dist/plugin-kit.cjs +1 -1
- package/dist/plugin-kit.d.cts +0 -8
- package/dist/plugin-kit.d.ts +0 -8
- package/dist/plugin-kit.js +1 -1
- package/dist/smart-dom-reader.d.cts +1212 -14
- package/dist/smart-dom-reader.d.ts +1212 -14
- package/dist/theme-editor-preview.cjs +970 -73
- package/dist/theme-editor-preview.d.cts +1331 -15
- package/dist/theme-editor-preview.d.ts +1331 -15
- package/dist/theme-editor-preview.js +81 -81
- package/dist/theme-editor.cjs +7 -7
- package/dist/theme-editor.d.cts +1215 -15
- package/dist/theme-editor.d.ts +1215 -15
- package/dist/theme-editor.js +9 -9
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +226 -7
- package/dist/theme-reference.d.ts +226 -7
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +16 -4
- package/src/client-history-rest.test.ts +1034 -0
- package/src/client-visitor-history.test.ts +596 -0
- package/src/client.ts +1040 -24
- package/src/components/artifact-inline.ts +8 -1
- package/src/components/artifact-pane.test.ts +278 -1
- package/src/components/artifact-pane.ts +135 -67
- package/src/components/artifact-preview.test.ts +14 -1
- package/src/components/artifact-preview.ts +9 -0
- package/src/components/header-builder.ts +3 -0
- package/src/components/header-layouts.test.ts +171 -7
- package/src/components/header-layouts.ts +83 -53
- package/src/components/header-parts.test.ts +154 -1
- package/src/components/header-parts.ts +266 -138
- package/src/components/history-confirm-dialog.ts +177 -0
- package/src/components/history-shell-copy.ts +99 -0
- package/src/components/history-view/announcer.ts +40 -0
- package/src/components/history-view/copy.ts +105 -0
- package/src/components/history-view/css.ts +1078 -0
- package/src/components/history-view/grouping.test.ts +142 -0
- package/src/components/history-view/grouping.ts +142 -0
- package/src/components/history-view/icons.ts +73 -0
- package/src/components/history-view/rows.ts +341 -0
- package/src/components/history-view/state.ts +309 -0
- package/src/components/history-view.test.ts +2660 -0
- package/src/components/history-view.ts +1891 -0
- package/src/components/launcher-surface.test.ts +133 -0
- package/src/components/launcher.test.ts +18 -0
- package/src/components/launcher.ts +7 -7
- package/src/components/panel.ts +17 -7
- package/src/components/welcome.ts +72 -2
- package/src/defaults.ts +36 -3
- package/src/event-stream-view-bundle.test.ts +69 -0
- package/src/event-stream-view-entry.ts +19 -0
- package/src/event-stream-view-loader.ts +31 -0
- package/src/event-stream-view.ts +19 -0
- package/src/generated/runtype-openapi-contract.ts +1 -0
- package/src/history-render.ts +325 -0
- package/src/history-view-bundle.test.ts +78 -0
- package/src/history-view-entry.ts +41 -0
- package/src/history-view-loader.test.ts +30 -0
- package/src/history-view-loader.ts +32 -0
- package/src/history-view.ts +28 -0
- package/src/index-core.ts +50 -2
- package/src/index-global.ts +55 -0
- package/src/internal/demo-history-provider.test.ts +402 -0
- package/src/internal/demo-history-provider.ts +689 -0
- package/src/internal/history-provider-registry.ts +25 -0
- package/src/internal/history-provider.ts +150 -0
- package/src/internal/runtype-history-provider.test.ts +313 -0
- package/src/internal/runtype-history-provider.ts +240 -0
- package/src/plugin-kit.test.ts +16 -0
- package/src/plugin-kit.ts +13 -5
- package/src/plugins/types.ts +177 -1
- package/src/session-history.test.ts +1397 -0
- package/src/session.ts +1200 -26
- package/src/styles/widget-styles.test.ts +133 -0
- package/src/styles/widget.css +227 -8
- package/src/theme-editor/preview-utils.test.ts +19 -0
- package/src/theme-editor/preview-utils.ts +15 -1
- package/src/theme-editor/preview.ts +7 -1
- package/src/theme-editor/role-mappings.ts +4 -1
- package/src/theme-editor/sections.test.ts +168 -1
- package/src/theme-editor/sections.ts +136 -7
- package/src/theme-editor/webmcp/summary.ts +16 -2
- package/src/theme-reference.ts +20 -6
- package/src/types/theme.ts +213 -7
- package/src/types.ts +927 -7
- package/src/ui.artifact-rail-resize.test.ts +235 -0
- package/src/ui.attachments-drop.test.ts +1 -0
- package/src/ui.header-update-stability.test.ts +2 -2
- package/src/ui.history-chunk.test.ts +72 -0
- package/src/ui.history-config-provider.test.ts +318 -0
- package/src/ui.history-controller.test.ts +711 -0
- package/src/ui.history-lifecycle.test.ts +456 -0
- package/src/ui.history-render-hooks.test.ts +904 -0
- package/src/ui.history-shell.test.ts +1887 -0
- package/src/ui.rail-overlay.test.ts +573 -0
- package/src/ui.rail-resize.test.ts +284 -0
- package/src/ui.rebuild-composer.test.ts +21 -0
- package/src/ui.render-welcome.test.ts +42 -0
- package/src/ui.shortcuts.test.ts +166 -0
- package/src/ui.ts +3071 -240
- package/src/utils/__fixtures__/client-conversation-history-wire.json +108 -0
- package/src/utils/artifact-file.ts +15 -0
- package/src/utils/artifact-gate.test.ts +4 -0
- package/src/utils/artifact-gate.ts +13 -17
- package/src/utils/artifact-resize.test.ts +28 -0
- package/src/utils/artifact-resize.ts +9 -2
- package/src/utils/buttons.ts +188 -1
- package/src/utils/config-merge.test.ts +91 -0
- package/src/utils/dropdown.ts +10 -1
- package/src/utils/history-messages.test.ts +284 -0
- package/src/utils/history-messages.ts +284 -0
- package/src/utils/icons.ts +3 -0
- package/src/utils/shortcuts.test.ts +288 -0
- package/src/utils/shortcuts.ts +221 -0
- package/src/utils/theme.test.ts +501 -4
- package/src/utils/theme.ts +42 -3
- package/src/utils/tokens.ts +153 -15
- package/src/utils/tooltip-timing.ts +4 -0
- package/src/utils/tooltip.test.ts +249 -7
- package/src/utils/tooltip.ts +146 -4
- package/src/utils/visitor-store.test.ts +425 -0
- package/src/utils/visitor-store.ts +337 -0
- package/dist/chunk-QJ4LV2KW.js +0 -1
|
@@ -206,6 +206,12 @@ export type ArtifactPreviewBodyHandle = {
|
|
|
206
206
|
el: HTMLElement;
|
|
207
207
|
/** Apply streaming deltas / status changes (may be a different record). */
|
|
208
208
|
update(record: PersonaArtifactRecord): void;
|
|
209
|
+
/**
|
|
210
|
+
* Release the file-preview loading state machine: pending overlay/settle
|
|
211
|
+
* timers and the window `message` listener, which otherwise outlive a
|
|
212
|
+
* discarded preview. Idempotent; call whenever the handle is dropped.
|
|
213
|
+
*/
|
|
214
|
+
destroy(): void;
|
|
209
215
|
};
|
|
210
216
|
|
|
211
217
|
const PRE_CLASS =
|
|
@@ -990,5 +996,8 @@ export function renderArtifactPreviewBody(
|
|
|
990
996
|
update(rec: PersonaArtifactRecord) {
|
|
991
997
|
render(rec);
|
|
992
998
|
},
|
|
999
|
+
destroy() {
|
|
1000
|
+
resetFilePreview();
|
|
1001
|
+
},
|
|
993
1002
|
};
|
|
994
1003
|
}
|
|
@@ -11,6 +11,8 @@ export const HEADER_THEME_CSS = {
|
|
|
11
11
|
"var(--persona-header-subtitle-fg, var(--persona-text-muted, var(--persona-muted, #9ca3af)))",
|
|
12
12
|
actionIconColor:
|
|
13
13
|
"var(--persona-header-action-icon-fg, var(--persona-muted, #9ca3af))",
|
|
14
|
+
/** One knob for both header builders; the Messages rail header shares it. */
|
|
15
|
+
minHeight: "var(--persona-header-min-height, auto)",
|
|
14
16
|
} as const;
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -65,6 +67,7 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
|
|
|
65
67
|
"persona-widget-header persona-flex persona-items-center persona-gap-3 persona-px-6 persona-py-5",
|
|
66
68
|
attrs: { "data-persona-theme-zone": "header" },
|
|
67
69
|
style: {
|
|
70
|
+
minHeight: HEADER_THEME_CSS.minHeight,
|
|
68
71
|
backgroundColor: "var(--persona-header-bg, var(--persona-surface, #ffffff))",
|
|
69
72
|
borderBottomColor: "var(--persona-header-border, var(--persona-divider, #f1f5f9))",
|
|
70
73
|
boxShadow: "var(--persona-header-shadow, none)",
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { describe, expect, it, vi } from "vitest";
|
|
4
4
|
|
|
5
|
-
import { buildMinimalHeader } from "./header-layouts";
|
|
5
|
+
import { buildHeaderWithLayout, buildMinimalHeader } from "./header-layouts";
|
|
6
6
|
import { HEADER_THEME_CSS } from "./header-builder";
|
|
7
|
+
import type { AgentWidgetConfig } from "../types";
|
|
8
|
+
import { setMacPlatformOverride } from "../utils/shortcuts";
|
|
7
9
|
|
|
8
10
|
const build = (layoutHeaderConfig: Parameters<typeof buildMinimalHeader>[0]["layoutHeaderConfig"]) =>
|
|
9
11
|
buildMinimalHeader({
|
|
@@ -27,21 +29,105 @@ describe("minimal header trailing actions", () => {
|
|
|
27
29
|
);
|
|
28
30
|
expect(button).not.toBeNull();
|
|
29
31
|
|
|
30
|
-
// Trailing cluster, not the title row: the
|
|
31
|
-
// the close button wrapper and does not precede the title.
|
|
32
|
-
expect(button!.parentElement).toBe(
|
|
32
|
+
// Trailing cluster, not the title row: the action's wrapper shares a
|
|
33
|
+
// parent with the close button wrapper and does not precede the title.
|
|
34
|
+
expect(button!.parentElement!.parentElement).toBe(
|
|
35
|
+
elements.closeButtonWrapper.parentElement
|
|
36
|
+
);
|
|
33
37
|
expect(elements.headerTitle.parentElement!.contains(button!)).toBe(false);
|
|
34
38
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
expect(button!.
|
|
39
|
+
// Shared header control chrome: token-sized box, no inline size, header
|
|
40
|
+
// action-icon color.
|
|
41
|
+
expect(button!.classList.contains("persona-header-control")).toBe(true);
|
|
42
|
+
expect(button!.classList.contains("persona-header-control--glyph")).toBe(true);
|
|
43
|
+
expect(button!.style.width).toBe("");
|
|
44
|
+
expect(button!.style.height).toBe("");
|
|
38
45
|
expect(button!.style.color).toBe(HEADER_THEME_CSS.actionIconColor);
|
|
39
46
|
expect(button!.className).toContain("persona-rounded-full");
|
|
47
|
+
// Flex wrapper: an inline-flex button in a block wrapper reserves baseline
|
|
48
|
+
// slack and rides high.
|
|
49
|
+
expect(button!.parentElement!.className).toContain("persona-inline-flex");
|
|
40
50
|
|
|
41
51
|
button!.click();
|
|
42
52
|
expect(onAction).toHaveBeenCalledWith("home");
|
|
43
53
|
});
|
|
44
54
|
|
|
55
|
+
it("stamps a declared shortcut on the button and hints it in the tooltip", () => {
|
|
56
|
+
setMacPlatformOverride(false);
|
|
57
|
+
const elements = build({
|
|
58
|
+
layout: "minimal",
|
|
59
|
+
trailingActions: [
|
|
60
|
+
{
|
|
61
|
+
id: "home",
|
|
62
|
+
icon: "house",
|
|
63
|
+
ariaLabel: "Back to home",
|
|
64
|
+
shortcut: "mod+shift+h",
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
const button = elements.header.querySelector<HTMLButtonElement>(
|
|
69
|
+
'button[aria-label="Back to home"]'
|
|
70
|
+
)!;
|
|
71
|
+
// The shell finds the button by this id to run the binding.
|
|
72
|
+
expect(button.getAttribute("data-persona-header-action")).toBe("home");
|
|
73
|
+
expect(button.getAttribute("aria-keyshortcuts")).toBe("Control+Shift+H");
|
|
74
|
+
|
|
75
|
+
document.body.appendChild(elements.header);
|
|
76
|
+
button.parentElement!.dispatchEvent(new MouseEvent("mouseenter"));
|
|
77
|
+
expect(
|
|
78
|
+
document.querySelector(".persona-control-tooltip__hint")!.textContent
|
|
79
|
+
).toBe("Ctrl+Shift+H");
|
|
80
|
+
document.body.innerHTML = "";
|
|
81
|
+
setMacPlatformOverride(null);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("leaves aria-keyshortcuts off an action with no shortcut", () => {
|
|
85
|
+
const elements = build({
|
|
86
|
+
layout: "minimal",
|
|
87
|
+
trailingActions: [{ id: "home", icon: "house", ariaLabel: "Back to home" }],
|
|
88
|
+
});
|
|
89
|
+
const button = elements.header.querySelector<HTMLButtonElement>(
|
|
90
|
+
'button[aria-label="Back to home"]'
|
|
91
|
+
)!;
|
|
92
|
+
expect(button.hasAttribute("aria-keyshortcuts")).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("keeps the close button transparent so the UA button fill never shows", () => {
|
|
96
|
+
const elements = build({ layout: "minimal" });
|
|
97
|
+
expect(elements.closeButton.className).toContain("persona-bg-transparent");
|
|
98
|
+
expect(elements.closeButton.className).toContain("hover:persona-bg-gray-100");
|
|
99
|
+
expect(elements.closeButton.style.backgroundColor).toBe("");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("shares the header control chrome and a flex wrapper with the default layout", () => {
|
|
103
|
+
const elements = build({ layout: "minimal" });
|
|
104
|
+
expect(elements.closeButton.classList.contains("persona-header-control")).toBe(true);
|
|
105
|
+
// Sparse X glyph: the stylesheet scales it up from the icon-size token.
|
|
106
|
+
expect(
|
|
107
|
+
elements.closeButton.classList.contains("persona-header-control--sparse")
|
|
108
|
+
).toBe(true);
|
|
109
|
+
expect(elements.closeButton.style.width).toBe("");
|
|
110
|
+
// The centering bug: a block wrapper reserves inline baseline slack, so
|
|
111
|
+
// the button rides high inside it.
|
|
112
|
+
expect(elements.closeButtonWrapper.className).toContain("persona-inline-flex");
|
|
113
|
+
expect(elements.closeButtonWrapper.className).toContain("persona-items-center");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("applies closeButtonBackgroundColor instead of the transparent default", () => {
|
|
117
|
+
const elements = buildMinimalHeader({
|
|
118
|
+
config: {
|
|
119
|
+
apiUrl: "https://example.com/api",
|
|
120
|
+
launcher: { closeButtonBackgroundColor: "red" },
|
|
121
|
+
},
|
|
122
|
+
showClose: true,
|
|
123
|
+
onClose: () => {},
|
|
124
|
+
layoutHeaderConfig: { layout: "minimal" },
|
|
125
|
+
});
|
|
126
|
+
expect(elements.closeButton.style.backgroundColor).toBe("red");
|
|
127
|
+
expect(elements.closeButton.className).not.toContain("persona-bg-transparent");
|
|
128
|
+
expect(elements.closeButton.className).not.toContain("hover:persona-bg-gray-100");
|
|
129
|
+
});
|
|
130
|
+
|
|
45
131
|
it("still ignores trailingActions when titleMenu is configured", () => {
|
|
46
132
|
const elements = build({
|
|
47
133
|
layout: "minimal",
|
|
@@ -54,3 +140,81 @@ describe("minimal header trailing actions", () => {
|
|
|
54
140
|
).toBeNull();
|
|
55
141
|
});
|
|
56
142
|
});
|
|
143
|
+
|
|
144
|
+
describe("minimal header clear chat", () => {
|
|
145
|
+
it("renders clear chat in the cluster between trailing actions and close", () => {
|
|
146
|
+
const elements = build({
|
|
147
|
+
layout: "minimal",
|
|
148
|
+
trailingActions: [{ id: "home", icon: "house", ariaLabel: "Back to home" }],
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const clear = elements.clearChatButton;
|
|
152
|
+
expect(clear).not.toBeNull();
|
|
153
|
+
expect(clear!.classList.contains("persona-header-control")).toBe(true);
|
|
154
|
+
expect(elements.clearChatButtonWrapper!.parentElement).toBe(
|
|
155
|
+
elements.closeButtonWrapper.parentElement
|
|
156
|
+
);
|
|
157
|
+
const action = elements.header.querySelector('button[aria-label="Back to home"]')!;
|
|
158
|
+
expect(
|
|
159
|
+
action.compareDocumentPosition(clear!) & Node.DOCUMENT_POSITION_FOLLOWING
|
|
160
|
+
).toBeTruthy();
|
|
161
|
+
expect(
|
|
162
|
+
clear!.compareDocumentPosition(elements.closeButton) &
|
|
163
|
+
Node.DOCUMENT_POSITION_FOLLOWING
|
|
164
|
+
).toBeTruthy();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("renders nothing when launcher.clearChat.enabled is false", () => {
|
|
168
|
+
const elements = buildMinimalHeader({
|
|
169
|
+
config: {
|
|
170
|
+
apiUrl: "https://example.com/api",
|
|
171
|
+
launcher: { clearChat: { enabled: false } },
|
|
172
|
+
},
|
|
173
|
+
showClose: true,
|
|
174
|
+
onClose: () => {},
|
|
175
|
+
layoutHeaderConfig: { layout: "minimal" },
|
|
176
|
+
});
|
|
177
|
+
expect(elements.clearChatButton).toBeNull();
|
|
178
|
+
expect(elements.clearChatButtonWrapper).toBeNull();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("hides the wrapper under layout.header.showClearChat false", () => {
|
|
182
|
+
const config = {
|
|
183
|
+
apiUrl: "https://example.com/api",
|
|
184
|
+
layout: { header: { layout: "minimal", showClearChat: false } },
|
|
185
|
+
} as unknown as AgentWidgetConfig;
|
|
186
|
+
const elements = buildHeaderWithLayout(config, config.layout!.header!, {
|
|
187
|
+
config,
|
|
188
|
+
showClose: true,
|
|
189
|
+
onClose: () => {},
|
|
190
|
+
});
|
|
191
|
+
expect(elements.clearChatButtonWrapper).not.toBeNull();
|
|
192
|
+
expect(elements.clearChatButtonWrapper!.style.display).toBe("none");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("keeps a top-right placement out of the header for the container mount", () => {
|
|
196
|
+
const elements = buildMinimalHeader({
|
|
197
|
+
config: {
|
|
198
|
+
apiUrl: "https://example.com/api",
|
|
199
|
+
launcher: { clearChat: { placement: "top-right" } },
|
|
200
|
+
},
|
|
201
|
+
showClose: true,
|
|
202
|
+
onClose: () => {},
|
|
203
|
+
layoutHeaderConfig: { layout: "minimal" },
|
|
204
|
+
});
|
|
205
|
+
expect(elements.clearChatButtonWrapper).not.toBeNull();
|
|
206
|
+
expect(elements.header.contains(elements.clearChatButtonWrapper!)).toBe(false);
|
|
207
|
+
expect(elements.clearChatButtonWrapper!.style.right).toBe("48px");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("still renders clear chat and close when titleMenu is configured", () => {
|
|
211
|
+
const elements = build({
|
|
212
|
+
layout: "minimal",
|
|
213
|
+
titleMenu: { menuItems: [{ id: "a", label: "A" }], onSelect: () => {} },
|
|
214
|
+
});
|
|
215
|
+
expect(elements.clearChatButton).not.toBeNull();
|
|
216
|
+
expect(elements.clearChatButtonWrapper!.parentElement).toBe(
|
|
217
|
+
elements.closeButtonWrapper.parentElement
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createElement } from "../utils/dom";
|
|
2
|
-
import { renderLucideIcon } from "../utils/icons";
|
|
3
2
|
import { createDropdownMenu } from "../utils/dropdown";
|
|
4
3
|
import { createComboButton } from "../utils/buttons";
|
|
4
|
+
import { ariaCombo, formatCombo } from "../utils/shortcuts";
|
|
5
5
|
import {
|
|
6
6
|
AgentWidgetConfig,
|
|
7
7
|
AgentWidgetHeaderLayoutConfig,
|
|
@@ -14,6 +14,11 @@ import {
|
|
|
14
14
|
HeaderElements,
|
|
15
15
|
attachHeaderToContainer as _attachHeaderToContainer,
|
|
16
16
|
} from "./header-builder";
|
|
17
|
+
import {
|
|
18
|
+
createClearChatButton,
|
|
19
|
+
createCloseButton,
|
|
20
|
+
createHeaderIconButton,
|
|
21
|
+
} from "./header-parts";
|
|
17
22
|
|
|
18
23
|
export interface HeaderLayoutContext {
|
|
19
24
|
config: AgentWidgetConfig;
|
|
@@ -71,29 +76,25 @@ function appendTrailingHeaderActions(
|
|
|
71
76
|
): void {
|
|
72
77
|
if (!actions?.length) return;
|
|
73
78
|
for (const a of actions) {
|
|
74
|
-
// Same chrome as the close button beside them
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
btn.textContent = a.label;
|
|
79
|
+
// Same shared chrome as the close button beside them. The wrapper stays
|
|
80
|
+
// relative so a dropdown can anchor to it.
|
|
81
|
+
const { button: btn, wrapper } = createHeaderIconButton({
|
|
82
|
+
ariaLabel: a.ariaLabel ?? a.label ?? a.id,
|
|
83
|
+
iconName: a.icon,
|
|
84
|
+
iconText: a.icon ? undefined : a.label,
|
|
85
|
+
tooltipHint: a.shortcut ? formatCombo(a.shortcut) : undefined,
|
|
86
|
+
wrapperClassName:
|
|
87
|
+
"persona-relative persona-inline-flex persona-items-center persona-justify-center",
|
|
88
|
+
});
|
|
89
|
+
// The keydown binding is registered by the shell, which finds the button
|
|
90
|
+
// by this id; the shortcut's other two artifacts are stamped here.
|
|
91
|
+
btn.setAttribute("data-persona-header-action", a.id);
|
|
92
|
+
if (a.shortcut) {
|
|
93
|
+
const aria = ariaCombo(a.shortcut);
|
|
94
|
+
if (aria) btn.setAttribute("aria-keyshortcuts", aria);
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
if (a.menuItems?.length) {
|
|
94
|
-
// Wrap in a relative container for dropdown positioning
|
|
95
|
-
const wrapper = createElement("div", "persona-relative");
|
|
96
|
-
wrapper.appendChild(btn);
|
|
97
98
|
const dropdown = createDropdownMenu({
|
|
98
99
|
items: a.menuItems,
|
|
99
100
|
onSelect: (itemId) => onAction?.(itemId),
|
|
@@ -107,11 +108,10 @@ function appendTrailingHeaderActions(
|
|
|
107
108
|
e.stopPropagation();
|
|
108
109
|
dropdown.toggle();
|
|
109
110
|
});
|
|
110
|
-
container.appendChild(wrapper);
|
|
111
111
|
} else {
|
|
112
112
|
btn.addEventListener("click", () => onAction?.(a.id));
|
|
113
|
-
container.appendChild(btn);
|
|
114
113
|
}
|
|
114
|
+
container.appendChild(wrapper);
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -119,11 +119,13 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
|
|
|
119
119
|
const { config, showClose = true, onClose, layoutHeaderConfig, onHeaderAction } = context;
|
|
120
120
|
const launcher = config?.launcher ?? {};
|
|
121
121
|
|
|
122
|
+
// max(title 24px, cluster 32px) + py-4 + border = the strip's 65px.
|
|
122
123
|
const header = createElement(
|
|
123
124
|
"div",
|
|
124
125
|
"persona-flex persona-items-center persona-justify-between persona-px-6 persona-py-4"
|
|
125
126
|
);
|
|
126
127
|
header.setAttribute("data-persona-theme-zone", "header");
|
|
128
|
+
header.style.minHeight = HEADER_THEME_CSS.minHeight;
|
|
127
129
|
header.style.backgroundColor = 'var(--persona-header-bg, var(--persona-surface, #ffffff))';
|
|
128
130
|
header.style.borderBottomColor = 'var(--persona-header-border, var(--persona-divider, #f1f5f9))';
|
|
129
131
|
header.style.boxShadow = 'var(--persona-header-shadow, none)';
|
|
@@ -140,7 +142,28 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
|
|
|
140
142
|
const combo = createComboButton({
|
|
141
143
|
label: launcher.title ?? "Chat Assistant",
|
|
142
144
|
menuItems: titleMenuConfig.menuItems,
|
|
143
|
-
|
|
145
|
+
// `false` = not handled: bubble the id to the shell's built-ins (star,
|
|
146
|
+
// delete on the active conversation). A DOM event, not a context
|
|
147
|
+
// callback, because the initial header is built by the pure panel
|
|
148
|
+
// builder with no shell in reach.
|
|
149
|
+
onSelect: (itemId) => {
|
|
150
|
+
const finish = (handled: unknown): void => {
|
|
151
|
+
if (handled !== false) return;
|
|
152
|
+
combo.element.dispatchEvent(
|
|
153
|
+
new CustomEvent("persona:title-menu-builtin", {
|
|
154
|
+
bubbles: true,
|
|
155
|
+
composed: true,
|
|
156
|
+
detail: { actionId: itemId },
|
|
157
|
+
})
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
const result = titleMenuConfig.onSelect(itemId);
|
|
161
|
+
if (result instanceof Promise) {
|
|
162
|
+
void result.then(finish, () => {});
|
|
163
|
+
} else {
|
|
164
|
+
finish(result);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
144
167
|
hover: titleMenuConfig.hover,
|
|
145
168
|
className: "",
|
|
146
169
|
});
|
|
@@ -203,38 +226,20 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
|
|
|
203
226
|
|
|
204
227
|
header.appendChild(titleRow);
|
|
205
228
|
|
|
206
|
-
// Close button
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
closeButton.style.width = closeButtonSize;
|
|
216
|
-
closeButton.type = "button";
|
|
217
|
-
closeButton.setAttribute("aria-label", "Close chat");
|
|
218
|
-
closeButton.style.display = showClose ? "" : "none";
|
|
219
|
-
closeButton.style.color =
|
|
220
|
-
launcher.closeButtonColor || HEADER_THEME_CSS.actionIconColor;
|
|
221
|
-
|
|
222
|
-
const closeButtonIconName = launcher.closeButtonIconName ?? "x";
|
|
223
|
-
// Larger intrinsic size compensates for the X glyph's sparse viewBox
|
|
224
|
-
// (paths only occupy the middle 50%). Matches header-builder.ts.
|
|
225
|
-
const closeIconSvg = renderLucideIcon(closeButtonIconName, "28px", "currentColor", 1);
|
|
226
|
-
if (closeIconSvg) {
|
|
227
|
-
closeButton.appendChild(closeIconSvg);
|
|
228
|
-
} else {
|
|
229
|
-
closeButton.textContent = "×";
|
|
230
|
-
}
|
|
229
|
+
// Close button: same shared factory the default layout uses. The wrapper is
|
|
230
|
+
// flex so an inline-flex button can never reserve baseline slack and ride
|
|
231
|
+
// high inside it.
|
|
232
|
+
const trailingWrapperClass =
|
|
233
|
+
"persona-relative persona-inline-flex persona-items-center persona-justify-center";
|
|
234
|
+
const { button: closeButton, wrapper: closeButtonWrapper } = createCloseButton(
|
|
235
|
+
config,
|
|
236
|
+
{ showClose, wrapperClassName: trailingWrapperClass }
|
|
237
|
+
);
|
|
231
238
|
|
|
232
239
|
if (onClose) {
|
|
233
240
|
closeButton.addEventListener("click", onClose);
|
|
234
241
|
}
|
|
235
242
|
|
|
236
|
-
closeButtonWrapper.appendChild(closeButton);
|
|
237
|
-
|
|
238
243
|
// Trailing edge: action buttons cluster with the close button, matching
|
|
239
244
|
// its chrome. `titleMenu` still ignores `trailingActions` (documented).
|
|
240
245
|
const trailingCluster = createElement(
|
|
@@ -248,6 +253,31 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
|
|
|
248
253
|
layoutHeaderConfig?.onAction ?? onHeaderAction
|
|
249
254
|
);
|
|
250
255
|
}
|
|
256
|
+
|
|
257
|
+
// Clear chat honors the same config surface as the default layout. Click
|
|
258
|
+
// wiring is owned by setupClearChatButton() in ui.ts via the returned ref;
|
|
259
|
+
// top-right placement is mounted by attachHeaderToContainer, also via ref.
|
|
260
|
+
const clearChatConfig = launcher.clearChat ?? {};
|
|
261
|
+
let clearChatButton: HTMLButtonElement | null = null;
|
|
262
|
+
let clearChatButtonWrapper: HTMLElement | null = null;
|
|
263
|
+
if (clearChatConfig.enabled ?? true) {
|
|
264
|
+
const clearChatPlacement = clearChatConfig.placement ?? "inline";
|
|
265
|
+
const parts = createClearChatButton(config, {
|
|
266
|
+
wrapperClassName:
|
|
267
|
+
clearChatPlacement === "top-right"
|
|
268
|
+
? "persona-absolute persona-top-4 persona-z-50"
|
|
269
|
+
: trailingWrapperClass,
|
|
270
|
+
});
|
|
271
|
+
clearChatButton = parts.button;
|
|
272
|
+
clearChatButtonWrapper = parts.wrapper;
|
|
273
|
+
if (clearChatPlacement === "top-right") {
|
|
274
|
+
clearChatButtonWrapper.style.right = "48px";
|
|
275
|
+
} else {
|
|
276
|
+
// Close stays outermost.
|
|
277
|
+
trailingCluster.appendChild(clearChatButtonWrapper);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
251
281
|
trailingCluster.appendChild(closeButtonWrapper);
|
|
252
282
|
header.appendChild(trailingCluster);
|
|
253
283
|
|
|
@@ -266,8 +296,8 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
|
|
|
266
296
|
headerSubtitle,
|
|
267
297
|
closeButton,
|
|
268
298
|
closeButtonWrapper,
|
|
269
|
-
clearChatButton
|
|
270
|
-
clearChatButtonWrapper
|
|
299
|
+
clearChatButton,
|
|
300
|
+
clearChatButtonWrapper
|
|
271
301
|
};
|
|
272
302
|
};
|
|
273
303
|
|
|
@@ -1,11 +1,152 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createCloseButton,
|
|
6
|
+
createClearChatButton,
|
|
7
|
+
createHeaderIconButton,
|
|
8
|
+
HEADER_CONTROL_CLASS,
|
|
9
|
+
HEADER_CONTROL_GLYPH_CLASS,
|
|
10
|
+
HEADER_CONTROL_SPARSE_CLASS,
|
|
11
|
+
} from "./header-parts";
|
|
12
|
+
import { HEADER_THEME_CSS } from "./header-builder";
|
|
5
13
|
import type { AgentWidgetConfig } from "../types";
|
|
6
14
|
|
|
7
15
|
const baseConfig: AgentWidgetConfig = { apiUrl: "/api" };
|
|
8
16
|
|
|
17
|
+
describe("createHeaderIconButton", () => {
|
|
18
|
+
it("leaves the box and glyph to the tokens when nothing is configured", () => {
|
|
19
|
+
const { button } = createHeaderIconButton({
|
|
20
|
+
ariaLabel: "Do it",
|
|
21
|
+
iconName: "refresh-cw",
|
|
22
|
+
});
|
|
23
|
+
expect(button.classList.contains(HEADER_CONTROL_CLASS)).toBe(true);
|
|
24
|
+
expect(button.classList.contains(HEADER_CONTROL_GLYPH_CLASS)).toBe(true);
|
|
25
|
+
expect(button.classList.contains(HEADER_CONTROL_SPARSE_CLASS)).toBe(false);
|
|
26
|
+
expect(button.style.width).toBe("");
|
|
27
|
+
expect(button.style.height).toBe("");
|
|
28
|
+
expect(button.style.minWidth).toBe("");
|
|
29
|
+
expect(button.type).toBe("button");
|
|
30
|
+
expect(button.getAttribute("aria-label")).toBe("Do it");
|
|
31
|
+
expect(button.style.color).toBe(HEADER_THEME_CSS.actionIconColor);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("writes an inline box and drops the glyph class for explicit sizes", () => {
|
|
35
|
+
const { button } = createHeaderIconButton({
|
|
36
|
+
ariaLabel: "Do it",
|
|
37
|
+
iconName: "refresh-cw",
|
|
38
|
+
size: "16px",
|
|
39
|
+
iconSize: "14px",
|
|
40
|
+
});
|
|
41
|
+
expect(button.classList.contains(HEADER_CONTROL_CLASS)).toBe(true);
|
|
42
|
+
expect(button.classList.contains(HEADER_CONTROL_GLYPH_CLASS)).toBe(false);
|
|
43
|
+
expect(button.style.width).toBe("16px");
|
|
44
|
+
expect(button.style.minHeight).toBe("16px");
|
|
45
|
+
const svg = button.querySelector("svg")!;
|
|
46
|
+
expect(svg.getAttribute("width")).toBe("14px");
|
|
47
|
+
expect(svg.getAttribute("stroke-width")).toBe("1.5");
|
|
48
|
+
expect(svg.style.strokeWidth).toBe(
|
|
49
|
+
"calc(var(--persona-components-header-controlStrokeWidth, 1.5) * 1)"
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("routes the stroke token inline for explicitly sized glyphs", () => {
|
|
54
|
+
const { button } = createHeaderIconButton({
|
|
55
|
+
ariaLabel: "Close",
|
|
56
|
+
iconName: "x",
|
|
57
|
+
iconSize: "12px",
|
|
58
|
+
});
|
|
59
|
+
// No class hooks, so the stylesheet rules can't reach this control: the
|
|
60
|
+
// inline style carries the stroke token, with the attribute as the
|
|
61
|
+
// no-token fallback and the sparse 0.7 factor preserved.
|
|
62
|
+
expect(button.classList.contains(HEADER_CONTROL_GLYPH_CLASS)).toBe(false);
|
|
63
|
+
expect(button.classList.contains(HEADER_CONTROL_SPARSE_CLASS)).toBe(false);
|
|
64
|
+
const svg = button.querySelector("svg")!;
|
|
65
|
+
expect(svg.getAttribute("stroke-width")).toBe("1.05");
|
|
66
|
+
expect(svg.style.strokeWidth).toBe(
|
|
67
|
+
"calc(var(--persona-components-header-controlStrokeWidth, 1.5) * 0.7)"
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("compensates the sparse X glyph in both the class hook and the attributes", () => {
|
|
72
|
+
const token = createHeaderIconButton({ ariaLabel: "Close", iconName: "x" });
|
|
73
|
+
expect(token.button.classList.contains(HEADER_CONTROL_SPARSE_CLASS)).toBe(true);
|
|
74
|
+
|
|
75
|
+
const explicit = createHeaderIconButton({
|
|
76
|
+
ariaLabel: "Close",
|
|
77
|
+
iconName: "x",
|
|
78
|
+
iconSize: "10px",
|
|
79
|
+
});
|
|
80
|
+
// Nominal 10px scaled by 1.4, stroke thinned by the same factor so the
|
|
81
|
+
// rendered weight still matches a dense sibling glyph.
|
|
82
|
+
const svg = explicit.button.querySelector("svg")!;
|
|
83
|
+
expect(svg.getAttribute("width")).toBe("14px");
|
|
84
|
+
expect(svg.getAttribute("stroke-width")).toBe("1.05");
|
|
85
|
+
expect(explicit.button.classList.contains(HEADER_CONTROL_SPARSE_CLASS)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("swaps the transparent default classes for an explicit background", () => {
|
|
89
|
+
const { button } = createHeaderIconButton({
|
|
90
|
+
ariaLabel: "Do it",
|
|
91
|
+
iconName: "refresh-cw",
|
|
92
|
+
backgroundColor: "rgb(1, 2, 3)",
|
|
93
|
+
color: "rgb(9, 9, 9)",
|
|
94
|
+
});
|
|
95
|
+
expect(button.className).not.toContain("persona-bg-transparent");
|
|
96
|
+
expect(button.className).not.toContain("hover:persona-bg-gray-100");
|
|
97
|
+
expect(button.style.backgroundColor).toBe("rgb(1, 2, 3)");
|
|
98
|
+
expect(button.style.color).toBe("rgb(9, 9, 9)");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("falls back to the text glyph when the icon is not in the registry", () => {
|
|
102
|
+
const { button } = createHeaderIconButton({
|
|
103
|
+
ariaLabel: "Close",
|
|
104
|
+
iconName: "not-a-real-icon",
|
|
105
|
+
iconText: "×",
|
|
106
|
+
});
|
|
107
|
+
expect(button.querySelector("svg")).toBeNull();
|
|
108
|
+
expect(button.textContent).toBe("×");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("wraps the button in a flex box so it can never ride off-center", () => {
|
|
112
|
+
const { button, wrapper } = createHeaderIconButton({
|
|
113
|
+
ariaLabel: "Do it",
|
|
114
|
+
iconName: "refresh-cw",
|
|
115
|
+
});
|
|
116
|
+
expect(button.parentElement).toBe(wrapper);
|
|
117
|
+
expect(wrapper.className).toContain("persona-inline-flex");
|
|
118
|
+
expect(wrapper.className).toContain("persona-items-center");
|
|
119
|
+
expect(wrapper.className).toContain("persona-justify-center");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("attaches the styled tooltip by default, reading the live aria-label", () => {
|
|
123
|
+
const { button, wrapper } = createHeaderIconButton({
|
|
124
|
+
ariaLabel: "Messages",
|
|
125
|
+
iconName: "history",
|
|
126
|
+
});
|
|
127
|
+
document.body.appendChild(wrapper);
|
|
128
|
+
button.setAttribute("aria-label", "Opening conversation");
|
|
129
|
+
wrapper.dispatchEvent(new MouseEvent("mouseenter"));
|
|
130
|
+
const tooltip = document.body.querySelector(".persona-control-tooltip");
|
|
131
|
+
expect(tooltip).not.toBeNull();
|
|
132
|
+
expect(tooltip!.textContent).toContain("Opening conversation");
|
|
133
|
+
wrapper.remove();
|
|
134
|
+
tooltip!.remove();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("attaches no tooltip when tooltip is false", () => {
|
|
138
|
+
const { wrapper } = createHeaderIconButton({
|
|
139
|
+
ariaLabel: "Silent",
|
|
140
|
+
iconName: "x",
|
|
141
|
+
tooltip: false,
|
|
142
|
+
});
|
|
143
|
+
document.body.appendChild(wrapper);
|
|
144
|
+
wrapper.dispatchEvent(new MouseEvent("mouseenter"));
|
|
145
|
+
expect(document.body.querySelector(".persona-control-tooltip")).toBeNull();
|
|
146
|
+
wrapper.remove();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
9
150
|
describe("createCloseButton", () => {
|
|
10
151
|
it("keeps the default utility classes when no style overrides are set", () => {
|
|
11
152
|
const { button } = createCloseButton(baseConfig, {});
|
|
@@ -15,6 +156,18 @@ describe("createCloseButton", () => {
|
|
|
15
156
|
expect(button.classList.contains("persona-rounded-full")).toBe(true);
|
|
16
157
|
expect(button.classList.contains("persona-border-none")).toBe(true);
|
|
17
158
|
expect(button.classList.contains("hover:persona-bg-gray-100")).toBe(true);
|
|
159
|
+
// Unset `launcher.closeButtonSize` leaves the control-size token in charge.
|
|
160
|
+
expect(button.classList.contains(HEADER_CONTROL_CLASS)).toBe(true);
|
|
161
|
+
expect(button.style.width).toBe("");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("still lets launcher.closeButtonSize pin the box past the token", () => {
|
|
165
|
+
const { button } = createCloseButton(
|
|
166
|
+
{ ...baseConfig, launcher: { closeButtonSize: "28px" } },
|
|
167
|
+
{}
|
|
168
|
+
);
|
|
169
|
+
expect(button.style.width).toBe("28px");
|
|
170
|
+
expect(button.style.minWidth).toBe("28px");
|
|
18
171
|
});
|
|
19
172
|
|
|
20
173
|
it("hides the button when showClose is false", () => {
|