@runtypelabs/persona 4.13.0 → 4.15.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 (106) hide show
  1. package/README.md +4 -4
  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-DveIaNx6.d.cts → types-BpWGm7_6.d.cts} +17 -0
  5. package/dist/animations/{types-DveIaNx6.d.ts → types-BpWGm7_6.d.ts} +17 -0
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/chunk-QJ4LV2KW.js +1 -0
  9. package/dist/codegen.cjs +12 -11
  10. package/dist/codegen.js +14 -13
  11. package/dist/{context-mentions-ONG7A7M6.js → context-mentions-BTSTIBFC.js} +1 -1
  12. package/dist/context-mentions.cjs +11 -8
  13. package/dist/context-mentions.d.cts +339 -22
  14. package/dist/context-mentions.d.ts +339 -22
  15. package/dist/context-mentions.js +11 -8
  16. package/dist/index.cjs +68 -67
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +483 -48
  19. package/dist/index.d.ts +483 -48
  20. package/dist/index.global.js +61 -58
  21. package/dist/index.global.js.map +1 -1
  22. package/dist/index.js +70 -69
  23. package/dist/index.js.map +1 -1
  24. package/dist/launcher.global.js +5 -2
  25. package/dist/launcher.global.js.map +1 -1
  26. package/dist/smart-dom-reader.d.cts +339 -22
  27. package/dist/smart-dom-reader.d.ts +339 -22
  28. package/dist/theme-editor-preview.cjs +67 -67
  29. package/dist/theme-editor-preview.d.cts +355 -23
  30. package/dist/theme-editor-preview.d.ts +355 -23
  31. package/dist/theme-editor-preview.js +75 -75
  32. package/dist/theme-editor.cjs +10 -10
  33. package/dist/theme-editor.d.cts +376 -24
  34. package/dist/theme-editor.d.ts +376 -24
  35. package/dist/theme-editor.js +10 -10
  36. package/dist/theme-reference.cjs +1 -1
  37. package/dist/theme-reference.d.cts +38 -0
  38. package/dist/theme-reference.d.ts +38 -0
  39. package/dist/theme-reference.js +1 -1
  40. package/dist/widget.css +1 -1
  41. package/package.json +1 -1
  42. package/src/artifacts-session.test.ts +106 -0
  43. package/src/ask-user-question-tool.ts +10 -7
  44. package/src/client.test.ts +2 -2
  45. package/src/client.ts +4 -1
  46. package/src/components/artifact-card.ts +13 -2
  47. package/src/components/artifact-inline.ts +11 -2
  48. package/src/components/artifact-preview.ts +1 -1
  49. package/src/components/composer-builder.test.ts +2 -0
  50. package/src/components/composer-builder.ts +1 -1
  51. package/src/components/composer-parts.test.ts +105 -4
  52. package/src/components/composer-parts.ts +54 -16
  53. package/src/components/message-bubble.test.ts +7 -1
  54. package/src/components/message-bubble.ts +11 -8
  55. package/src/components/messages.ts +5 -2
  56. package/src/components/panel.ts +31 -5
  57. package/src/components/pill-composer-builder.test.ts +18 -0
  58. package/src/components/suggestions.test.ts +233 -0
  59. package/src/components/suggestions.ts +463 -81
  60. package/src/components/widget-view.ts +4 -0
  61. package/src/defaults.ts +5 -3
  62. package/src/generated/runtype-openapi-contract.ts +17 -0
  63. package/src/index-core.ts +52 -2
  64. package/src/launcher-global.ts +1 -1
  65. package/src/plugins/types.ts +71 -3
  66. package/src/presets.ts +3 -3
  67. package/src/session.ts +55 -12
  68. package/src/styles/widget-styles.test.ts +162 -0
  69. package/src/styles/widget.css +440 -1
  70. package/src/suggest-replies-tool.test.ts +233 -2
  71. package/src/suggest-replies-tool.ts +115 -39
  72. package/src/theme-editor/index.ts +5 -0
  73. package/src/theme-editor/presets.test.ts +43 -0
  74. package/src/theme-editor/presets.ts +101 -87
  75. package/src/theme-editor/preview-utils.test.ts +34 -0
  76. package/src/theme-editor/preview-utils.ts +77 -2
  77. package/src/theme-editor/sections.test.ts +72 -7
  78. package/src/theme-editor/sections.ts +113 -13
  79. package/src/theme-editor/state-preferences.test.ts +15 -0
  80. package/src/theme-editor/state.ts +46 -0
  81. package/src/theme-editor/types.ts +15 -1
  82. package/src/theme-editor/webmcp/tools.test.ts +13 -0
  83. package/src/theme-editor/webmcp/tools.ts +5 -0
  84. package/src/theme-reference.ts +9 -2
  85. package/src/types/theme.ts +32 -0
  86. package/src/types.ts +291 -25
  87. package/src/ui.artifact-pane-gating.test.ts +5 -5
  88. package/src/ui.config-preferences.test.ts +166 -0
  89. package/src/ui.detached-panel.test.ts +1 -1
  90. package/src/ui.suggest-replies.test.ts +1066 -1
  91. package/src/ui.ts +311 -45
  92. package/src/ui.typing-indicator.test.ts +135 -0
  93. package/src/utils/artifact-display.test.ts +180 -16
  94. package/src/utils/artifact-display.ts +155 -14
  95. package/src/utils/artifact-file.test.ts +1 -1
  96. package/src/utils/code-generators.test.ts +40 -0
  97. package/src/utils/code-generators.ts +22 -0
  98. package/src/utils/config-merge.test.ts +84 -0
  99. package/src/utils/config-merge.ts +6 -1
  100. package/src/utils/feature-preferences.test.ts +326 -0
  101. package/src/utils/feature-preferences.ts +498 -0
  102. package/src/utils/icons.test.ts +26 -0
  103. package/src/utils/icons.ts +8 -0
  104. package/src/utils/theme.test.ts +171 -0
  105. package/src/utils/tokens.ts +71 -7
  106. package/dist/chunk-MMUPR2JW.js +0 -1
package/src/index-core.ts CHANGED
@@ -5,6 +5,8 @@ import {
5
5
 
6
6
  export type {
7
7
  AgentWidgetConfig,
8
+ AgentWidgetStorageAdapter,
9
+ AgentWidgetStoredState,
8
10
  AgentWidgetConfigPatch,
9
11
  TargetResolver,
10
12
  ResolvedTarget,
@@ -12,12 +14,19 @@ export type {
12
14
  AgentWidgetArtifactsFeature,
13
15
  AgentWidgetArtifactsLayoutConfig,
14
16
  PersonaArtifactDisplayMode,
17
+ PersonaArtifactDisplayModeInput,
18
+ PersonaArtifactDisplayRules,
19
+ PersonaArtifactFilesDisplayRules,
15
20
  PersonaArtifactKind,
21
+ PersonaArtifactPresentation,
16
22
  PersonaArtifactRecord,
17
23
  PersonaArtifactActionContext,
18
24
  PersonaArtifactStatusLabelContext,
19
25
  PersonaArtifactCustomAction,
20
26
  PersonaArtifactManualUpsert,
27
+ WidgetPreferenceCapabilities,
28
+ WidgetArtifactLayoutPreference,
29
+ WidgetPreferenceSlice,
21
30
  ArtifactConfigPayload,
22
31
  AgentWidgetInitOptions,
23
32
  AgentWidgetMessage,
@@ -131,9 +140,45 @@ export type {
131
140
  AskUserQuestionPrompt,
132
141
  AskUserQuestionOption,
133
142
  AgentWidgetAskUserQuestionFeature,
134
- AgentWidgetAskUserQuestionStyles
143
+ AgentWidgetAskUserQuestionStyles,
144
+ AgentWidgetSuggestion,
145
+ AgentWidgetSuggestionChipsConfig,
146
+ AgentWidgetResolvedSuggestion,
147
+ AgentWidgetSuggestionBehavior,
148
+ AgentWidgetSuggestionVariant,
149
+ AgentWidgetSuggestionSurface,
150
+ AgentWidgetSuggestionSource,
151
+ AgentWidgetSuggestionSurfaceConfig,
152
+ AgentWidgetStarterSuggestionsConfig,
153
+ AgentWidgetFollowUpSuggestionsConfig,
154
+ AgentWidgetSuggestionsConfig
135
155
  } from "./types";
136
156
 
157
+ export {
158
+ applyFeaturePreferences,
159
+ parseWidgetPreferenceSlice,
160
+ resolveConfigPreferences,
161
+ normalizeMediaType,
162
+ } from "./utils/feature-preferences";
163
+
164
+ export type {
165
+ WidgetPreferenceParseIssueCode,
166
+ WidgetPreferenceParseIssue,
167
+ WidgetPreferenceParseResult,
168
+ } from "./utils/feature-preferences";
169
+
170
+ export {
171
+ canonicalArtifactDisplayMode,
172
+ resolveArtifactDisplay,
173
+ resolveArtifactDisplayMode,
174
+ } from "./utils/artifact-display";
175
+
176
+ export type {
177
+ PersonaArtifactDisplayDescriptor,
178
+ PersonaArtifactDisplayMatch,
179
+ PersonaArtifactDisplayResolution,
180
+ } from "./utils/artifact-display";
181
+
137
182
  export type {
138
183
  RuntypeExecutionStreamEvent,
139
184
  RuntypeStreamEventOf,
@@ -292,7 +337,12 @@ export { VERSION } from "./version";
292
337
  export type { AgentWidgetInitHandle };
293
338
 
294
339
  // Plugin system exports
295
- export type { AgentWidgetPlugin } from "./plugins/types";
340
+ export type {
341
+ AgentWidgetPlugin,
342
+ AgentWidgetTransformSuggestionsContext,
343
+ AgentWidgetRenderSuggestionContext,
344
+ AgentWidgetSuggestionSelectContext
345
+ } from "./plugins/types";
296
346
  export { pluginRegistry } from "./plugins/registry";
297
347
 
298
348
  // Stream animation plugin API: lets consumers register custom animations
@@ -76,7 +76,7 @@ const mergeCriticalLauncherConfig = (
76
76
  * Mirrors the full widget's DOM exactly (`runtime/init.ts` + `ui.ts`): a
77
77
  * `[data-persona-root]` wrapper carries the theme CSS variables and the launcher
78
78
  * button is its child. Keeping the theme vars on the wrapper (not the button)
79
- * leaves the button's own inline style byte-identical to the full widget's, so
79
+ * leaves the button's own inline style matching the full widget's, so
80
80
  * the eventual mount-then-remove handoff is invisible.
81
81
  */
82
82
  export const mount = (
@@ -8,9 +8,51 @@ import {
8
8
  EventStreamViewRenderContext,
9
9
  EventStreamRowRenderContext,
10
10
  EventStreamToolbarRenderContext,
11
- EventStreamPayloadRenderContext
11
+ EventStreamPayloadRenderContext,
12
+ AgentWidgetSuggestion,
13
+ AgentWidgetResolvedSuggestion,
14
+ AgentWidgetSuggestionSource,
15
+ AgentWidgetSuggestionSurface,
16
+ AgentWidgetSuggestionVariant
12
17
  } from "../types";
13
18
 
19
+ export type AgentWidgetTransformSuggestionsContext = {
20
+ /**
21
+ * Fresh copy of the suggestions entering this plugin, already normalized:
22
+ * shorthand strings expanded and the effective send/fill behavior resolved.
23
+ */
24
+ suggestions: AgentWidgetResolvedSuggestion[];
25
+ surface: AgentWidgetSuggestionSurface;
26
+ source: AgentWidgetSuggestionSource;
27
+ config: AgentWidgetConfig;
28
+ };
29
+
30
+ export type AgentWidgetRenderSuggestionContext = {
31
+ /** Fully normalized suggestion, including the effective send/fill behavior. */
32
+ suggestion: AgentWidgetResolvedSuggestion;
33
+ index: number;
34
+ surface: AgentWidgetSuggestionSurface;
35
+ source: AgentWidgetSuggestionSource;
36
+ variant: AgentWidgetSuggestionVariant;
37
+ streaming: boolean;
38
+ config: AgentWidgetConfig;
39
+ /** Build Persona's standard chip, card, or list item. */
40
+ defaultRenderer: () => HTMLElement;
41
+ /**
42
+ * Run the normal selection pipeline: DOM event, plugin selection hooks, then
43
+ * send or fill. Custom renderers should call this instead of sending directly.
44
+ */
45
+ select: () => void;
46
+ };
47
+
48
+ export type AgentWidgetSuggestionSelectContext = {
49
+ suggestion: AgentWidgetResolvedSuggestion;
50
+ surface: AgentWidgetSuggestionSurface;
51
+ source: AgentWidgetSuggestionSource;
52
+ variant: AgentWidgetSuggestionVariant;
53
+ config: AgentWidgetConfig;
54
+ };
55
+
14
56
  /**
15
57
  * Plugin interface for customizing widget components
16
58
  */
@@ -25,6 +67,34 @@ export interface AgentWidgetPlugin {
25
67
  */
26
68
  priority?: number;
27
69
 
70
+ /**
71
+ * Transform, filter, enrich, or reorder suggestions. Hooks receive normalized
72
+ * items and compose in plugin priority order; the return value may use the
73
+ * loose shape (string shorthand included) and is re-normalized before the next
74
+ * hook. A per-item `behavior` set here survives. `maxItems` caps the list
75
+ * after the full chain. Return an empty array to hide the surface.
76
+ */
77
+ transformSuggestions?: (
78
+ context: AgentWidgetTransformSuggestionsContext
79
+ ) => AgentWidgetSuggestion[];
80
+
81
+ /**
82
+ * Render one starter or follow-up suggestion. Return null to use Persona's
83
+ * default chip/card/list renderer. Call `select()` from custom interaction
84
+ * controls to preserve events, selection hooks, and send/fill behavior.
85
+ */
86
+ renderSuggestion?: (
87
+ context: AgentWidgetRenderSuggestionContext
88
+ ) => HTMLElement | null;
89
+
90
+ /**
91
+ * Observe or intercept a suggestion selection. Return false to cancel the
92
+ * built-in send/fill action. Hooks run in plugin priority order.
93
+ */
94
+ onSuggestionSelect?: (
95
+ context: AgentWidgetSuggestionSelectContext
96
+ ) => void | boolean;
97
+
28
98
  /**
29
99
  * Custom renderer for message bubbles
30
100
  * Return null to use default renderer
@@ -298,5 +368,3 @@ export interface AgentWidgetPlugin {
298
368
 
299
369
 
300
370
 
301
-
302
-
package/src/presets.ts CHANGED
@@ -75,9 +75,9 @@ export const PRESET_SHOP: WidgetPreset = {
75
75
  sendButtonLabel: "Send",
76
76
  },
77
77
  suggestionChips: [
78
- "What can you help me with?",
79
- "Tell me about your features",
80
- "How does this work?",
78
+ "Show me what you can help with",
79
+ "Walk me through getting started",
80
+ "Answer a question about this page",
81
81
  ],
82
82
  },
83
83
  };
package/src/session.ts CHANGED
@@ -2,6 +2,7 @@ import { AgentWidgetClient, type SSEEventCallback } from "./client";
2
2
  import { isWebMcpToolName } from "./webmcp-bridge";
3
3
  import {
4
4
  SUGGEST_REPLIES_TOOL_NAME,
5
+ resolveFollowUpsFeature,
5
6
  suggestRepliesToolResult,
6
7
  } from "./suggest-replies-tool";
7
8
  import {
@@ -77,10 +78,11 @@ export type AgentWidgetSessionStatus =
77
78
  * request. When NONE of these change, `updateConfig` can refresh the live
78
79
  * client in place (preserving the WebMCP bridge and any in-flight stream/resume)
79
80
  * instead of swapping in a fresh client and tearing down WebMCP state. Fields
80
- * outside this list (theme, copy, layout, suggestionChips, iterationDisplay,
81
- * postprocessMessage, feature display toggles, …) are display-only and safe to
82
- * apply mid-turn: which is what a self-styling widget needs when a `webmcp:*`
83
- * theme tool re-renders the widget while the agent's turn is still streaming.
81
+ * outside this list (theme, copy, layout, suggestions, suggestionChips,
82
+ * iterationDisplay, postprocessMessage, feature display toggles, …) are
83
+ * display-only and safe to apply mid-turn: which is what a self-styling widget
84
+ * needs when a `webmcp:*` theme tool re-renders the widget while the agent's
85
+ * turn is still streaming.
84
86
  *
85
87
  * Compared by identity (`!==`): primitives by value, functions/objects by
86
88
  * reference. A consumer that rebuilds these objects on every render simply
@@ -858,7 +860,11 @@ export class AgentWidgetSession {
858
860
  }
859
861
 
860
862
  public updateConfig(next: AgentWidgetConfig) {
863
+ const previousArtifactDisplay = this.config.features?.artifacts?.display;
861
864
  const merged = { ...this.config, ...next };
865
+ const artifactDisplayChanged =
866
+ JSON.stringify(previousArtifactDisplay) !==
867
+ JSON.stringify(merged.features?.artifacts?.display);
862
868
 
863
869
  // Connection/request-shaping change (apiUrl, clientToken, webmcp, headers,
864
870
  // parser, agent, …) → full client rebuild. UI-only change (theme, copy,
@@ -870,6 +876,7 @@ export class AgentWidgetSession {
870
876
  if (!connectionConfigChanged(this.config, merged)) {
871
877
  this.config = merged;
872
878
  this.client.updateConfig(merged);
879
+ if (artifactDisplayChanged) this.refreshArtifactReferenceBlocks();
873
880
  return;
874
881
  }
875
882
 
@@ -886,6 +893,7 @@ export class AgentWidgetSession {
886
893
  const prevSSECallback = this.client.getSSEEventCallback();
887
894
  this.config = merged;
888
895
  this.client = new AgentWidgetClient(this.config);
896
+ if (artifactDisplayChanged) this.refreshArtifactReferenceBlocks();
889
897
  this.wireDefaultWebMcpConfirm();
890
898
  if (prevSSECallback) {
891
899
  this.client.setSSEEventCallback(prevSSECallback);
@@ -2735,7 +2743,8 @@ export class AgentWidgetSession {
2735
2743
  title: manual.title,
2736
2744
  status: "complete",
2737
2745
  markdown: manual.content,
2738
- ...(manual.file ? { file: manual.file } : {})
2746
+ ...(manual.file ? { file: manual.file } : {}),
2747
+ ...(manual.presentation ? { presentation: manual.presentation } : {})
2739
2748
  }
2740
2749
  : {
2741
2750
  id,
@@ -2743,7 +2752,8 @@ export class AgentWidgetSession {
2743
2752
  title: manual.title,
2744
2753
  status: "complete",
2745
2754
  component: manual.component,
2746
- props: manual.props ?? {}
2755
+ props: manual.props ?? {},
2756
+ ...(manual.presentation ? { presentation: manual.presentation } : {})
2747
2757
  };
2748
2758
  this.artifacts.set(id, rec);
2749
2759
  this.selectedArtifactId = id;
@@ -2757,7 +2767,7 @@ export class AgentWidgetSession {
2757
2767
  /**
2758
2768
  * Injects (or refreshes) the in-thread artifact block for a programmatically
2759
2769
  * upserted artifact, matching the streamed UX: the resolved display mode
2760
- * picks the component ("card"/"panel" → reference card, "inline" → inline
2770
+ * picks the component ("collapsed"/"panel" → reference card, "inline" → inline
2761
2771
  * preview).
2762
2772
  *
2763
2773
  * Unlike the streamed path (which embeds content on `artifact_complete`),
@@ -2774,7 +2784,7 @@ export class AgentWidgetSession {
2774
2784
  const refId = `artifact-ref-${rec.id}`;
2775
2785
  const displayMode = resolveArtifactDisplayMode(
2776
2786
  this.config.features?.artifacts,
2777
- rec.artifactType
2787
+ rec
2778
2788
  );
2779
2789
  const rawContent = buildArtifactRefRawContent(displayMode, {
2780
2790
  artifactId: rec.id,
@@ -2782,6 +2792,7 @@ export class AgentWidgetSession {
2782
2792
  artifactType: rec.artifactType,
2783
2793
  status: "complete",
2784
2794
  ...(rec.file ? { file: rec.file } : {}),
2795
+ ...(rec.presentation ? { presentation: rec.presentation } : {}),
2785
2796
  ...(rec.component ? { component: rec.component } : {}),
2786
2797
  ...(rec.props ? { componentProps: rec.props } : {}),
2787
2798
  ...(rec.markdown !== undefined ? { markdown: rec.markdown } : {})
@@ -2803,6 +2814,38 @@ export class AgentWidgetSession {
2803
2814
  });
2804
2815
  }
2805
2816
 
2817
+ /**
2818
+ * Re-materialize existing transcript artifact blocks after a live display
2819
+ * preference update. One messages callback keeps conversion atomic.
2820
+ */
2821
+ private refreshArtifactReferenceBlocks(): void {
2822
+ let changed = false;
2823
+ for (const rec of this.artifacts.values()) {
2824
+ const message = this.messages.find((candidate) => candidate.id === `artifact-ref-${rec.id}`);
2825
+ if (!message) continue;
2826
+ const displayMode = resolveArtifactDisplayMode(
2827
+ this.config.features?.artifacts,
2828
+ rec
2829
+ );
2830
+ const rawContent = buildArtifactRefRawContent(displayMode, {
2831
+ artifactId: rec.id,
2832
+ title: rec.title,
2833
+ artifactType: rec.artifactType,
2834
+ status: rec.status,
2835
+ ...(rec.file ? { file: rec.file } : {}),
2836
+ ...(rec.presentation ? { presentation: rec.presentation } : {}),
2837
+ ...(rec.component ? { component: rec.component } : {}),
2838
+ ...(rec.props ? { componentProps: rec.props } : {}),
2839
+ ...(rec.markdown !== undefined ? { markdown: rec.markdown } : {}),
2840
+ });
2841
+ if (message.rawContent === rawContent) continue;
2842
+ message.rawContent = rawContent;
2843
+ message.streaming = rec.status === "streaming";
2844
+ changed = true;
2845
+ }
2846
+ if (changed) this.callbacks.onMessagesChanged([...this.messages]);
2847
+ }
2848
+
2806
2849
  private clearArtifactState(): void {
2807
2850
  if (this.artifacts.size === 0 && this.selectedArtifactId === null) return;
2808
2851
  this.artifacts.clear();
@@ -2942,15 +2985,15 @@ export class AgentWidgetSession {
2942
2985
  // indefinitely: `resolveWebMcpToolCall` translates the missing-bridge
2943
2986
  // case into an isError result that resumes the flow cleanly.
2944
2987
  // `suggest_replies`, by contrast, is gated on its feature flag: when
2945
- // `features.suggestReplies.enabled === false` the widget neither
2946
- // renders chips nor resumes: the same parked-execution posture as a
2947
- // server-declared ask_user_question with its sheet disabled.
2988
+ // the resolved `enabled` is false the widget neither renders chips nor
2989
+ // resumes: the same parked-execution posture as a server-declared
2990
+ // ask_user_question with its sheet disabled.
2948
2991
  const tc = event.message.toolCall;
2949
2992
  const autoResolvable =
2950
2993
  !!tc?.name &&
2951
2994
  (isWebMcpToolName(tc.name) ||
2952
2995
  (tc.name === SUGGEST_REPLIES_TOOL_NAME &&
2953
- this.config.features?.suggestReplies?.enabled !== false));
2996
+ resolveFollowUpsFeature(this.config).enabled));
2954
2997
  if (
2955
2998
  event.message.agentMetadata?.awaitingLocalTool === true &&
2956
2999
  autoResolvable
@@ -0,0 +1,162 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ const widgetCss = readFileSync(
6
+ new URL("./widget.css", import.meta.url),
7
+ "utf8",
8
+ );
9
+
10
+ describe("suggestion interaction styles", () => {
11
+ it("keeps suggestion surfaces from collapsing inside constrained transcripts", () => {
12
+ const selector = ".persona-suggestions {";
13
+ const start = widgetCss.indexOf(selector);
14
+ const end = widgetCss.indexOf("\n}", start);
15
+ const surfaceRule = widgetCss.slice(start, end);
16
+
17
+ expect(start).toBeGreaterThan(-1);
18
+ expect(surfaceRule).toContain("flex-shrink: 0");
19
+ });
20
+
21
+ it("keeps the primary emphasis a quiet accent rather than a solid fill", () => {
22
+ const selector = '.persona-suggestion[data-emphasis="primary"] {';
23
+ const start = widgetCss.indexOf(selector);
24
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
25
+
26
+ expect(start).toBeGreaterThan(-1);
27
+ expect(rule).toContain("border-color: var(--persona-primary)");
28
+ // Plain declaration first so engines without color-mix stay transparent.
29
+ expect(rule).toContain("background: transparent");
30
+ expect(rule).toContain(
31
+ "background: color-mix(in srgb, var(--persona-primary) 6%, transparent)",
32
+ );
33
+ expect(rule).not.toContain("--persona-text-inverse");
34
+ });
35
+
36
+ it("strengthens the primary wash on hover instead of filling it", () => {
37
+ const selector =
38
+ '.persona-suggestion[data-emphasis="primary"]:hover {';
39
+ const start = widgetCss.indexOf(selector);
40
+ const hoverRule = widgetCss.slice(start, widgetCss.indexOf("\n }", start));
41
+
42
+ expect(start).toBeGreaterThan(-1);
43
+ expect(hoverRule).toContain("border-color: var(--persona-primary)");
44
+ expect(hoverRule).toContain(
45
+ "color-mix(in srgb, var(--persona-primary) 12%, transparent)",
46
+ );
47
+ expect(hoverRule).not.toContain("filter: brightness");
48
+ });
49
+
50
+ it("rests cards flat and reserves the shadow plus lift for hover", () => {
51
+ const restStart = widgetCss.indexOf(".persona-suggestion--card {");
52
+ const restRule = widgetCss.slice(
53
+ restStart,
54
+ widgetCss.indexOf("\n}", restStart),
55
+ );
56
+
57
+ expect(restStart).toBeGreaterThan(-1);
58
+ expect(restRule).toContain(
59
+ "box-shadow: var(--persona-components-suggestion-card-shadow, none)",
60
+ );
61
+
62
+ const hoverStart = widgetCss.indexOf(".persona-suggestion--card:hover {");
63
+ const hoverRule = widgetCss.slice(
64
+ hoverStart,
65
+ widgetCss.indexOf("\n }", hoverStart),
66
+ );
67
+
68
+ expect(hoverStart).toBeGreaterThan(-1);
69
+ expect(hoverRule).toContain("box-shadow: var(--persona-shadow-sm)");
70
+ expect(hoverRule).toContain("transform: translateY(-1px)");
71
+ });
72
+
73
+ it("reveals the suggestion arrow only on hover and keyboard focus", () => {
74
+ // Searched past `__copy`: the bare selector also appears in the shared
75
+ // `flex: 0 0 auto` group above it and the coarse-pointer override below.
76
+ const restStart = widgetCss.indexOf(
77
+ ".persona-suggestion__arrow {",
78
+ widgetCss.indexOf(".persona-suggestion__copy {"),
79
+ );
80
+ const restRule = widgetCss.slice(
81
+ restStart,
82
+ widgetCss.indexOf("\n}", restStart),
83
+ );
84
+
85
+ expect(restStart).toBeGreaterThan(-1);
86
+ expect(restRule).toContain("opacity: 0;");
87
+ expect(restRule).toContain("transition: opacity");
88
+ expect(widgetCss).toContain(
89
+ ".persona-suggestion:focus-visible .persona-suggestion__arrow",
90
+ );
91
+ expect(widgetCss).toContain(
92
+ ".persona-suggestion:hover .persona-suggestion__arrow",
93
+ );
94
+ });
95
+
96
+ it("caps the starter card grid so wide panels get two columns", () => {
97
+ expect(widgetCss).toContain(
98
+ "grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr))",
99
+ );
100
+ const selector =
101
+ '.persona-suggestions[data-variant="card"][data-persona-suggestion-surface="starter"] {';
102
+ const start = widgetCss.indexOf(selector);
103
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
104
+
105
+ expect(start).toBeGreaterThan(-1);
106
+ expect(rule).toContain("max-width: 600px");
107
+ });
108
+
109
+ it("masks only the scrollable chip edges that still contain content", () => {
110
+ expect(widgetCss).toContain(
111
+ '[data-overflow="scroll"][data-scroll-right]:not(',
112
+ );
113
+ expect(widgetCss).toContain(
114
+ '[data-overflow="scroll"][data-scroll-left]:not(',
115
+ );
116
+ expect(widgetCss).toContain(
117
+ '[data-overflow="scroll"][data-scroll-left][data-scroll-right]',
118
+ );
119
+ expect(widgetCss).toContain(
120
+ "--persona-suggestion-scroll-fade-size: 32px",
121
+ );
122
+ });
123
+ });
124
+
125
+ describe("composer spacing styles", () => {
126
+ it("themes the composer form padding and gap with utility-matching fallbacks", () => {
127
+ const selector =
128
+ "[data-persona-root] .persona-widget-composer:not(.persona-pill-composer) {";
129
+ const start = widgetCss.indexOf(selector);
130
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
131
+
132
+ expect(start).toBeGreaterThan(-1);
133
+ // Fallbacks reproduce `persona-px-4 persona-py-3` and `persona-gap-2`.
134
+ expect(rule).toContain(
135
+ "padding: var(--persona-composer-padding, 0.75rem 1rem)",
136
+ );
137
+ expect(rule).toContain("gap: var(--persona-composer-gap, 0.5rem)");
138
+ });
139
+
140
+ it("themes the composer textarea type with text-sm fallbacks", () => {
141
+ const selector = "[data-persona-root] .persona-composer-textarea {";
142
+ const start = widgetCss.indexOf(selector);
143
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
144
+
145
+ expect(start).toBeGreaterThan(-1);
146
+ expect(rule).toContain(
147
+ "font-size: var(--persona-composer-font-size, 0.875rem)",
148
+ );
149
+ expect(rule).toContain(
150
+ "line-height: var(--persona-composer-line-height, 1.25rem)",
151
+ );
152
+ });
153
+
154
+ it("leaves the pill composer on its own single-row geometry", () => {
155
+ const start = widgetCss.indexOf(".persona-pill-composer {");
156
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
157
+
158
+ expect(start).toBeGreaterThan(-1);
159
+ expect(rule).toContain("padding: 6px 14px");
160
+ expect(rule).toContain("gap: 8px");
161
+ });
162
+ });