@runtypelabs/persona 4.14.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.
- package/README.md +4 -4
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-DveIaNx6.d.cts → types-BpWGm7_6.d.cts} +17 -0
- package/dist/animations/{types-DveIaNx6.d.ts → types-BpWGm7_6.d.ts} +17 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-QJ4LV2KW.js +1 -0
- package/dist/codegen.cjs +12 -11
- package/dist/codegen.js +14 -13
- package/dist/{context-mentions-ONG7A7M6.js → context-mentions-BTSTIBFC.js} +1 -1
- package/dist/context-mentions.cjs +11 -8
- package/dist/context-mentions.d.cts +520 -21
- package/dist/context-mentions.d.ts +520 -21
- package/dist/context-mentions.js +11 -8
- package/dist/index.cjs +69 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +595 -46
- package/dist/index.d.ts +595 -46
- package/dist/index.global.js +60 -57
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +69 -68
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +5 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +520 -21
- package/dist/smart-dom-reader.d.ts +520 -21
- 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 +67 -67
- package/dist/theme-editor-preview.d.cts +534 -22
- package/dist/theme-editor-preview.d.ts +534 -22
- package/dist/theme-editor-preview.js +57 -57
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +521 -23
- package/dist/theme-editor.d.ts +521 -23
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +104 -3
- package/dist/theme-reference.d.ts +104 -3
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/ask-user-question-tool.ts +10 -7
- package/src/components/artifact-pane.test.ts +27 -0
- package/src/components/artifact-pane.ts +7 -3
- package/src/components/artifact-preview.ts +1 -1
- package/src/components/composer-parts.test.ts +103 -4
- package/src/components/composer-parts.ts +53 -15
- 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/messages.ts +5 -2
- package/src/components/panel.ts +92 -71
- package/src/components/pill-composer-builder.test.ts +18 -0
- package/src/components/suggestions.test.ts +273 -0
- package/src/components/suggestions.ts +528 -86
- 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 +18 -7
- package/src/defaults.ts +7 -5
- package/src/generated/runtype-openapi-contract.ts +17 -0
- package/src/index-core.ts +29 -2
- package/src/launcher-global.ts +11 -8
- package/src/plugins/types.ts +136 -3
- package/src/presets.ts +3 -3
- package/src/session.ts +10 -8
- package/src/styles/widget-styles.test.ts +382 -0
- package/src/styles/widget.css +778 -4
- package/src/suggest-replies-tool.test.ts +233 -2
- package/src/suggest-replies-tool.ts +115 -39
- package/src/testing/mock-stream.test.ts +19 -0
- package/src/testing/mock-stream.ts +20 -0
- package/src/theme-editor/preview-utils.test.ts +34 -0
- package/src/theme-editor/preview-utils.ts +77 -2
- package/src/theme-editor/sections.test.ts +33 -0
- package/src/theme-editor/sections.ts +83 -6
- package/src/theme-reference.ts +34 -5
- package/src/types/theme.ts +72 -3
- package/src/types.ts +312 -18
- package/src/ui.body-top-slot.test.ts +95 -0
- package/src/ui.content-width.test.ts +91 -0
- package/src/ui.detached-panel.test.ts +1 -1
- 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 +1112 -1
- package/src/ui.ts +1008 -149
- package/src/ui.welcome.test.ts +504 -0
- package/src/utils/artifact-file.test.ts +1 -1
- 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/code-generators.test.ts +40 -0
- package/src/utils/code-generators.ts +22 -0
- package/src/utils/config-merge.test.ts +24 -0
- 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/icons.test.ts +26 -0
- package/src/utils/icons.ts +8 -0
- package/src/utils/plugin-storage.test.ts +94 -0
- package/src/utils/plugin-storage.ts +98 -0
- package/src/utils/theme.test.ts +216 -0
- package/src/utils/tokens.ts +113 -3
- package/src/welcome.test.ts +213 -0
- package/src/welcome.ts +130 -0
- package/dist/chunk-MMUPR2JW.js +0 -1
package/src/utils/theme.test.ts
CHANGED
|
@@ -137,6 +137,36 @@ describe('theme utils', () => {
|
|
|
137
137
|
expect(cssVars['--persona-button-radius']).toBe('6px');
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
it('zeroes horizontal intro-card padding when the card resolves flat', () => {
|
|
141
|
+
const cssVars = themeToCssVariables(createTheme({} as any));
|
|
142
|
+
expect(cssVars['--persona-intro-card-padding']).toBe('1.5rem 0');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('keeps full intro-card padding when a background or shadow makes it a card', () => {
|
|
146
|
+
const withBackground = themeToCssVariables(
|
|
147
|
+
createTheme({
|
|
148
|
+
components: { introCard: { background: '#ffffff' } },
|
|
149
|
+
} as any)
|
|
150
|
+
);
|
|
151
|
+
expect(withBackground['--persona-intro-card-padding']).toBe('1.5rem');
|
|
152
|
+
|
|
153
|
+
const withShadow = themeToCssVariables(
|
|
154
|
+
createTheme({
|
|
155
|
+
components: { introCard: { shadow: '0 1px 2px rgba(0,0,0,0.1)' } },
|
|
156
|
+
} as any)
|
|
157
|
+
);
|
|
158
|
+
expect(withShadow['--persona-intro-card-padding']).toBe('1.5rem');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('lets an explicit intro-card padding token win over the flat default', () => {
|
|
162
|
+
const cssVars = themeToCssVariables(
|
|
163
|
+
createTheme({
|
|
164
|
+
components: { introCard: { padding: '2rem' } },
|
|
165
|
+
} as any)
|
|
166
|
+
);
|
|
167
|
+
expect(cssVars['--persona-intro-card-padding']).toBe('2rem');
|
|
168
|
+
});
|
|
169
|
+
|
|
140
170
|
it.each([
|
|
141
171
|
{
|
|
142
172
|
role: 'surface',
|
|
@@ -287,6 +317,61 @@ describe('theme utils', () => {
|
|
|
287
317
|
expect(customVars['--persona-header-action-icon-fg']).toBe('#9ca3af');
|
|
288
318
|
});
|
|
289
319
|
|
|
320
|
+
it('emits full-path CSS variables for header and welcome text style tokens', () => {
|
|
321
|
+
const theme = createTheme({
|
|
322
|
+
components: {
|
|
323
|
+
introCard: {
|
|
324
|
+
title: { fontFamily: 'Georgia, serif', fontSize: '1.5rem' },
|
|
325
|
+
},
|
|
326
|
+
header: {
|
|
327
|
+
title: {
|
|
328
|
+
fontFamily: 'Inter, sans-serif',
|
|
329
|
+
color: 'palette.colors.secondary.500',
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
} as any);
|
|
334
|
+
const cssVars = themeToCssVariables(theme);
|
|
335
|
+
|
|
336
|
+
expect(cssVars['--persona-components-introCard-title-fontFamily']).toBe(
|
|
337
|
+
'Georgia, serif'
|
|
338
|
+
);
|
|
339
|
+
expect(cssVars['--persona-components-introCard-title-fontSize']).toBe('1.5rem');
|
|
340
|
+
expect(cssVars['--persona-components-header-title-fontFamily']).toBe(
|
|
341
|
+
'Inter, sans-serif'
|
|
342
|
+
);
|
|
343
|
+
// title.color supersedes the legacy titleForeground alias.
|
|
344
|
+
expect(cssVars['--persona-header-title-fg']).toBe('#8b5cf6');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('keeps titleForeground working when no header.title.color is set', () => {
|
|
348
|
+
const cssVars = themeToCssVariables(
|
|
349
|
+
createTheme({
|
|
350
|
+
components: {
|
|
351
|
+
header: { titleForeground: 'palette.colors.gray.500' },
|
|
352
|
+
},
|
|
353
|
+
} as any)
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
expect(cssVars['--persona-header-title-fg']).toBe('#6b7280');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('emits suggestion itemGap for every variant, defaulting to 8px', () => {
|
|
360
|
+
const defaults = themeToCssVariables(createTheme());
|
|
361
|
+
expect(defaults['--persona-components-suggestion-chip-itemGap']).toBe('8px');
|
|
362
|
+
expect(defaults['--persona-components-suggestion-card-itemGap']).toBe('8px');
|
|
363
|
+
expect(defaults['--persona-components-suggestion-list-itemGap']).toBe('8px');
|
|
364
|
+
|
|
365
|
+
const custom = themeToCssVariables(
|
|
366
|
+
createTheme({
|
|
367
|
+
components: { suggestion: { list: { itemGap: '20px' } } },
|
|
368
|
+
} as any)
|
|
369
|
+
);
|
|
370
|
+
expect(custom['--persona-components-suggestion-list-itemGap']).toBe('20px');
|
|
371
|
+
// Sibling variants keep the default.
|
|
372
|
+
expect(custom['--persona-components-suggestion-chip-itemGap']).toBe('8px');
|
|
373
|
+
});
|
|
374
|
+
|
|
290
375
|
it('maps button.ghost tokens to the composer ghost icon-button CSS variables', () => {
|
|
291
376
|
const cssVars = themeToCssVariables(createTheme());
|
|
292
377
|
|
|
@@ -393,6 +478,40 @@ describe('theme utils', () => {
|
|
|
393
478
|
expect(cssVars['--persona-composer-shadow']).toBe('none');
|
|
394
479
|
});
|
|
395
480
|
|
|
481
|
+
it('falls back composer spacing/type vars to the utility-class defaults', () => {
|
|
482
|
+
const cssVars = themeToCssVariables(createTheme());
|
|
483
|
+
|
|
484
|
+
expect(cssVars['--persona-composer-padding']).toBe('0.75rem 1rem');
|
|
485
|
+
expect(cssVars['--persona-composer-gap']).toBe('0.5rem');
|
|
486
|
+
expect(cssVars['--persona-composer-font-size']).toBe('0.875rem');
|
|
487
|
+
expect(cssVars['--persona-composer-line-height']).toBe('1.25rem');
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('maps composer spacing/type tokens to dedicated CSS variables', () => {
|
|
491
|
+
const theme = createTheme({
|
|
492
|
+
components: {
|
|
493
|
+
composer: {
|
|
494
|
+
padding: '1rem 1.25rem',
|
|
495
|
+
gap: '0.75rem',
|
|
496
|
+
fontSize: '1rem',
|
|
497
|
+
lineHeight: '1.5rem',
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
} as any);
|
|
501
|
+
|
|
502
|
+
const cssVars = themeToCssVariables(theme);
|
|
503
|
+
|
|
504
|
+
expect(cssVars['--persona-components-composer-padding']).toBe('1rem 1.25rem');
|
|
505
|
+
expect(cssVars['--persona-components-composer-gap']).toBe('0.75rem');
|
|
506
|
+
expect(cssVars['--persona-components-composer-fontSize']).toBe('1rem');
|
|
507
|
+
expect(cssVars['--persona-components-composer-lineHeight']).toBe('1.5rem');
|
|
508
|
+
|
|
509
|
+
expect(cssVars['--persona-composer-padding']).toBe('1rem 1.25rem');
|
|
510
|
+
expect(cssVars['--persona-composer-gap']).toBe('0.75rem');
|
|
511
|
+
expect(cssVars['--persona-composer-font-size']).toBe('1rem');
|
|
512
|
+
expect(cssVars['--persona-composer-line-height']).toBe('1.5rem');
|
|
513
|
+
});
|
|
514
|
+
|
|
396
515
|
it('maps scroll-to-bottom component tokens to dedicated CSS variables', () => {
|
|
397
516
|
const theme = createTheme({
|
|
398
517
|
components: {
|
|
@@ -462,6 +581,51 @@ describe('theme utils', () => {
|
|
|
462
581
|
expect(cssVars['--persona-intro-card-bg']).toBe('transparent');
|
|
463
582
|
});
|
|
464
583
|
|
|
584
|
+
it('resolves semantic suggestion tokens for every variant and state', () => {
|
|
585
|
+
const theme = createTheme({
|
|
586
|
+
components: {
|
|
587
|
+
suggestion: {
|
|
588
|
+
chip: {
|
|
589
|
+
background: 'palette.colors.accent.50',
|
|
590
|
+
hoverBackground: 'palette.colors.accent.100',
|
|
591
|
+
focusRing: 'semantic.colors.interactive.focus',
|
|
592
|
+
},
|
|
593
|
+
card: {
|
|
594
|
+
borderRadius: 'palette.radius.lg',
|
|
595
|
+
shadow: 'palette.shadows.md',
|
|
596
|
+
},
|
|
597
|
+
list: {
|
|
598
|
+
minHeight: '48px',
|
|
599
|
+
disabledOpacity: '0.4',
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
const cssVars = themeToCssVariables(theme);
|
|
605
|
+
|
|
606
|
+
expect(cssVars['--persona-components-suggestion-chip-background']).toBe(
|
|
607
|
+
'#ecfeff'
|
|
608
|
+
);
|
|
609
|
+
expect(
|
|
610
|
+
cssVars['--persona-components-suggestion-chip-hoverBackground']
|
|
611
|
+
).toBe('#cffafe');
|
|
612
|
+
expect(cssVars['--persona-components-suggestion-chip-focusRing']).toBe(
|
|
613
|
+
'#0f0f0f'
|
|
614
|
+
);
|
|
615
|
+
expect(
|
|
616
|
+
cssVars['--persona-components-suggestion-card-borderRadius']
|
|
617
|
+
).toBe('0.5rem');
|
|
618
|
+
expect(cssVars['--persona-components-suggestion-card-shadow']).toContain(
|
|
619
|
+
'0 4px 6px'
|
|
620
|
+
);
|
|
621
|
+
expect(cssVars['--persona-components-suggestion-list-minHeight']).toBe(
|
|
622
|
+
'48px'
|
|
623
|
+
);
|
|
624
|
+
expect(
|
|
625
|
+
cssVars['--persona-components-suggestion-list-disabledOpacity']
|
|
626
|
+
).toBe('0.4');
|
|
627
|
+
});
|
|
628
|
+
|
|
465
629
|
it('drives --persona-tool-bubble-shadow from the theme token (config.toolCall.shadow is applied inline on the bubble, not the root var)', () => {
|
|
466
630
|
const el = document.createElement('div');
|
|
467
631
|
applyThemeVariables(el, {
|
|
@@ -495,4 +659,56 @@ describe('theme utils', () => {
|
|
|
495
659
|
const cssVars = themeToCssVariables(theme);
|
|
496
660
|
expect(cssVars['--persona-code-bg']).toBe('#fafafa');
|
|
497
661
|
});
|
|
662
|
+
|
|
663
|
+
it('derives the shared scrollbar variables from components.scrollbar with border fallback', () => {
|
|
664
|
+
const defaults = themeToCssVariables(createTheme({} as any));
|
|
665
|
+
expect(defaults['--persona-scrollbar-thumb']).toBe(defaults['--persona-border']);
|
|
666
|
+
expect(defaults['--persona-scrollbar-track']).toBe('transparent');
|
|
667
|
+
|
|
668
|
+
const themed = themeToCssVariables(
|
|
669
|
+
createTheme({
|
|
670
|
+
components: { scrollbar: { thumb: '#00dfc1', track: '#111111' } },
|
|
671
|
+
} as any)
|
|
672
|
+
);
|
|
673
|
+
expect(themed['--persona-scrollbar-thumb']).toBe('#00dfc1');
|
|
674
|
+
expect(themed['--persona-scrollbar-track']).toBe('#111111');
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('exposes intro card border and composer border color tokens', () => {
|
|
678
|
+
const defaults = themeToCssVariables(createTheme({} as any));
|
|
679
|
+
expect(defaults['--persona-intro-card-border']).toBe('none');
|
|
680
|
+
expect(defaults['--persona-composer-border-color']).toBe(defaults['--persona-border']);
|
|
681
|
+
|
|
682
|
+
const themed = themeToCssVariables(
|
|
683
|
+
createTheme({
|
|
684
|
+
components: {
|
|
685
|
+
introCard: { border: '1px solid rgba(0, 0, 0, 0.1)' },
|
|
686
|
+
composer: { borderColor: 'rgba(29, 28, 23, 0.25)' },
|
|
687
|
+
},
|
|
688
|
+
} as any)
|
|
689
|
+
);
|
|
690
|
+
expect(themed['--persona-intro-card-border']).toBe('1px solid rgba(0, 0, 0, 0.1)');
|
|
691
|
+
expect(themed['--persona-composer-border-color']).toBe('rgba(29, 28, 23, 0.25)');
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it('follows the palette radius for markdown code blocks so square themes get square code', () => {
|
|
695
|
+
const defaults = themeToCssVariables(createTheme({} as any));
|
|
696
|
+
expect(defaults['--persona-md-code-block-border-radius']).toBe('0.375rem');
|
|
697
|
+
|
|
698
|
+
const square = themeToCssVariables(
|
|
699
|
+
createTheme({
|
|
700
|
+
palette: {
|
|
701
|
+
radius: { sm: '0px', md: '0px', lg: '0px', xl: '0px', '2xl': '0px' },
|
|
702
|
+
},
|
|
703
|
+
} as any)
|
|
704
|
+
);
|
|
705
|
+
expect(square['--persona-md-code-block-border-radius']).toBe('0px');
|
|
706
|
+
|
|
707
|
+
const explicit = themeToCssVariables(
|
|
708
|
+
createTheme({
|
|
709
|
+
components: { markdown: { codeBlock: { borderRadius: '2px' } } },
|
|
710
|
+
} as any)
|
|
711
|
+
);
|
|
712
|
+
expect(explicit['--persona-md-code-block-border-radius']).toBe('2px');
|
|
713
|
+
});
|
|
498
714
|
});
|
package/src/utils/tokens.ts
CHANGED
|
@@ -336,6 +336,63 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
336
336
|
padding: 'semantic.spacing.lg',
|
|
337
337
|
shadow: 'palette.shadows.none',
|
|
338
338
|
},
|
|
339
|
+
suggestion: {
|
|
340
|
+
chip: {
|
|
341
|
+
background: 'semantic.colors.surface',
|
|
342
|
+
foreground: 'semantic.colors.text',
|
|
343
|
+
border: 'semantic.colors.border',
|
|
344
|
+
borderRadius: 'palette.radius.full',
|
|
345
|
+
padding: '0.5rem 0.875rem',
|
|
346
|
+
gap: '0.5rem',
|
|
347
|
+
itemGap: '8px',
|
|
348
|
+
minHeight: '36px',
|
|
349
|
+
fontSize: '0.8125rem',
|
|
350
|
+
lineHeight: '1.25',
|
|
351
|
+
iconSize: '16px',
|
|
352
|
+
hoverBackground: 'palette.colors.gray.100',
|
|
353
|
+
hoverBorder: 'palette.colors.gray.300',
|
|
354
|
+
pressedBackground: 'palette.colors.gray.200',
|
|
355
|
+
focusRing: 'semantic.colors.interactive.focus',
|
|
356
|
+
disabledOpacity: '0.5',
|
|
357
|
+
},
|
|
358
|
+
card: {
|
|
359
|
+
background: 'semantic.colors.surface',
|
|
360
|
+
foreground: 'semantic.colors.text',
|
|
361
|
+
border: 'semantic.colors.border',
|
|
362
|
+
borderRadius: 'palette.radius.xl',
|
|
363
|
+
padding: '1rem',
|
|
364
|
+
shadow: 'palette.shadows.sm',
|
|
365
|
+
gap: '0.625rem',
|
|
366
|
+
itemGap: '8px',
|
|
367
|
+
minHeight: '72px',
|
|
368
|
+
fontSize: '0.875rem',
|
|
369
|
+
lineHeight: '1.35',
|
|
370
|
+
iconSize: '18px',
|
|
371
|
+
hoverBackground: 'palette.colors.gray.100',
|
|
372
|
+
hoverBorder: 'palette.colors.gray.300',
|
|
373
|
+
pressedBackground: 'palette.colors.gray.200',
|
|
374
|
+
focusRing: 'semantic.colors.interactive.focus',
|
|
375
|
+
disabledOpacity: '0.5',
|
|
376
|
+
},
|
|
377
|
+
list: {
|
|
378
|
+
background: 'transparent',
|
|
379
|
+
foreground: 'semantic.colors.text',
|
|
380
|
+
border: 'semantic.colors.border',
|
|
381
|
+
borderRadius: 'palette.radius.md',
|
|
382
|
+
padding: '0.75rem',
|
|
383
|
+
gap: '0.625rem',
|
|
384
|
+
itemGap: '8px',
|
|
385
|
+
minHeight: '44px',
|
|
386
|
+
fontSize: '0.875rem',
|
|
387
|
+
lineHeight: '1.35',
|
|
388
|
+
iconSize: '17px',
|
|
389
|
+
hoverBackground: 'palette.colors.gray.100',
|
|
390
|
+
hoverBorder: 'palette.colors.gray.300',
|
|
391
|
+
pressedBackground: 'palette.colors.gray.200',
|
|
392
|
+
focusRing: 'semantic.colors.interactive.focus',
|
|
393
|
+
disabledOpacity: '0.5',
|
|
394
|
+
},
|
|
395
|
+
},
|
|
339
396
|
toolBubble: {
|
|
340
397
|
shadow: 'palette.shadows.sm',
|
|
341
398
|
},
|
|
@@ -345,6 +402,10 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
345
402
|
composer: {
|
|
346
403
|
shadow: 'palette.shadows.none',
|
|
347
404
|
},
|
|
405
|
+
scrollbar: {
|
|
406
|
+
thumb: 'semantic.colors.border',
|
|
407
|
+
track: 'transparent',
|
|
408
|
+
},
|
|
348
409
|
markdown: {
|
|
349
410
|
inlineCode: {
|
|
350
411
|
background: 'palette.colors.gray.50',
|
|
@@ -361,6 +422,7 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
361
422
|
background: 'semantic.colors.container',
|
|
362
423
|
borderColor: 'semantic.colors.border',
|
|
363
424
|
textColor: 'inherit',
|
|
425
|
+
borderRadius: 'palette.radius.md',
|
|
364
426
|
},
|
|
365
427
|
table: {
|
|
366
428
|
headerBackground: 'semantic.colors.container',
|
|
@@ -797,10 +859,15 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
797
859
|
cssVars['--persona-components-header-iconBackground'] ?? cssVars['--persona-primary'];
|
|
798
860
|
cssVars['--persona-header-icon-fg'] =
|
|
799
861
|
cssVars['--persona-components-header-iconForeground'] ?? cssVars['--persona-text-inverse'];
|
|
862
|
+
// `title.color` / `subtitle.color` supersede the legacy *Foreground tokens.
|
|
800
863
|
cssVars['--persona-header-title-fg'] =
|
|
801
|
-
cssVars['--persona-components-header-
|
|
864
|
+
cssVars['--persona-components-header-title-color'] ??
|
|
865
|
+
cssVars['--persona-components-header-titleForeground'] ??
|
|
866
|
+
cssVars['--persona-primary'];
|
|
802
867
|
cssVars['--persona-header-subtitle-fg'] =
|
|
803
|
-
cssVars['--persona-components-header-
|
|
868
|
+
cssVars['--persona-components-header-subtitle-color'] ??
|
|
869
|
+
cssVars['--persona-components-header-subtitleForeground'] ??
|
|
870
|
+
cssVars['--persona-text-muted'];
|
|
804
871
|
cssVars['--persona-header-action-icon-fg'] =
|
|
805
872
|
cssVars['--persona-components-header-actionIconForeground'] ?? cssVars['--persona-muted'];
|
|
806
873
|
|
|
@@ -816,10 +883,25 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
816
883
|
cssVars['--persona-components-introCard-background'] ?? 'transparent';
|
|
817
884
|
cssVars['--persona-intro-card-radius'] =
|
|
818
885
|
cssVars['--persona-components-introCard-borderRadius'] ?? '1rem';
|
|
819
|
-
|
|
886
|
+
// Flat cards (transparent background, no shadow) drop the horizontal
|
|
887
|
+
// component of the stock padding so the welcome text shares the content
|
|
888
|
+
// column's left edge instead of carrying an invisible card inset. Compared
|
|
889
|
+
// by resolved value, not key presence: DEFAULT_COMPONENTS materializes the
|
|
890
|
+
// introCard tokens into every theme. A non-stock padding is emitted as-is;
|
|
891
|
+
// '1.5rem 1.5rem' forces the symmetric inset on a flat card.
|
|
892
|
+
const introBg = cssVars['--persona-components-introCard-background'];
|
|
893
|
+
const introShadow = cssVars['--persona-components-introCard-shadow'];
|
|
894
|
+
const introCardFlat =
|
|
895
|
+
(!introBg || introBg === 'transparent' || introBg === 'none') &&
|
|
896
|
+
(!introShadow || introShadow === 'none');
|
|
897
|
+
const introPadding =
|
|
820
898
|
cssVars['--persona-components-introCard-padding'] ?? '1.5rem';
|
|
899
|
+
cssVars['--persona-intro-card-padding'] =
|
|
900
|
+
introCardFlat && introPadding === '1.5rem' ? '1.5rem 0' : introPadding;
|
|
821
901
|
cssVars['--persona-intro-card-shadow'] =
|
|
822
902
|
cssVars['--persona-components-introCard-shadow'] ?? 'none';
|
|
903
|
+
cssVars['--persona-intro-card-border'] =
|
|
904
|
+
cssVars['--persona-components-introCard-border'] ?? 'none';
|
|
823
905
|
|
|
824
906
|
cssVars['--persona-input-background'] =
|
|
825
907
|
cssVars['--persona-components-input-background'] ?? cssVars['--persona-surface'];
|
|
@@ -883,6 +965,28 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
883
965
|
cssVars['--persona-components-reasoningBubble-shadow'] ?? '0 5px 15px rgba(15, 23, 42, 0.08)';
|
|
884
966
|
cssVars['--persona-composer-shadow'] =
|
|
885
967
|
cssVars['--persona-components-composer-shadow'] ?? 'none';
|
|
968
|
+
// Composer spacing/type. Fallbacks reproduce the utility classes the form
|
|
969
|
+
// used to carry (`px-4 py-3`, `gap-2`, `text-sm`) exactly.
|
|
970
|
+
cssVars['--persona-composer-padding'] =
|
|
971
|
+
cssVars['--persona-components-composer-padding'] ?? '0.75rem 1rem';
|
|
972
|
+
cssVars['--persona-composer-gap'] =
|
|
973
|
+
cssVars['--persona-components-composer-gap'] ?? '0.5rem';
|
|
974
|
+
cssVars['--persona-composer-font-size'] =
|
|
975
|
+
cssVars['--persona-components-composer-fontSize'] ?? '0.875rem';
|
|
976
|
+
cssVars['--persona-composer-line-height'] =
|
|
977
|
+
cssVars['--persona-components-composer-lineHeight'] ?? '1.25rem';
|
|
978
|
+
cssVars['--persona-composer-border-color'] =
|
|
979
|
+
cssVars['--persona-components-composer-borderColor'] ??
|
|
980
|
+
cssVars['--persona-border'] ??
|
|
981
|
+
'#e5e7eb';
|
|
982
|
+
|
|
983
|
+
// Scrollbars (every scroller in the widget consumes these two)
|
|
984
|
+
cssVars['--persona-scrollbar-thumb'] =
|
|
985
|
+
cssVars['--persona-components-scrollbar-thumb'] ??
|
|
986
|
+
cssVars['--persona-border'] ??
|
|
987
|
+
'#e5e7eb';
|
|
988
|
+
cssVars['--persona-scrollbar-track'] =
|
|
989
|
+
cssVars['--persona-components-scrollbar-track'] ?? 'transparent';
|
|
886
990
|
|
|
887
991
|
cssVars['--persona-md-inline-code-bg'] =
|
|
888
992
|
cssVars['--persona-components-markdown-inlineCode-background'] ?? cssVars['--persona-container'];
|
|
@@ -915,6 +1019,12 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
915
1019
|
cssVars['--persona-components-markdown-codeBlock-borderColor'] ?? cssVars['--persona-border'];
|
|
916
1020
|
cssVars['--persona-md-code-block-text-color'] =
|
|
917
1021
|
cssVars['--persona-components-markdown-codeBlock-textColor'] ?? 'inherit';
|
|
1022
|
+
// Follows the palette radius so square-corner themes get square code blocks;
|
|
1023
|
+
// the 0.375rem fallback matches palette.radius.md's default.
|
|
1024
|
+
cssVars['--persona-md-code-block-border-radius'] =
|
|
1025
|
+
cssVars['--persona-components-markdown-codeBlock-borderRadius'] ??
|
|
1026
|
+
cssVars['--persona-radius-md'] ??
|
|
1027
|
+
'0.375rem';
|
|
918
1028
|
|
|
919
1029
|
// Markdown table
|
|
920
1030
|
cssVars['--persona-md-table-header-bg'] =
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_WIDGET_CONFIG, mergeWithDefaults } from "./defaults";
|
|
4
|
+
import { mergeConfigUpdate } from "./utils/config-merge";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_WELCOME_SUBTITLE,
|
|
7
|
+
DEFAULT_WELCOME_TITLE,
|
|
8
|
+
isWelcomeVisible,
|
|
9
|
+
resolveWelcomeConfig,
|
|
10
|
+
} from "./welcome";
|
|
11
|
+
import type { AgentWidgetConfig, AgentWidgetMessage } from "./types";
|
|
12
|
+
|
|
13
|
+
const config = (partial: Partial<AgentWidgetConfig>): AgentWidgetConfig =>
|
|
14
|
+
partial as AgentWidgetConfig;
|
|
15
|
+
|
|
16
|
+
const userMessage: AgentWidgetMessage = {
|
|
17
|
+
id: "u1",
|
|
18
|
+
role: "user",
|
|
19
|
+
content: "hi",
|
|
20
|
+
createdAt: "2026-08-01T00:00:00.000Z",
|
|
21
|
+
streaming: false,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const assistantMessage: AgentWidgetMessage = {
|
|
25
|
+
...userMessage,
|
|
26
|
+
id: "a1",
|
|
27
|
+
role: "assistant",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe("resolveWelcomeConfig", () => {
|
|
31
|
+
it("falls back to resolver-owned defaults", () => {
|
|
32
|
+
const resolved = resolveWelcomeConfig(config({}));
|
|
33
|
+
expect(resolved.title).toBe(DEFAULT_WELCOME_TITLE);
|
|
34
|
+
expect(resolved.subtitle).toBe(DEFAULT_WELCOME_SUBTITLE);
|
|
35
|
+
expect(resolved.variant).toBe("card");
|
|
36
|
+
expect(resolved.dismiss).toBe("never");
|
|
37
|
+
expect(resolved.message).toBeUndefined();
|
|
38
|
+
expect(resolved.icon).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("prefers welcome.* over the legacy copy aliases, per field", () => {
|
|
42
|
+
const resolved = resolveWelcomeConfig(
|
|
43
|
+
config({
|
|
44
|
+
welcome: { title: "New title" },
|
|
45
|
+
copy: { welcomeTitle: "Legacy title", welcomeSubtitle: "Legacy subtitle" },
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
expect(resolved.title).toBe("New title");
|
|
49
|
+
expect(resolved.subtitle).toBe("Legacy subtitle");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("uses the legacy aliases when welcome does not set the field", () => {
|
|
53
|
+
const resolved = resolveWelcomeConfig(
|
|
54
|
+
config({ copy: { welcomeTitle: "Legacy", welcomeSubtitle: "Scope" } })
|
|
55
|
+
);
|
|
56
|
+
expect(resolved.title).toBe("Legacy");
|
|
57
|
+
expect(resolved.subtitle).toBe("Scope");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("honors an explicitly empty title or subtitle", () => {
|
|
61
|
+
const resolved = resolveWelcomeConfig(
|
|
62
|
+
config({ welcome: { title: "", subtitle: "" } })
|
|
63
|
+
);
|
|
64
|
+
expect(resolved.title).toBe("");
|
|
65
|
+
expect(resolved.subtitle).toBe("");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("maps showWelcomeCard: false onto variant none", () => {
|
|
69
|
+
expect(
|
|
70
|
+
resolveWelcomeConfig(config({ copy: { showWelcomeCard: false } })).variant
|
|
71
|
+
).toBe("none");
|
|
72
|
+
expect(
|
|
73
|
+
resolveWelcomeConfig(config({ copy: { showWelcomeCard: true } })).variant
|
|
74
|
+
).toBe("card");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("lets welcome.variant win over showWelcomeCard", () => {
|
|
78
|
+
expect(
|
|
79
|
+
resolveWelcomeConfig(
|
|
80
|
+
config({ welcome: { variant: "hero" }, copy: { showWelcomeCard: false } })
|
|
81
|
+
).variant
|
|
82
|
+
).toBe("hero");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("forces on-first-message dismissal for hero and suppresses the greeting", () => {
|
|
86
|
+
const resolved = resolveWelcomeConfig(
|
|
87
|
+
config({
|
|
88
|
+
welcome: { variant: "hero", dismiss: "never", message: "Hi there" },
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
expect(resolved.dismiss).toBe("on-first-message");
|
|
92
|
+
expect(resolved.message).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("keeps the greeting for card and none variants", () => {
|
|
96
|
+
expect(
|
|
97
|
+
resolveWelcomeConfig(config({ welcome: { message: "Hi there" } })).message
|
|
98
|
+
).toBe("Hi there");
|
|
99
|
+
expect(
|
|
100
|
+
resolveWelcomeConfig(
|
|
101
|
+
config({ welcome: { variant: "none", message: "Hi there" } })
|
|
102
|
+
).message
|
|
103
|
+
).toBe("Hi there");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("warns once in debug mode when a field is set through both namespaces", () => {
|
|
107
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
108
|
+
const withBoth = config({
|
|
109
|
+
debug: true,
|
|
110
|
+
welcome: { title: "New" },
|
|
111
|
+
copy: { welcomeTitle: "Legacy" },
|
|
112
|
+
});
|
|
113
|
+
resolveWelcomeConfig(withBoth);
|
|
114
|
+
resolveWelcomeConfig(withBoth);
|
|
115
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
116
|
+
expect(warn.mock.calls[0][0]).toContain("copy.welcomeTitle");
|
|
117
|
+
warn.mockRestore();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("warns in debug mode when message is set with variant hero", () => {
|
|
121
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
122
|
+
resolveWelcomeConfig(
|
|
123
|
+
config({ debug: true, welcome: { variant: "hero", message: "Hi" } })
|
|
124
|
+
);
|
|
125
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
126
|
+
expect(warn.mock.calls[0][0]).toContain("welcome.message is ignored");
|
|
127
|
+
warn.mockRestore();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("stays silent outside debug mode", () => {
|
|
131
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
132
|
+
resolveWelcomeConfig(
|
|
133
|
+
config({ welcome: { title: "New" }, copy: { welcomeTitle: "Legacy" } })
|
|
134
|
+
);
|
|
135
|
+
expect(warn).not.toHaveBeenCalled();
|
|
136
|
+
warn.mockRestore();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("welcome defaults are never materialized into config", () => {
|
|
141
|
+
it("keeps welcome copy out of DEFAULT_WIDGET_CONFIG", () => {
|
|
142
|
+
expect(DEFAULT_WIDGET_CONFIG.copy).not.toHaveProperty("welcomeTitle");
|
|
143
|
+
expect(DEFAULT_WIDGET_CONFIG.copy).not.toHaveProperty("welcomeSubtitle");
|
|
144
|
+
expect(DEFAULT_WIDGET_CONFIG).not.toHaveProperty("welcome");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("keeps welcome copy out of a defaults-merged config", () => {
|
|
148
|
+
const merged = mergeWithDefaults({ apiUrl: "https://example.com" });
|
|
149
|
+
expect(merged.copy).not.toHaveProperty("welcomeTitle");
|
|
150
|
+
expect(merged.copy).not.toHaveProperty("welcomeSubtitle");
|
|
151
|
+
expect(merged.welcome).toBeUndefined();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("resets to the resolver default on an explicit-undefined patch", () => {
|
|
155
|
+
const initial = mergeWithDefaults({
|
|
156
|
+
apiUrl: "https://example.com",
|
|
157
|
+
welcome: { title: "Set by host" },
|
|
158
|
+
}) as AgentWidgetConfig;
|
|
159
|
+
expect(resolveWelcomeConfig(initial).title).toBe("Set by host");
|
|
160
|
+
|
|
161
|
+
const cleared = mergeConfigUpdate(initial, {
|
|
162
|
+
welcome: { title: undefined },
|
|
163
|
+
});
|
|
164
|
+
expect(cleared.welcome).not.toHaveProperty("title");
|
|
165
|
+
expect(resolveWelcomeConfig(cleared).title).toBe(DEFAULT_WELCOME_TITLE);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("falls back to the legacy alias after clearing the welcome field", () => {
|
|
169
|
+
const initial = mergeWithDefaults({
|
|
170
|
+
apiUrl: "https://example.com",
|
|
171
|
+
welcome: { title: "New" },
|
|
172
|
+
copy: { welcomeTitle: "Legacy" },
|
|
173
|
+
}) as AgentWidgetConfig;
|
|
174
|
+
const cleared = mergeConfigUpdate(initial, { welcome: { title: undefined } });
|
|
175
|
+
expect(resolveWelcomeConfig(cleared).title).toBe("Legacy");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("replaces the icon wholesale instead of splicing union variants", () => {
|
|
179
|
+
const initial = mergeWithDefaults({
|
|
180
|
+
apiUrl: "https://example.com",
|
|
181
|
+
welcome: { icon: { type: "image", url: "/logo.png", alt: "Logo" } },
|
|
182
|
+
}) as AgentWidgetConfig;
|
|
183
|
+
const updated = mergeConfigUpdate(initial, {
|
|
184
|
+
welcome: { icon: { type: "lucide", name: "sparkles" } },
|
|
185
|
+
});
|
|
186
|
+
expect(updated.welcome?.icon).toEqual({ type: "lucide", name: "sparkles" });
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("isWelcomeVisible", () => {
|
|
191
|
+
const card = resolveWelcomeConfig(config({}));
|
|
192
|
+
const hero = resolveWelcomeConfig(config({ welcome: { variant: "hero" } }));
|
|
193
|
+
const none = resolveWelcomeConfig(config({ welcome: { variant: "none" } }));
|
|
194
|
+
|
|
195
|
+
it("shows a never-dismissing card regardless of transcript content", () => {
|
|
196
|
+
expect(isWelcomeVisible(card, [])).toBe(true);
|
|
197
|
+
expect(isWelcomeVisible(card, [userMessage])).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("hides an on-first-message surface once a user message exists", () => {
|
|
201
|
+
expect(isWelcomeVisible(hero, [])).toBe(true);
|
|
202
|
+
expect(isWelcomeVisible(hero, [assistantMessage])).toBe(true);
|
|
203
|
+
expect(isWelcomeVisible(hero, [userMessage, assistantMessage])).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("never shows variant none", () => {
|
|
207
|
+
expect(isWelcomeVisible(none, [])).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("treats a missing message list as empty", () => {
|
|
211
|
+
expect(isWelcomeVisible(hero, undefined)).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
});
|