@runtypelabs/persona 3.7.0 → 3.8.1

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.
@@ -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
+ }
@@ -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: {
@@ -823,6 +846,46 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
823
846
  cssVars['--persona-md-prose-font-family'] = mdProseFont;
824
847
  }
825
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
+
826
889
  // Icon button tokens
827
890
  const components = theme.components;
828
891
  const iconBtn = components?.iconButton;