@runtypelabs/persona 2.3.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +222 -5
- package/dist/index.cjs +42 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +832 -571
- package/dist/index.d.ts +832 -571
- package/dist/index.global.js +88 -88
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +42 -42
- package/dist/index.js.map +1 -1
- package/dist/widget.css +257 -67
- package/package.json +2 -4
- package/src/components/artifact-card.ts +39 -5
- package/src/components/artifact-pane.ts +68 -127
- package/src/components/composer-builder.ts +3 -23
- package/src/components/header-builder.ts +29 -34
- package/src/components/header-layouts.ts +109 -41
- package/src/components/launcher.ts +10 -7
- package/src/components/message-bubble.ts +7 -11
- package/src/components/panel.ts +4 -4
- package/src/defaults.ts +22 -93
- package/src/index.ts +20 -7
- package/src/presets.ts +66 -51
- package/src/runtime/host-layout.test.ts +333 -0
- package/src/runtime/host-layout.ts +346 -27
- package/src/runtime/init.test.ts +113 -8
- package/src/runtime/init.ts +1 -1
- package/src/styles/widget.css +257 -67
- package/src/types/theme.ts +76 -0
- package/src/types.ts +86 -97
- package/src/ui.docked.test.ts +203 -7
- package/src/ui.ts +125 -92
- package/src/utils/artifact-gate.ts +1 -1
- package/src/utils/buttons.ts +417 -0
- package/src/utils/code-generators.test.ts +43 -7
- package/src/utils/code-generators.ts +9 -25
- package/src/utils/deep-merge.ts +26 -0
- package/src/utils/dock.ts +18 -5
- package/src/utils/dropdown.ts +178 -0
- package/src/utils/theme.test.ts +90 -15
- package/src/utils/theme.ts +20 -46
- package/src/utils/tokens.ts +108 -11
- package/src/styles/tailwind.css +0 -20
- package/src/utils/migration.ts +0 -220
|
@@ -4,6 +4,8 @@ import { escapeHtml, createMarkdownProcessorFromConfig } from "../postprocessors
|
|
|
4
4
|
import { resolveSanitizer } from "../utils/sanitize";
|
|
5
5
|
import { componentRegistry, type ComponentContext } from "./registry";
|
|
6
6
|
import { renderLucideIcon } from "../utils/icons";
|
|
7
|
+
import { createDropdownMenu, type DropdownMenuHandle } from "../utils/dropdown";
|
|
8
|
+
import { createIconButton, createLabelButton } from "../utils/buttons";
|
|
7
9
|
|
|
8
10
|
export type ArtifactPaneApi = {
|
|
9
11
|
element: HTMLElement;
|
|
@@ -27,65 +29,6 @@ function fallbackComponentCard(sel: PersonaArtifactRecord): HTMLElement {
|
|
|
27
29
|
return card;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
function iconButton(iconName: string, label: string, extraClass = ""): HTMLButtonElement {
|
|
31
|
-
const btn = createElement(
|
|
32
|
-
"button",
|
|
33
|
-
`persona-inline-flex persona-items-center persona-justify-center persona-rounded-md persona-border persona-border-persona-border persona-bg-persona-surface persona-p-1 persona-text-persona-primary hover:persona-bg-persona-container ${extraClass}`
|
|
34
|
-
) as HTMLButtonElement;
|
|
35
|
-
btn.type = "button";
|
|
36
|
-
btn.setAttribute("aria-label", label);
|
|
37
|
-
btn.title = label;
|
|
38
|
-
const icon = renderLucideIcon(iconName, 16, "currentColor", 2);
|
|
39
|
-
if (icon) btn.appendChild(icon);
|
|
40
|
-
return btn;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function documentToolbarIconButton(
|
|
44
|
-
iconName: string,
|
|
45
|
-
label: string,
|
|
46
|
-
extraClass: string
|
|
47
|
-
): HTMLButtonElement {
|
|
48
|
-
const btn = createElement(
|
|
49
|
-
"button",
|
|
50
|
-
`persona-artifact-doc-icon-btn ${extraClass}`.trim()
|
|
51
|
-
) as HTMLButtonElement;
|
|
52
|
-
btn.type = "button";
|
|
53
|
-
btn.setAttribute("aria-label", label);
|
|
54
|
-
btn.title = label;
|
|
55
|
-
const icon = renderLucideIcon(iconName, 16, "currentColor", 2);
|
|
56
|
-
if (icon) btn.appendChild(icon);
|
|
57
|
-
return btn;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function documentToolbarCopyMainButton(showLabel: boolean): HTMLButtonElement {
|
|
61
|
-
const btn = createElement("button", "persona-artifact-doc-copy-btn") as HTMLButtonElement;
|
|
62
|
-
btn.type = "button";
|
|
63
|
-
btn.setAttribute("aria-label", "Copy");
|
|
64
|
-
btn.title = "Copy";
|
|
65
|
-
const icon = renderLucideIcon("copy", showLabel ? 14 : 16, "currentColor", 2);
|
|
66
|
-
if (icon) btn.appendChild(icon);
|
|
67
|
-
if (showLabel) {
|
|
68
|
-
const span = createElement("span", "persona-artifact-doc-copy-label");
|
|
69
|
-
span.textContent = "Copy";
|
|
70
|
-
btn.appendChild(span);
|
|
71
|
-
}
|
|
72
|
-
return btn;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function documentToolbarChevronMenuButton(): HTMLButtonElement {
|
|
76
|
-
const btn = createElement(
|
|
77
|
-
"button",
|
|
78
|
-
"persona-artifact-doc-copy-menu-chevron persona-artifact-doc-icon-btn"
|
|
79
|
-
) as HTMLButtonElement;
|
|
80
|
-
btn.type = "button";
|
|
81
|
-
btn.setAttribute("aria-label", "More copy options");
|
|
82
|
-
btn.setAttribute("aria-haspopup", "true");
|
|
83
|
-
btn.setAttribute("aria-expanded", "false");
|
|
84
|
-
const chev = renderLucideIcon("chevron-down", 14, "currentColor", 2);
|
|
85
|
-
if (chev) btn.appendChild(chev);
|
|
86
|
-
return btn;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
32
|
/**
|
|
90
33
|
* Right-hand artifact sidebar / mobile drawer content.
|
|
91
34
|
*/
|
|
@@ -119,6 +62,8 @@ export function createArtifactPane(
|
|
|
119
62
|
const dismissLocalUi = () => {
|
|
120
63
|
backdrop?.classList.add("persona-hidden");
|
|
121
64
|
shell.classList.remove("persona-artifact-drawer-open");
|
|
65
|
+
// Hide portaled copy menu
|
|
66
|
+
copyMenuDropdown?.hide();
|
|
122
67
|
};
|
|
123
68
|
|
|
124
69
|
if (backdrop) {
|
|
@@ -162,13 +107,13 @@ export function createArtifactPane(
|
|
|
162
107
|
|
|
163
108
|
/** Document preset: view vs raw source */
|
|
164
109
|
let viewMode: "rendered" | "source" = "rendered";
|
|
165
|
-
const leftTools = createElement("div", "persona-flex persona-items-center persona-gap-1 persona-shrink-0");
|
|
110
|
+
const leftTools = createElement("div", "persona-flex persona-items-center persona-gap-1 persona-shrink-0 persona-artifact-toggle-group");
|
|
166
111
|
const viewBtn = documentChrome
|
|
167
|
-
?
|
|
168
|
-
:
|
|
112
|
+
? createIconButton({ icon: "eye", label: "Rendered view", className: "persona-artifact-doc-icon-btn persona-artifact-view-btn" })
|
|
113
|
+
: createIconButton({ icon: "eye", label: "Rendered view" });
|
|
169
114
|
const codeBtn = documentChrome
|
|
170
|
-
?
|
|
171
|
-
:
|
|
115
|
+
? createIconButton({ icon: "code-2", label: "Source", className: "persona-artifact-doc-icon-btn persona-artifact-code-btn" })
|
|
116
|
+
: createIconButton({ icon: "code-2", label: "Source" });
|
|
172
117
|
const actionsRight = createElement("div", "persona-flex persona-items-center persona-gap-1 persona-shrink-0");
|
|
173
118
|
const showCopyLabel = layout?.documentToolbarShowCopyLabel === true;
|
|
174
119
|
const showCopyChevron = layout?.documentToolbarShowCopyChevron === true;
|
|
@@ -178,10 +123,12 @@ export function createArtifactPane(
|
|
|
178
123
|
let copyWrap: HTMLElement | null = null;
|
|
179
124
|
let copyBtn: HTMLButtonElement;
|
|
180
125
|
let copyMenuChevronBtn: HTMLButtonElement | null = null;
|
|
181
|
-
let
|
|
126
|
+
let copyMenuDropdown: DropdownMenuHandle | null = null;
|
|
182
127
|
|
|
183
128
|
if (documentChrome && (showCopyLabel || showCopyChevron) && !showCopyMenu) {
|
|
184
|
-
copyBtn =
|
|
129
|
+
copyBtn = showCopyLabel
|
|
130
|
+
? createLabelButton({ icon: "copy", label: "Copy", iconSize: 14, className: "persona-artifact-doc-copy-btn" })
|
|
131
|
+
: createIconButton({ icon: "copy", label: "Copy", className: "persona-artifact-doc-copy-btn" });
|
|
185
132
|
if (showCopyChevron) {
|
|
186
133
|
const chev = renderLucideIcon("chevron-down", 14, "currentColor", 2);
|
|
187
134
|
if (chev) copyBtn.appendChild(chev);
|
|
@@ -191,36 +138,29 @@ export function createArtifactPane(
|
|
|
191
138
|
"div",
|
|
192
139
|
"persona-relative persona-inline-flex persona-items-center persona-gap-0 persona-rounded-md"
|
|
193
140
|
);
|
|
194
|
-
copyBtn =
|
|
195
|
-
|
|
141
|
+
copyBtn = showCopyLabel
|
|
142
|
+
? createLabelButton({ icon: "copy", label: "Copy", iconSize: 14, className: "persona-artifact-doc-copy-btn" })
|
|
143
|
+
: createIconButton({ icon: "copy", label: "Copy", className: "persona-artifact-doc-copy-btn" });
|
|
144
|
+
copyMenuChevronBtn = createIconButton({
|
|
145
|
+
icon: "chevron-down",
|
|
146
|
+
label: "More copy options",
|
|
147
|
+
size: 14,
|
|
148
|
+
className: "persona-artifact-doc-copy-menu-chevron persona-artifact-doc-icon-btn",
|
|
149
|
+
aria: { "aria-haspopup": "true", "aria-expanded": "false" }
|
|
150
|
+
});
|
|
196
151
|
copyWrap.append(copyBtn, copyMenuChevronBtn);
|
|
197
|
-
copyMenuEl = createElement(
|
|
198
|
-
"div",
|
|
199
|
-
"persona-artifact-doc-copy-menu persona-absolute persona-right-0 persona-top-full persona-z-20 persona-mt-1 persona-min-w-[10rem] persona-rounded-md persona-border persona-border-persona-border persona-bg-persona-surface persona-py-1 persona-shadow-md persona-hidden"
|
|
200
|
-
);
|
|
201
|
-
copyWrap.appendChild(copyMenuEl);
|
|
202
|
-
for (const item of copyMenuItems!) {
|
|
203
|
-
const opt = createElement(
|
|
204
|
-
"button",
|
|
205
|
-
"persona-block persona-w-full persona-text-left persona-px-3 persona-py-2 persona-text-xs persona-text-persona-primary hover:persona-bg-persona-container"
|
|
206
|
-
) as HTMLButtonElement;
|
|
207
|
-
opt.type = "button";
|
|
208
|
-
opt.textContent = item.label;
|
|
209
|
-
opt.dataset.copyMenuId = item.id;
|
|
210
|
-
copyMenuEl.appendChild(opt);
|
|
211
|
-
}
|
|
212
152
|
} else if (documentChrome) {
|
|
213
|
-
copyBtn =
|
|
153
|
+
copyBtn = createIconButton({ icon: "copy", label: "Copy", className: "persona-artifact-doc-icon-btn" });
|
|
214
154
|
} else {
|
|
215
|
-
copyBtn =
|
|
155
|
+
copyBtn = createIconButton({ icon: "copy", label: "Copy" });
|
|
216
156
|
}
|
|
217
157
|
|
|
218
158
|
const refreshBtn = documentChrome
|
|
219
|
-
?
|
|
220
|
-
:
|
|
159
|
+
? createIconButton({ icon: "refresh-cw", label: "Refresh", className: "persona-artifact-doc-icon-btn" })
|
|
160
|
+
: createIconButton({ icon: "refresh-cw", label: "Refresh" });
|
|
221
161
|
const closeIconBtn = documentChrome
|
|
222
|
-
?
|
|
223
|
-
:
|
|
162
|
+
? createIconButton({ icon: "x", label: "Close", className: "persona-artifact-doc-icon-btn" })
|
|
163
|
+
: createIconButton({ icon: "x", label: "Close" });
|
|
224
164
|
|
|
225
165
|
const getSelectedArtifactText = (): { markdown: string; jsonPayload: string; id: string | null } => {
|
|
226
166
|
const sel = records.find((r) => r.id === selectedId) ?? records[records.length - 1];
|
|
@@ -262,45 +202,46 @@ export function createArtifactPane(
|
|
|
262
202
|
await defaultCopy();
|
|
263
203
|
});
|
|
264
204
|
|
|
265
|
-
if (copyMenuChevronBtn &&
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
205
|
+
if (copyMenuChevronBtn && copyMenuItems?.length) {
|
|
206
|
+
// Resolve the portal target — widget root for CSS var inheritance, escaping overflow: hidden
|
|
207
|
+
const resolvePortal = (): HTMLElement => shell.closest("[data-persona-root]") as HTMLElement ?? document.body;
|
|
208
|
+
|
|
209
|
+
const initDropdown = () => {
|
|
210
|
+
copyMenuDropdown = createDropdownMenu({
|
|
211
|
+
items: copyMenuItems.map((item) => ({ id: item.id, label: item.label })),
|
|
212
|
+
onSelect: async (actionId) => {
|
|
213
|
+
const { markdown, jsonPayload, id } = getSelectedArtifactText();
|
|
214
|
+
const handler = layout?.onDocumentToolbarCopyMenuSelect;
|
|
215
|
+
try {
|
|
216
|
+
if (handler) {
|
|
217
|
+
await handler({ actionId, artifactId: id, markdown, jsonPayload });
|
|
218
|
+
} else if (actionId === "markdown" || actionId === "md") {
|
|
219
|
+
await navigator.clipboard.writeText(markdown);
|
|
220
|
+
} else if (actionId === "json" || actionId === "source") {
|
|
221
|
+
await navigator.clipboard.writeText(jsonPayload);
|
|
222
|
+
} else {
|
|
223
|
+
await navigator.clipboard.writeText(markdown || jsonPayload);
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
/* ignore */
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
anchor: copyWrap ?? copyMenuChevronBtn!,
|
|
230
|
+
position: 'bottom-right',
|
|
231
|
+
portal: resolvePortal(),
|
|
232
|
+
});
|
|
269
233
|
};
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
} else {
|
|
277
|
-
closeMenu();
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
if (typeof document !== "undefined") {
|
|
281
|
-
document.addEventListener("click", closeMenu);
|
|
234
|
+
|
|
235
|
+
// Defer init until shell is in the DOM (may not be attached yet)
|
|
236
|
+
if (shell.isConnected) {
|
|
237
|
+
initDropdown();
|
|
238
|
+
} else {
|
|
239
|
+
requestAnimationFrame(initDropdown);
|
|
282
240
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (!t?.dataset.copyMenuId) return;
|
|
241
|
+
|
|
242
|
+
copyMenuChevronBtn.addEventListener("click", (e) => {
|
|
286
243
|
e.stopPropagation();
|
|
287
|
-
|
|
288
|
-
const { markdown, jsonPayload, id } = getSelectedArtifactText();
|
|
289
|
-
const handler = layout?.onDocumentToolbarCopyMenuSelect;
|
|
290
|
-
try {
|
|
291
|
-
if (handler) {
|
|
292
|
-
await handler({ actionId, artifactId: id, markdown, jsonPayload });
|
|
293
|
-
} else if (actionId === "markdown" || actionId === "md") {
|
|
294
|
-
await navigator.clipboard.writeText(markdown);
|
|
295
|
-
} else if (actionId === "json" || actionId === "source") {
|
|
296
|
-
await navigator.clipboard.writeText(jsonPayload);
|
|
297
|
-
} else {
|
|
298
|
-
await navigator.clipboard.writeText(markdown || jsonPayload);
|
|
299
|
-
}
|
|
300
|
-
} catch {
|
|
301
|
-
/* ignore */
|
|
302
|
-
}
|
|
303
|
-
closeMenu();
|
|
244
|
+
copyMenuDropdown?.toggle();
|
|
304
245
|
});
|
|
305
246
|
}
|
|
306
247
|
|
|
@@ -388,7 +329,7 @@ export function createArtifactPane(
|
|
|
388
329
|
for (const r of records) {
|
|
389
330
|
const tab = createElement(
|
|
390
331
|
"button",
|
|
391
|
-
"persona-artifact-tab persona-shrink-0 persona-rounded-lg persona-px-2 persona-py-1 persona-text-xs persona-border persona-border-transparent persona-text-persona-primary
|
|
332
|
+
"persona-artifact-tab persona-shrink-0 persona-rounded-lg persona-px-2 persona-py-1 persona-text-xs persona-border persona-border-transparent persona-text-persona-primary"
|
|
392
333
|
);
|
|
393
334
|
tab.type = "button";
|
|
394
335
|
tab.textContent = r.title || r.id.slice(0, 8);
|
|
@@ -461,7 +402,7 @@ export function createArtifactPane(
|
|
|
461
402
|
shell.classList.toggle("persona-hidden", !has);
|
|
462
403
|
if (backdrop) {
|
|
463
404
|
const root =
|
|
464
|
-
typeof shell.closest === "function" ? shell.closest("
|
|
405
|
+
typeof shell.closest === "function" ? shell.closest("[data-persona-root]") : null;
|
|
465
406
|
const narrowHost = root?.classList.contains("persona-artifact-narrow-host") ?? false;
|
|
466
407
|
const isMobile =
|
|
467
408
|
narrowHost ||
|
|
@@ -40,23 +40,6 @@ export interface ComposerBuildContext {
|
|
|
40
40
|
disabled?: boolean;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
* Helper to get font family CSS value from config preset
|
|
45
|
-
*/
|
|
46
|
-
const getFontFamilyValue = (
|
|
47
|
-
family: "sans-serif" | "serif" | "mono"
|
|
48
|
-
): string => {
|
|
49
|
-
switch (family) {
|
|
50
|
-
case "serif":
|
|
51
|
-
return 'Georgia, "Times New Roman", Times, serif';
|
|
52
|
-
case "mono":
|
|
53
|
-
return '"Courier New", Courier, "Lucida Console", Monaco, monospace';
|
|
54
|
-
case "sans-serif":
|
|
55
|
-
default:
|
|
56
|
-
return '-apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif';
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
43
|
/**
|
|
61
44
|
* Build the composer/footer section of the panel.
|
|
62
45
|
* Extracted for reuse and plugin override support.
|
|
@@ -91,12 +74,9 @@ export const buildComposer = (context: ComposerBuildContext): ComposerElements =
|
|
|
91
74
|
"persona-w-full persona-min-h-[24px] persona-resize-none persona-border-none persona-bg-transparent persona-text-sm persona-text-persona-primary focus:persona-outline-none focus:persona-border-none persona-composer-textarea";
|
|
92
75
|
textarea.rows = 1;
|
|
93
76
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
textarea.style.fontFamily = getFontFamilyValue(fontFamily);
|
|
99
|
-
textarea.style.fontWeight = fontWeight;
|
|
77
|
+
textarea.style.fontFamily =
|
|
78
|
+
'var(--persona-input-font-family, var(--persona-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif))';
|
|
79
|
+
textarea.style.fontWeight = "var(--persona-input-font-weight, var(--persona-font-weight, 400))";
|
|
100
80
|
|
|
101
81
|
// Set up auto-resize: expand up to 3 lines, then scroll
|
|
102
82
|
// Line height is ~20px for text-sm (14px * 1.25 line-height), so 3 lines ≈ 60px
|
|
@@ -2,6 +2,16 @@ import { createElement, createElementInDocument } from "../utils/dom";
|
|
|
2
2
|
import { renderLucideIcon } from "../utils/icons";
|
|
3
3
|
import { AgentWidgetConfig } from "../types";
|
|
4
4
|
|
|
5
|
+
/** CSS `color` values; variables are set on `[data-persona-root]` from `theme.components.header`. */
|
|
6
|
+
export const HEADER_THEME_CSS = {
|
|
7
|
+
titleColor:
|
|
8
|
+
"var(--persona-header-title-fg, var(--persona-primary, #2563eb))",
|
|
9
|
+
subtitleColor:
|
|
10
|
+
"var(--persona-header-subtitle-fg, var(--persona-text-muted, var(--persona-muted, #9ca3af)))",
|
|
11
|
+
actionIconColor:
|
|
12
|
+
"var(--persona-header-action-icon-fg, var(--persona-muted, #9ca3af))",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
5
15
|
export interface HeaderElements {
|
|
6
16
|
header: HTMLElement;
|
|
7
17
|
iconHolder: HTMLElement;
|
|
@@ -33,9 +43,9 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
|
|
|
33
43
|
);
|
|
34
44
|
header.setAttribute("data-persona-theme-zone", "header");
|
|
35
45
|
header.style.backgroundColor = 'var(--persona-header-bg, var(--persona-surface, #ffffff))';
|
|
36
|
-
header.style.borderBottomWidth = '1px';
|
|
37
|
-
header.style.borderBottomStyle = 'solid';
|
|
38
46
|
header.style.borderBottomColor = 'var(--persona-header-border, var(--persona-divider, #f1f5f9))';
|
|
47
|
+
header.style.boxShadow = 'var(--persona-header-shadow, none)';
|
|
48
|
+
header.style.borderBottom = 'var(--persona-header-border-bottom, 1px solid var(--persona-header-border, var(--persona-divider, #f1f5f9)))';
|
|
39
49
|
|
|
40
50
|
const launcher = config?.launcher ?? {};
|
|
41
51
|
const headerIconSize = launcher.headerIconSize ?? "48px";
|
|
@@ -46,17 +56,21 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
|
|
|
46
56
|
|
|
47
57
|
const iconHolder = createElement(
|
|
48
58
|
"div",
|
|
49
|
-
"persona-flex persona-items-center persona-justify-center persona-rounded-xl persona-
|
|
59
|
+
"persona-flex persona-items-center persona-justify-center persona-rounded-xl persona-text-xl"
|
|
50
60
|
);
|
|
51
61
|
iconHolder.style.height = headerIconSize;
|
|
52
62
|
iconHolder.style.width = headerIconSize;
|
|
63
|
+
iconHolder.style.backgroundColor =
|
|
64
|
+
"var(--persona-header-icon-bg, var(--persona-primary, #2563eb))";
|
|
65
|
+
iconHolder.style.color =
|
|
66
|
+
"var(--persona-header-icon-fg, var(--persona-text-inverse, #ffffff))";
|
|
53
67
|
|
|
54
68
|
// Render icon based on priority: Lucide icon > iconUrl > agentIconText
|
|
55
69
|
if (!headerIconHidden) {
|
|
56
70
|
if (headerIconName) {
|
|
57
71
|
// Use Lucide icon
|
|
58
72
|
const iconSize = parseFloat(headerIconSize) || 24;
|
|
59
|
-
const iconSvg = renderLucideIcon(headerIconName, iconSize * 0.6, "
|
|
73
|
+
const iconSvg = renderLucideIcon(headerIconName, iconSize * 0.6, "currentColor", 1);
|
|
60
74
|
if (iconSvg) {
|
|
61
75
|
iconHolder.replaceChildren(iconSvg);
|
|
62
76
|
} else {
|
|
@@ -80,8 +94,10 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
|
|
|
80
94
|
|
|
81
95
|
const headerCopy = createElement("div", "persona-flex persona-flex-col");
|
|
82
96
|
const title = createElement("span", "persona-text-base persona-font-semibold");
|
|
97
|
+
title.style.color = HEADER_THEME_CSS.titleColor;
|
|
83
98
|
title.textContent = config?.launcher?.title ?? "Chat Assistant";
|
|
84
|
-
const subtitle = createElement("span", "persona-text-xs
|
|
99
|
+
const subtitle = createElement("span", "persona-text-xs");
|
|
100
|
+
subtitle.style.color = HEADER_THEME_CSS.subtitleColor;
|
|
85
101
|
subtitle.textContent =
|
|
86
102
|
config?.launcher?.subtitle ?? "Here to help you get answers fast";
|
|
87
103
|
|
|
@@ -132,31 +148,22 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
|
|
|
132
148
|
|
|
133
149
|
clearChatButton = createElement(
|
|
134
150
|
"button",
|
|
135
|
-
"persona-inline-flex persona-items-center persona-justify-center persona-rounded-full
|
|
151
|
+
"persona-inline-flex persona-items-center persona-justify-center persona-rounded-full hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none"
|
|
136
152
|
) as HTMLButtonElement;
|
|
137
153
|
|
|
138
154
|
clearChatButton.style.height = clearChatSize;
|
|
139
155
|
clearChatButton.style.width = clearChatSize;
|
|
140
156
|
clearChatButton.type = "button";
|
|
141
157
|
clearChatButton.setAttribute("aria-label", clearChatTooltipText);
|
|
158
|
+
clearChatButton.style.color =
|
|
159
|
+
clearChatIconColor || HEADER_THEME_CSS.actionIconColor;
|
|
142
160
|
|
|
143
161
|
// Add icon
|
|
144
|
-
const iconSvg = renderLucideIcon(
|
|
145
|
-
clearChatIconName,
|
|
146
|
-
"20px",
|
|
147
|
-
clearChatIconColor || "",
|
|
148
|
-
1
|
|
149
|
-
);
|
|
162
|
+
const iconSvg = renderLucideIcon(clearChatIconName, "20px", "currentColor", 1);
|
|
150
163
|
if (iconSvg) {
|
|
151
164
|
clearChatButton.appendChild(iconSvg);
|
|
152
165
|
}
|
|
153
166
|
|
|
154
|
-
// Apply styling from config
|
|
155
|
-
if (clearChatIconColor) {
|
|
156
|
-
clearChatButton.style.color = clearChatIconColor;
|
|
157
|
-
clearChatButton.classList.remove("persona-text-persona-muted");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
167
|
if (clearChatBgColor) {
|
|
161
168
|
clearChatButton.style.backgroundColor = clearChatBgColor;
|
|
162
169
|
clearChatButton.classList.remove("hover:persona-bg-gray-100");
|
|
@@ -281,7 +288,7 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
|
|
|
281
288
|
// Create close button with base classes
|
|
282
289
|
const closeButton = createElement(
|
|
283
290
|
"button",
|
|
284
|
-
"persona-inline-flex persona-items-center persona-justify-center persona-rounded-full
|
|
291
|
+
"persona-inline-flex persona-items-center persona-justify-center persona-rounded-full hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none"
|
|
285
292
|
) as HTMLButtonElement;
|
|
286
293
|
closeButton.style.height = closeButtonSize;
|
|
287
294
|
closeButton.style.width = closeButtonSize;
|
|
@@ -297,29 +304,17 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
|
|
|
297
304
|
// Add icon or fallback text
|
|
298
305
|
const closeButtonIconName = launcher.closeButtonIconName ?? "x";
|
|
299
306
|
const closeButtonIconText = launcher.closeButtonIconText ?? "×";
|
|
307
|
+
closeButton.style.color =
|
|
308
|
+
launcher.closeButtonColor || HEADER_THEME_CSS.actionIconColor;
|
|
300
309
|
|
|
301
310
|
// Try to render Lucide icon, fallback to text if not provided or fails
|
|
302
|
-
const closeIconSvg = renderLucideIcon(
|
|
303
|
-
closeButtonIconName,
|
|
304
|
-
"20px",
|
|
305
|
-
launcher.closeButtonColor || "",
|
|
306
|
-
1
|
|
307
|
-
);
|
|
311
|
+
const closeIconSvg = renderLucideIcon(closeButtonIconName, "20px", "currentColor", 1);
|
|
308
312
|
if (closeIconSvg) {
|
|
309
313
|
closeButton.appendChild(closeIconSvg);
|
|
310
314
|
} else {
|
|
311
315
|
closeButton.textContent = closeButtonIconText;
|
|
312
316
|
}
|
|
313
317
|
|
|
314
|
-
// Apply close button styling from config
|
|
315
|
-
if (launcher.closeButtonColor) {
|
|
316
|
-
closeButton.style.color = launcher.closeButtonColor;
|
|
317
|
-
closeButton.classList.remove("persona-text-persona-muted");
|
|
318
|
-
} else {
|
|
319
|
-
closeButton.style.color = "";
|
|
320
|
-
closeButton.classList.add("persona-text-persona-muted");
|
|
321
|
-
}
|
|
322
|
-
|
|
323
318
|
if (launcher.closeButtonBackgroundColor) {
|
|
324
319
|
closeButton.style.backgroundColor = launcher.closeButtonBackgroundColor;
|
|
325
320
|
closeButton.classList.remove("hover:persona-bg-gray-100");
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { createElement } from "../utils/dom";
|
|
2
2
|
import { renderLucideIcon } from "../utils/icons";
|
|
3
|
+
import { createDropdownMenu } from "../utils/dropdown";
|
|
4
|
+
import { createComboButton } from "../utils/buttons";
|
|
3
5
|
import {
|
|
4
6
|
AgentWidgetConfig,
|
|
5
7
|
AgentWidgetHeaderLayoutConfig,
|
|
6
8
|
AgentWidgetHeaderTrailingAction
|
|
7
9
|
} from "../types";
|
|
8
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
buildHeader,
|
|
12
|
+
HEADER_THEME_CSS,
|
|
13
|
+
HeaderElements,
|
|
14
|
+
attachHeaderToContainer as _attachHeaderToContainer,
|
|
15
|
+
} from "./header-builder";
|
|
9
16
|
|
|
10
17
|
export interface HeaderLayoutContext {
|
|
11
18
|
config: AgentWidgetConfig;
|
|
@@ -75,8 +82,27 @@ function appendTrailingHeaderActions(
|
|
|
75
82
|
} else if (a.label) {
|
|
76
83
|
btn.textContent = a.label;
|
|
77
84
|
}
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
|
|
86
|
+
if (a.menuItems?.length) {
|
|
87
|
+
// Wrap in a relative container for dropdown positioning
|
|
88
|
+
const wrapper = createElement("div", "persona-relative");
|
|
89
|
+
wrapper.appendChild(btn);
|
|
90
|
+
const dropdown = createDropdownMenu({
|
|
91
|
+
items: a.menuItems,
|
|
92
|
+
onSelect: (itemId) => onAction?.(itemId),
|
|
93
|
+
anchor: wrapper,
|
|
94
|
+
position: 'bottom-left',
|
|
95
|
+
});
|
|
96
|
+
wrapper.appendChild(dropdown.element);
|
|
97
|
+
btn.addEventListener("click", (e) => {
|
|
98
|
+
e.stopPropagation();
|
|
99
|
+
dropdown.toggle();
|
|
100
|
+
});
|
|
101
|
+
container.appendChild(wrapper);
|
|
102
|
+
} else {
|
|
103
|
+
btn.addEventListener("click", () => onAction?.(a.id));
|
|
104
|
+
container.appendChild(btn);
|
|
105
|
+
}
|
|
80
106
|
}
|
|
81
107
|
}
|
|
82
108
|
|
|
@@ -86,43 +112,88 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
|
|
|
86
112
|
|
|
87
113
|
const header = createElement(
|
|
88
114
|
"div",
|
|
89
|
-
"persona-flex persona-items-center persona-justify-between persona-
|
|
115
|
+
"persona-flex persona-items-center persona-justify-between persona-px-6 persona-py-4"
|
|
90
116
|
);
|
|
91
117
|
header.setAttribute("data-persona-theme-zone", "header");
|
|
118
|
+
header.style.backgroundColor = 'var(--persona-header-bg, var(--persona-surface, #ffffff))';
|
|
119
|
+
header.style.borderBottomColor = 'var(--persona-header-border, var(--persona-divider, #f1f5f9))';
|
|
120
|
+
header.style.boxShadow = 'var(--persona-header-shadow, none)';
|
|
121
|
+
header.style.borderBottom =
|
|
122
|
+
'var(--persona-header-border-bottom, 1px solid var(--persona-header-border, var(--persona-divider, #f1f5f9)))';
|
|
92
123
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
124
|
+
// Build the title area — either a combo button (titleMenu) or standard title row
|
|
125
|
+
const titleMenuConfig = layoutHeaderConfig?.titleMenu;
|
|
126
|
+
let titleRow: HTMLElement;
|
|
127
|
+
let headerTitle: HTMLElement;
|
|
128
|
+
|
|
129
|
+
if (titleMenuConfig) {
|
|
130
|
+
// Combo button replaces title + trailing actions + hover
|
|
131
|
+
const combo = createComboButton({
|
|
132
|
+
label: launcher.title ?? "Chat Assistant",
|
|
133
|
+
menuItems: titleMenuConfig.menuItems,
|
|
134
|
+
onSelect: titleMenuConfig.onSelect,
|
|
135
|
+
hover: titleMenuConfig.hover,
|
|
136
|
+
className: "",
|
|
137
|
+
});
|
|
138
|
+
titleRow = combo.element;
|
|
139
|
+
titleRow.style.color = HEADER_THEME_CSS.titleColor;
|
|
140
|
+
// The combo button's label span acts as headerTitle for update()
|
|
141
|
+
headerTitle = titleRow.querySelector(".persona-combo-btn-label") ?? titleRow;
|
|
142
|
+
} else {
|
|
143
|
+
titleRow = createElement(
|
|
144
|
+
"div",
|
|
145
|
+
"persona-flex persona-min-w-0 persona-flex-1 persona-items-center persona-gap-1"
|
|
146
|
+
);
|
|
97
147
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
148
|
+
// Title only (no icon, no subtitle)
|
|
149
|
+
headerTitle = createElement("span", "persona-text-base persona-font-semibold persona-truncate");
|
|
150
|
+
headerTitle.style.color = HEADER_THEME_CSS.titleColor;
|
|
151
|
+
headerTitle.textContent = launcher.title ?? "Chat Assistant";
|
|
101
152
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
153
|
+
titleRow.appendChild(headerTitle);
|
|
154
|
+
appendTrailingHeaderActions(
|
|
155
|
+
titleRow,
|
|
156
|
+
layoutHeaderConfig?.trailingActions,
|
|
157
|
+
layoutHeaderConfig?.onAction ?? onHeaderAction
|
|
158
|
+
);
|
|
108
159
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if ((e.target as HTMLElement).closest("button")) return;
|
|
118
|
-
handleTitleClick();
|
|
119
|
-
});
|
|
120
|
-
titleRow.addEventListener("keydown", (e) => {
|
|
121
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
122
|
-
e.preventDefault();
|
|
160
|
+
// Make title row clickable when onTitleClick is provided
|
|
161
|
+
if (layoutHeaderConfig?.onTitleClick) {
|
|
162
|
+
titleRow.style.cursor = "pointer";
|
|
163
|
+
titleRow.setAttribute("role", "button");
|
|
164
|
+
titleRow.setAttribute("tabindex", "0");
|
|
165
|
+
const handleTitleClick = layoutHeaderConfig.onTitleClick;
|
|
166
|
+
titleRow.addEventListener("click", (e) => {
|
|
167
|
+
if ((e.target as HTMLElement).closest("button")) return;
|
|
123
168
|
handleTitleClick();
|
|
124
|
-
}
|
|
125
|
-
|
|
169
|
+
});
|
|
170
|
+
titleRow.addEventListener("keydown", (e) => {
|
|
171
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
handleTitleClick();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Title row hover pill effect
|
|
179
|
+
const hoverCfg = layoutHeaderConfig?.titleRowHover;
|
|
180
|
+
if (hoverCfg) {
|
|
181
|
+
titleRow.style.borderRadius = hoverCfg.borderRadius ?? '10px';
|
|
182
|
+
titleRow.style.padding = hoverCfg.padding ?? '6px 4px 6px 12px';
|
|
183
|
+
titleRow.style.margin = '-6px 0 -6px -12px';
|
|
184
|
+
titleRow.style.border = '1px solid transparent';
|
|
185
|
+
titleRow.style.transition = 'background-color 0.15s ease, border-color 0.15s ease';
|
|
186
|
+
titleRow.style.width = 'fit-content';
|
|
187
|
+
titleRow.style.flex = 'none';
|
|
188
|
+
titleRow.addEventListener('mouseenter', () => {
|
|
189
|
+
titleRow.style.backgroundColor = hoverCfg.background ?? '';
|
|
190
|
+
titleRow.style.borderColor = hoverCfg.border ?? '';
|
|
191
|
+
});
|
|
192
|
+
titleRow.addEventListener('mouseleave', () => {
|
|
193
|
+
titleRow.style.backgroundColor = '';
|
|
194
|
+
titleRow.style.borderColor = 'transparent';
|
|
195
|
+
});
|
|
196
|
+
}
|
|
126
197
|
}
|
|
127
198
|
|
|
128
199
|
header.appendChild(titleRow);
|
|
@@ -133,21 +204,18 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
|
|
|
133
204
|
|
|
134
205
|
const closeButton = createElement(
|
|
135
206
|
"button",
|
|
136
|
-
"persona-inline-flex persona-items-center persona-justify-center persona-rounded-full
|
|
207
|
+
"persona-inline-flex persona-items-center persona-justify-center persona-rounded-full hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none"
|
|
137
208
|
) as HTMLButtonElement;
|
|
138
209
|
closeButton.style.height = closeButtonSize;
|
|
139
210
|
closeButton.style.width = closeButtonSize;
|
|
140
211
|
closeButton.type = "button";
|
|
141
212
|
closeButton.setAttribute("aria-label", "Close chat");
|
|
142
213
|
closeButton.style.display = showClose ? "" : "none";
|
|
214
|
+
closeButton.style.color =
|
|
215
|
+
launcher.closeButtonColor || HEADER_THEME_CSS.actionIconColor;
|
|
143
216
|
|
|
144
217
|
const closeButtonIconName = launcher.closeButtonIconName ?? "x";
|
|
145
|
-
const closeIconSvg = renderLucideIcon(
|
|
146
|
-
closeButtonIconName,
|
|
147
|
-
"20px",
|
|
148
|
-
launcher.closeButtonColor || "",
|
|
149
|
-
2
|
|
150
|
-
);
|
|
218
|
+
const closeIconSvg = renderLucideIcon(closeButtonIconName, "20px", "currentColor", 2);
|
|
151
219
|
if (closeIconSvg) {
|
|
152
220
|
closeButton.appendChild(closeIconSvg);
|
|
153
221
|
} else {
|
|
@@ -172,7 +240,7 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
|
|
|
172
240
|
return {
|
|
173
241
|
header,
|
|
174
242
|
iconHolder,
|
|
175
|
-
headerTitle
|
|
243
|
+
headerTitle,
|
|
176
244
|
headerSubtitle,
|
|
177
245
|
closeButton,
|
|
178
246
|
closeButtonWrapper,
|