@runtypelabs/persona 4.8.0 → 4.9.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 (117) hide show
  1. package/README.md +1 -0
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-BsZtXPKK.d.cts → types-4ROVJ1gA.d.cts} +42 -0
  5. package/dist/animations/{types-BsZtXPKK.d.ts → types-4ROVJ1gA.d.ts} +42 -0
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/chunk-IO5VVUKP.js +3 -0
  9. package/dist/chunk-IPVK3KOM.js +1 -0
  10. package/dist/chunk-UPO4GUFC.js +1 -0
  11. package/dist/codegen.cjs +6 -6
  12. package/dist/codegen.js +8 -8
  13. package/dist/context-mentions-7S5KVUTG.js +169 -0
  14. package/dist/context-mentions-inline-TTCN7ZM2.js +4 -0
  15. package/dist/context-mentions-inline.cjs +4 -0
  16. package/dist/context-mentions-inline.d.cts +203 -0
  17. package/dist/context-mentions-inline.d.ts +203 -0
  18. package/dist/context-mentions-inline.js +4 -0
  19. package/dist/context-mentions.cjs +295 -0
  20. package/dist/context-mentions.d.cts +7025 -0
  21. package/dist/context-mentions.d.ts +7025 -0
  22. package/dist/context-mentions.js +295 -0
  23. package/dist/index.cjs +59 -51
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +622 -2
  26. package/dist/index.d.ts +622 -2
  27. package/dist/index.global.js +54 -46
  28. package/dist/index.global.js.map +1 -1
  29. package/dist/index.js +59 -51
  30. package/dist/index.js.map +1 -1
  31. package/dist/launcher.global.js +2 -2
  32. package/dist/launcher.global.js.map +1 -1
  33. package/dist/plugin-kit.cjs +1 -1
  34. package/dist/plugin-kit.d.cts +17 -0
  35. package/dist/plugin-kit.d.ts +17 -0
  36. package/dist/plugin-kit.js +1 -1
  37. package/dist/smart-dom-reader.cjs +17 -16
  38. package/dist/smart-dom-reader.d.cts +507 -1
  39. package/dist/smart-dom-reader.d.ts +507 -1
  40. package/dist/smart-dom-reader.js +17 -16
  41. package/dist/theme-editor-preview.cjs +234 -55
  42. package/dist/theme-editor-preview.d.cts +473 -0
  43. package/dist/theme-editor-preview.d.ts +473 -0
  44. package/dist/theme-editor-preview.js +48 -42
  45. package/dist/theme-editor.cjs +7 -7
  46. package/dist/theme-editor.d.cts +473 -0
  47. package/dist/theme-editor.d.ts +473 -0
  48. package/dist/theme-editor.js +5 -5
  49. package/dist/theme-reference.cjs +1 -1
  50. package/dist/theme-reference.d.cts +2 -0
  51. package/dist/theme-reference.d.ts +2 -0
  52. package/dist/widget.css +1 -1
  53. package/package.json +15 -3
  54. package/src/client.test.ts +69 -0
  55. package/src/client.ts +65 -51
  56. package/src/components/composer-parts.ts +3 -12
  57. package/src/components/context-mention-button.test.ts +70 -0
  58. package/src/components/context-mention-button.ts +82 -0
  59. package/src/components/context-mention-chip.ts +134 -0
  60. package/src/components/context-mention-menu.test.ts +508 -0
  61. package/src/components/context-mention-menu.ts +0 -0
  62. package/src/components/message-bubble.test.ts +175 -0
  63. package/src/components/message-bubble.ts +177 -19
  64. package/src/components/panel.ts +7 -10
  65. package/src/context-mentions-bundle.test.ts +163 -0
  66. package/src/context-mentions-entry.ts +185 -0
  67. package/src/context-mentions-inline-entry.test.ts +136 -0
  68. package/src/context-mentions-inline-entry.ts +226 -0
  69. package/src/context-mentions-inline-loader.test.ts +30 -0
  70. package/src/context-mentions-inline-loader.ts +36 -0
  71. package/src/context-mentions-inline.ts +15 -0
  72. package/src/context-mentions-loader.ts +32 -0
  73. package/src/context-mentions.ts +16 -0
  74. package/src/index-core.ts +26 -0
  75. package/src/index-global.ts +51 -0
  76. package/src/markdown-parsers-loader.ts +35 -44
  77. package/src/plugin-kit.test.ts +40 -0
  78. package/src/plugin-kit.ts +39 -5
  79. package/src/session.mentions.test.ts +175 -0
  80. package/src/session.test.ts +52 -4
  81. package/src/session.ts +121 -5
  82. package/src/smart-dom-reader.test.ts +129 -2
  83. package/src/smart-dom-reader.ts +127 -1
  84. package/src/styles/context-mention-menu-css.ts +176 -0
  85. package/src/styles/widget.css +229 -126
  86. package/src/theme-editor/sections.ts +3 -3
  87. package/src/types/theme.ts +2 -0
  88. package/src/types.ts +525 -0
  89. package/src/ui.mention-submit.test.ts +235 -0
  90. package/src/ui.ts +301 -51
  91. package/src/utils/chunk-loader.test.ts +97 -0
  92. package/src/utils/chunk-loader.ts +88 -0
  93. package/src/utils/composer-contenteditable.test.ts +507 -0
  94. package/src/utils/composer-contenteditable.ts +626 -0
  95. package/src/utils/composer-document.test.ts +280 -0
  96. package/src/utils/composer-document.ts +293 -0
  97. package/src/utils/composer-history.test.ts +35 -7
  98. package/src/utils/composer-history.ts +30 -20
  99. package/src/utils/composer-input.ts +159 -0
  100. package/src/utils/context-mention-controller.test.ts +1215 -0
  101. package/src/utils/context-mention-controller.ts +1186 -0
  102. package/src/utils/context-mention-manager.test.ts +422 -0
  103. package/src/utils/context-mention-manager.ts +410 -0
  104. package/src/utils/context-mention-orchestrator.test.ts +538 -0
  105. package/src/utils/context-mention-orchestrator.ts +348 -0
  106. package/src/utils/live-region.test.ts +108 -0
  107. package/src/utils/live-region.ts +94 -0
  108. package/src/utils/mention-channels.ts +63 -0
  109. package/src/utils/mention-llm-format.test.ts +91 -0
  110. package/src/utils/mention-llm-format.ts +79 -0
  111. package/src/utils/mention-matcher.test.ts +86 -0
  112. package/src/utils/mention-matcher.ts +221 -0
  113. package/src/utils/mention-token.ts +72 -0
  114. package/src/utils/mention-trigger.test.ts +155 -0
  115. package/src/utils/mention-trigger.ts +156 -0
  116. package/src/utils/theme.test.ts +28 -4
  117. package/src/utils/tokens.ts +27 -11
@@ -0,0 +1,155 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ parseMentionTrigger,
4
+ parseAnyTrigger,
5
+ isMenuOpeningInput,
6
+ stripMentionQuery,
7
+ MENTION_PLACEHOLDER,
8
+ } from "./mention-trigger";
9
+
10
+ describe("parseMentionTrigger", () => {
11
+ it("activates on @ at the start of input", () => {
12
+ const m = parseMentionTrigger("@App", 4);
13
+ expect(m).toEqual({ triggerIndex: 0, query: "App" });
14
+ });
15
+
16
+ it("activates on @ after whitespace mid-sentence", () => {
17
+ const m = parseMentionTrigger("check @fo", 9);
18
+ expect(m).toEqual({ triggerIndex: 6, query: "fo" });
19
+ });
20
+
21
+ it("returns an empty query right after a bare @", () => {
22
+ const m = parseMentionTrigger("hi @", 4);
23
+ expect(m).toEqual({ triggerIndex: 3, query: "" });
24
+ });
25
+
26
+ it("does NOT activate for an email (@ glued to a word char)", () => {
27
+ expect(parseMentionTrigger("user@example.com", 16)).toBeNull();
28
+ expect(parseMentionTrigger("user@", 5)).toBeNull();
29
+ });
30
+
31
+ it("closes once a space follows the trigger", () => {
32
+ // caret after "fo bar" — whitespace between query and caret ends the mention
33
+ expect(parseMentionTrigger("@fo bar", 7)).toBeNull();
34
+ });
35
+
36
+ it("tracks the nearest active trigger when caret is mid-query", () => {
37
+ const m = parseMentionTrigger("a @one @two", 11);
38
+ expect(m).toEqual({ triggerIndex: 7, query: "two" });
39
+ });
40
+
41
+ it("returns null at caret 0 or out of range", () => {
42
+ expect(parseMentionTrigger("@x", 0)).toBeNull();
43
+ expect(parseMentionTrigger("@x", 99)).toBeNull();
44
+ });
45
+
46
+ it("honors a custom trigger character", () => {
47
+ expect(parseMentionTrigger("see #iss", 8, "#")).toEqual({
48
+ triggerIndex: 4,
49
+ query: "iss",
50
+ });
51
+ expect(parseMentionTrigger("see #iss", 8, "@")).toBeNull();
52
+ });
53
+
54
+ it("handles multi-byte characters in the query", () => {
55
+ const m = parseMentionTrigger("@café", 5);
56
+ expect(m).toEqual({ triggerIndex: 0, query: "café" });
57
+ });
58
+
59
+ it("exports MENTION_PLACEHOLDER as the U+FFFC object-replacement char", () => {
60
+ expect(MENTION_PLACEHOLDER).toBe("");
61
+ });
62
+
63
+ it("treats U+FFFC (inline mention token placeholder) as a query terminator", () => {
64
+ // In inline-mode logical text an existing token is a single `` char. A new
65
+ // `@query` must not scan back THROUGH that token: "@ab" with caret after "b"
66
+ // has no active trigger (the `` ends the backscan like whitespace/newline).
67
+ expect(parseMentionTrigger(`@a${MENTION_PLACEHOLDER}b`, 4)).toBeNull();
68
+ // A fresh `@` typed AFTER a token still activates (scan stops at ``, not the @).
69
+ expect(parseMentionTrigger(" @fo", 5)).toEqual({
70
+ triggerIndex: 2,
71
+ query: "fo",
72
+ });
73
+ });
74
+
75
+ describe("position gating (for / slash-commands)", () => {
76
+ it("line-start: activates at input start and after a newline", () => {
77
+ expect(parseMentionTrigger("/dep", 4, "/", "line-start")).toEqual({
78
+ triggerIndex: 0,
79
+ query: "dep",
80
+ });
81
+ expect(parseMentionTrigger("hi\n/dep", 7, "/", "line-start")).toEqual({
82
+ triggerIndex: 3,
83
+ query: "dep",
84
+ });
85
+ });
86
+
87
+ it("line-start: does NOT activate mid-line (after a space)", () => {
88
+ expect(parseMentionTrigger("hi /dep", 7, "/", "line-start")).toBeNull();
89
+ });
90
+
91
+ it("input-start: only at index 0", () => {
92
+ expect(parseMentionTrigger("/x", 2, "/", "input-start")).toEqual({
93
+ triggerIndex: 0,
94
+ query: "x",
95
+ });
96
+ expect(parseMentionTrigger("hi\n/x", 5, "/", "input-start")).toBeNull();
97
+ });
98
+ });
99
+ });
100
+
101
+ describe("parseAnyTrigger", () => {
102
+ const channels = [
103
+ { trigger: "@", position: "anywhere" as const },
104
+ { trigger: "/", position: "line-start" as const },
105
+ ];
106
+
107
+ it("picks the @ channel for a mid-sentence mention", () => {
108
+ const hit = parseAnyTrigger("hey @fo", 7, channels);
109
+ expect(hit?.channel.trigger).toBe("@");
110
+ expect(hit?.match).toEqual({ triggerIndex: 4, query: "fo" });
111
+ });
112
+
113
+ it("picks the / channel only at line-start", () => {
114
+ const hit = parseAnyTrigger("/dep", 4, channels);
115
+ expect(hit?.channel.trigger).toBe("/");
116
+ expect(hit?.match).toEqual({ triggerIndex: 0, query: "dep" });
117
+ });
118
+
119
+ it("does not match / mid-line even though @ is anywhere", () => {
120
+ expect(parseAnyTrigger("hi /dep", 7, channels)).toBeNull();
121
+ });
122
+
123
+ it("returns null when no channel is active", () => {
124
+ expect(parseAnyTrigger("plain text", 10, channels)).toBeNull();
125
+ });
126
+ });
127
+
128
+ describe("isMenuOpeningInput", () => {
129
+ it("opens on typed text and unknown input types", () => {
130
+ expect(isMenuOpeningInput("insertText")).toBe(true);
131
+ expect(isMenuOpeningInput(undefined)).toBe(true);
132
+ });
133
+
134
+ it("does not open on paste or drop", () => {
135
+ expect(isMenuOpeningInput("insertFromPaste")).toBe(false);
136
+ expect(isMenuOpeningInput("insertFromDrop")).toBe(false);
137
+ });
138
+ });
139
+
140
+ describe("stripMentionQuery", () => {
141
+ it("removes the @query span and moves the caret to the trigger", () => {
142
+ const value = "check @App for bugs";
143
+ const match = parseMentionTrigger("check @App", 10)!;
144
+ // caret sits right after "@App" (index 10)
145
+ const out = stripMentionQuery(value, match, 10);
146
+ expect(out.value).toBe("check for bugs");
147
+ expect(out.caret).toBe(6);
148
+ });
149
+
150
+ it("strips a bare trigger with empty query", () => {
151
+ const out = stripMentionQuery("hi @", { triggerIndex: 3, query: "" }, 4);
152
+ expect(out.value).toBe("hi ");
153
+ expect(out.caret).toBe(3);
154
+ });
155
+ });
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Pure `@`-trigger detection for the composer mention menu.
3
+ *
4
+ * No DOM, no contenteditable — a small function over `(value, caret)` mirroring
5
+ * the Slack/Discord approach. Shared by the core orchestrator's lightweight
6
+ * "should I open?" pre-check and the lazy-loaded controller's full parse, so the
7
+ * trigger rule lives in exactly one place.
8
+ */
9
+
10
+ export type MentionTriggerMatch = {
11
+ /** Index of the trigger character in `value`. */
12
+ triggerIndex: number;
13
+ /** The text typed after the trigger, up to the caret (no leading trigger). */
14
+ query: string;
15
+ };
16
+
17
+ /**
18
+ * Where a trigger may open. `"anywhere"` is the `@` rule (after any whitespace/
19
+ * start); `"line-start"` and `"input-start"` are the natural rules for `/`
20
+ * slash-commands. Kept as a local string union so the parser stays free of the
21
+ * public config types.
22
+ */
23
+ export type MentionTriggerPosition = "anywhere" | "line-start" | "input-start";
24
+
25
+ /** One trigger channel for `parseAnyTrigger` — a char plus its position rule. */
26
+ export type MentionTriggerSpec = {
27
+ trigger: string;
28
+ position?: MentionTriggerPosition;
29
+ /**
30
+ * Allow the query to span spaces/tabs (a newline still ends it). Needed for
31
+ * slash-command ARGS (`/deploy staging` → query `"deploy staging"`); leave
32
+ * false for single-token `@` mentions (a space ends the mention).
33
+ */
34
+ allowSpaces?: boolean;
35
+ };
36
+
37
+ const WHITESPACE = /\s/;
38
+
39
+ /**
40
+ * OBJECT REPLACEMENT CHAR (U+FFFC) — one inline mention token in the composer's
41
+ * *logical* text (see `toLogicalText` in composer-document.ts). Defined HERE, in
42
+ * the dependency-free trigger module, and re-exported from composer-document.ts so
43
+ * the constant has a single source without dragging the document-model runtime
44
+ * into the chip chunk (which imports this module but not composer-document).
45
+ */
46
+ export const MENTION_PLACEHOLDER = "";
47
+
48
+ /** Does `triggerIndex` satisfy the channel's position rule? */
49
+ function positionAllowed(
50
+ value: string,
51
+ triggerIndex: number,
52
+ position: MentionTriggerPosition
53
+ ): boolean {
54
+ if (position === "input-start") return triggerIndex === 0;
55
+ const before = triggerIndex > 0 ? value[triggerIndex - 1] : "";
56
+ if (position === "line-start") return before === "" || before === "\n";
57
+ // "anywhere": preceded by whitespace or start (also excludes `user@example.com`).
58
+ return before === "" || WHITESPACE.test(before);
59
+ }
60
+
61
+ /**
62
+ * Detect an active mention trigger ending at `caret`.
63
+ *
64
+ * Active when, scanning back from the caret, a trigger char is reached with no
65
+ * intervening whitespace AND the trigger satisfies `position`. Returns `null`
66
+ * otherwise — notably for `user@example.com` (trigger glued to a word char) and
67
+ * once a space follows the trigger.
68
+ *
69
+ * @param value Full textarea value.
70
+ * @param caret Caret offset (selectionStart). Out-of-range → `null`.
71
+ * @param trigger Single trigger character. @default "@"
72
+ * @param position Where the trigger may open. @default "anywhere"
73
+ * @param allowSpaces Let the query span spaces/tabs (for command args). @default false
74
+ */
75
+ export function parseMentionTrigger(
76
+ value: string,
77
+ caret: number,
78
+ trigger = "@",
79
+ position: MentionTriggerPosition = "anywhere",
80
+ allowSpaces = false
81
+ ): MentionTriggerMatch | null {
82
+ if (!trigger) return null;
83
+ if (caret <= 0 || caret > value.length) return null;
84
+
85
+ let i = caret - 1;
86
+ while (i >= 0) {
87
+ const ch = value[i];
88
+ if (ch === trigger) {
89
+ if (positionAllowed(value, i, position)) {
90
+ return { triggerIndex: i, query: value.slice(i + 1, caret) };
91
+ }
92
+ // Trigger present but disallowed here (glued word char / not line-start).
93
+ return null;
94
+ }
95
+ // A newline always ends the query. Spaces/tabs end it too, unless the
96
+ // channel allows multi-word queries (slash-command args).
97
+ if (ch === "\n") return null;
98
+ // The mention-token placeholder always terminates the backscan so a query
99
+ // never spans an existing token (`@a<token>b` must not report a query through
100
+ // the token). Never appears in a plain textarea value, so chip mode is
101
+ // unaffected.
102
+ if (ch === MENTION_PLACEHOLDER) return null;
103
+ if (!allowSpaces && WHITESPACE.test(ch)) return null;
104
+ i--;
105
+ }
106
+ return null;
107
+ }
108
+
109
+ /**
110
+ * Resolve which of several trigger channels is active at `caret`. Channels are
111
+ * tested in order; the FIRST with an active match wins (put `@` first, `/`
112
+ * next). At any caret at most one channel can match — the scan stops at the
113
+ * nearest trigger/whitespace — so ordering only matters if two channels share a
114
+ * trigger char (don't do that). Returns the winning channel + its match.
115
+ */
116
+ export function parseAnyTrigger<T extends MentionTriggerSpec>(
117
+ value: string,
118
+ caret: number,
119
+ channels: readonly T[]
120
+ ): { channel: T; match: MentionTriggerMatch } | null {
121
+ for (const channel of channels) {
122
+ const match = parseMentionTrigger(
123
+ value,
124
+ caret,
125
+ channel.trigger,
126
+ channel.position ?? "anywhere",
127
+ channel.allowSpaces ?? false
128
+ );
129
+ if (match) return { channel, match };
130
+ }
131
+ return null;
132
+ }
133
+
134
+ /**
135
+ * Whether a given `inputType` (from an `InputEvent`) is one that may OPEN the
136
+ * menu. Paste never opens it (`insertFromPaste`); only real typing does. The
137
+ * caller separately guards `event.isComposing` for IME safety.
138
+ */
139
+ export function isMenuOpeningInput(inputType: string | undefined): boolean {
140
+ if (!inputType) return true; // older browsers omit inputType; treat as typing
141
+ return inputType !== "insertFromPaste" && inputType !== "insertFromDrop";
142
+ }
143
+
144
+ /**
145
+ * Remove the `@query` span (trigger char through the caret) from `value`,
146
+ * returning the new value + the caret position after removal. Used on select to
147
+ * strip the typed query while leaving the rest of the prose intact.
148
+ */
149
+ export function stripMentionQuery(
150
+ value: string,
151
+ match: MentionTriggerMatch,
152
+ caret: number
153
+ ): { value: string; caret: number } {
154
+ const next = value.slice(0, match.triggerIndex) + value.slice(caret);
155
+ return { value: next, caret: match.triggerIndex };
156
+ }
@@ -169,6 +169,31 @@ describe('theme utils', () => {
169
169
  expect(customVars['--persona-header-action-icon-fg']).toBe('#9ca3af');
170
170
  });
171
171
 
172
+ it('maps button.ghost tokens to the composer ghost icon-button CSS variables', () => {
173
+ const cssVars = themeToCssVariables(createTheme());
174
+
175
+ // The composer's transparent icon buttons (.persona-mention-button /
176
+ // .persona-attachment-button) read these instead of hardcoded inline styles.
177
+ expect(cssVars['--persona-button-ghost-bg']).toBe('transparent');
178
+ expect(cssVars['--persona-button-ghost-fg']).toBe('#111827'); // semantic.text → gray.900
179
+ expect(cssVars['--persona-button-ghost-radius']).toBe('0.375rem'); // radius.md
180
+ expect(cssVars['--persona-button-ghost-hover-bg']).toBe('rgba(0, 0, 0, 0.05)');
181
+
182
+ const custom = createTheme({
183
+ components: {
184
+ button: {
185
+ ghost: {
186
+ foreground: 'palette.colors.accent.500',
187
+ hoverBackground: 'palette.colors.gray.100',
188
+ },
189
+ },
190
+ },
191
+ } as any);
192
+ const customVars = themeToCssVariables(custom);
193
+ expect(customVars['--persona-button-ghost-fg']).toBe('#06b6d4'); // accent.500
194
+ expect(customVars['--persona-button-ghost-hover-bg']).toBe('#f3f4f6'); // gray.100
195
+ });
196
+
172
197
  it('defaults artifact pane fill from semantic container and resolves toolbar background token refs', () => {
173
198
  const theme = createTheme();
174
199
  const cssVars = themeToCssVariables(theme);
@@ -312,12 +337,11 @@ describe('theme utils', () => {
312
337
  );
313
338
  });
314
339
 
315
- it('falls back to the legacy intro-card shadow when no token is set', () => {
340
+ it('defaults the intro card to flat (transparent, no shadow) when no token is set', () => {
316
341
  const theme = createTheme({});
317
342
  const cssVars = themeToCssVariables(theme);
318
- expect(cssVars['--persona-intro-card-shadow']).toBe(
319
- '0 5px 15px rgba(15, 23, 42, 0.08)'
320
- );
343
+ expect(cssVars['--persona-intro-card-shadow']).toBe('none');
344
+ expect(cssVars['--persona-intro-card-bg']).toBe('transparent');
321
345
  });
322
346
 
323
347
  it('drives --persona-tool-bubble-shadow from the theme token (config.toolCall.shadow is applied inline on the bubble, not the root var)', () => {
@@ -263,6 +263,9 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
263
263
  foreground: 'semantic.colors.text',
264
264
  borderRadius: 'palette.radius.md',
265
265
  padding: 'semantic.spacing.sm',
266
+ // Subtle neutral tint on hover — the composer's transparent icon buttons
267
+ // (attachment, mention) read this via `--persona-button-ghost-hover-bg`.
268
+ hoverBackground: 'rgba(0, 0, 0, 0.05)',
266
269
  },
267
270
  },
268
271
  input: {
@@ -326,12 +329,12 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
326
329
  border: 'semantic.colors.border',
327
330
  },
328
331
  introCard: {
329
- // Defaults preserve the legacy `persona-shadow-sm` look exactly so existing
330
- // pages render unchanged when no token is set.
331
- background: 'semantic.colors.surface',
332
+ // Flat by default: the greeting reads as plain text on the transcript
333
+ // background (the industry norm), not an elevated card.
334
+ background: 'transparent',
332
335
  borderRadius: 'palette.radius.2xl',
333
336
  padding: 'semantic.spacing.lg',
334
- shadow: '0 5px 15px rgba(15, 23, 42, 0.08)',
337
+ shadow: 'palette.shadows.none',
335
338
  },
336
339
  toolBubble: {
337
340
  shadow: 'palette.shadows.sm',
@@ -739,6 +742,21 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
739
742
  cssVars['--persona-components-button-primary-borderRadius'] ??
740
743
  cssVars['--persona-palette-radius-full'] ??
741
744
  '9999px';
745
+ // Ghost variant: transparent, neutral-foreground icon buttons (the composer's
746
+ // attachment + mention affordances). Wired to `components.button.ghost.*`.
747
+ cssVars['--persona-button-ghost-bg'] =
748
+ cssVars['--persona-components-button-ghost-background'] ??
749
+ 'transparent';
750
+ cssVars['--persona-button-ghost-fg'] =
751
+ cssVars['--persona-components-button-ghost-foreground'] ??
752
+ cssVars['--persona-text'];
753
+ cssVars['--persona-button-ghost-radius'] =
754
+ cssVars['--persona-components-button-ghost-borderRadius'] ??
755
+ cssVars['--persona-radius-md'] ??
756
+ '0.375rem';
757
+ cssVars['--persona-button-ghost-hover-bg'] =
758
+ cssVars['--persona-components-button-ghost-hoverBackground'] ??
759
+ 'rgba(0, 0, 0, 0.05)';
742
760
  cssVars['--persona-panel-radius'] =
743
761
  cssVars['--persona-components-panel-borderRadius'] ??
744
762
  cssVars['--persona-radius-xl'] ??
@@ -791,19 +809,17 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
791
809
  if (headerTokens?.borderBottom) cssVars['--persona-header-border-bottom'] = headerTokens.borderBottom;
792
810
 
793
811
  // Intro card aliases: short names the panel inline-styles read directly.
794
- // The full-path `--persona-components-introCard-*` variables auto-emit above;
795
- // these mirror them with sensible fallbacks so existing pages keep their look.
796
- const introCardTokens = theme.components?.introCard;
812
+ // The full-path `--persona-components-introCard-*` variables auto-emit above.
813
+ // Default is flat (transparent, no shadow): the greeting renders as plain
814
+ // text on the transcript background; set introCard tokens for a card look.
797
815
  cssVars['--persona-intro-card-bg'] =
798
- cssVars['--persona-components-introCard-background'] ?? cssVars['--persona-surface'];
816
+ cssVars['--persona-components-introCard-background'] ?? 'transparent';
799
817
  cssVars['--persona-intro-card-radius'] =
800
818
  cssVars['--persona-components-introCard-borderRadius'] ?? '1rem';
801
819
  cssVars['--persona-intro-card-padding'] =
802
820
  cssVars['--persona-components-introCard-padding'] ?? '1.5rem';
803
821
  cssVars['--persona-intro-card-shadow'] =
804
- introCardTokens?.shadow
805
- ?? cssVars['--persona-components-introCard-shadow']
806
- ?? '0 5px 15px rgba(15, 23, 42, 0.08)';
822
+ cssVars['--persona-components-introCard-shadow'] ?? 'none';
807
823
 
808
824
  cssVars['--persona-input-background'] =
809
825
  cssVars['--persona-components-input-background'] ?? cssVars['--persona-surface'];