@runtypelabs/persona 3.18.0 → 3.19.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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/dist/index.cjs +47 -47
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +281 -4
  5. package/dist/index.d.ts +281 -4
  6. package/dist/index.global.js +102 -1636
  7. package/dist/index.global.js.map +1 -1
  8. package/dist/index.js +47 -47
  9. package/dist/index.js.map +1 -1
  10. package/dist/theme-editor.cjs +1438 -619
  11. package/dist/theme-editor.d.cts +119 -1
  12. package/dist/theme-editor.d.ts +119 -1
  13. package/dist/theme-editor.js +1552 -619
  14. package/dist/widget.css +348 -0
  15. package/package.json +1 -1
  16. package/src/components/composer-builder.test.ts +52 -0
  17. package/src/components/composer-builder.ts +67 -490
  18. package/src/components/composer-parts.test.ts +152 -0
  19. package/src/components/composer-parts.ts +452 -0
  20. package/src/components/header-builder.ts +22 -299
  21. package/src/components/header-parts.ts +360 -0
  22. package/src/components/panel.test.ts +61 -0
  23. package/src/components/panel.ts +262 -5
  24. package/src/components/pill-composer-builder.test.ts +85 -0
  25. package/src/components/pill-composer-builder.ts +183 -0
  26. package/src/index.ts +4 -0
  27. package/src/runtime/init.ts +4 -2
  28. package/src/runtime/persist-state.test.ts +152 -0
  29. package/src/styles/widget.css +348 -0
  30. package/src/types.ts +121 -1
  31. package/src/ui.component-directive.test.ts +183 -0
  32. package/src/ui.composer-bar.test.ts +1009 -0
  33. package/src/ui.ts +809 -72
  34. package/src/utils/attachment-manager.ts +1 -1
  35. package/src/utils/dock.test.ts +45 -0
  36. package/src/utils/dock.ts +3 -0
  37. package/src/utils/icons.ts +314 -58
  38. package/src/utils/stream-animation.ts +7 -2
@@ -1,7 +1,7 @@
1
- import { createElement, createElementInDocument } from "../utils/dom";
1
+ import { createElement } from "../utils/dom";
2
2
  import { renderLucideIcon } from "../utils/icons";
3
3
  import { AgentWidgetConfig } from "../types";
4
- import { PORTALED_OVERLAY_Z_INDEX } from "../utils/constants";
4
+ import { createCloseButton, createClearChatButton } from "./header-parts";
5
5
 
6
6
  /** CSS `color` values; variables are set on `[data-persona-root]` from `theme.components.header`. */
7
7
  export const HEADER_THEME_CSS = {
@@ -50,7 +50,6 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
50
50
 
51
51
  const launcher = config?.launcher ?? {};
52
52
  const headerIconSize = launcher.headerIconSize ?? "48px";
53
- const closeButtonSize = launcher.closeButtonSize ?? "32px";
54
53
  const closeButtonPlacement = launcher.closeButtonPlacement ?? "inline";
55
54
  const headerIconHidden = launcher.headerIconHidden ?? false;
56
55
  const headerIconName = launcher.headerIconName;
@@ -119,320 +118,44 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
119
118
  let clearChatButtonWrapper: HTMLElement | null = null;
120
119
 
121
120
  if (clearChatEnabled) {
122
- const clearChatSize = clearChatConfig.size ?? "32px";
123
- const clearChatIconName = clearChatConfig.iconName ?? "refresh-cw";
124
- const clearChatIconColor = clearChatConfig.iconColor ?? "";
125
- const clearChatBgColor = clearChatConfig.backgroundColor ?? "";
126
- const clearChatBorderWidth = clearChatConfig.borderWidth ?? "";
127
- const clearChatBorderColor = clearChatConfig.borderColor ?? "";
128
- const clearChatBorderRadius = clearChatConfig.borderRadius ?? "";
129
- const clearChatPaddingX = clearChatConfig.paddingX ?? "";
130
- const clearChatPaddingY = clearChatConfig.paddingY ?? "";
131
- const clearChatTooltipText = clearChatConfig.tooltipText ?? "Clear chat";
132
- const clearChatShowTooltip = clearChatConfig.showTooltip ?? true;
133
-
134
- // Create button wrapper for tooltip - positioned based on placement
135
- // Note: Don't use persona-clear-chat-button-wrapper class for top-right mode as its
136
- // display: inline-flex causes alignment issues with the close button
137
- clearChatButtonWrapper = createElement(
138
- "div",
121
+ // Top-right placement uses an absolute wrapper offset from the close
122
+ // button (which lives at right: 16px and is ~32px wide, leaving ~48px
123
+ // from the panel's right edge for the clear icon).
124
+ const wrapperClassName =
139
125
  clearChatPlacement === "top-right"
140
126
  ? "persona-absolute persona-top-4 persona-z-50"
141
- : "persona-relative persona-ml-auto persona-clear-chat-button-wrapper"
142
- );
127
+ : "persona-relative persona-ml-auto persona-clear-chat-button-wrapper";
128
+
129
+ const parts = createClearChatButton(config, { wrapperClassName });
130
+ clearChatButton = parts.button;
131
+ clearChatButtonWrapper = parts.wrapper;
143
132
 
144
- // Position to the left of the close button (which is at right: 1rem/16px)
145
- // Close button is ~32px wide, plus small gap = 48px from right
146
133
  if (clearChatPlacement === "top-right") {
147
134
  clearChatButtonWrapper.style.right = "48px";
148
135
  }
149
136
 
150
- clearChatButton = createElement(
151
- "button",
152
- "persona-inline-flex persona-items-center persona-justify-center persona-rounded-full hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none"
153
- ) as HTMLButtonElement;
154
-
155
- clearChatButton.style.height = clearChatSize;
156
- clearChatButton.style.width = clearChatSize;
157
- clearChatButton.type = "button";
158
- clearChatButton.setAttribute("aria-label", clearChatTooltipText);
159
- clearChatButton.style.color =
160
- clearChatIconColor || HEADER_THEME_CSS.actionIconColor;
161
-
162
- // Add icon. display:block eliminates inline-baseline spacing that can
163
- // push the icon a fractional pixel off-center inside the button.
164
- const iconSvg = renderLucideIcon(clearChatIconName, "20px", "currentColor", 1);
165
- if (iconSvg) {
166
- iconSvg.style.display = "block";
167
- clearChatButton.appendChild(iconSvg);
168
- }
169
-
170
- if (clearChatBgColor) {
171
- clearChatButton.style.backgroundColor = clearChatBgColor;
172
- clearChatButton.classList.remove("hover:persona-bg-gray-100");
173
- }
174
-
175
- if (clearChatBorderWidth || clearChatBorderColor) {
176
- const borderWidth = clearChatBorderWidth || "0px";
177
- const borderColor = clearChatBorderColor || "transparent";
178
- clearChatButton.style.border = `${borderWidth} solid ${borderColor}`;
179
- clearChatButton.classList.remove("persona-border-none");
180
- }
181
-
182
- if (clearChatBorderRadius) {
183
- clearChatButton.style.borderRadius = clearChatBorderRadius;
184
- clearChatButton.classList.remove("persona-rounded-full");
185
- }
186
-
187
- // Apply padding styling
188
- if (clearChatPaddingX) {
189
- clearChatButton.style.paddingLeft = clearChatPaddingX;
190
- clearChatButton.style.paddingRight = clearChatPaddingX;
191
- } else {
192
- clearChatButton.style.paddingLeft = "";
193
- clearChatButton.style.paddingRight = "";
194
- }
195
- if (clearChatPaddingY) {
196
- clearChatButton.style.paddingTop = clearChatPaddingY;
197
- clearChatButton.style.paddingBottom = clearChatPaddingY;
198
- } else {
199
- clearChatButton.style.paddingTop = "";
200
- clearChatButton.style.paddingBottom = "";
201
- }
202
-
203
- clearChatButtonWrapper.appendChild(clearChatButton);
204
-
205
- // Add tooltip with portaling to document.body to escape overflow clipping
206
- if (
207
- clearChatShowTooltip &&
208
- clearChatTooltipText &&
209
- clearChatButton &&
210
- clearChatButtonWrapper
211
- ) {
212
- let portaledTooltip: HTMLElement | null = null;
213
-
214
- const showTooltip = () => {
215
- if (portaledTooltip || !clearChatButton) return; // Already showing or button doesn't exist
216
-
217
- const tooltipDocument = clearChatButton.ownerDocument;
218
- const tooltipContainer = tooltipDocument.body;
219
- if (!tooltipContainer) return;
220
-
221
- // Create tooltip element
222
- portaledTooltip = createElementInDocument(
223
- tooltipDocument,
224
- "div",
225
- "persona-clear-chat-tooltip"
226
- );
227
- portaledTooltip.textContent = clearChatTooltipText;
228
-
229
- // Add arrow
230
- const arrow = createElementInDocument(tooltipDocument, "div");
231
- arrow.className = "persona-clear-chat-tooltip-arrow";
232
- portaledTooltip.appendChild(arrow);
233
-
234
- // Get button position
235
- const buttonRect = clearChatButton.getBoundingClientRect();
236
-
237
- // Position tooltip above button
238
- portaledTooltip.style.position = "fixed";
239
- portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
240
- portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
241
- portaledTooltip.style.top = `${buttonRect.top - 8}px`;
242
- portaledTooltip.style.transform = "translate(-50%, -100%)";
243
-
244
- // Append to body
245
- tooltipContainer.appendChild(portaledTooltip);
246
- };
247
-
248
- const hideTooltip = () => {
249
- if (portaledTooltip && portaledTooltip.parentNode) {
250
- portaledTooltip.parentNode.removeChild(portaledTooltip);
251
- portaledTooltip = null;
252
- }
253
- };
254
-
255
- // Add event listeners
256
- clearChatButtonWrapper.addEventListener("mouseenter", showTooltip);
257
- clearChatButtonWrapper.addEventListener("mouseleave", hideTooltip);
258
- clearChatButton.addEventListener("focus", showTooltip);
259
- clearChatButton.addEventListener("blur", hideTooltip);
260
-
261
- // Store cleanup function on the button for later use
262
- (clearChatButtonWrapper as any)._cleanupTooltip = () => {
263
- hideTooltip();
264
- if (clearChatButtonWrapper) {
265
- clearChatButtonWrapper.removeEventListener("mouseenter", showTooltip);
266
- clearChatButtonWrapper.removeEventListener("mouseleave", hideTooltip);
267
- }
268
- if (clearChatButton) {
269
- clearChatButton.removeEventListener("focus", showTooltip);
270
- clearChatButton.removeEventListener("blur", hideTooltip);
271
- }
272
- };
273
- }
274
-
275
137
  // Only append to header if inline placement
276
138
  if (clearChatPlacement === "inline") {
277
139
  header.appendChild(clearChatButtonWrapper);
278
140
  }
279
141
  }
280
142
 
281
- // Create close button wrapper for tooltip positioning.
282
- // Mirrors the clear-chat wrapper's inline-flex centering so both
283
- // header action buttons vertically align identically within the
284
- // header's flex row.
285
- const closeButtonWrapper = createElement(
286
- "div",
143
+ // Build the close (×) button via the shared factory. The wrapper class
144
+ // mirrors the clear-chat wrapper's inline-flex centering so both header
145
+ // action buttons vertically align identically within the header's flex
146
+ // row. composer-bar mode uses the same factory directly with its own
147
+ // wrapper class to render a top-right-only close button.
148
+ const closeButtonWrapperClass =
287
149
  closeButtonPlacement === "top-right"
288
150
  ? "persona-absolute persona-top-4 persona-right-4 persona-z-50"
289
151
  : clearChatEnabled && clearChatPlacement === "inline"
290
152
  ? "persona-relative persona-inline-flex persona-items-center persona-justify-center"
291
- : "persona-relative persona-ml-auto persona-inline-flex persona-items-center persona-justify-center"
292
- );
293
-
294
- // Create close button with base classes
295
- const closeButton = createElement(
296
- "button",
297
- "persona-inline-flex persona-items-center persona-justify-center persona-rounded-full hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none"
298
- ) as HTMLButtonElement;
299
- closeButton.style.height = closeButtonSize;
300
- closeButton.style.width = closeButtonSize;
301
- closeButton.type = "button";
302
-
303
- // Get tooltip config
304
- const closeButtonTooltipText = launcher.closeButtonTooltipText ?? "Close chat";
305
- const closeButtonShowTooltip = launcher.closeButtonShowTooltip ?? true;
153
+ : "persona-relative persona-ml-auto persona-inline-flex persona-items-center persona-justify-center";
306
154
 
307
- closeButton.setAttribute("aria-label", closeButtonTooltipText);
308
- closeButton.style.display = showClose ? "" : "none";
309
-
310
- // Add icon or fallback text
311
- const closeButtonIconName = launcher.closeButtonIconName ?? "x";
312
- const closeButtonIconText = launcher.closeButtonIconText ?? "×";
313
- closeButton.style.color =
314
- launcher.closeButtonColor || HEADER_THEME_CSS.actionIconColor;
315
-
316
- // Try to render Lucide icon, fallback to text if not provided or fails.
317
- // The X glyph's paths occupy only the middle 50% of its 24x24 viewBox
318
- // (from 6,6 to 18,18), while other header icons (e.g. refresh-cw) span
319
- // ~75% of the viewBox. Rendering X at a larger intrinsic size brings
320
- // its visible extent into parity with sibling icons in the header.
321
- // display:block eliminates inline-baseline spacing that can push the
322
- // icon a fractional pixel off-center inside the button.
323
- const closeIconSvg = renderLucideIcon(closeButtonIconName, "28px", "currentColor", 1);
324
- if (closeIconSvg) {
325
- closeIconSvg.style.display = "block";
326
- closeButton.appendChild(closeIconSvg);
327
- } else {
328
- closeButton.textContent = closeButtonIconText;
329
- }
330
-
331
- if (launcher.closeButtonBackgroundColor) {
332
- closeButton.style.backgroundColor = launcher.closeButtonBackgroundColor;
333
- closeButton.classList.remove("hover:persona-bg-gray-100");
334
- } else {
335
- closeButton.style.backgroundColor = "";
336
- closeButton.classList.add("hover:persona-bg-gray-100");
337
- }
338
-
339
- // Apply border if width and/or color are provided
340
- if (launcher.closeButtonBorderWidth || launcher.closeButtonBorderColor) {
341
- const borderWidth = launcher.closeButtonBorderWidth || "0px";
342
- const borderColor = launcher.closeButtonBorderColor || "transparent";
343
- closeButton.style.border = `${borderWidth} solid ${borderColor}`;
344
- closeButton.classList.remove("persona-border-none");
345
- } else {
346
- closeButton.style.border = "";
347
- closeButton.classList.add("persona-border-none");
348
- }
349
-
350
- if (launcher.closeButtonBorderRadius) {
351
- closeButton.style.borderRadius = launcher.closeButtonBorderRadius;
352
- closeButton.classList.remove("persona-rounded-full");
353
- } else {
354
- closeButton.style.borderRadius = "";
355
- closeButton.classList.add("persona-rounded-full");
356
- }
357
-
358
- // Apply padding styling
359
- if (launcher.closeButtonPaddingX) {
360
- closeButton.style.paddingLeft = launcher.closeButtonPaddingX;
361
- closeButton.style.paddingRight = launcher.closeButtonPaddingX;
362
- } else {
363
- closeButton.style.paddingLeft = "";
364
- closeButton.style.paddingRight = "";
365
- }
366
- if (launcher.closeButtonPaddingY) {
367
- closeButton.style.paddingTop = launcher.closeButtonPaddingY;
368
- closeButton.style.paddingBottom = launcher.closeButtonPaddingY;
369
- } else {
370
- closeButton.style.paddingTop = "";
371
- closeButton.style.paddingBottom = "";
372
- }
373
-
374
- closeButtonWrapper.appendChild(closeButton);
375
-
376
- // Add tooltip with portaling to document.body to escape overflow clipping
377
- if (closeButtonShowTooltip && closeButtonTooltipText) {
378
- let portaledTooltip: HTMLElement | null = null;
379
-
380
- const showTooltip = () => {
381
- if (portaledTooltip) return; // Already showing
382
-
383
- const tooltipDocument = closeButton.ownerDocument;
384
- const tooltipContainer = tooltipDocument.body;
385
- if (!tooltipContainer) return;
386
-
387
- // Create tooltip element
388
- portaledTooltip = createElementInDocument(
389
- tooltipDocument,
390
- "div",
391
- "persona-clear-chat-tooltip"
392
- );
393
- portaledTooltip.textContent = closeButtonTooltipText;
394
-
395
- // Add arrow
396
- const arrow = createElementInDocument(tooltipDocument, "div");
397
- arrow.className = "persona-clear-chat-tooltip-arrow";
398
- portaledTooltip.appendChild(arrow);
399
-
400
- // Get button position
401
- const buttonRect = closeButton.getBoundingClientRect();
402
-
403
- // Position tooltip above button
404
- portaledTooltip.style.position = "fixed";
405
- portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
406
- portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
407
- portaledTooltip.style.top = `${buttonRect.top - 8}px`;
408
- portaledTooltip.style.transform = "translate(-50%, -100%)";
409
-
410
- // Append to body
411
- tooltipContainer.appendChild(portaledTooltip);
412
- };
413
-
414
- const hideTooltip = () => {
415
- if (portaledTooltip && portaledTooltip.parentNode) {
416
- portaledTooltip.parentNode.removeChild(portaledTooltip);
417
- portaledTooltip = null;
418
- }
419
- };
420
-
421
- // Add event listeners
422
- closeButtonWrapper.addEventListener("mouseenter", showTooltip);
423
- closeButtonWrapper.addEventListener("mouseleave", hideTooltip);
424
- closeButton.addEventListener("focus", showTooltip);
425
- closeButton.addEventListener("blur", hideTooltip);
426
-
427
- // Store cleanup function on the wrapper for later use
428
- (closeButtonWrapper as any)._cleanupTooltip = () => {
429
- hideTooltip();
430
- closeButtonWrapper.removeEventListener("mouseenter", showTooltip);
431
- closeButtonWrapper.removeEventListener("mouseleave", hideTooltip);
432
- closeButton.removeEventListener("focus", showTooltip);
433
- closeButton.removeEventListener("blur", hideTooltip);
434
- };
435
- }
155
+ const { button: closeButton, wrapper: closeButtonWrapper } = createCloseButton(
156
+ config,
157
+ { showClose, wrapperClassName: closeButtonWrapperClass }
158
+ );
436
159
 
437
160
  // Inline placement: append close button to header
438
161
  if (closeButtonPlacement !== "top-right") {