@runtypelabs/persona 4.15.0 → 4.16.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 (86) hide show
  1. package/dist/codegen.cjs +1 -1
  2. package/dist/codegen.js +1 -1
  3. package/dist/context-mentions.d.cts +283 -9
  4. package/dist/context-mentions.d.ts +283 -9
  5. package/dist/index.cjs +68 -68
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +299 -10
  8. package/dist/index.d.ts +299 -10
  9. package/dist/index.global.js +57 -57
  10. package/dist/index.global.js.map +1 -1
  11. package/dist/index.js +69 -69
  12. package/dist/index.js.map +1 -1
  13. package/dist/launcher.global.js +2 -2
  14. package/dist/launcher.global.js.map +1 -1
  15. package/dist/smart-dom-reader.d.cts +283 -9
  16. package/dist/smart-dom-reader.d.ts +283 -9
  17. package/dist/testing.cjs +3 -3
  18. package/dist/testing.d.cts +7 -0
  19. package/dist/testing.d.ts +7 -0
  20. package/dist/testing.js +3 -3
  21. package/dist/theme-editor-preview.cjs +64 -64
  22. package/dist/theme-editor-preview.d.cts +283 -9
  23. package/dist/theme-editor-preview.d.ts +283 -9
  24. package/dist/theme-editor-preview.js +56 -56
  25. package/dist/theme-editor.cjs +10 -10
  26. package/dist/theme-editor.d.cts +283 -10
  27. package/dist/theme-editor.d.ts +283 -10
  28. package/dist/theme-editor.js +10 -10
  29. package/dist/theme-reference.cjs +1 -1
  30. package/dist/theme-reference.d.cts +66 -3
  31. package/dist/theme-reference.d.ts +66 -3
  32. package/dist/theme-reference.js +1 -1
  33. package/dist/widget.css +1 -1
  34. package/package.json +1 -1
  35. package/src/components/artifact-pane.test.ts +27 -0
  36. package/src/components/artifact-pane.ts +7 -3
  37. package/src/components/header-builder.ts +27 -2
  38. package/src/components/header-layouts.test.ts +56 -0
  39. package/src/components/header-layouts.ts +29 -9
  40. package/src/components/launcher-surface.test.ts +403 -0
  41. package/src/components/launcher.ts +297 -2
  42. package/src/components/panel.ts +69 -74
  43. package/src/components/suggestions.test.ts +40 -0
  44. package/src/components/suggestions.ts +213 -153
  45. package/src/components/welcome.test.ts +121 -0
  46. package/src/components/welcome.ts +305 -0
  47. package/src/components/widget-view.test.ts +5 -3
  48. package/src/components/widget-view.ts +14 -7
  49. package/src/defaults.ts +2 -2
  50. package/src/index-core.ts +12 -1
  51. package/src/launcher-global.ts +10 -7
  52. package/src/plugins/types.ts +66 -1
  53. package/src/styles/widget-styles.test.ts +220 -0
  54. package/src/styles/widget.css +353 -9
  55. package/src/testing/mock-stream.test.ts +19 -0
  56. package/src/testing/mock-stream.ts +20 -0
  57. package/src/theme-editor/sections.ts +30 -1
  58. package/src/theme-reference.ts +26 -4
  59. package/src/types/theme.ts +40 -3
  60. package/src/types.ts +164 -6
  61. package/src/ui.body-top-slot.test.ts +95 -0
  62. package/src/ui.content-width.test.ts +91 -0
  63. package/src/ui.docked.test.ts +7 -4
  64. package/src/ui.launcher-teaser.test.ts +162 -0
  65. package/src/ui.rebuild-composer.test.ts +491 -0
  66. package/src/ui.render-welcome.test.ts +505 -0
  67. package/src/ui.scroll-additive.test.ts +91 -21
  68. package/src/ui.scroll.test.ts +302 -14
  69. package/src/ui.suggest-replies.test.ts +46 -0
  70. package/src/ui.ts +747 -136
  71. package/src/ui.welcome.test.ts +504 -0
  72. package/src/utils/artifact-gate.test.ts +27 -0
  73. package/src/utils/artifact-gate.ts +9 -0
  74. package/src/utils/attachment-manager.test.ts +37 -0
  75. package/src/utils/attachment-manager.ts +15 -0
  76. package/src/utils/auto-follow.test.ts +23 -0
  77. package/src/utils/auto-follow.ts +11 -1
  78. package/src/utils/config-merge.ts +2 -0
  79. package/src/utils/content-width.test.ts +37 -0
  80. package/src/utils/content-width.ts +17 -0
  81. package/src/utils/plugin-storage.test.ts +94 -0
  82. package/src/utils/plugin-storage.ts +98 -0
  83. package/src/utils/theme.test.ts +137 -0
  84. package/src/utils/tokens.ts +49 -3
  85. package/src/welcome.test.ts +213 -0
  86. package/src/welcome.ts +130 -0
@@ -106,6 +106,36 @@ describe("suggestion interaction styles", () => {
106
106
  expect(rule).toContain("max-width: 600px");
107
107
  });
108
108
 
109
+ it("centers label-only items and top-aligns only description cards", () => {
110
+ const cardStart = widgetCss.indexOf(".persona-suggestion--card {");
111
+ const cardRule = widgetCss.slice(
112
+ cardStart,
113
+ widgetCss.indexOf("\n}", cardStart),
114
+ );
115
+ expect(cardRule).toContain("align-items: center");
116
+
117
+ const listStart = widgetCss.indexOf(".persona-suggestion--list {");
118
+ const listRule = widgetCss.slice(
119
+ listStart,
120
+ widgetCss.indexOf("\n}", listStart),
121
+ );
122
+ expect(listRule).toContain("align-items: center");
123
+
124
+ // Only content that actually wraps to two lines earns the top alignment.
125
+ const hasStart = widgetCss.indexOf(
126
+ ".persona-suggestion--card:has(.persona-suggestion__description)",
127
+ );
128
+ const hasRule = widgetCss.slice(
129
+ hasStart,
130
+ widgetCss.indexOf("\n}", hasStart),
131
+ );
132
+ expect(hasStart).toBeGreaterThan(-1);
133
+ expect(hasRule).toContain(
134
+ ".persona-suggestion--list:has(.persona-suggestion__description)",
135
+ );
136
+ expect(hasRule).toContain("align-items: flex-start");
137
+ });
138
+
109
139
  it("masks only the scrollable chip edges that still contain content", () => {
110
140
  expect(widgetCss).toContain(
111
141
  '[data-overflow="scroll"][data-scroll-right]:not(',
@@ -120,6 +150,43 @@ describe("suggestion interaction styles", () => {
120
150
  "--persona-suggestion-scroll-fade-size: 32px",
121
151
  );
122
152
  });
153
+
154
+ it("centers the chip row on the composer surface only", () => {
155
+ const selector =
156
+ '[data-persona-composer-suggestions].persona-suggestions[data-variant="chip"] {';
157
+ const start = widgetCss.indexOf(selector);
158
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
159
+
160
+ expect(start).toBeGreaterThan(-1);
161
+ // fit-content + auto margins center a single row while wrapped rows keep
162
+ // a shared left edge; per-row justify-content would rag them.
163
+ expect(rule).toContain("width: fit-content");
164
+ expect(rule).toContain("max-width: 100%");
165
+ expect(rule).toContain("margin-inline: auto");
166
+ expect(rule).not.toContain("justify-content");
167
+
168
+ // Transcript follow-ups and welcome rows keep the default left alignment.
169
+ const sharedStart = widgetCss.indexOf(
170
+ '.persona-suggestions[data-variant="chip"] {',
171
+ );
172
+ const sharedRule = widgetCss.slice(
173
+ sharedStart,
174
+ widgetCss.indexOf("\n}", sharedStart),
175
+ );
176
+ expect(sharedStart).toBeGreaterThan(-1);
177
+ expect(sharedRule).not.toContain("justify-content");
178
+ });
179
+
180
+ it("only pulls follow-ups up against the roomy welcome-visible body gap", () => {
181
+ // Unscoped, the -12px offset would zero out the tightened 12px gap that
182
+ // applies when the welcome host is hidden, gluing follow-ups to the answer.
183
+ expect(widgetCss).toContain(
184
+ '.persona-gap-6 > [data-persona-suggestions="follow-up"] {',
185
+ );
186
+ expect(widgetCss).not.toMatch(
187
+ /\n\[data-persona-suggestions="follow-up"\] \{/,
188
+ );
189
+ });
123
190
  });
124
191
 
125
192
  describe("composer spacing styles", () => {
@@ -160,3 +227,156 @@ describe("composer spacing styles", () => {
160
227
  expect(rule).toContain("gap: 8px");
161
228
  });
162
229
  });
230
+
231
+ describe("transcript layout styles", () => {
232
+ it("drops the empty messages wrapper from the flex flow", () => {
233
+ // A zero-height flex child still triggers the body's gap, so an empty
234
+ // transcript added phantom scrollable space below the welcome (and a
235
+ // scroll-to-bottom arrow with nothing to scroll to).
236
+ expect(widgetCss).toContain(
237
+ ".persona-widget-messages:empty {\n display: none;\n}",
238
+ );
239
+ });
240
+ });
241
+
242
+ describe("launcher teaser styles", () => {
243
+ it("keeps the [hidden] dismiss control hidden despite its own display: grid", () => {
244
+ // dismissible: false sets the hidden property; without this companion
245
+ // rule the block's display: grid outranks the UA [hidden] stylesheet.
246
+ expect(widgetCss).toContain(
247
+ ".persona-launcher-teaser-dismiss[hidden] {\n display: none;\n}",
248
+ );
249
+ });
250
+ });
251
+
252
+ describe("plugin welcome styles", () => {
253
+ it("hides the default welcome content while a plugin element owns the host", () => {
254
+ const selector =
255
+ '.persona-welcome[data-persona-welcome-content="plugin"]\n > :not([data-persona-welcome-plugin]) {';
256
+ const start = widgetCss.indexOf(selector);
257
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
258
+
259
+ expect(start).toBeGreaterThan(-1);
260
+ expect(rule).toContain("display: none !important");
261
+ });
262
+
263
+ it("centers the welcome column at wide widths but never the overlay", () => {
264
+ const start = widgetCss.indexOf(
265
+ ".persona-welcome:not([data-persona-welcome-overlay]) {",
266
+ );
267
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
268
+
269
+ expect(start).toBeGreaterThan(-1);
270
+ // The card variant shares the transcript column so its left-aligned
271
+ // text lines up with messages and composer.
272
+ expect(rule).toContain("max-width: var(--persona-content-max-width, 768px)");
273
+ expect(rule).toContain("margin-inline: auto");
274
+ // The var is published on the root so plugin content can consume it.
275
+ expect(widgetCss).toContain("--persona-welcome-max-width: 640px");
276
+ });
277
+
278
+ it("keeps the narrower centered column for the hero variant only", () => {
279
+ const start = widgetCss.indexOf(
280
+ '.persona-welcome[data-persona-welcome-variant="hero"]:not(',
281
+ );
282
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
283
+
284
+ expect(start).toBeGreaterThan(-1);
285
+ expect(rule).toContain("max-width: var(--persona-welcome-max-width, 640px)");
286
+ });
287
+
288
+ it("aligns the greeting bubble to the transcript column", () => {
289
+ const start = widgetCss.indexOf(".persona-welcome-greeting {");
290
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
291
+
292
+ expect(start).toBeGreaterThan(-1);
293
+ expect(rule).toContain("max-width: var(--persona-content-max-width, 768px)");
294
+ expect(rule).toContain("margin-inline: auto");
295
+ });
296
+
297
+ it("pads the welcome host from the intro-card alias, full padding on overlays", () => {
298
+ // Flat default resolves the alias to `1.5rem 0` (tokens.ts), aligning
299
+ // the text with the content column; overlays are opaque surfaces and
300
+ // keep a fixed symmetric inset.
301
+ const start = widgetCss.indexOf(".persona-welcome {");
302
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
303
+
304
+ expect(start).toBeGreaterThan(-1);
305
+ expect(rule).toContain("padding: var(--persona-intro-card-padding, 1.5rem)");
306
+
307
+ const overlayStart = widgetCss.indexOf(
308
+ ".persona-welcome[data-persona-welcome-overlay] {",
309
+ );
310
+ const overlayRule = widgetCss.slice(
311
+ overlayStart,
312
+ widgetCss.indexOf("\n}", overlayStart),
313
+ );
314
+ expect(overlayRule).toContain("padding: 1.5rem");
315
+ });
316
+
317
+ it("overlays the messages area while plugin content is active", () => {
318
+ const start = widgetCss.indexOf(
319
+ ".persona-welcome[data-persona-welcome-overlay] {",
320
+ );
321
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
322
+
323
+ expect(start).toBeGreaterThan(-1);
324
+ expect(rule).toContain("position: absolute");
325
+ expect(rule).toContain("inset: 0");
326
+ // !important: only way to beat the host's inline intro-card background
327
+ // (default transparent), which otherwise lets the transcript bleed
328
+ // through the overlay.
329
+ expect(rule).toMatch(/background:[\s\S]*!important/);
330
+ // Two classes so it outranks the body's own overflow utility.
331
+ expect(widgetCss).toContain(
332
+ ".persona-widget-body.persona-welcome-overlay-active {\n overflow: hidden;\n}",
333
+ );
334
+ });
335
+ });
336
+
337
+ describe("scrollbar policy styles", () => {
338
+ it("themes every scroller from the shared tokens", () => {
339
+ // scrollbar-color inherits from the root; scrollbar-width does not, so
340
+ // the width needs the descendant star.
341
+ expect(widgetCss).toMatch(
342
+ /\[data-persona-root\] \{\n {2}scrollbar-color: var\(\n\s+--persona-scrollbar-thumb,/,
343
+ );
344
+ expect(widgetCss).toContain("var(--persona-scrollbar-track, transparent)");
345
+ expect(widgetCss).toContain(
346
+ "[data-persona-root],\n[data-persona-root] * {\n scrollbar-width: thin;\n}",
347
+ );
348
+ // The artifact tab strip keeps its own token as an alias layered on the
349
+ // shared thumb token.
350
+ expect(widgetCss).toContain(
351
+ "--persona-artifact-tab-list-scrollbar,\n var(--persona-scrollbar-thumb, var(--persona-border, #e5e7eb))",
352
+ );
353
+ });
354
+
355
+ it("hides the transcript bar at rest under the on-scroll policy without reflow", () => {
356
+ // Rest state keys off the ui.ts-owned attributes; the reveal attribute
357
+ // must appear as a :not() so flipping it shows the bar again.
358
+ expect(widgetCss).toContain(
359
+ '[data-persona-scrollbar="on-scroll"]\n .persona-widget-body:not([data-persona-scrollbar-visible]),\n[data-persona-scrollbar="hidden"] .persona-widget-body {\n scrollbar-width: none;\n}',
360
+ );
361
+ // Old-Safari fallback: webkit hiding agrees with the standard property.
362
+ expect(widgetCss).toContain(
363
+ '[data-persona-scrollbar="hidden"] .persona-widget-body::-webkit-scrollbar {\n display: none;\n}',
364
+ );
365
+ // Classic-scrollbar platforms must not reflow the transcript on toggle.
366
+ expect(widgetCss).toContain(
367
+ '[data-persona-scrollbar="on-scroll"] .persona-widget-body {\n scrollbar-gutter: stable;\n}',
368
+ );
369
+ });
370
+
371
+ it("hover-reveals inner scroller bars under the on-scroll policy", () => {
372
+ expect(widgetCss).toContain(
373
+ '[data-persona-scrollbar="on-scroll"] .persona-artifact-list:not(:hover)',
374
+ );
375
+ expect(widgetCss).toContain(
376
+ '[data-persona-scrollbar="on-scroll"]\n .persona-suggestions[data-overflow="scroll"]:not(:hover)',
377
+ );
378
+ expect(widgetCss).toContain(
379
+ '[data-persona-scrollbar="hidden"] .persona-artifact-list,',
380
+ );
381
+ });
382
+ });
@@ -4,6 +4,10 @@
4
4
  font-size: var(--persona-font-size, 1rem);
5
5
  font-weight: var(--persona-font-weight, 400);
6
6
  line-height: var(--persona-line-height, 1.5);
7
+
8
+ /* Column widths plugin content can consume; --persona-content-max-width is
9
+ re-set inline from layout.contentMaxWidth (default 768px). */
10
+ --persona-welcome-max-width: 640px;
7
11
 
8
12
  /* Theme-aware markdown variables - inherit from theme colors set by JavaScript */
9
13
  --persona-md-code-block-bg: var(--persona-container, #f3f4f6);
@@ -236,6 +240,157 @@
236
240
  border-radius: var(--persona-button-radius);
237
241
  }
238
242
 
243
+ /* Launcher surface + teaser --------------------------------------------- */
244
+
245
+ /* Without a teaser the wrapper generates no box, so the launcher button keeps
246
+ its own fixed placement exactly as it did before the wrapper existed. */
247
+ .persona-launcher-surface {
248
+ display: contents;
249
+ }
250
+
251
+ .persona-launcher-surface[data-teaser] {
252
+ display: flex;
253
+ position: fixed;
254
+ flex-direction: column;
255
+ align-items: flex-end;
256
+ gap: 10px;
257
+ }
258
+
259
+ .persona-launcher-surface[data-teaser^="top"] {
260
+ flex-direction: column-reverse;
261
+ }
262
+
263
+ .persona-launcher-surface[data-teaser$="left"] {
264
+ align-items: flex-start;
265
+ }
266
+
267
+ .persona-launcher-teaser {
268
+ display: flex;
269
+ align-items: flex-start;
270
+ gap: 2px;
271
+ max-width: min(260px, calc(100vw - 48px));
272
+ padding: 2px;
273
+ border: 1px solid var(--persona-border, #e5e7eb);
274
+ border-radius: var(--persona-radius-lg, 1rem);
275
+ background-color: var(--persona-surface, #ffffff);
276
+ box-shadow: var(
277
+ --persona-launcher-shadow,
278
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1),
279
+ 0 4px 6px -4px rgba(0, 0, 0, 0.1)
280
+ );
281
+ }
282
+
283
+ .persona-launcher-teaser[hidden] {
284
+ display: none;
285
+ }
286
+
287
+ .persona-launcher-teaser-text {
288
+ flex: 1 1 auto;
289
+ min-width: 0;
290
+ padding: 8px 10px;
291
+ border: 0;
292
+ border-radius: inherit;
293
+ background: transparent;
294
+ color: var(--persona-text, #111827);
295
+ font: inherit;
296
+ font-size: 0.875rem;
297
+ line-height: 1.4;
298
+ text-align: left;
299
+ cursor: pointer;
300
+ }
301
+
302
+ .persona-launcher-teaser-dismiss {
303
+ flex: 0 0 auto;
304
+ display: grid;
305
+ place-items: center;
306
+ width: 22px;
307
+ height: 22px;
308
+ margin-top: 4px;
309
+ padding: 0;
310
+ border: 0;
311
+ border-radius: var(--persona-launcher-radius, 9999px);
312
+ background: transparent;
313
+ color: var(--persona-muted, #6b7280);
314
+ font-size: 0.875rem;
315
+ line-height: 1;
316
+ cursor: pointer;
317
+ }
318
+
319
+ .persona-launcher-teaser-dismiss:hover {
320
+ background-color: var(--persona-border, #e5e7eb);
321
+ }
322
+
323
+ /* display: grid above beats the UA [hidden] rule; dismissible: false relies on this. */
324
+ .persona-launcher-teaser-dismiss[hidden] {
325
+ display: none;
326
+ }
327
+
328
+ /* An empty transcript still counts as a flex child, so the body's gap
329
+ inserts phantom space below the welcome and fakes scrollable height. */
330
+ .persona-widget-messages:empty {
331
+ display: none;
332
+ }
333
+
334
+ /* Scrollbars ------------------------------------------------------------- */
335
+
336
+ /* One appearance for every scroller in the widget: thin, themed thumb, bare
337
+ track. Standard properties only: mixing ::-webkit-scrollbar styling with
338
+ them makes Chromium drop the pseudo-element rules, so webkit selectors are
339
+ reserved for the hidden states below, where both mechanisms agree.
340
+ scrollbar-color inherits from the root; scrollbar-width does not, hence
341
+ the descendant rule. */
342
+ [data-persona-root] {
343
+ scrollbar-color: var(
344
+ --persona-scrollbar-thumb,
345
+ var(--persona-border, #e5e7eb)
346
+ )
347
+ var(--persona-scrollbar-track, transparent);
348
+ }
349
+ [data-persona-root],
350
+ [data-persona-root] * {
351
+ scrollbar-width: thin;
352
+ }
353
+
354
+ /* "on-scroll" policy (the default): bars hide at rest. ui.ts stamps the
355
+ resolved policy on the root and flips data-persona-scrollbar-visible on
356
+ the transcript while the reader is scrolling or away from the resting
357
+ position. The reserved gutter keeps classic scrollbars (Windows,
358
+ always-show macOS) from reflowing the transcript on toggle. */
359
+ [data-persona-scrollbar="on-scroll"] .persona-widget-body {
360
+ scrollbar-gutter: stable;
361
+ }
362
+ [data-persona-scrollbar="on-scroll"]
363
+ .persona-widget-body:not([data-persona-scrollbar-visible]),
364
+ [data-persona-scrollbar="hidden"] .persona-widget-body {
365
+ scrollbar-width: none;
366
+ }
367
+ [data-persona-scrollbar="on-scroll"]
368
+ .persona-widget-body:not([data-persona-scrollbar-visible])::-webkit-scrollbar,
369
+ [data-persona-scrollbar="hidden"] .persona-widget-body::-webkit-scrollbar {
370
+ display: none;
371
+ }
372
+
373
+ /* Inner scrollers under the same policy: hidden at rest, hover reveals for
374
+ pointer users. Edge fades stay the primary overflow affordance there, and
375
+ morph re-renders make per-element JS wiring fragile, so hover is the whole
376
+ reveal story for these surfaces. */
377
+ [data-persona-scrollbar="on-scroll"] .persona-artifact-list:not(:hover),
378
+ [data-persona-scrollbar="on-scroll"]
379
+ .persona-suggestions[data-overflow="scroll"]:not(:hover),
380
+ [data-persona-scrollbar="hidden"] .persona-artifact-list,
381
+ [data-persona-scrollbar="hidden"] .persona-suggestions[data-overflow="scroll"] {
382
+ scrollbar-width: none;
383
+ }
384
+ [data-persona-scrollbar="on-scroll"]
385
+ .persona-artifact-list:not(:hover)::-webkit-scrollbar,
386
+ [data-persona-scrollbar="on-scroll"]
387
+ .persona-suggestions[data-overflow="scroll"]:not(:hover)::-webkit-scrollbar,
388
+ [data-persona-scrollbar="hidden"] .persona-artifact-list::-webkit-scrollbar,
389
+ [data-persona-scrollbar="hidden"]
390
+ .persona-suggestions[data-overflow="scroll"]::-webkit-scrollbar {
391
+ display: none;
392
+ }
393
+
239
394
  /* Suggestions ----------------------------------------------------------- */
240
395
 
241
396
  .persona-suggestions {
@@ -254,13 +409,28 @@
254
409
  display: none !important;
255
410
  }
256
411
 
412
+ /* `itemGap` is the space between items; the variant-less 8px above stays the
413
+ fallback for containers rendered before a variant is assigned. */
414
+ .persona-suggestions[data-variant="chip"] {
415
+ gap: var(--persona-components-suggestion-chip-itemGap, 8px);
416
+ }
417
+
418
+ .persona-suggestions[data-variant="card"] {
419
+ gap: var(--persona-components-suggestion-card-itemGap, 8px);
420
+ }
421
+
422
+ .persona-suggestions[data-variant="list"] {
423
+ gap: var(--persona-components-suggestion-list-itemGap, 8px);
424
+ }
425
+
257
426
  .persona-suggestions[data-persona-suggestion-surface="starter"] {
258
427
  margin-top: 16px;
259
428
  }
260
429
 
261
- [data-persona-suggestions="follow-up"] {
262
- /* The transcript body uses a roomy 24px section gap; pull contextual
263
- follow-ups closer to the answer they belong to. */
430
+ /* Follow-ups sit 12px from their answer. The body gap is 24px with the
431
+ welcome host visible and already 12px when it is hidden, so the pull-up
432
+ only applies in the roomy case. */
433
+ .persona-gap-6 > [data-persona-suggestions="follow-up"] {
264
434
  margin-top: -12px;
265
435
  }
266
436
 
@@ -352,6 +522,15 @@
352
522
  flex-wrap: wrap;
353
523
  }
354
524
 
525
+ /* Composer surface only: transcript follow-ups and welcome rows stay
526
+ left-aligned. fit-content centers a single row while wrapped rows keep a
527
+ shared left edge; per-row `justify-content: center` would rag them. */
528
+ [data-persona-composer-suggestions].persona-suggestions[data-variant="chip"] {
529
+ width: fit-content;
530
+ max-width: 100%;
531
+ margin-inline: auto;
532
+ }
533
+
355
534
  .persona-suggestion {
356
535
  appearance: none;
357
536
  display: flex;
@@ -398,7 +577,7 @@
398
577
  }
399
578
 
400
579
  .persona-suggestion--card {
401
- align-items: flex-start;
580
+ align-items: center;
402
581
  min-height: var(--persona-components-suggestion-card-minHeight, 72px);
403
582
  gap: var(--persona-components-suggestion-card-gap, 0.625rem);
404
583
  padding: var(--persona-components-suggestion-card-padding, 1rem);
@@ -422,7 +601,7 @@
422
601
  }
423
602
 
424
603
  .persona-suggestion--list {
425
- align-items: flex-start;
604
+ align-items: center;
426
605
  width: 100%;
427
606
  min-height: var(--persona-components-suggestion-list-minHeight, 44px);
428
607
  gap: var(--persona-components-suggestion-list-gap, 0.625rem);
@@ -445,6 +624,13 @@
445
624
  line-height: var(--persona-components-suggestion-list-lineHeight, 1.35);
446
625
  }
447
626
 
627
+ /* Title + description content reads top-aligned; a lone label centers in
628
+ the min-height instead of leaving dead space under the text. */
629
+ .persona-suggestion--card:has(.persona-suggestion__description),
630
+ .persona-suggestion--list:has(.persona-suggestion__description) {
631
+ align-items: flex-start;
632
+ }
633
+
448
634
  .persona-suggestion__icon,
449
635
  .persona-suggestion__arrow {
450
636
  flex: 0 0 auto;
@@ -648,6 +834,154 @@
648
834
  }
649
835
  }
650
836
 
837
+ /* Welcome surface ------------------------------------------------------- */
838
+
839
+ /* Interior padding rides the intro-card alias: the tokens layer zeroes the
840
+ horizontal component when the card resolves flat (no background, no
841
+ shadow), so default installs align the title with the content column
842
+ instead of indenting it by an invisible card inset. */
843
+ .persona-welcome {
844
+ padding: var(--persona-intro-card-padding, 1.5rem);
845
+ }
846
+
847
+ .persona-welcome[hidden],
848
+ .persona-welcome-greeting[hidden] {
849
+ display: none !important;
850
+ }
851
+
852
+ .persona-welcome-icon {
853
+ display: flex;
854
+ margin-bottom: 12px;
855
+ color: var(--persona-accent, currentColor);
856
+ }
857
+
858
+ .persona-welcome-icon[hidden] {
859
+ display: none !important;
860
+ }
861
+
862
+ .persona-welcome-icon-image {
863
+ width: 40px;
864
+ height: 40px;
865
+ border-radius: 999px;
866
+ object-fit: cover;
867
+ }
868
+
869
+ .persona-welcome-icon-text {
870
+ font-size: 32px;
871
+ line-height: 1;
872
+ }
873
+
874
+ /* Welcome copy: `components.introCard.title` / `.subtitle` typography.
875
+ Fallbacks reproduce the utility classes these rules replaced, so an
876
+ unset token renders pixel-identically. Margins stay untokenized. */
877
+ .persona-welcome-title {
878
+ font-family: var(--persona-components-introCard-title-fontFamily, inherit);
879
+ font-size: var(--persona-components-introCard-title-fontSize, 1.125rem);
880
+ font-weight: var(--persona-components-introCard-title-fontWeight, 600);
881
+ line-height: var(--persona-components-introCard-title-lineHeight, 1.75rem);
882
+ letter-spacing: var(--persona-components-introCard-title-letterSpacing, normal);
883
+ color: var(--persona-components-introCard-title-color, var(--persona-primary, #171717));
884
+ }
885
+
886
+ .persona-welcome-subtitle {
887
+ margin-top: 0.5rem;
888
+ font-family: var(--persona-components-introCard-subtitle-fontFamily, inherit);
889
+ font-size: var(--persona-components-introCard-subtitle-fontSize, 0.875rem);
890
+ font-weight: var(--persona-components-introCard-subtitle-fontWeight, inherit);
891
+ line-height: var(--persona-components-introCard-subtitle-lineHeight, 1.25rem);
892
+ letter-spacing: var(--persona-components-introCard-subtitle-letterSpacing, normal);
893
+ color: var(--persona-components-introCard-subtitle-color, var(--persona-muted, #6b7280));
894
+ }
895
+
896
+ /* Plugin-owned welcome (`renderWelcome`): the host keeps its position and the
897
+ default children are hidden by selector, so nothing about their own hidden
898
+ state has to be saved and restored when the plugin returns null again. */
899
+ .persona-welcome[data-persona-welcome-content="plugin"]
900
+ > :not([data-persona-welcome-plugin]) {
901
+ display: none !important;
902
+ }
903
+
904
+ .persona-welcome[data-persona-welcome-content="plugin"] {
905
+ background: transparent;
906
+ box-shadow: none;
907
+ }
908
+
909
+ /* Overlay state: a plugin element covers the messages area while it is active.
910
+ The body stops scrolling underneath so the absolute box maps to the visible
911
+ transcript rather than scrolling away with the content. */
912
+ /* Wide panels: the card variant shares the transcript column
913
+ (--persona-content-max-width, re-set inline from layout.contentMaxWidth)
914
+ so its left-aligned text lines up with messages and composer; at launcher
915
+ width the cap never bites. The plugin overlay stays full-bleed via the
916
+ :not(). */
917
+ .persona-welcome:not([data-persona-welcome-overlay]) {
918
+ width: 100%;
919
+ max-width: var(--persona-content-max-width, 768px);
920
+ margin-inline: auto;
921
+ }
922
+
923
+ /* The centered hero keeps the narrower consumer-product column, where
924
+ centered text makes the split against the transcript read as intended. */
925
+ .persona-welcome[data-persona-welcome-variant="hero"]:not(
926
+ [data-persona-welcome-overlay]
927
+ ) {
928
+ max-width: var(--persona-welcome-max-width, 640px);
929
+ }
930
+
931
+ /* The greeting bubble is a sibling of the transcript wrapper, outside its
932
+ inline width cap; mirror the cap so it shares the same column. */
933
+ .persona-welcome-greeting {
934
+ width: 100%;
935
+ max-width: var(--persona-content-max-width, 768px);
936
+ margin-inline: auto;
937
+ }
938
+
939
+ .persona-welcome[data-persona-welcome-overlay] {
940
+ position: absolute;
941
+ inset: 0;
942
+ z-index: 5;
943
+ overflow-y: auto;
944
+ /* Plugin overlays are opaque surfaces regardless of the intro-card theme,
945
+ so they keep full interior padding even when the card resolves flat. */
946
+ padding: 1.5rem;
947
+ /* !important: the host doubles as the intro card and carries an inline
948
+ background (--persona-intro-card-bg, default transparent); the overlay
949
+ must stay opaque or the transcript bleeds through the plugin stack. */
950
+ background: var(
951
+ --persona-container,
952
+ var(--persona-surface, #ffffff)
953
+ ) !important;
954
+ }
955
+
956
+ .persona-widget-body.persona-welcome-overlay-active {
957
+ overflow: hidden;
958
+ }
959
+
960
+ /* Hero: auto block margins center the surface in the empty conversation
961
+ without `justify-content`, which would make overflowing content
962
+ unreachable in the scroll body. */
963
+ .persona-welcome[data-persona-welcome-variant="hero"] {
964
+ margin-block: auto;
965
+ text-align: center;
966
+ }
967
+
968
+ .persona-welcome[data-persona-welcome-variant="hero"] .persona-welcome-icon {
969
+ justify-content: center;
970
+ }
971
+
972
+ .persona-welcome[data-persona-welcome-variant="hero"] .persona-suggestions {
973
+ margin-inline: auto;
974
+ justify-content: center;
975
+ }
976
+
977
+ .persona-welcome[data-persona-welcome-variant="hero"]
978
+ .persona-suggestions[data-variant="card"],
979
+ .persona-welcome[data-persona-welcome-variant="hero"]
980
+ .persona-suggestions[data-variant="list"] {
981
+ text-align: start;
982
+ width: 100%;
983
+ }
984
+
651
985
  /* Composer ghost icon buttons (attachment + mention): transparent, neutral
652
986
  foreground, subtle hover. Themeable via `components.button.ghost.*`
653
987
  → `--persona-button-ghost-*`. The `[data-persona-root]` prefix outranks the
@@ -1410,6 +1744,9 @@
1410
1744
  [data-persona-root] .persona-widget-composer:not(.persona-pill-composer) {
1411
1745
  padding: var(--persona-composer-padding, 0.75rem 1rem);
1412
1746
  gap: var(--persona-composer-gap, 0.5rem);
1747
+ /* `components.composer.borderColor`; the fallback matches the
1748
+ `persona-border-persona-border` utility the form carries. */
1749
+ border-color: var(--persona-composer-border-color, var(--persona-border, #e5e7eb));
1413
1750
  }
1414
1751
 
1415
1752
  /* Composer type scale, themeable via `components.composer.fontSize` /
@@ -3176,7 +3513,10 @@
3176
3513
  }
3177
3514
 
3178
3515
  [data-persona-root] .persona-artifact-list::-webkit-scrollbar-thumb {
3179
- background: var(--persona-artifact-tab-list-scrollbar, var(--persona-border, #e5e7eb));
3516
+ background: var(
3517
+ --persona-artifact-tab-list-scrollbar,
3518
+ var(--persona-scrollbar-thumb, var(--persona-border, #e5e7eb))
3519
+ );
3180
3520
  border-radius: 9999px;
3181
3521
  }
3182
3522
 
@@ -4237,13 +4577,17 @@
4237
4577
  }
4238
4578
 
4239
4579
  @media (min-width: 641px) {
4580
+ /* Drawer width is its own knob (`layout.drawerWidth`), not the split's
4581
+ pane vars: those are always materialized with split defaults, so reading
4582
+ them here would resize every host's drawer. The outer min() keeps an
4583
+ oversized value inside the panel. */
4240
4584
  [data-persona-root].persona-artifact-narrow-host .persona-artifact-pane {
4241
4585
  position: absolute;
4242
4586
  top: 0;
4243
4587
  right: 0;
4244
4588
  bottom: 0;
4245
- width: min(calc(100% - 1rem), 22rem) !important;
4246
- max-width: 22rem !important;
4589
+ width: min(100%, var(--persona-artifact-drawer-width, min(calc(100% - 1rem), 22rem))) !important;
4590
+ max-width: var(--persona-artifact-drawer-width, 22rem) !important;
4247
4591
  z-index: 60;
4248
4592
  transform: translateX(100%);
4249
4593
  transition: transform 0.2s ease;
@@ -4260,7 +4604,7 @@
4260
4604
  top: 0;
4261
4605
  right: 0;
4262
4606
  bottom: 0;
4263
- width: min(100vw - 1.5rem, 22rem);
4607
+ width: min(100vw, var(--persona-artifact-drawer-width, min(100vw - 1.5rem, 22rem)));
4264
4608
  z-index: 60;
4265
4609
  transform: translateX(100%);
4266
4610
  transition: transform 0.2s ease;