@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
|
@@ -4,6 +4,229 @@ import { AgentWidgetConfig } from "../types";
|
|
|
4
4
|
import { attachTooltip } from "../utils/tooltip";
|
|
5
5
|
import { HEADER_THEME_CSS } from "./header-builder";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Class hook for the shared header control box. The stylesheet sizes it from
|
|
9
|
+
* `--persona-header-control-size` (theme `components.header.controlSize`).
|
|
10
|
+
*/
|
|
11
|
+
export const HEADER_CONTROL_CLASS = "persona-header-control";
|
|
12
|
+
/** Adds `--persona-header-control-icon-size` sizing to the control's glyph. */
|
|
13
|
+
export const HEADER_CONTROL_GLYPH_CLASS = "persona-header-control--glyph";
|
|
14
|
+
/** Scales the glyph up for icons whose paths under-fill the 24x24 viewBox. */
|
|
15
|
+
export const HEADER_CONTROL_SPARSE_CLASS = "persona-header-control--sparse";
|
|
16
|
+
/**
|
|
17
|
+
* Icon-size token default in CSS pixels. Only derives the SVG attributes when a
|
|
18
|
+
* per-control size is unset; the box itself is owned by the stylesheet.
|
|
19
|
+
*/
|
|
20
|
+
const ICON_FALLBACK_PX = 20;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Icons whose Lucide paths occupy only the middle of the 24x24 viewBox (X spans
|
|
24
|
+
* 6,6 to 18,18; siblings such as refresh-cw span ~75%). Rendering them larger
|
|
25
|
+
* brings their visible extent into parity; the stroke is thinned by the same
|
|
26
|
+
* factor so the weight still matches. Keep the scale in sync with the
|
|
27
|
+
* `.persona-header-control--sparse` multiplier in widget.css.
|
|
28
|
+
*/
|
|
29
|
+
const SPARSE_GLYPHS: Record<string, [number, number]> = { x: [1.4, 1.05] };
|
|
30
|
+
|
|
31
|
+
// Flex, not block: an inline-flex button in a block wrapper reserves baseline
|
|
32
|
+
// slack and rides high inside it.
|
|
33
|
+
const DEFAULT_WRAPPER_CLASS =
|
|
34
|
+
"persona-relative persona-ml-auto persona-inline-flex persona-items-center persona-justify-center";
|
|
35
|
+
|
|
36
|
+
const DEFAULT_CLEAR_CHAT_WRAPPER_CLASS =
|
|
37
|
+
"persona-relative persona-ml-auto persona-clear-chat-button-wrapper";
|
|
38
|
+
|
|
39
|
+
export interface HeaderControlGlyphOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Explicit nominal glyph size. Unset hands the box to
|
|
42
|
+
* `--persona-header-control-icon-size` via the glyph class hook.
|
|
43
|
+
*/
|
|
44
|
+
iconSize?: string;
|
|
45
|
+
/** Text glyph rendered when the icon name is not in the registry. */
|
|
46
|
+
iconText?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Render (or re-render) a header control's glyph with the shared optical
|
|
51
|
+
* compensation, syncing the class hooks that let the stylesheet own the box.
|
|
52
|
+
* Both the factory and the live `update()` restyle path go through it so a
|
|
53
|
+
* config change can never repaint a control at a different size or weight.
|
|
54
|
+
*/
|
|
55
|
+
export const applyHeaderControlGlyph = (
|
|
56
|
+
button: HTMLButtonElement,
|
|
57
|
+
iconName: string,
|
|
58
|
+
options: HeaderControlGlyphOptions = {},
|
|
59
|
+
): void => {
|
|
60
|
+
const { iconSize, iconText } = options;
|
|
61
|
+
// [glyph scale, stroke width]; dense glyphs take the standard pair.
|
|
62
|
+
const [scale, stroke] = SPARSE_GLYPHS[iconName] ?? [1, 1.5];
|
|
63
|
+
// Without an explicit size the stylesheet owns the glyph box, so the sparse
|
|
64
|
+
// multiplier has to live there too.
|
|
65
|
+
const tokenGlyph = !iconSize;
|
|
66
|
+
button.classList.toggle(HEADER_CONTROL_GLYPH_CLASS, tokenGlyph);
|
|
67
|
+
button.classList.toggle(HEADER_CONTROL_SPARSE_CLASS, tokenGlyph && scale !== 1);
|
|
68
|
+
|
|
69
|
+
// Attribute sizing only decides the glyph when the stylesheet is not driving
|
|
70
|
+
// it; with the glyph class the CSS width/height wins over these attributes.
|
|
71
|
+
const nominalPx = parseFloat(iconSize ?? "") || ICON_FALLBACK_PX;
|
|
72
|
+
const icon = renderLucideIcon(
|
|
73
|
+
iconName,
|
|
74
|
+
`${Math.round(nominalPx * scale)}px`,
|
|
75
|
+
"currentColor",
|
|
76
|
+
stroke,
|
|
77
|
+
);
|
|
78
|
+
if (icon) {
|
|
79
|
+
// Inline SVG baseline spacing pushes the glyph a fractional pixel
|
|
80
|
+
// off-center inside the button.
|
|
81
|
+
icon.style.display = "block";
|
|
82
|
+
if (!tokenGlyph) {
|
|
83
|
+
// Explicit sizing detaches the glyph classes, so the stroke token has to
|
|
84
|
+
// ride inline; the attribute stroke stays as the no-token fallback.
|
|
85
|
+
icon.style.strokeWidth = `calc(var(--persona-components-header-controlStrokeWidth, 1.5) * ${Number((stroke / 1.5).toFixed(4))})`;
|
|
86
|
+
}
|
|
87
|
+
button.replaceChildren(icon);
|
|
88
|
+
} else if (iconText) {
|
|
89
|
+
button.textContent = iconText;
|
|
90
|
+
}
|
|
91
|
+
// Unrenderable glyph with no text fallback keeps whatever was mounted; an
|
|
92
|
+
// empty button is worse than a stale one.
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export interface HeaderIconButtonParts {
|
|
96
|
+
button: HTMLButtonElement;
|
|
97
|
+
wrapper: HTMLElement;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface CreateHeaderIconButtonOptions {
|
|
101
|
+
/** Required: also the default tooltip text. */
|
|
102
|
+
ariaLabel: string;
|
|
103
|
+
/**
|
|
104
|
+
* Attach the shared styled tooltip, reading the button's live `aria-label`
|
|
105
|
+
* so a relabel (e.g. Messages while busy) stays accurate. `false` opts out;
|
|
106
|
+
* default true.
|
|
107
|
+
*/
|
|
108
|
+
tooltip?: boolean;
|
|
109
|
+
/** Muted chip after the tooltip label, e.g. a keyboard shortcut. */
|
|
110
|
+
tooltipHint?: string | (() => string);
|
|
111
|
+
/** Lucide icon name; falls back to `iconText` when it is not in the registry. */
|
|
112
|
+
iconName?: string;
|
|
113
|
+
/** Text glyph rendered when no icon resolves. */
|
|
114
|
+
iconText?: string;
|
|
115
|
+
/** Wrapper className. Always flex so the button can never ride off-center. */
|
|
116
|
+
wrapperClassName?: string;
|
|
117
|
+
/**
|
|
118
|
+
* Explicit box size. Unset leaves `--persona-header-control-size` in charge;
|
|
119
|
+
* set, it writes an inline box that overrides the token.
|
|
120
|
+
*/
|
|
121
|
+
size?: string;
|
|
122
|
+
/**
|
|
123
|
+
* Explicit nominal glyph size. Unset leaves `--persona-header-control-icon-size`
|
|
124
|
+
* in charge. Sparse-viewBox compensation applies on top either way.
|
|
125
|
+
*/
|
|
126
|
+
iconSize?: string;
|
|
127
|
+
/** Falls back to the header zone's action-icon color. */
|
|
128
|
+
color?: string;
|
|
129
|
+
/** Set, it replaces the transparent + hover-fill default classes. */
|
|
130
|
+
backgroundColor?: string;
|
|
131
|
+
borderWidth?: string;
|
|
132
|
+
borderColor?: string;
|
|
133
|
+
borderRadius?: string;
|
|
134
|
+
paddingX?: string;
|
|
135
|
+
paddingY?: string;
|
|
136
|
+
/** Renders `display: none` without removing the control. */
|
|
137
|
+
hidden?: boolean;
|
|
138
|
+
/** Appended to the button's class list (e.g. a stable selector hook). */
|
|
139
|
+
extraClassName?: string;
|
|
140
|
+
/** Extra attributes, applied after `type` and `aria-label`. */
|
|
141
|
+
attrs?: Record<string, string>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The one header icon-button recipe: close, clear chat, trailing actions, and
|
|
146
|
+
* the Messages control all build through it so their box, glyph, and stroke
|
|
147
|
+
* can never drift apart. Config keys stay inline styles, which win over the
|
|
148
|
+
* token-driven stylesheet rules.
|
|
149
|
+
*/
|
|
150
|
+
export const createHeaderIconButton = (
|
|
151
|
+
options: CreateHeaderIconButtonOptions,
|
|
152
|
+
): HeaderIconButtonParts => {
|
|
153
|
+
const {
|
|
154
|
+
ariaLabel,
|
|
155
|
+
tooltip = true,
|
|
156
|
+
tooltipHint,
|
|
157
|
+
iconName,
|
|
158
|
+
iconText,
|
|
159
|
+
wrapperClassName = DEFAULT_WRAPPER_CLASS,
|
|
160
|
+
size,
|
|
161
|
+
iconSize,
|
|
162
|
+
color,
|
|
163
|
+
backgroundColor,
|
|
164
|
+
borderWidth,
|
|
165
|
+
borderColor,
|
|
166
|
+
borderRadius,
|
|
167
|
+
paddingX,
|
|
168
|
+
paddingY,
|
|
169
|
+
hidden,
|
|
170
|
+
extraClassName,
|
|
171
|
+
attrs,
|
|
172
|
+
} = options;
|
|
173
|
+
|
|
174
|
+
const wrapper = createElement("div", wrapperClassName);
|
|
175
|
+
|
|
176
|
+
const hasBorder = Boolean(borderWidth || borderColor);
|
|
177
|
+
|
|
178
|
+
// hover-bg / border-none / rounded-full are default utility classes that
|
|
179
|
+
// apply only when the matching style override is absent; an override sets
|
|
180
|
+
// the inline style instead (cx omits the class).
|
|
181
|
+
const button = createNode("button", {
|
|
182
|
+
className: cx(
|
|
183
|
+
HEADER_CONTROL_CLASS,
|
|
184
|
+
"persona-inline-flex persona-items-center persona-justify-center persona-cursor-pointer",
|
|
185
|
+
!backgroundColor && "persona-bg-transparent hover:persona-bg-gray-100",
|
|
186
|
+
!hasBorder && "persona-border-none",
|
|
187
|
+
!borderRadius && "persona-rounded-full",
|
|
188
|
+
extraClassName,
|
|
189
|
+
),
|
|
190
|
+
attrs: { type: "button", "aria-label": ariaLabel, ...attrs },
|
|
191
|
+
style: {
|
|
192
|
+
width: size,
|
|
193
|
+
height: size,
|
|
194
|
+
minWidth: size,
|
|
195
|
+
minHeight: size,
|
|
196
|
+
display: hidden ? "none" : undefined,
|
|
197
|
+
color: color || HEADER_THEME_CSS.actionIconColor,
|
|
198
|
+
// Empty strings are a no-op on a fresh element, so they need no guard.
|
|
199
|
+
backgroundColor,
|
|
200
|
+
border: hasBorder
|
|
201
|
+
? `${borderWidth || "0px"} solid ${borderColor || "transparent"}`
|
|
202
|
+
: undefined,
|
|
203
|
+
borderRadius,
|
|
204
|
+
paddingLeft: paddingX,
|
|
205
|
+
paddingRight: paddingX,
|
|
206
|
+
paddingTop: paddingY,
|
|
207
|
+
paddingBottom: paddingY,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (iconName) {
|
|
212
|
+
applyHeaderControlGlyph(button, iconName, { iconSize, iconText });
|
|
213
|
+
} else if (iconText) {
|
|
214
|
+
button.textContent = iconText;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
wrapper.appendChild(button);
|
|
218
|
+
// attachTooltip is per-anchor idempotent, and a disabled attach replaces any
|
|
219
|
+
// previous enabled one, so update() rebuilds stay safe.
|
|
220
|
+
attachTooltip({
|
|
221
|
+
anchor: button,
|
|
222
|
+
trigger: wrapper,
|
|
223
|
+
text: () => button.getAttribute("aria-label") ?? ariaLabel,
|
|
224
|
+
hint: tooltipHint,
|
|
225
|
+
enabled: tooltip,
|
|
226
|
+
});
|
|
227
|
+
return { button, wrapper };
|
|
228
|
+
};
|
|
229
|
+
|
|
7
230
|
export interface CloseButtonParts {
|
|
8
231
|
button: HTMLButtonElement;
|
|
9
232
|
wrapper: HTMLElement;
|
|
@@ -31,9 +254,8 @@ export interface CreateCloseButtonOptions {
|
|
|
31
254
|
*/
|
|
32
255
|
buttonSize?: string;
|
|
33
256
|
/**
|
|
34
|
-
*
|
|
35
|
-
* `buttonSize` when scaling the whole control down
|
|
36
|
-
* 28px icon will overflow a smaller button.
|
|
257
|
+
* Explicit nominal icon size. Unset means the header control icon-size token
|
|
258
|
+
* owns the glyph. Pair with `buttonSize` when scaling the whole control down.
|
|
37
259
|
*/
|
|
38
260
|
iconSize?: string;
|
|
39
261
|
}
|
|
@@ -48,22 +270,17 @@ export interface CreateClearChatButtonOptions {
|
|
|
48
270
|
/**
|
|
49
271
|
* Explicit button-size override that wins over `launcher.clearChat.size`.
|
|
50
272
|
* Composer-bar mode uses this so the clear icon visually matches the
|
|
51
|
-
* shrunken close button
|
|
52
|
-
* launcher's 32px default.
|
|
273
|
+
* shrunken close button and doesn't inherit the header control size.
|
|
53
274
|
*/
|
|
54
275
|
buttonSize?: string;
|
|
55
|
-
/**
|
|
276
|
+
/** Explicit icon size; unset leaves the header control icon-size token. */
|
|
56
277
|
iconSize?: string;
|
|
57
278
|
}
|
|
58
279
|
|
|
59
|
-
const DEFAULT_WRAPPER_CLASS =
|
|
60
|
-
"persona-relative persona-ml-auto persona-inline-flex persona-items-center persona-justify-center";
|
|
61
|
-
|
|
62
280
|
/**
|
|
63
|
-
* Build the close (×) button + tooltip used in the panel header.
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* + config-driven styling logic.
|
|
281
|
+
* Build the close (×) button + tooltip used in the panel header. Every header
|
|
282
|
+
* layout routes through this so composer-bar mode can render just a close
|
|
283
|
+
* button (no full header strip) without duplicating the config-driven styling.
|
|
67
284
|
*/
|
|
68
285
|
export const createCloseButton = (
|
|
69
286
|
config: AgentWidgetConfig | undefined,
|
|
@@ -73,87 +290,37 @@ export const createCloseButton = (
|
|
|
73
290
|
showClose = true,
|
|
74
291
|
wrapperClassName = DEFAULT_WRAPPER_CLASS,
|
|
75
292
|
buttonSize,
|
|
76
|
-
iconSize
|
|
293
|
+
iconSize,
|
|
77
294
|
} = options;
|
|
78
295
|
const launcher = config?.launcher ?? {};
|
|
79
|
-
// Call-site `buttonSize` (if provided) wins over launcher config. The
|
|
80
|
-
// launcher's `closeButtonSize` is set in DEFAULT_WIDGET_CONFIG so it's
|
|
81
|
-
// never undefined, which means the call-site override is the only way
|
|
82
|
-
// to opt a specific render path (like composer-bar's minimal close) into
|
|
83
|
-
// a different size.
|
|
84
|
-
const closeButtonSize = buttonSize ?? launcher.closeButtonSize ?? "32px";
|
|
85
|
-
|
|
86
|
-
const wrapper = createElement("div", wrapperClassName);
|
|
87
|
-
|
|
88
296
|
const closeButtonTooltipText = launcher.closeButtonTooltipText ?? "Close chat";
|
|
89
297
|
const closeButtonShowTooltip = launcher.closeButtonShowTooltip ?? true;
|
|
90
|
-
const closeButtonIconName = launcher.closeButtonIconName ?? "x";
|
|
91
|
-
const closeButtonIconText = launcher.closeButtonIconText ?? "×";
|
|
92
|
-
// hover-bg / border-none / rounded-full are default utility classes that
|
|
93
|
-
// apply only when the matching style override is absent; an override sets
|
|
94
|
-
// the inline style instead (cx omits the class).
|
|
95
|
-
const hasCloseBorder = Boolean(
|
|
96
|
-
launcher.closeButtonBorderWidth || launcher.closeButtonBorderColor
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
const button = createNode("button", {
|
|
100
|
-
className: cx(
|
|
101
|
-
"persona-inline-flex persona-items-center persona-justify-center persona-cursor-pointer",
|
|
102
|
-
!launcher.closeButtonBackgroundColor && "hover:persona-bg-gray-100",
|
|
103
|
-
!hasCloseBorder && "persona-border-none",
|
|
104
|
-
!launcher.closeButtonBorderRadius && "persona-rounded-full"
|
|
105
|
-
),
|
|
106
|
-
attrs: { type: "button", "aria-label": closeButtonTooltipText },
|
|
107
|
-
style: {
|
|
108
|
-
height: closeButtonSize,
|
|
109
|
-
width: closeButtonSize,
|
|
110
|
-
display: showClose ? undefined : "none",
|
|
111
|
-
color: launcher.closeButtonColor || HEADER_THEME_CSS.actionIconColor,
|
|
112
|
-
backgroundColor: launcher.closeButtonBackgroundColor || undefined,
|
|
113
|
-
border: hasCloseBorder
|
|
114
|
-
? `${launcher.closeButtonBorderWidth || "0px"} solid ${launcher.closeButtonBorderColor || "transparent"}`
|
|
115
|
-
: undefined,
|
|
116
|
-
borderRadius: launcher.closeButtonBorderRadius || undefined,
|
|
117
|
-
paddingLeft: launcher.closeButtonPaddingX || undefined,
|
|
118
|
-
paddingRight: launcher.closeButtonPaddingX || undefined,
|
|
119
|
-
paddingTop: launcher.closeButtonPaddingY || undefined,
|
|
120
|
-
paddingBottom: launcher.closeButtonPaddingY || undefined,
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
298
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
text: () => button.getAttribute("aria-label") ?? closeButtonTooltipText,
|
|
143
|
-
enabled: closeButtonShowTooltip,
|
|
299
|
+
const { button, wrapper } = createHeaderIconButton({
|
|
300
|
+
ariaLabel: closeButtonTooltipText,
|
|
301
|
+
tooltip: closeButtonShowTooltip,
|
|
302
|
+
iconName: launcher.closeButtonIconName ?? "x",
|
|
303
|
+
iconText: launcher.closeButtonIconText ?? "×",
|
|
304
|
+
wrapperClassName,
|
|
305
|
+
// Call-site `buttonSize` wins over launcher config; both unset means the
|
|
306
|
+
// header control-size token owns the box.
|
|
307
|
+
size: buttonSize ?? launcher.closeButtonSize,
|
|
308
|
+
iconSize,
|
|
309
|
+
color: launcher.closeButtonColor,
|
|
310
|
+
backgroundColor: launcher.closeButtonBackgroundColor,
|
|
311
|
+
borderWidth: launcher.closeButtonBorderWidth,
|
|
312
|
+
borderColor: launcher.closeButtonBorderColor,
|
|
313
|
+
borderRadius: launcher.closeButtonBorderRadius,
|
|
314
|
+
paddingX: launcher.closeButtonPaddingX,
|
|
315
|
+
paddingY: launcher.closeButtonPaddingY,
|
|
316
|
+
hidden: !showClose,
|
|
144
317
|
});
|
|
145
318
|
|
|
146
319
|
return { button, wrapper };
|
|
147
320
|
};
|
|
148
321
|
|
|
149
|
-
const DEFAULT_CLEAR_CHAT_WRAPPER_CLASS =
|
|
150
|
-
"persona-relative persona-ml-auto persona-clear-chat-button-wrapper";
|
|
151
|
-
|
|
152
322
|
/**
|
|
153
323
|
* Build the clear-chat (refresh) button + tooltip used in the panel header.
|
|
154
|
-
* Extracted from `header-builder.ts` so composer-bar mode can render a
|
|
155
|
-
* "start over" button alongside its close icon without duplicating the
|
|
156
|
-
* tooltip + config-driven styling logic.
|
|
157
324
|
*
|
|
158
325
|
* The factory only handles construction. Wiring the click to the
|
|
159
326
|
* clear-history handler is owned by `setupClearChatButton()` in `ui.ts`,
|
|
@@ -166,69 +333,30 @@ export const createClearChatButton = (
|
|
|
166
333
|
const {
|
|
167
334
|
wrapperClassName = DEFAULT_CLEAR_CHAT_WRAPPER_CLASS,
|
|
168
335
|
buttonSize,
|
|
169
|
-
iconSize
|
|
336
|
+
iconSize,
|
|
170
337
|
} = options;
|
|
171
338
|
|
|
172
339
|
const launcher = config?.launcher ?? {};
|
|
173
340
|
const clearChatConfig = launcher.clearChat ?? {};
|
|
174
|
-
// Call-site `buttonSize` (when provided) wins over launcher.clearChat.size.
|
|
175
|
-
// Same precedence rule as createCloseButton: callers like composer-bar
|
|
176
|
-
// intentionally override the inherited launcher default to fit their UX.
|
|
177
|
-
const clearChatSize = buttonSize ?? clearChatConfig.size ?? "32px";
|
|
178
|
-
const clearChatIconName = clearChatConfig.iconName ?? "refresh-cw";
|
|
179
|
-
const clearChatIconColor = clearChatConfig.iconColor ?? "";
|
|
180
|
-
const clearChatBgColor = clearChatConfig.backgroundColor ?? "";
|
|
181
|
-
const clearChatBorderWidth = clearChatConfig.borderWidth ?? "";
|
|
182
|
-
const clearChatBorderColor = clearChatConfig.borderColor ?? "";
|
|
183
|
-
const clearChatBorderRadius = clearChatConfig.borderRadius ?? "";
|
|
184
|
-
const clearChatPaddingX = clearChatConfig.paddingX ?? "";
|
|
185
|
-
const clearChatPaddingY = clearChatConfig.paddingY ?? "";
|
|
186
341
|
const clearChatTooltipText = clearChatConfig.tooltipText ?? "Clear chat";
|
|
187
342
|
const clearChatShowTooltip = clearChatConfig.showTooltip ?? true;
|
|
188
343
|
|
|
189
|
-
const wrapper =
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
height: clearChatSize,
|
|
206
|
-
width: clearChatSize,
|
|
207
|
-
color: clearChatIconColor || HEADER_THEME_CSS.actionIconColor,
|
|
208
|
-
backgroundColor: clearChatBgColor || undefined,
|
|
209
|
-
border: hasClearChatBorder
|
|
210
|
-
? `${clearChatBorderWidth || "0px"} solid ${clearChatBorderColor || "transparent"}`
|
|
211
|
-
: undefined,
|
|
212
|
-
borderRadius: clearChatBorderRadius || undefined,
|
|
213
|
-
paddingLeft: clearChatPaddingX || undefined,
|
|
214
|
-
paddingRight: clearChatPaddingX || undefined,
|
|
215
|
-
paddingTop: clearChatPaddingY || undefined,
|
|
216
|
-
paddingBottom: clearChatPaddingY || undefined,
|
|
217
|
-
},
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const iconSvg = renderLucideIcon(clearChatIconName, iconSize, "currentColor", 1);
|
|
221
|
-
if (iconSvg) {
|
|
222
|
-
iconSvg.style.display = "block";
|
|
223
|
-
button.appendChild(iconSvg);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
wrapper.appendChild(button);
|
|
227
|
-
attachTooltip({
|
|
228
|
-
anchor: button,
|
|
229
|
-
trigger: wrapper,
|
|
230
|
-
text: () => button.getAttribute("aria-label") ?? clearChatTooltipText,
|
|
231
|
-
enabled: clearChatShowTooltip,
|
|
344
|
+
const { button, wrapper } = createHeaderIconButton({
|
|
345
|
+
ariaLabel: clearChatTooltipText,
|
|
346
|
+
tooltip: clearChatShowTooltip,
|
|
347
|
+
iconName: clearChatConfig.iconName ?? "refresh-cw",
|
|
348
|
+
wrapperClassName,
|
|
349
|
+
// Call-site `buttonSize` wins over `launcher.clearChat.size`; both unset
|
|
350
|
+
// means the header control-size token owns the box.
|
|
351
|
+
size: buttonSize ?? clearChatConfig.size,
|
|
352
|
+
iconSize,
|
|
353
|
+
color: clearChatConfig.iconColor,
|
|
354
|
+
backgroundColor: clearChatConfig.backgroundColor,
|
|
355
|
+
borderWidth: clearChatConfig.borderWidth,
|
|
356
|
+
borderColor: clearChatConfig.borderColor,
|
|
357
|
+
borderRadius: clearChatConfig.borderRadius,
|
|
358
|
+
paddingX: clearChatConfig.paddingX,
|
|
359
|
+
paddingY: clearChatConfig.paddingY,
|
|
232
360
|
});
|
|
233
361
|
|
|
234
362
|
return { button, wrapper };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell-owned destructive confirmation for history actions
|
|
3
|
+
* (`docs/visitor-history-implementation-plan.md` D7, accessibility contract).
|
|
4
|
+
*
|
|
5
|
+
* An alert dialog: labelled and described, focus-trapped, least-destructive
|
|
6
|
+
* action focused first, Escape resolves `false`, and focus returns to whatever
|
|
7
|
+
* was focused when it opened. Styling is inline so the dialog costs no bytes in
|
|
8
|
+
* `widget.css`; colors come from the same theme variables the panel uses.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createNode } from "../utils/dom";
|
|
12
|
+
|
|
13
|
+
export interface HistoryConfirmOptions {
|
|
14
|
+
title: string;
|
|
15
|
+
description: string;
|
|
16
|
+
confirmLabel: string;
|
|
17
|
+
cancelLabel: string;
|
|
18
|
+
/** Where the modal mounts. Normally the widget container. */
|
|
19
|
+
host: HTMLElement;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const FOCUSABLE = "button:not([disabled])";
|
|
23
|
+
|
|
24
|
+
/** Resolves `true` only for the confirming action. */
|
|
25
|
+
export function showHistoryConfirm(
|
|
26
|
+
options: HistoryConfirmOptions
|
|
27
|
+
): Promise<boolean> {
|
|
28
|
+
const previouslyFocused =
|
|
29
|
+
document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
30
|
+
const id = Math.random().toString(36).slice(2, 8);
|
|
31
|
+
const titleId = `persona-history-confirm-title-${id}`;
|
|
32
|
+
const descriptionId = `persona-history-confirm-desc-${id}`;
|
|
33
|
+
|
|
34
|
+
const cancel = createNode("button", {
|
|
35
|
+
className: "persona-history-confirm__cancel",
|
|
36
|
+
text: options.cancelLabel,
|
|
37
|
+
attrs: { type: "button" },
|
|
38
|
+
});
|
|
39
|
+
const confirm = createNode("button", {
|
|
40
|
+
className: "persona-history-confirm__confirm",
|
|
41
|
+
text: options.confirmLabel,
|
|
42
|
+
attrs: { type: "button", "data-persona-destructive": "true" },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const dialog = createNode(
|
|
46
|
+
"div",
|
|
47
|
+
{
|
|
48
|
+
className: "persona-history-confirm__dialog",
|
|
49
|
+
attrs: {
|
|
50
|
+
role: "alertdialog",
|
|
51
|
+
"aria-modal": "true",
|
|
52
|
+
"aria-labelledby": titleId,
|
|
53
|
+
"aria-describedby": descriptionId,
|
|
54
|
+
},
|
|
55
|
+
style: {
|
|
56
|
+
maxWidth: "22rem",
|
|
57
|
+
width: "100%",
|
|
58
|
+
borderRadius: "var(--persona-radius-lg, 0.75rem)",
|
|
59
|
+
background: "var(--persona-surface, #ffffff)",
|
|
60
|
+
// The real text token, never `inherit`: the host is the panel in
|
|
61
|
+
// split layouts, whose cascaded color is the page's, not the theme's.
|
|
62
|
+
color: "var(--persona-text, #111827)",
|
|
63
|
+
boxShadow:
|
|
64
|
+
"var(--persona-history-confirm-shadow, 0 20px 40px -12px rgba(0, 0, 0, 0.35))",
|
|
65
|
+
padding: "20px",
|
|
66
|
+
display: "flex",
|
|
67
|
+
flexDirection: "column",
|
|
68
|
+
gap: "12px",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
createNode("h2", {
|
|
72
|
+
className: "persona-history-confirm__title",
|
|
73
|
+
text: options.title,
|
|
74
|
+
attrs: { id: titleId },
|
|
75
|
+
style: { margin: "0", fontSize: "1rem", fontWeight: "600" },
|
|
76
|
+
}),
|
|
77
|
+
createNode("p", {
|
|
78
|
+
className: "persona-history-confirm__description",
|
|
79
|
+
text: options.description,
|
|
80
|
+
attrs: { id: descriptionId },
|
|
81
|
+
style: { margin: "0", fontSize: "0.875rem", lineHeight: "1.4" },
|
|
82
|
+
}),
|
|
83
|
+
createNode(
|
|
84
|
+
"div",
|
|
85
|
+
{
|
|
86
|
+
className: "persona-history-confirm__actions",
|
|
87
|
+
style: {
|
|
88
|
+
display: "flex",
|
|
89
|
+
gap: "8px",
|
|
90
|
+
justifyContent: "flex-end",
|
|
91
|
+
flexWrap: "wrap",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
cancel,
|
|
95
|
+
confirm
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
for (const button of [cancel, confirm]) {
|
|
100
|
+
button.style.minHeight = "44px";
|
|
101
|
+
button.style.minWidth = "88px";
|
|
102
|
+
button.style.padding = "0 16px";
|
|
103
|
+
button.style.borderRadius = "var(--persona-radius-md, 0.5rem)";
|
|
104
|
+
button.style.cursor = "pointer";
|
|
105
|
+
button.style.font = "inherit";
|
|
106
|
+
}
|
|
107
|
+
cancel.style.border = "1px solid var(--persona-border, rgba(0,0,0,0.12))";
|
|
108
|
+
cancel.style.background = "transparent";
|
|
109
|
+
cancel.style.color = "inherit";
|
|
110
|
+
confirm.style.border = "none";
|
|
111
|
+
confirm.style.background = "var(--persona-danger, #b42318)";
|
|
112
|
+
confirm.style.color = "var(--persona-danger-fg, #ffffff)";
|
|
113
|
+
|
|
114
|
+
const overlay = createNode(
|
|
115
|
+
"div",
|
|
116
|
+
{
|
|
117
|
+
className: "persona-history-confirm",
|
|
118
|
+
style: {
|
|
119
|
+
position: "absolute",
|
|
120
|
+
inset: "0",
|
|
121
|
+
zIndex: "40",
|
|
122
|
+
display: "flex",
|
|
123
|
+
alignItems: "center",
|
|
124
|
+
justifyContent: "center",
|
|
125
|
+
padding: "16px",
|
|
126
|
+
background: "var(--persona-history-confirm-scrim, rgba(15, 23, 42, 0.45))",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
dialog
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return new Promise<boolean>((resolve) => {
|
|
133
|
+
let settled = false;
|
|
134
|
+
const close = (result: boolean): void => {
|
|
135
|
+
if (settled) return;
|
|
136
|
+
settled = true;
|
|
137
|
+
overlay.removeEventListener("keydown", onKeydown, true);
|
|
138
|
+
overlay.remove();
|
|
139
|
+
previouslyFocused?.focus();
|
|
140
|
+
resolve(result);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
function onKeydown(event: KeyboardEvent): void {
|
|
144
|
+
if (event.key === "Escape") {
|
|
145
|
+
event.preventDefault();
|
|
146
|
+
event.stopPropagation();
|
|
147
|
+
close(false);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (event.key !== "Tab") return;
|
|
151
|
+
// Trap: only the two actions are reachable while the alert is open.
|
|
152
|
+
const focusable = Array.from(dialog.querySelectorAll<HTMLElement>(FOCUSABLE));
|
|
153
|
+
if (focusable.length === 0) return;
|
|
154
|
+
const first = focusable[0]!;
|
|
155
|
+
const last = focusable[focusable.length - 1]!;
|
|
156
|
+
const active = document.activeElement;
|
|
157
|
+
if (event.shiftKey && (active === first || !dialog.contains(active))) {
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
last.focus();
|
|
160
|
+
} else if (!event.shiftKey && active === last) {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
first.focus();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
overlay.addEventListener("keydown", onKeydown, true);
|
|
167
|
+
// A click on the scrim is not a destructive confirmation.
|
|
168
|
+
overlay.addEventListener("pointerdown", (event) => {
|
|
169
|
+
if (event.target === overlay) close(false);
|
|
170
|
+
});
|
|
171
|
+
cancel.addEventListener("click", () => close(false));
|
|
172
|
+
confirm.addEventListener("click", () => close(true));
|
|
173
|
+
|
|
174
|
+
options.host.appendChild(overlay);
|
|
175
|
+
cancel.focus();
|
|
176
|
+
});
|
|
177
|
+
}
|