@runtypelabs/persona 4.15.0 → 4.17.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 (91) 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/install.global.js +1 -1
  14. package/dist/install.global.js.map +1 -1
  15. package/dist/launcher.global.js +2 -2
  16. package/dist/launcher.global.js.map +1 -1
  17. package/dist/markdown-parsers.js +36 -36
  18. package/dist/smart-dom-reader.d.cts +283 -9
  19. package/dist/smart-dom-reader.d.ts +283 -9
  20. package/dist/testing.cjs +3 -3
  21. package/dist/testing.d.cts +7 -0
  22. package/dist/testing.d.ts +7 -0
  23. package/dist/testing.js +3 -3
  24. package/dist/theme-editor-preview.cjs +64 -64
  25. package/dist/theme-editor-preview.d.cts +283 -9
  26. package/dist/theme-editor-preview.d.ts +283 -9
  27. package/dist/theme-editor-preview.js +56 -56
  28. package/dist/theme-editor.cjs +10 -10
  29. package/dist/theme-editor.d.cts +283 -10
  30. package/dist/theme-editor.d.ts +283 -10
  31. package/dist/theme-editor.js +10 -10
  32. package/dist/theme-reference.cjs +1 -1
  33. package/dist/theme-reference.d.cts +66 -3
  34. package/dist/theme-reference.d.ts +66 -3
  35. package/dist/theme-reference.js +1 -1
  36. package/dist/widget.css +1 -1
  37. package/package.json +2 -2
  38. package/src/components/artifact-pane.test.ts +27 -0
  39. package/src/components/artifact-pane.ts +7 -3
  40. package/src/components/header-builder.ts +27 -2
  41. package/src/components/header-layouts.test.ts +56 -0
  42. package/src/components/header-layouts.ts +29 -9
  43. package/src/components/launcher-surface.test.ts +403 -0
  44. package/src/components/launcher.ts +297 -2
  45. package/src/components/panel.ts +69 -74
  46. package/src/components/suggestions.test.ts +40 -0
  47. package/src/components/suggestions.ts +213 -153
  48. package/src/components/welcome.test.ts +121 -0
  49. package/src/components/welcome.ts +305 -0
  50. package/src/components/widget-view.test.ts +5 -3
  51. package/src/components/widget-view.ts +14 -7
  52. package/src/defaults.ts +2 -2
  53. package/src/index-core.ts +12 -1
  54. package/src/install.test.ts +35 -1
  55. package/src/install.ts +47 -11
  56. package/src/launcher-global.ts +10 -7
  57. package/src/plugins/types.ts +66 -1
  58. package/src/styles/widget-styles.test.ts +220 -0
  59. package/src/styles/widget.css +353 -9
  60. package/src/testing/mock-stream.test.ts +19 -0
  61. package/src/testing/mock-stream.ts +20 -0
  62. package/src/theme-editor/sections.ts +30 -1
  63. package/src/theme-reference.ts +26 -4
  64. package/src/types/theme.ts +40 -3
  65. package/src/types.ts +164 -6
  66. package/src/ui.body-top-slot.test.ts +95 -0
  67. package/src/ui.content-width.test.ts +91 -0
  68. package/src/ui.docked.test.ts +7 -4
  69. package/src/ui.launcher-teaser.test.ts +162 -0
  70. package/src/ui.rebuild-composer.test.ts +491 -0
  71. package/src/ui.render-welcome.test.ts +505 -0
  72. package/src/ui.scroll-additive.test.ts +91 -21
  73. package/src/ui.scroll.test.ts +302 -14
  74. package/src/ui.suggest-replies.test.ts +46 -0
  75. package/src/ui.ts +747 -136
  76. package/src/ui.welcome.test.ts +504 -0
  77. package/src/utils/artifact-gate.test.ts +27 -0
  78. package/src/utils/artifact-gate.ts +9 -0
  79. package/src/utils/attachment-manager.test.ts +37 -0
  80. package/src/utils/attachment-manager.ts +15 -0
  81. package/src/utils/auto-follow.test.ts +23 -0
  82. package/src/utils/auto-follow.ts +11 -1
  83. package/src/utils/config-merge.ts +2 -0
  84. package/src/utils/content-width.test.ts +37 -0
  85. package/src/utils/content-width.ts +17 -0
  86. package/src/utils/plugin-storage.test.ts +94 -0
  87. package/src/utils/plugin-storage.ts +98 -0
  88. package/src/utils/theme.test.ts +137 -0
  89. package/src/utils/tokens.ts +49 -3
  90. package/src/welcome.test.ts +213 -0
  91. package/src/welcome.ts +130 -0
@@ -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;
@@ -39,6 +39,25 @@ describe("createMockSSEStream", () => {
39
39
  );
40
40
  expect(text.startsWith("event: message\n")).toBe(true);
41
41
  });
42
+
43
+ it("rejects reads with AbortError once the signal aborts, like a real fetch", async () => {
44
+ const abort = new AbortController();
45
+ const frames = Array.from({ length: 5 }, (_, i) => ({
46
+ type: "agent_turn_delta",
47
+ delta: `chunk-${i}`,
48
+ }));
49
+ const reader = createMockSSEStream(frames, {
50
+ delayMs: 0,
51
+ signal: abort.signal,
52
+ }).getReader();
53
+
54
+ const first = await reader.read();
55
+ expect(first.done).toBe(false);
56
+
57
+ abort.abort();
58
+
59
+ await expect(reader.read()).rejects.toMatchObject({ name: "AbortError" });
60
+ });
42
61
  });
43
62
 
44
63
  describe("buildAssistantTurnFrames", () => {
@@ -13,16 +13,27 @@ export interface CreateMockSSEStreamOptions {
13
13
  * Omit for bare `data: ...\n\n` form (both are valid SSE and the widget parser accepts either).
14
14
  */
15
15
  eventName?: string;
16
+ /**
17
+ * Abort signal, typically the `RequestInit.signal` a `customFetch` receives.
18
+ * A real `fetch` rejects its reads when the dispatch is aborted (cancel,
19
+ * clearChat, a superseding send); without this the mock keeps emitting into
20
+ * the client's read loop after the widget thinks the turn is dead.
21
+ */
22
+ signal?: AbortSignal | null;
16
23
  }
17
24
 
18
25
  const encoder = new TextEncoder();
19
26
 
27
+ const abortError = () =>
28
+ new DOMException("The operation was aborted.", "AbortError");
29
+
20
30
  export function createMockSSEStream(
21
31
  frames: ReadonlyArray<MockSSEFrame>,
22
32
  options?: CreateMockSSEStreamOptions
23
33
  ): ReadableStream<Uint8Array> {
24
34
  const delayMs = options?.delayMs ?? 100;
25
35
  const prefix = options?.eventName ? `event: ${options.eventName}\n` : "";
36
+ const signal = options?.signal ?? null;
26
37
  let index = 0;
27
38
 
28
39
  return new ReadableStream<Uint8Array>({
@@ -31,7 +42,16 @@ export function createMockSSEStream(
31
42
  controller.close();
32
43
  return;
33
44
  }
45
+ // Check on both sides of the delay: the abort usually lands mid-wait.
46
+ if (signal?.aborted) {
47
+ controller.error(abortError());
48
+ return;
49
+ }
34
50
  await new Promise<void>((resolve) => setTimeout(resolve, delayMs));
51
+ if (signal?.aborted) {
52
+ controller.error(abortError());
53
+ return;
54
+ }
35
55
  const payload = JSON.stringify(frames[index]);
36
56
  controller.enqueue(encoder.encode(`${prefix}data: ${payload}\n\n`));
37
57
  index += 1;
@@ -501,6 +501,9 @@ const suggestionStyleSectionDef: SectionDef = {
501
501
  ] },
502
502
  { id: 'suggestion-list-bg', label: 'List Background', type: 'color', path: 'theme.components.suggestion.list.background', defaultValue: 'transparent' },
503
503
  { id: 'suggestion-list-border', label: 'List Border', type: 'token-ref', path: 'theme.components.suggestion.list.border', defaultValue: 'semantic.colors.border', tokenRef: { tokenType: 'color' } },
504
+ { id: 'suggestion-chip-item-gap', label: 'Chip Spacing', description: 'Space between chips', type: 'text', path: 'theme.components.suggestion.chip.itemGap', defaultValue: '8px' },
505
+ { id: 'suggestion-card-item-gap', label: 'Card Spacing', description: 'Space between cards', type: 'text', path: 'theme.components.suggestion.card.itemGap', defaultValue: '8px' },
506
+ { id: 'suggestion-list-item-gap', label: 'List Spacing', description: 'Space between list rows', type: 'text', path: 'theme.components.suggestion.list.itemGap', defaultValue: '8px' },
504
507
  ],
505
508
  };
506
509
 
@@ -535,6 +538,31 @@ const componentShadowsSectionDef: SectionDef = {
535
538
  };
536
539
 
537
540
  /** Shared shape sections (not scoped to light/dark) */
541
+ // Blank keeps the built-in fallback: parseOptionalCssValue drops the path so
542
+ // the CSS variable never emits.
543
+ const TEXT_FONT_WEIGHT_OPTIONS: { value: string; label: string }[] = [
544
+ { value: '', label: 'Default' },
545
+ { value: '400', label: 'Normal (400)' },
546
+ { value: '500', label: 'Medium (500)' },
547
+ { value: '600', label: 'Semibold (600)' },
548
+ { value: '700', label: 'Bold (700)' },
549
+ ];
550
+
551
+ const textStylesSectionDef: SectionDef = {
552
+ id: 'comp-text-styles',
553
+ title: 'Header & Welcome Text',
554
+ description: 'Typography for the header title and the welcome greeting. Leave blank to keep the built-in defaults.',
555
+ collapsed: true,
556
+ fields: [
557
+ { id: 'header-title-font-family', label: 'Header Title Font', description: 'CSS font-family, e.g. Georgia, serif', type: 'text', path: 'theme.components.header.title.fontFamily', defaultValue: '', parseValue: parseOptionalCssValue },
558
+ { id: 'header-title-font-size', label: 'Header Title Size', description: 'CSS length, e.g. 1.125rem', type: 'text', path: 'theme.components.header.title.fontSize', defaultValue: '', parseValue: parseOptionalCssValue },
559
+ { id: 'header-title-font-weight', label: 'Header Title Weight', type: 'select', path: 'theme.components.header.title.fontWeight', defaultValue: '', options: TEXT_FONT_WEIGHT_OPTIONS, parseValue: parseOptionalCssValue },
560
+ { id: 'welcome-title-font-family', label: 'Welcome Title Font', description: 'CSS font-family, e.g. Georgia, serif', type: 'text', path: 'theme.components.introCard.title.fontFamily', defaultValue: '', parseValue: parseOptionalCssValue },
561
+ { id: 'welcome-title-font-size', label: 'Welcome Title Size', description: 'CSS length, e.g. 1.5rem', type: 'text', path: 'theme.components.introCard.title.fontSize', defaultValue: '', parseValue: parseOptionalCssValue },
562
+ { id: 'welcome-title-font-weight', label: 'Welcome Title Weight', type: 'select', path: 'theme.components.introCard.title.fontWeight', defaultValue: '', options: TEXT_FONT_WEIGHT_OPTIONS, parseValue: parseOptionalCssValue },
563
+ ],
564
+ };
565
+
538
566
  export const COMPONENT_SHAPE_SECTIONS: SectionDef[] = [
539
567
  panelLayoutSectionDef,
540
568
  launcherLayoutSectionDef,
@@ -542,6 +570,7 @@ export const COMPONENT_SHAPE_SECTIONS: SectionDef[] = [
542
570
  inputShapeSectionDef,
543
571
  composerSpacingSectionDef,
544
572
  buttonShapeSectionDef,
573
+ textStylesSectionDef,
545
574
  componentShadowsSectionDef,
546
575
  ];
547
576
 
@@ -609,7 +638,7 @@ const copySectionDef: SectionDef = {
609
638
  fields: [
610
639
  { id: 'copy-show-welcome-card', label: 'Show Welcome Card', type: 'toggle', path: 'copy.showWelcomeCard', defaultValue: true },
611
640
  { id: 'copy-welcome-title', label: 'Welcome Title', type: 'text', path: 'copy.welcomeTitle', defaultValue: 'Hello 👋' },
612
- { id: 'copy-welcome-subtitle', label: 'Welcome Subtitle', type: 'text', path: 'copy.welcomeSubtitle', defaultValue: 'Ask anything about your account or products.' },
641
+ { id: 'copy-welcome-subtitle', label: 'Welcome Subtitle', type: 'text', path: 'copy.welcomeSubtitle', defaultValue: 'I can answer questions and help you get things done here.' },
613
642
  { id: 'copy-placeholder', label: 'Input Placeholder', type: 'text', path: 'copy.inputPlaceholder', defaultValue: 'Type your message…' },
614
643
  { id: 'copy-send-label', label: 'Send Button Label', type: 'text', path: 'copy.sendButtonLabel', defaultValue: 'Send' },
615
644
  ],
@@ -120,7 +120,9 @@ export const THEME_TOKEN_DOCS = {
120
120
  header: {
121
121
  description: 'Chat panel header.',
122
122
  properties:
123
- 'background, border, borderRadius, padding, iconBackground, iconForeground, titleForeground, subtitleForeground, actionIconForeground, shadow, borderBottom.',
123
+ 'background, border, borderRadius, padding, iconBackground, iconForeground, actionIconForeground, shadow, borderBottom, title and subtitle (each fontFamily, fontSize, fontWeight, lineHeight, letterSpacing, color).',
124
+ legacy:
125
+ 'titleForeground and subtitleForeground are legacy aliases of header.title.color and header.subtitle.color. Both still work; the title/subtitle color wins when both are set.',
124
126
  },
125
127
  message: {
126
128
  description: 'Chat message bubbles.',
@@ -131,7 +133,19 @@ export const THEME_TOKEN_DOCS = {
131
133
  markdown: {
132
134
  description: 'Markdown rendering in messages and artifact pane.',
133
135
  properties:
134
- 'inlineCode (background, foreground), link.foreground, prose.fontFamily, heading.h1/h2 (fontSize, fontWeight).',
136
+ 'inlineCode (background, foreground), link.foreground, prose.fontFamily, heading.h1/h2 (fontSize, fontWeight), codeBlock (background, borderColor, textColor, borderRadius: follows palette.radius.md), table (headerBackground, borderColor), hr.color, blockquote (borderColor, background, textColor).',
137
+ },
138
+ introCard: {
139
+ description:
140
+ 'Welcome surface above the message list. Flat by default (transparent, no shadow).',
141
+ properties:
142
+ 'background, borderRadius, padding, shadow, border (full CSS shorthand, default none), title and subtitle (each fontFamily, fontSize, fontWeight, lineHeight, letterSpacing, color).',
143
+ },
144
+ suggestion: {
145
+ description:
146
+ 'Starter prompt and follow-up suggestion chrome. Three variants: chip, card, list.',
147
+ properties:
148
+ 'background, foreground, border, borderRadius, padding, shadow, gap (icon to copy inside one item), itemGap (space between items, default 8px), minHeight, fontSize, lineHeight, iconSize, hoverBackground, hoverForeground, hoverBorder, pressedBackground, focusRing, disabledOpacity.',
135
149
  },
136
150
  voice:
137
151
  'recording (indicator, background, border), processing (icon, background), speaking (icon).',
@@ -141,9 +155,11 @@ export const THEME_TOKEN_DOCS = {
141
155
  scrollToBottom:
142
156
  'Floating scroll-to-bottom affordance shared by transcript and event stream: background, foreground, border, size, borderRadius, shadow, padding, gap, fontSize, iconSize.',
143
157
  toolBubble: 'shadow: tool call row box-shadow.',
158
+ scrollbar:
159
+ 'Shared scrollbar appearance for every scroller: thumb (default semantic.colors.border), track (default transparent).',
144
160
  reasoningBubble: 'shadow: reasoning/thinking row box-shadow.',
145
161
  composer:
146
- 'Message input form: shadow (box-shadow), padding (form inset), gap (textarea row to actions row), fontSize and lineHeight (textarea type).',
162
+ 'Message input form: shadow (box-shadow), borderColor, padding (form inset), gap (textarea row to actions row), fontSize and lineHeight (textarea type).',
147
163
  artifact:
148
164
  'toolbar (icon styling, copy menu), tab (background, active states), pane (background, toolbarBackground).',
149
165
  },
@@ -235,7 +251,7 @@ export const THEME_TOKEN_DOCS = {
235
251
  copy: {
236
252
  description: 'Widget text content.',
237
253
  properties:
238
- 'showWelcomeCard (boolean), welcomeTitle, welcomeSubtitle, inputPlaceholder, sendButtonLabel.',
254
+ 'inputPlaceholder, sendButtonLabel, stopButtonLabel. (showWelcomeCard, welcomeTitle, welcomeSubtitle are deprecated aliases of the welcome namespace.)',
239
255
  },
240
256
  voiceRecognition: {
241
257
  description: 'Voice input configuration.',
@@ -252,6 +268,12 @@ export const THEME_TOKEN_DOCS = {
252
268
  properties:
253
269
  'starters.items, starters.variant ("card" | "chip" | "list"), starters.placement ("auto" | "welcome" | "composer"), starters.behavior ("send" | "fill"), starters.overflow ("scroll" | "wrap"), starters.maxItems, followUps.enabled, followUps.expose, followUps.variant, followUps.placement ("auto" | "after-message" | "composer"), followUps.behavior, followUps.overflow ("scroll" | "wrap"), followUps.maxItems.',
254
270
  },
271
+ welcome: {
272
+ description:
273
+ 'First-open welcome surface. Supersedes copy.welcomeTitle / welcomeSubtitle / showWelcomeCard.',
274
+ properties:
275
+ 'title, subtitle, icon ({ type: "lucide", name } | { type: "image", url, alt } | { type: "text", text } | function), variant ("card" | "hero" | "none"), dismiss ("never" | "on-first-message"), message (display-only greeting bubble, ignored under "hero").',
276
+ },
255
277
  suggestionChips:
256
278
  'Deprecated string[] shorthand for starter prompts. Prefer suggestions.starters.items.',
257
279
  messageActions: {