@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.
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions.d.cts +283 -9
- package/dist/context-mentions.d.ts +283 -9
- package/dist/index.cjs +68 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +299 -10
- package/dist/index.d.ts +299 -10
- package/dist/index.global.js +57 -57
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +69 -69
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +36 -36
- package/dist/smart-dom-reader.d.cts +283 -9
- package/dist/smart-dom-reader.d.ts +283 -9
- package/dist/testing.cjs +3 -3
- package/dist/testing.d.cts +7 -0
- package/dist/testing.d.ts +7 -0
- package/dist/testing.js +3 -3
- package/dist/theme-editor-preview.cjs +64 -64
- package/dist/theme-editor-preview.d.cts +283 -9
- package/dist/theme-editor-preview.d.ts +283 -9
- package/dist/theme-editor-preview.js +56 -56
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +283 -10
- package/dist/theme-editor.d.ts +283 -10
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +66 -3
- package/dist/theme-reference.d.ts +66 -3
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +2 -2
- package/src/components/artifact-pane.test.ts +27 -0
- package/src/components/artifact-pane.ts +7 -3
- package/src/components/header-builder.ts +27 -2
- package/src/components/header-layouts.test.ts +56 -0
- package/src/components/header-layouts.ts +29 -9
- package/src/components/launcher-surface.test.ts +403 -0
- package/src/components/launcher.ts +297 -2
- package/src/components/panel.ts +69 -74
- package/src/components/suggestions.test.ts +40 -0
- package/src/components/suggestions.ts +213 -153
- package/src/components/welcome.test.ts +121 -0
- package/src/components/welcome.ts +305 -0
- package/src/components/widget-view.test.ts +5 -3
- package/src/components/widget-view.ts +14 -7
- package/src/defaults.ts +2 -2
- package/src/index-core.ts +12 -1
- package/src/install.test.ts +35 -1
- package/src/install.ts +47 -11
- package/src/launcher-global.ts +10 -7
- package/src/plugins/types.ts +66 -1
- package/src/styles/widget-styles.test.ts +220 -0
- package/src/styles/widget.css +353 -9
- package/src/testing/mock-stream.test.ts +19 -0
- package/src/testing/mock-stream.ts +20 -0
- package/src/theme-editor/sections.ts +30 -1
- package/src/theme-reference.ts +26 -4
- package/src/types/theme.ts +40 -3
- package/src/types.ts +164 -6
- package/src/ui.body-top-slot.test.ts +95 -0
- package/src/ui.content-width.test.ts +91 -0
- package/src/ui.docked.test.ts +7 -4
- package/src/ui.launcher-teaser.test.ts +162 -0
- package/src/ui.rebuild-composer.test.ts +491 -0
- package/src/ui.render-welcome.test.ts +505 -0
- package/src/ui.scroll-additive.test.ts +91 -21
- package/src/ui.scroll.test.ts +302 -14
- package/src/ui.suggest-replies.test.ts +46 -0
- package/src/ui.ts +747 -136
- package/src/ui.welcome.test.ts +504 -0
- package/src/utils/artifact-gate.test.ts +27 -0
- package/src/utils/artifact-gate.ts +9 -0
- package/src/utils/attachment-manager.test.ts +37 -0
- package/src/utils/attachment-manager.ts +15 -0
- package/src/utils/auto-follow.test.ts +23 -0
- package/src/utils/auto-follow.ts +11 -1
- package/src/utils/config-merge.ts +2 -0
- package/src/utils/content-width.test.ts +37 -0
- package/src/utils/content-width.ts +17 -0
- package/src/utils/plugin-storage.test.ts +94 -0
- package/src/utils/plugin-storage.ts +98 -0
- package/src/utils/theme.test.ts +137 -0
- package/src/utils/tokens.ts +49 -3
- package/src/welcome.test.ts +213 -0
- package/src/welcome.ts +130 -0
package/src/styles/widget.css
CHANGED
|
@@ -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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
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: '
|
|
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
|
],
|
package/src/theme-reference.ts
CHANGED
|
@@ -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,
|
|
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
|
-
'
|
|
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: {
|