@runtypelabs/persona 3.6.0 → 3.8.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 (50) hide show
  1. package/dist/index.cjs +40 -40
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +73 -4
  4. package/dist/index.d.ts +73 -4
  5. package/dist/index.global.js +69 -69
  6. package/dist/index.global.js.map +1 -1
  7. package/dist/index.js +40 -40
  8. package/dist/index.js.map +1 -1
  9. package/dist/theme-editor.cjs +704 -243
  10. package/dist/theme-editor.d.cts +75 -5
  11. package/dist/theme-editor.d.ts +75 -5
  12. package/dist/theme-editor.js +703 -243
  13. package/dist/theme-reference.cjs +1 -1
  14. package/dist/theme-reference.d.cts +53 -0
  15. package/dist/theme-reference.d.ts +53 -0
  16. package/dist/theme-reference.js +1 -1
  17. package/dist/widget.css +44 -0
  18. package/package.json +1 -1
  19. package/src/components/artifact-card.ts +1 -1
  20. package/src/components/demo-carousel.ts +1 -1
  21. package/src/components/event-stream-view.test.ts +142 -0
  22. package/src/components/event-stream-view.ts +67 -28
  23. package/src/components/header-builder.ts +3 -0
  24. package/src/components/launcher.ts +7 -2
  25. package/src/components/panel.ts +3 -1
  26. package/src/defaults.ts +15 -0
  27. package/src/runtime/host-layout.test.ts +1 -1
  28. package/src/runtime/host-layout.ts +2 -1
  29. package/src/scroll-to-bottom-defaults.test.ts +13 -0
  30. package/src/styles/widget.css +44 -0
  31. package/src/theme-editor/index.ts +1 -0
  32. package/src/theme-editor/role-mappings.ts +12 -0
  33. package/src/theme-editor/sections.test.ts +43 -0
  34. package/src/theme-editor/sections.ts +42 -0
  35. package/src/theme-reference.ts +8 -0
  36. package/src/types/theme.ts +45 -0
  37. package/src/types.ts +31 -4
  38. package/src/ui.overlay-z-index.test.ts +34 -2
  39. package/src/ui.scroll.test.ts +554 -0
  40. package/src/ui.ts +264 -90
  41. package/src/utils/auto-follow.test.ts +110 -0
  42. package/src/utils/auto-follow.ts +112 -0
  43. package/src/utils/constants.ts +13 -0
  44. package/src/utils/dropdown.ts +2 -1
  45. package/src/utils/overlay-host-stacking.test.ts +61 -0
  46. package/src/utils/overlay-host-stacking.ts +38 -0
  47. package/src/utils/scroll-lock.test.ts +64 -0
  48. package/src/utils/scroll-lock.ts +62 -0
  49. package/src/utils/theme.test.ts +34 -0
  50. package/src/utils/tokens.ts +112 -0
@@ -0,0 +1,112 @@
1
+ export type FollowStateAction = "none" | "pause" | "resume";
2
+
3
+ export type FollowStateController = {
4
+ isFollowing: () => boolean;
5
+ pause: () => boolean;
6
+ resume: () => boolean;
7
+ };
8
+
9
+ export type FollowStateScrollInput = {
10
+ following: boolean;
11
+ currentScrollTop: number;
12
+ lastScrollTop: number;
13
+ nearBottom: boolean;
14
+ userScrollThreshold: number;
15
+ isAutoScrolling?: boolean;
16
+ pauseOnUpwardScroll?: boolean;
17
+ pauseWhenAwayFromBottom?: boolean;
18
+ resumeRequiresDownwardScroll?: boolean;
19
+ };
20
+
21
+ export type FollowStateWheelInput = {
22
+ following: boolean;
23
+ deltaY: number;
24
+ nearBottom?: boolean;
25
+ resumeWhenNearBottom?: boolean;
26
+ };
27
+
28
+ export function createFollowStateController(initiallyFollowing = true): FollowStateController {
29
+ let following = initiallyFollowing;
30
+
31
+ return {
32
+ isFollowing: () => following,
33
+ pause: () => {
34
+ if (!following) return false;
35
+ following = false;
36
+ return true;
37
+ },
38
+ resume: () => {
39
+ if (following) return false;
40
+ following = true;
41
+ return true;
42
+ }
43
+ };
44
+ }
45
+
46
+ export function getScrollBottomOffset(element: Pick<HTMLElement, "scrollHeight" | "clientHeight">): number {
47
+ return Math.max(0, element.scrollHeight - element.clientHeight);
48
+ }
49
+
50
+ export function isElementNearBottom(
51
+ element: Pick<HTMLElement, "scrollTop" | "scrollHeight" | "clientHeight">,
52
+ threshold: number
53
+ ): boolean {
54
+ return getScrollBottomOffset(element) - element.scrollTop <= threshold;
55
+ }
56
+
57
+ export function resolveFollowStateFromScroll(
58
+ input: FollowStateScrollInput
59
+ ): { action: FollowStateAction; delta: number; nextLastScrollTop: number } {
60
+ const {
61
+ following,
62
+ currentScrollTop,
63
+ lastScrollTop,
64
+ nearBottom,
65
+ userScrollThreshold,
66
+ isAutoScrolling = false,
67
+ pauseOnUpwardScroll = false,
68
+ pauseWhenAwayFromBottom = true,
69
+ resumeRequiresDownwardScroll = false
70
+ } = input;
71
+
72
+ const delta = currentScrollTop - lastScrollTop;
73
+
74
+ if (isAutoScrolling || Math.abs(delta) < userScrollThreshold) {
75
+ return { action: "none", delta, nextLastScrollTop: currentScrollTop };
76
+ }
77
+
78
+ if (!following && nearBottom && (!resumeRequiresDownwardScroll || delta > 0)) {
79
+ return { action: "resume", delta, nextLastScrollTop: currentScrollTop };
80
+ }
81
+
82
+ if (following && pauseOnUpwardScroll && delta < 0) {
83
+ return { action: "pause", delta, nextLastScrollTop: currentScrollTop };
84
+ }
85
+
86
+ if (following && pauseWhenAwayFromBottom && !nearBottom) {
87
+ return { action: "pause", delta, nextLastScrollTop: currentScrollTop };
88
+ }
89
+
90
+ return { action: "none", delta, nextLastScrollTop: currentScrollTop };
91
+ }
92
+
93
+ export function resolveFollowStateFromWheel(
94
+ input: FollowStateWheelInput
95
+ ): FollowStateAction {
96
+ const {
97
+ following,
98
+ deltaY,
99
+ nearBottom = false,
100
+ resumeWhenNearBottom = false
101
+ } = input;
102
+
103
+ if (following && deltaY < 0) {
104
+ return "pause";
105
+ }
106
+
107
+ if (!following && resumeWhenNearBottom && deltaY > 0 && nearBottom) {
108
+ return "resume";
109
+ }
110
+
111
+ return "none";
112
+ }
@@ -7,6 +7,19 @@ export const statusCopy: Record<AgentWidgetSessionStatus, string> = {
7
7
  error: "Offline"
8
8
  };
9
9
 
10
+ /**
11
+ * Default z-index for widget overlays. Used for the floating panel, launcher
12
+ * button, sidebar, mobile fullscreen, and docked mobile fullscreen modes.
13
+ * Integrators can override via `launcher.zIndex`.
14
+ */
15
+ export const DEFAULT_OVERLAY_Z_INDEX = 100000;
16
+
17
+ /**
18
+ * Z-index for elements portaled to document.body (tooltips, dropdowns).
19
+ * Must be above the widget overlay so portaled UI is not clipped.
20
+ */
21
+ export const PORTALED_OVERLAY_Z_INDEX = DEFAULT_OVERLAY_Z_INDEX + 1;
22
+
10
23
 
11
24
 
12
25
 
@@ -1,5 +1,6 @@
1
1
  import { createElement } from "./dom";
2
2
  import { renderLucideIcon } from "./icons";
3
+ import { PORTALED_OVERLAY_Z_INDEX } from "./constants";
3
4
 
4
5
  export interface DropdownMenuItem {
5
6
  id: string;
@@ -73,7 +74,7 @@ export function createDropdownMenu(options: CreateDropdownOptions): DropdownMenu
73
74
  if (portal) {
74
75
  // Fixed positioning — menu is portaled outside the anchor's overflow context
75
76
  menu.style.position = "fixed";
76
- menu.style.zIndex = "10000";
77
+ menu.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
77
78
  } else {
78
79
  // Absolute positioning — menu lives inside the anchor
79
80
  menu.style.position = "absolute";
@@ -0,0 +1,61 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, describe, expect, it } from "vitest";
4
+ import { syncOverlayHostStacking } from "./overlay-host-stacking";
5
+
6
+ describe("syncOverlayHostStacking", () => {
7
+ afterEach(() => {
8
+ document.body.innerHTML = "";
9
+ });
10
+
11
+ it("sets position relative on a static element and restores on teardown", () => {
12
+ const host = document.createElement("div");
13
+ document.body.appendChild(host);
14
+
15
+ const teardown = syncOverlayHostStacking(host);
16
+ expect(host.style.position).toBe("relative");
17
+ expect(host.style.zIndex).toBe("100000");
18
+ expect(host.style.isolation).toBe("isolate");
19
+
20
+ teardown();
21
+ expect(host.style.position).toBe("");
22
+ expect(host.style.zIndex).toBe("");
23
+ expect(host.style.isolation).toBe("");
24
+ });
25
+
26
+ it("preserves existing positioned value", () => {
27
+ const host = document.createElement("div");
28
+ host.style.position = "absolute";
29
+ document.body.appendChild(host);
30
+
31
+ const teardown = syncOverlayHostStacking(host);
32
+ expect(host.style.position).toBe("absolute");
33
+ expect(host.style.zIndex).toBe("100000");
34
+
35
+ teardown();
36
+ expect(host.style.position).toBe("absolute");
37
+ expect(host.style.zIndex).toBe("");
38
+ });
39
+
40
+ it("accepts custom z-index", () => {
41
+ const host = document.createElement("div");
42
+ document.body.appendChild(host);
43
+
44
+ const teardown = syncOverlayHostStacking(host, 42);
45
+ expect(host.style.zIndex).toBe("42");
46
+
47
+ teardown();
48
+ });
49
+
50
+ it("restores previous inline z-index on teardown", () => {
51
+ const host = document.createElement("div");
52
+ host.style.zIndex = "5";
53
+ document.body.appendChild(host);
54
+
55
+ const teardown = syncOverlayHostStacking(host);
56
+ expect(host.style.zIndex).toBe("100000");
57
+
58
+ teardown();
59
+ expect(host.style.zIndex).toBe("5");
60
+ });
61
+ });
@@ -0,0 +1,38 @@
1
+ import { DEFAULT_OVERLAY_Z_INDEX } from "./constants";
2
+
3
+ /**
4
+ * Elevates the light-DOM host element's stacking context so viewport-covering
5
+ * overlays (sidebar, fullscreen) can escape parent stacking traps.
6
+ *
7
+ * - If the host has `position: static`, sets it to `relative` (required for
8
+ * `z-index` to take effect).
9
+ * - Applies `z-index` matching the overlay default.
10
+ * - Applies `isolation: isolate` to create a predictable stacking context.
11
+ *
12
+ * @returns A teardown function that restores only the properties that were changed.
13
+ */
14
+ export function syncOverlayHostStacking(
15
+ host: HTMLElement,
16
+ zIndex: number = DEFAULT_OVERLAY_Z_INDEX
17
+ ): () => void {
18
+ const originalPosition = host.style.position;
19
+ const originalZIndex = host.style.zIndex;
20
+ const originalIsolation = host.style.isolation;
21
+
22
+ const computed = getComputedStyle(host);
23
+ const positionWasSet = computed.position === "static" || computed.position === "";
24
+ if (positionWasSet) {
25
+ host.style.position = "relative";
26
+ }
27
+
28
+ host.style.zIndex = String(zIndex);
29
+ host.style.isolation = "isolate";
30
+
31
+ return () => {
32
+ if (positionWasSet) {
33
+ host.style.position = originalPosition;
34
+ }
35
+ host.style.zIndex = originalZIndex;
36
+ host.style.isolation = originalIsolation;
37
+ };
38
+ }
@@ -0,0 +1,64 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
4
+ import { acquireScrollLock } from "./scroll-lock";
5
+
6
+ describe("acquireScrollLock", () => {
7
+ beforeEach(() => {
8
+ document.body.style.overflow = "";
9
+ document.body.style.position = "";
10
+ document.body.style.top = "";
11
+ document.body.style.width = "";
12
+ });
13
+
14
+ afterEach(() => {
15
+ document.body.style.overflow = "";
16
+ document.body.style.position = "";
17
+ document.body.style.top = "";
18
+ document.body.style.width = "";
19
+ });
20
+
21
+ it("sets overflow hidden and position fixed on body", () => {
22
+ const release = acquireScrollLock();
23
+ expect(document.body.style.overflow).toBe("hidden");
24
+ expect(document.body.style.position).toBe("fixed");
25
+ expect(document.body.style.width).toBe("100%");
26
+
27
+ release();
28
+ expect(document.body.style.overflow).toBe("");
29
+ expect(document.body.style.position).toBe("");
30
+ expect(document.body.style.width).toBe("");
31
+ });
32
+
33
+ it("is ref-counted: first release does not unlock when second acquire is active", () => {
34
+ const release1 = acquireScrollLock();
35
+ const release2 = acquireScrollLock();
36
+ expect(document.body.style.overflow).toBe("hidden");
37
+
38
+ release1();
39
+ expect(document.body.style.overflow).toBe("hidden");
40
+
41
+ release2();
42
+ expect(document.body.style.overflow).toBe("");
43
+ });
44
+
45
+ it("release is idempotent", () => {
46
+ const release = acquireScrollLock();
47
+ release();
48
+ release();
49
+ expect(document.body.style.overflow).toBe("");
50
+ });
51
+
52
+ it("restores original body styles on release", () => {
53
+ document.body.style.overflow = "scroll";
54
+ document.body.style.position = "relative";
55
+
56
+ const release = acquireScrollLock();
57
+ expect(document.body.style.overflow).toBe("hidden");
58
+ expect(document.body.style.position).toBe("fixed");
59
+
60
+ release();
61
+ expect(document.body.style.overflow).toBe("scroll");
62
+ expect(document.body.style.position).toBe("relative");
63
+ });
64
+ });
@@ -0,0 +1,62 @@
1
+ interface ScrollLockState {
2
+ originalOverflow: string;
3
+ originalPosition: string;
4
+ originalTop: string;
5
+ originalWidth: string;
6
+ scrollY: number;
7
+ }
8
+
9
+ let lockCount = 0;
10
+ let savedState: ScrollLockState | null = null;
11
+
12
+ /**
13
+ * Acquire a document-level scroll lock. The page body becomes non-scrollable
14
+ * via `overflow: hidden` with an iOS-safe `position: fixed` pattern that
15
+ * preserves the visual scroll position.
16
+ *
17
+ * Ref-counted: multiple callers can acquire; the lock is only released when
18
+ * all callers have released. Each release call is idempotent.
19
+ *
20
+ * @returns A release function. Call it exactly once per acquisition.
21
+ */
22
+ export function acquireScrollLock(doc: Document = document): () => void {
23
+ lockCount++;
24
+
25
+ if (lockCount === 1) {
26
+ const body = doc.body;
27
+ const win = doc.defaultView ?? window;
28
+ const scrollY = win.scrollY || doc.documentElement.scrollTop;
29
+
30
+ savedState = {
31
+ originalOverflow: body.style.overflow,
32
+ originalPosition: body.style.position,
33
+ originalTop: body.style.top,
34
+ originalWidth: body.style.width,
35
+ scrollY,
36
+ };
37
+
38
+ body.style.overflow = "hidden";
39
+ body.style.position = "fixed";
40
+ body.style.top = `-${scrollY}px`;
41
+ body.style.width = "100%";
42
+ }
43
+
44
+ let released = false;
45
+
46
+ return () => {
47
+ if (released) return;
48
+ released = true;
49
+ lockCount = Math.max(0, lockCount - 1);
50
+
51
+ if (lockCount === 0 && savedState) {
52
+ const body = doc.body;
53
+ const win = doc.defaultView ?? window;
54
+ body.style.overflow = savedState.originalOverflow;
55
+ body.style.position = savedState.originalPosition;
56
+ body.style.top = savedState.originalTop;
57
+ body.style.width = savedState.originalWidth;
58
+ win.scrollTo(0, savedState.scrollY);
59
+ savedState = null;
60
+ }
61
+ };
62
+ }
@@ -218,6 +218,40 @@ describe('theme utils', () => {
218
218
  expect(cssVars['--persona-composer-shadow']).toBe('none');
219
219
  });
220
220
 
221
+ it('maps scroll-to-bottom component tokens to dedicated CSS variables', () => {
222
+ const theme = createTheme({
223
+ components: {
224
+ scrollToBottom: {
225
+ background: 'palette.colors.accent.500',
226
+ foreground: 'palette.colors.gray.50',
227
+ border: 'palette.colors.gray.900',
228
+ size: '40px',
229
+ borderRadius: 'palette.radius.full',
230
+ shadow: 'palette.shadows.md',
231
+ padding: '0.5rem 0.875rem',
232
+ gap: '0.5rem',
233
+ fontSize: '0.875rem',
234
+ iconSize: '14px',
235
+ },
236
+ },
237
+ } as any);
238
+
239
+ const cssVars = themeToCssVariables(theme);
240
+
241
+ expect(cssVars['--persona-scroll-to-bottom-bg']).toBe('#06b6d4');
242
+ expect(cssVars['--persona-scroll-to-bottom-fg']).toBe('#f9fafb');
243
+ expect(cssVars['--persona-scroll-to-bottom-border']).toBe('#111827');
244
+ expect(cssVars['--persona-scroll-to-bottom-size']).toBe('40px');
245
+ expect(cssVars['--persona-scroll-to-bottom-radius']).toBe('9999px');
246
+ expect(cssVars['--persona-scroll-to-bottom-shadow']).toBe(
247
+ '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)'
248
+ );
249
+ expect(cssVars['--persona-scroll-to-bottom-padding']).toBe('0.5rem 0.875rem');
250
+ expect(cssVars['--persona-scroll-to-bottom-gap']).toBe('0.5rem');
251
+ expect(cssVars['--persona-scroll-to-bottom-font-size']).toBe('0.875rem');
252
+ expect(cssVars['--persona-scroll-to-bottom-icon-size']).toBe('14px');
253
+ });
254
+
221
255
  it('lets config.toolCall.shadow override theme tool bubble shadow on the root element', () => {
222
256
  const el = document.createElement('div');
223
257
  applyThemeVariables(el, {
@@ -312,6 +312,7 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
312
312
  border: 'palette.colors.gray.200',
313
313
  shadow: 'palette.shadows.sm',
314
314
  },
315
+ border: 'semantic.colors.border',
315
316
  },
316
317
  toolBubble: {
317
318
  shadow: 'palette.shadows.sm',
@@ -334,6 +335,28 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
334
335
  prose: {
335
336
  fontFamily: 'inherit',
336
337
  },
338
+ codeBlock: {
339
+ background: 'semantic.colors.container',
340
+ borderColor: 'semantic.colors.border',
341
+ textColor: 'inherit',
342
+ },
343
+ table: {
344
+ headerBackground: 'semantic.colors.container',
345
+ borderColor: 'semantic.colors.border',
346
+ },
347
+ hr: {
348
+ color: 'semantic.colors.divider',
349
+ },
350
+ blockquote: {
351
+ borderColor: 'palette.colors.gray.900',
352
+ background: 'transparent',
353
+ textColor: 'palette.colors.gray.500',
354
+ },
355
+ },
356
+ collapsibleWidget: {
357
+ container: 'palette.colors.gray.50',
358
+ surface: 'semantic.colors.surface',
359
+ border: 'semantic.colors.border',
337
360
  },
338
361
  voice: {
339
362
  recording: {
@@ -374,6 +397,18 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
374
397
  border: 'palette.colors.gray.200',
375
398
  },
376
399
  },
400
+ scrollToBottom: {
401
+ background: 'components.button.primary.background',
402
+ foreground: 'components.button.primary.foreground',
403
+ border: 'semantic.colors.primary',
404
+ size: '40px',
405
+ borderRadius: 'palette.radius.full',
406
+ shadow: 'palette.shadows.sm',
407
+ padding: '0.5rem 0.875rem',
408
+ gap: '0.5rem',
409
+ fontSize: '0.875rem',
410
+ iconSize: '14px',
411
+ },
377
412
  artifact: {
378
413
  pane: {
379
414
  background: 'semantic.colors.container',
@@ -656,6 +691,7 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
656
691
  cssVars['--persona-radius-md'] = cssVars['--persona-palette-radius-md'] ?? '0.375rem';
657
692
  cssVars['--persona-radius-lg'] = cssVars['--persona-palette-radius-lg'] ?? '0.5rem';
658
693
  cssVars['--persona-radius-xl'] = cssVars['--persona-palette-radius-xl'] ?? '0.75rem';
694
+ cssVars['--persona-radius-full'] = cssVars['--persona-palette-radius-full'] ?? '9999px';
659
695
  cssVars['--persona-launcher-radius'] =
660
696
  cssVars['--persona-components-launcher-borderRadius'] ??
661
697
  cssVars['--persona-palette-radius-full'] ??
@@ -742,6 +778,42 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
742
778
  cssVars['--persona-components-message-assistant-border'] ?? cssVars['--persona-border'];
743
779
  cssVars['--persona-message-assistant-shadow'] =
744
780
  cssVars['--persona-components-message-assistant-shadow'] ?? '0 1px 2px 0 rgb(0 0 0 / 0.05)';
781
+ cssVars['--persona-scroll-to-bottom-bg'] =
782
+ cssVars['--persona-components-scrollToBottom-background'] ??
783
+ cssVars['--persona-button-primary-bg'] ??
784
+ cssVars['--persona-accent'];
785
+ cssVars['--persona-scroll-to-bottom-fg'] =
786
+ cssVars['--persona-components-scrollToBottom-foreground'] ??
787
+ cssVars['--persona-button-primary-fg'] ??
788
+ cssVars['--persona-text-inverse'];
789
+ cssVars['--persona-scroll-to-bottom-border'] =
790
+ cssVars['--persona-components-scrollToBottom-border'] ??
791
+ cssVars['--persona-primary'];
792
+ cssVars['--persona-scroll-to-bottom-size'] =
793
+ cssVars['--persona-components-scrollToBottom-size'] ??
794
+ '40px';
795
+ cssVars['--persona-scroll-to-bottom-radius'] =
796
+ cssVars['--persona-components-scrollToBottom-borderRadius'] ??
797
+ cssVars['--persona-button-radius'] ??
798
+ cssVars['--persona-radius-full'] ??
799
+ '9999px';
800
+ cssVars['--persona-scroll-to-bottom-shadow'] =
801
+ cssVars['--persona-components-scrollToBottom-shadow'] ??
802
+ cssVars['--persona-palette-shadows-sm'] ??
803
+ '0 1px 2px 0 rgb(0 0 0 / 0.05)';
804
+ cssVars['--persona-scroll-to-bottom-padding'] =
805
+ cssVars['--persona-components-scrollToBottom-padding'] ??
806
+ '0.5rem 0.875rem';
807
+ cssVars['--persona-scroll-to-bottom-gap'] =
808
+ cssVars['--persona-components-scrollToBottom-gap'] ??
809
+ '0.5rem';
810
+ cssVars['--persona-scroll-to-bottom-font-size'] =
811
+ cssVars['--persona-components-scrollToBottom-fontSize'] ??
812
+ cssVars['--persona-palette-typography-fontSize-sm'] ??
813
+ '0.875rem';
814
+ cssVars['--persona-scroll-to-bottom-icon-size'] =
815
+ cssVars['--persona-components-scrollToBottom-iconSize'] ??
816
+ '14px';
745
817
 
746
818
  cssVars['--persona-tool-bubble-shadow'] =
747
819
  cssVars['--persona-components-toolBubble-shadow'] ?? '0 5px 15px rgba(15, 23, 42, 0.08)';
@@ -774,6 +846,46 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
774
846
  cssVars['--persona-md-prose-font-family'] = mdProseFont;
775
847
  }
776
848
 
849
+ // Markdown code block
850
+ cssVars['--persona-md-code-block-bg'] =
851
+ cssVars['--persona-components-markdown-codeBlock-background'] ?? cssVars['--persona-container'];
852
+ cssVars['--persona-md-code-block-border-color'] =
853
+ cssVars['--persona-components-markdown-codeBlock-borderColor'] ?? cssVars['--persona-border'];
854
+ cssVars['--persona-md-code-block-text-color'] =
855
+ cssVars['--persona-components-markdown-codeBlock-textColor'] ?? 'inherit';
856
+
857
+ // Markdown table
858
+ cssVars['--persona-md-table-header-bg'] =
859
+ cssVars['--persona-components-markdown-table-headerBackground'] ?? cssVars['--persona-container'];
860
+ cssVars['--persona-md-table-border-color'] =
861
+ cssVars['--persona-components-markdown-table-borderColor'] ?? cssVars['--persona-border'];
862
+
863
+ // Markdown HR
864
+ cssVars['--persona-md-hr-color'] =
865
+ cssVars['--persona-components-markdown-hr-color'] ?? cssVars['--persona-divider'];
866
+
867
+ // Markdown blockquote
868
+ cssVars['--persona-md-blockquote-border-color'] =
869
+ cssVars['--persona-components-markdown-blockquote-borderColor'] ??
870
+ cssVars['--persona-palette-colors-gray-900'];
871
+ cssVars['--persona-md-blockquote-bg'] =
872
+ cssVars['--persona-components-markdown-blockquote-background'] ?? 'transparent';
873
+ cssVars['--persona-md-blockquote-text-color'] =
874
+ cssVars['--persona-components-markdown-blockquote-textColor'] ??
875
+ cssVars['--persona-palette-colors-gray-500'];
876
+
877
+ // Collapsible widget chrome (tool/reasoning/approval bubbles)
878
+ cssVars['--cw-container'] =
879
+ cssVars['--persona-components-collapsibleWidget-container'] ?? cssVars['--persona-surface'];
880
+ cssVars['--cw-surface'] =
881
+ cssVars['--persona-components-collapsibleWidget-surface'] ?? cssVars['--persona-surface'];
882
+ cssVars['--cw-border'] =
883
+ cssVars['--persona-components-collapsibleWidget-border'] ?? cssVars['--persona-border'];
884
+
885
+ // Message border
886
+ cssVars['--persona-message-border'] =
887
+ cssVars['--persona-components-message-border'] ?? cssVars['--persona-border'];
888
+
777
889
  // Icon button tokens
778
890
  const components = theme.components;
779
891
  const iconBtn = components?.iconButton;