@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.
Files changed (43) hide show
  1. package/README.md +222 -5
  2. package/dist/index.cjs +42 -42
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +832 -571
  5. package/dist/index.d.ts +832 -571
  6. package/dist/index.global.js +88 -88
  7. package/dist/index.global.js.map +1 -1
  8. package/dist/index.js +42 -42
  9. package/dist/index.js.map +1 -1
  10. package/dist/widget.css +257 -67
  11. package/package.json +2 -4
  12. package/src/components/artifact-card.ts +39 -5
  13. package/src/components/artifact-pane.ts +68 -127
  14. package/src/components/composer-builder.ts +3 -23
  15. package/src/components/header-builder.ts +29 -34
  16. package/src/components/header-layouts.ts +109 -41
  17. package/src/components/launcher.ts +10 -7
  18. package/src/components/message-bubble.ts +7 -11
  19. package/src/components/panel.ts +4 -4
  20. package/src/defaults.ts +22 -93
  21. package/src/index.ts +20 -7
  22. package/src/presets.ts +66 -51
  23. package/src/runtime/host-layout.test.ts +333 -0
  24. package/src/runtime/host-layout.ts +346 -27
  25. package/src/runtime/init.test.ts +113 -8
  26. package/src/runtime/init.ts +1 -1
  27. package/src/styles/widget.css +257 -67
  28. package/src/types/theme.ts +76 -0
  29. package/src/types.ts +86 -97
  30. package/src/ui.docked.test.ts +203 -7
  31. package/src/ui.ts +125 -92
  32. package/src/utils/artifact-gate.ts +1 -1
  33. package/src/utils/buttons.ts +417 -0
  34. package/src/utils/code-generators.test.ts +43 -7
  35. package/src/utils/code-generators.ts +9 -25
  36. package/src/utils/deep-merge.ts +26 -0
  37. package/src/utils/dock.ts +18 -5
  38. package/src/utils/dropdown.ts +178 -0
  39. package/src/utils/theme.test.ts +90 -15
  40. package/src/utils/theme.ts +20 -46
  41. package/src/utils/tokens.ts +108 -11
  42. package/src/styles/tailwind.css +0 -20
  43. package/src/utils/migration.ts +0 -220
package/src/ui.ts CHANGED
@@ -27,15 +27,17 @@ import {
27
27
  } from "./types";
28
28
  import { AttachmentManager } from "./utils/attachment-manager";
29
29
  import { createTextPart, ALL_SUPPORTED_MIME_TYPES } from "./utils/content";
30
- import { applyThemeVariables, createThemeObserver } from "./utils/theme";
30
+ import { applyThemeVariables, createThemeObserver, getActiveTheme } from "./utils/theme";
31
+ import { resolveTokenValue } from "./utils/tokens";
31
32
  import { renderLucideIcon } from "./utils/icons";
32
33
  import { createElement, createElementInDocument } from "./utils/dom";
33
34
  import { morphMessages } from "./utils/morph";
34
35
  import { computeMessageFingerprint, createMessageCache, getCachedWrapper, setCachedWrapper, pruneCache } from "./utils/message-fingerprint";
35
36
  import { statusCopy } from "./utils/constants";
36
- import { isDockedMountMode } from "./utils/dock";
37
+ import { isDockedMountMode, resolveDockConfig } from "./utils/dock";
37
38
  import { createLauncherButton } from "./components/launcher";
38
39
  import { createWrapper, buildPanel, buildHeader, buildComposer, attachHeaderToContainer } from "./components/panel";
40
+ import { HEADER_THEME_CSS } from "./components/header-builder";
39
41
  import { buildHeaderWithLayout } from "./components/header-layouts";
40
42
  import { positionMap } from "./utils/positioning";
41
43
  import type { HeaderElements as _HeaderElements, ComposerElements as _ComposerElements } from "./components/panel";
@@ -383,12 +385,14 @@ export const createAgentExperience = (
383
385
  initialConfig?: AgentWidgetConfig,
384
386
  runtimeOptions?: { debugTools?: boolean }
385
387
  ): Controller => {
386
- // Preserve original mount id as data attribute for window event instance scoping,
387
- // then set mount.id to "persona-root" (required by Tailwind important: "#persona-root")
388
- if (mount.id && mount.id !== "persona-root" && !mount.getAttribute("data-persona-instance")) {
388
+ // Preserve original mount id as data attribute for window event instance scoping
389
+ if (mount.id && !mount.getAttribute("data-persona-instance")) {
389
390
  mount.setAttribute("data-persona-instance", mount.id);
390
391
  }
391
- mount.id = "persona-root";
392
+ // Ensure root marker is present for Tailwind scoping and DOM traversal
393
+ if (!mount.hasAttribute("data-persona-root")) {
394
+ mount.setAttribute("data-persona-root", "true");
395
+ }
392
396
 
393
397
  let config = mergeWithDefaults(initialConfig) as AgentWidgetConfig;
394
398
  // Note: applyThemeVariables is called after applyFullHeightStyles() below
@@ -1402,6 +1406,7 @@ export const createAgentExperience = (
1402
1406
  if (!launcherEnabled || !artifactPaneApi) return;
1403
1407
  const sidebarMode = config.launcher?.sidebarMode ?? false;
1404
1408
  if (sidebarMode) return;
1409
+ if (isDockedMountMode(config) && resolveDockConfig(config).reveal === "emerge") return;
1405
1410
  const ownerWindow = mount.ownerDocument.defaultView ?? window;
1406
1411
  const mobileFullscreen = config.launcher?.mobileFullscreen ?? true;
1407
1412
  const mobileBreakpoint = config.launcher?.mobileBreakpoint ?? 640;
@@ -1442,7 +1447,12 @@ export const createAgentExperience = (
1442
1447
  const fullHeight = dockedMode || sidebarMode || (config.launcher?.fullHeight ?? false);
1443
1448
  /** Script-tag / div embed: launcher off, host supplies a sized mount. */
1444
1449
  const isInlineEmbed = config.launcher?.enabled === false;
1445
- const theme = config.theme ?? {};
1450
+ const panelPartial = config.theme?.components?.panel;
1451
+ const activeTheme = getActiveTheme(config);
1452
+ const resolvePanelChrome = (raw: string | undefined, fallback: string): string => {
1453
+ if (raw == null || raw === "") return fallback;
1454
+ return resolveTokenValue(activeTheme, raw) ?? raw;
1455
+ };
1446
1456
 
1447
1457
  // Mobile fullscreen detection
1448
1458
  // Use mount's ownerDocument window to get correct viewport width when widget is inside an iframe
@@ -1457,20 +1467,25 @@ export const createAgentExperience = (
1457
1467
  const isLeftSidebar = position === 'bottom-left' || position === 'top-left';
1458
1468
 
1459
1469
  // Default values based on mode
1460
- const defaultPanelBorder = (sidebarMode || shouldGoFullscreen) ? 'none' : '1px solid var(--persona-persona-border)';
1461
- const defaultPanelShadow = shouldGoFullscreen
1470
+ let defaultPanelBorder = (sidebarMode || shouldGoFullscreen) ? 'none' : '1px solid var(--persona-border)';
1471
+ let defaultPanelShadow = shouldGoFullscreen
1462
1472
  ? 'none'
1463
1473
  : sidebarMode
1464
1474
  ? (isLeftSidebar ? 'var(--persona-palette-shadows-sidebar-left, 2px 0 12px rgba(0, 0, 0, 0.08))' : 'var(--persona-palette-shadows-sidebar-right, -2px 0 12px rgba(0, 0, 0, 0.08))')
1465
1475
  : 'var(--persona-palette-shadows-xl, 0 25px 50px -12px rgba(0, 0, 0, 0.25))';
1476
+
1477
+ if (dockedMode && !shouldGoFullscreen) {
1478
+ defaultPanelShadow = 'none';
1479
+ defaultPanelBorder = 'none';
1480
+ }
1466
1481
  const defaultPanelBorderRadius = (sidebarMode || shouldGoFullscreen)
1467
1482
  ? '0'
1468
1483
  : 'var(--persona-panel-radius, var(--persona-radius-xl, 0.75rem))';
1469
1484
 
1470
- // Apply theme overrides or defaults
1471
- const panelBorder = theme.panelBorder ?? defaultPanelBorder;
1472
- const panelShadow = theme.panelShadow ?? defaultPanelShadow;
1473
- const panelBorderRadius = theme.panelBorderRadius ?? defaultPanelBorderRadius;
1485
+ // Apply theme overrides or defaults (components.panel.*)
1486
+ const panelBorder = resolvePanelChrome(panelPartial?.border, defaultPanelBorder);
1487
+ const panelShadow = resolvePanelChrome(panelPartial?.shadow, defaultPanelShadow);
1488
+ const panelBorderRadius = resolvePanelChrome(panelPartial?.borderRadius, defaultPanelBorderRadius);
1474
1489
 
1475
1490
  // Reset all inline styles first to handle mode toggling
1476
1491
  // This ensures styles don't persist when switching between modes
@@ -1557,8 +1572,15 @@ export const createAgentExperience = (
1557
1572
  panel.style.maxWidth = width;
1558
1573
  }
1559
1574
  } else if (dockedMode) {
1560
- panel.style.width = "100%";
1561
- panel.style.maxWidth = "100%";
1575
+ const dockReveal = resolveDockConfig(config).reveal;
1576
+ if (dockReveal === "emerge") {
1577
+ const dw = resolveDockConfig(config).width;
1578
+ panel.style.width = dw;
1579
+ panel.style.maxWidth = dw;
1580
+ } else {
1581
+ panel.style.width = "100%";
1582
+ panel.style.maxWidth = "100%";
1583
+ }
1562
1584
  }
1563
1585
  applyLauncherArtifactPanelWidth();
1564
1586
 
@@ -1571,6 +1593,16 @@ export const createAgentExperience = (
1571
1593
  container.style.border = panelBorder;
1572
1594
  container.style.borderRadius = panelBorderRadius;
1573
1595
 
1596
+ if (dockedMode && !shouldGoFullscreen && panelPartial?.border === undefined) {
1597
+ container.style.border = 'none';
1598
+ const dockSide = resolveDockConfig(config).side;
1599
+ if (dockSide === 'right') {
1600
+ container.style.borderLeft = '1px solid var(--persona-border)';
1601
+ } else {
1602
+ container.style.borderRight = '1px solid var(--persona-border)';
1603
+ }
1604
+ }
1605
+
1574
1606
  if (fullHeight) {
1575
1607
  // Mount container
1576
1608
  mount.style.display = 'flex';
@@ -2432,7 +2464,19 @@ export const createAgentExperience = (
2432
2464
  const updateOpenState = () => {
2433
2465
  if (!launcherEnabled) return;
2434
2466
  const dockedMode = isDockedMountMode(config);
2467
+ const ownerWindow = mount.ownerDocument.defaultView ?? window;
2468
+ const mobileBreakpoint = config.launcher?.mobileBreakpoint ?? 640;
2469
+ const mobileFullscreen = config.launcher?.mobileFullscreen ?? true;
2470
+ const isMobileViewport = ownerWindow.innerWidth <= mobileBreakpoint;
2471
+ const shouldGoFullscreen = mobileFullscreen && isMobileViewport && launcherEnabled;
2472
+ const dockReveal = resolveDockConfig(config).reveal;
2473
+ const dockRevealUsesTransform =
2474
+ dockedMode && (dockReveal === "overlay" || dockReveal === "push") && !shouldGoFullscreen;
2475
+
2435
2476
  if (open) {
2477
+ // Clear any display:none !important from a closed docked state so mobile fullscreen
2478
+ // (display:flex !important) and dock layout can apply in recalcPanelHeight.
2479
+ wrapper.style.removeProperty("display");
2436
2480
  wrapper.style.display = dockedMode ? "flex" : "";
2437
2481
  wrapper.classList.remove("persona-pointer-events-none", "persona-opacity-0");
2438
2482
  panel.classList.remove("persona-scale-95", "persona-opacity-0");
@@ -2445,20 +2489,29 @@ export const createAgentExperience = (
2445
2489
  }
2446
2490
  } else {
2447
2491
  if (dockedMode) {
2448
- wrapper.style.display = "none";
2449
- wrapper.classList.remove("persona-pointer-events-none", "persona-opacity-0");
2450
- panel.classList.remove("persona-scale-100", "persona-opacity-100", "persona-scale-95", "persona-opacity-0");
2492
+ if (dockRevealUsesTransform) {
2493
+ // Slide/push reveal: keep the panel painted so host-layout `transform` can animate.
2494
+ wrapper.style.removeProperty("display");
2495
+ wrapper.style.display = "flex";
2496
+ wrapper.classList.remove("persona-pointer-events-none", "persona-opacity-0");
2497
+ panel.classList.remove("persona-scale-100", "persona-opacity-100", "persona-scale-95", "persona-opacity-0");
2498
+ } else {
2499
+ // Must beat applyFullHeightStyles() mobile shell: display:flex !important on wrapper
2500
+ wrapper.style.setProperty("display", "none", "important");
2501
+ wrapper.classList.remove("persona-pointer-events-none", "persona-opacity-0");
2502
+ panel.classList.remove("persona-scale-100", "persona-opacity-100", "persona-scale-95", "persona-opacity-0");
2503
+ }
2451
2504
  } else {
2452
2505
  wrapper.style.display = "";
2453
2506
  wrapper.classList.add("persona-pointer-events-none", "persona-opacity-0");
2454
2507
  panel.classList.remove("persona-scale-100", "persona-opacity-100");
2455
2508
  panel.classList.add("persona-scale-95", "persona-opacity-0");
2456
2509
  }
2457
- // Show launcher button when widget is closed
2510
+ // Show launcher when closed, except docked mode (0px column — use controller.open()).
2458
2511
  if (launcherButtonInstance) {
2459
- launcherButtonInstance.element.style.display = "";
2512
+ launcherButtonInstance.element.style.display = dockedMode ? "none" : "";
2460
2513
  } else if (customLauncherElement) {
2461
- customLauncherElement.style.display = "";
2514
+ customLauncherElement.style.display = dockedMode ? "none" : "";
2462
2515
  }
2463
2516
  }
2464
2517
  };
@@ -2544,24 +2597,9 @@ export const createAgentExperience = (
2544
2597
  sendButton.textContent = config.copy?.sendButtonLabel ?? "Send";
2545
2598
  }
2546
2599
 
2547
- // Update textarea font family and weight
2548
- const fontFamily = config.theme?.inputFontFamily ?? "sans-serif";
2549
- const fontWeight = config.theme?.inputFontWeight ?? "400";
2550
-
2551
- const getFontFamilyValue = (family: "sans-serif" | "serif" | "mono"): string => {
2552
- switch (family) {
2553
- case "serif":
2554
- return 'Georgia, "Times New Roman", Times, serif';
2555
- case "mono":
2556
- return '"Courier New", Courier, "Lucida Console", Monaco, monospace';
2557
- case "sans-serif":
2558
- default:
2559
- return '-apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif';
2560
- }
2561
- };
2562
-
2563
- textarea.style.fontFamily = getFontFamilyValue(fontFamily);
2564
- textarea.style.fontWeight = fontWeight;
2600
+ textarea.style.fontFamily =
2601
+ 'var(--persona-input-font-family, var(--persona-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif))';
2602
+ textarea.style.fontWeight = "var(--persona-input-font-weight, var(--persona-font-weight, 400))";
2565
2603
  };
2566
2604
 
2567
2605
  // Add session ID persistence callbacks for client token mode
@@ -3383,43 +3421,49 @@ export const createAgentExperience = (
3383
3421
  const isMobileViewport = ownerWindow.innerWidth <= mobileBreakpoint;
3384
3422
  const shouldGoFullscreen = mobileFullscreen && isMobileViewport && launcherEnabled;
3385
3423
 
3386
- if (shouldGoFullscreen) {
3387
- applyFullHeightStyles();
3388
- applyThemeVariables(mount, config);
3389
- return;
3390
- }
3391
-
3392
- // Exiting mobile fullscreen (e.g., orientation change to landscape) — reset all styles
3393
- if (wasMobileFullscreen) {
3394
- wasMobileFullscreen = false;
3395
- applyFullHeightStyles();
3396
- applyThemeVariables(mount, config);
3397
- }
3424
+ try {
3425
+ if (shouldGoFullscreen) {
3426
+ applyFullHeightStyles();
3427
+ applyThemeVariables(mount, config);
3428
+ return;
3429
+ }
3398
3430
 
3399
- if (!launcherEnabled && !dockedMode) {
3400
- panel.style.height = "";
3401
- panel.style.width = "";
3402
- return;
3403
- }
3431
+ // Exiting mobile fullscreen (e.g., orientation change to landscape) — reset all styles
3432
+ if (wasMobileFullscreen) {
3433
+ wasMobileFullscreen = false;
3434
+ applyFullHeightStyles();
3435
+ applyThemeVariables(mount, config);
3436
+ }
3404
3437
 
3405
- // In sidebar/fullHeight mode, don't override the width - it's handled by applyFullHeightStyles
3406
- if (!sidebarMode && !dockedMode) {
3407
- const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
3408
- const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
3409
- panel.style.width = width;
3410
- panel.style.maxWidth = width;
3411
- }
3412
- applyLauncherArtifactPanelWidth();
3438
+ if (!launcherEnabled && !dockedMode) {
3439
+ panel.style.height = "";
3440
+ panel.style.width = "";
3441
+ return;
3442
+ }
3413
3443
 
3414
- // In fullHeight mode, don't set a fixed height
3415
- if (!fullHeight) {
3416
- const viewportHeight = ownerWindow.innerHeight;
3417
- const verticalMargin = 64; // leave space for launcher's offset
3418
- const heightOffset = config.launcher?.heightOffset ?? 0;
3419
- const available = Math.max(200, viewportHeight - verticalMargin);
3420
- const clamped = Math.min(640, available);
3421
- const finalHeight = Math.max(200, clamped - heightOffset);
3422
- panel.style.height = `${finalHeight}px`;
3444
+ // In sidebar/fullHeight mode, don't override the width - it's handled by applyFullHeightStyles
3445
+ if (!sidebarMode && !dockedMode) {
3446
+ const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
3447
+ const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
3448
+ panel.style.width = width;
3449
+ panel.style.maxWidth = width;
3450
+ }
3451
+ applyLauncherArtifactPanelWidth();
3452
+
3453
+ // In fullHeight mode, don't set a fixed height
3454
+ if (!fullHeight) {
3455
+ const viewportHeight = ownerWindow.innerHeight;
3456
+ const verticalMargin = 64; // leave space for launcher's offset
3457
+ const heightOffset = config.launcher?.heightOffset ?? 0;
3458
+ const available = Math.max(200, viewportHeight - verticalMargin);
3459
+ const clamped = Math.min(640, available);
3460
+ const finalHeight = Math.max(200, clamped - heightOffset);
3461
+ panel.style.height = `${finalHeight}px`;
3462
+ }
3463
+ } finally {
3464
+ // applyFullHeightStyles() assigns wrapper.style.cssText (e.g. display:flex !important), which
3465
+ // overwrites updateOpenState()'s display:none when docked+closed. Re-sync after every recalc.
3466
+ updateOpenState();
3423
3467
  }
3424
3468
  };
3425
3469
 
@@ -3959,14 +4003,9 @@ export const createAgentExperience = (
3959
4003
  }
3960
4004
  }
3961
4005
 
3962
- // Apply close button styling from config
3963
- if (launcher.closeButtonColor) {
3964
- closeButton.style.color = launcher.closeButtonColor;
3965
- closeButton.classList.remove("persona-text-persona-muted");
3966
- } else {
3967
- closeButton.style.color = "";
3968
- closeButton.classList.add("persona-text-persona-muted");
3969
- }
4006
+ // Close icon: launcher color wins; else theme.components.header.actionIconForeground
4007
+ closeButton.style.color =
4008
+ launcher.closeButtonColor || HEADER_THEME_CSS.actionIconColor;
3970
4009
 
3971
4010
  if (launcher.closeButtonBackgroundColor) {
3972
4011
  closeButton.style.backgroundColor = launcher.closeButtonBackgroundColor;
@@ -4017,7 +4056,7 @@ export const createAgentExperience = (
4017
4056
 
4018
4057
  // Clear existing content and render new icon
4019
4058
  closeButton.innerHTML = "";
4020
- const iconSvg = renderLucideIcon(closeButtonIconName, "20px", launcher.closeButtonColor || "", 2);
4059
+ const iconSvg = renderLucideIcon(closeButtonIconName, "20px", "currentColor", 2);
4021
4060
  if (iconSvg) {
4022
4061
  closeButton.appendChild(iconSvg);
4023
4062
  } else {
@@ -4184,22 +4223,16 @@ export const createAgentExperience = (
4184
4223
  const clearChatIconName = clearChatConfig.iconName ?? "refresh-cw";
4185
4224
  const clearChatIconColor = clearChatConfig.iconColor ?? "";
4186
4225
 
4226
+ clearChatButton.style.color =
4227
+ clearChatIconColor || HEADER_THEME_CSS.actionIconColor;
4228
+
4187
4229
  // Clear existing icon and render new one
4188
4230
  clearChatButton.innerHTML = "";
4189
- const iconSvg = renderLucideIcon(clearChatIconName, "20px", clearChatIconColor || "", 2);
4231
+ const iconSvg = renderLucideIcon(clearChatIconName, "20px", "currentColor", 2);
4190
4232
  if (iconSvg) {
4191
4233
  clearChatButton.appendChild(iconSvg);
4192
4234
  }
4193
4235
 
4194
- // Update icon color
4195
- if (clearChatIconColor) {
4196
- clearChatButton.style.color = clearChatIconColor;
4197
- clearChatButton.classList.remove("persona-text-persona-muted");
4198
- } else {
4199
- clearChatButton.style.color = "";
4200
- clearChatButton.classList.add("persona-text-persona-muted");
4201
- }
4202
-
4203
4236
  // Update background color
4204
4237
  if (clearChatConfig.backgroundColor) {
4205
4238
  clearChatButton.style.backgroundColor = clearChatConfig.backgroundColor;
@@ -32,7 +32,7 @@ export function applyArtifactPaneBorderTheme(mount: HTMLElement, config: AgentWi
32
32
  }
33
33
  }
34
34
 
35
- /** Set CSS variables on #persona-root for artifact split/pane sizing. Clears when artifacts disabled. */
35
+ /** Set CSS variables on the widget root for artifact split/pane sizing. Clears when artifacts disabled. */
36
36
  function clearDocumentToolbarLayoutVars(mount: HTMLElement): void {
37
37
  mount.style.removeProperty("--persona-artifact-doc-toolbar-icon-color");
38
38
  mount.style.removeProperty("--persona-artifact-doc-toggle-active-bg");