@runtypelabs/persona 4.17.1 → 4.18.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/chunk-3I2S3I5P.js +1 -0
- package/dist/chunk-76IELU7F.js +1 -0
- package/dist/chunk-EFJBUH5F.js +1 -0
- package/dist/chunk-PDPVORUF.js +1 -0
- package/dist/chunk-Q735PSZL.js +1 -0
- package/dist/chunk-VK6JOIYJ.js +1 -0
- package/dist/codegen.cjs +4 -4
- package/dist/codegen.js +4 -4
- package/dist/{context-mentions-BTSTIBFC.js → context-mentions-MJL6S6P2.js} +1 -1
- package/dist/context-mentions.cjs +9 -8
- package/dist/context-mentions.d.cts +1212 -14
- package/dist/context-mentions.d.ts +1212 -14
- package/dist/context-mentions.js +9 -8
- package/dist/event-stream-view-4EZ7BGFK.js +1 -0
- package/dist/event-stream-view.cjs +129 -0
- package/dist/event-stream-view.d.cts +8384 -0
- package/dist/event-stream-view.d.ts +8384 -0
- package/dist/event-stream-view.js +129 -0
- package/dist/history-view-BD6OXLNA.js +898 -0
- package/dist/history-view.cjs +898 -0
- package/dist/history-view.d.cts +1461 -0
- package/dist/history-view.d.ts +1461 -0
- package/dist/history-view.js +898 -0
- package/dist/index.cjs +77 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1987 -17
- package/dist/index.d.ts +1987 -17
- package/dist/index.global.js +67 -66
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +77 -77
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +5 -4
- package/dist/launcher.global.js.map +1 -1
- package/dist/plugin-kit.cjs +1 -1
- package/dist/plugin-kit.d.cts +0 -8
- package/dist/plugin-kit.d.ts +0 -8
- package/dist/plugin-kit.js +1 -1
- package/dist/smart-dom-reader.d.cts +1212 -14
- package/dist/smart-dom-reader.d.ts +1212 -14
- package/dist/theme-editor-preview.cjs +970 -73
- package/dist/theme-editor-preview.d.cts +1331 -15
- package/dist/theme-editor-preview.d.ts +1331 -15
- package/dist/theme-editor-preview.js +81 -81
- package/dist/theme-editor.cjs +7 -7
- package/dist/theme-editor.d.cts +1215 -15
- package/dist/theme-editor.d.ts +1215 -15
- package/dist/theme-editor.js +9 -9
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +226 -7
- package/dist/theme-reference.d.ts +226 -7
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +16 -4
- package/src/client-history-rest.test.ts +1034 -0
- package/src/client-visitor-history.test.ts +596 -0
- package/src/client.ts +1040 -24
- package/src/components/artifact-inline.ts +8 -1
- package/src/components/artifact-pane.test.ts +278 -1
- package/src/components/artifact-pane.ts +135 -67
- package/src/components/artifact-preview.test.ts +14 -1
- package/src/components/artifact-preview.ts +9 -0
- package/src/components/header-builder.ts +3 -0
- package/src/components/header-layouts.test.ts +171 -7
- package/src/components/header-layouts.ts +83 -53
- package/src/components/header-parts.test.ts +154 -1
- package/src/components/header-parts.ts +266 -138
- package/src/components/history-confirm-dialog.ts +177 -0
- package/src/components/history-shell-copy.ts +99 -0
- package/src/components/history-view/announcer.ts +40 -0
- package/src/components/history-view/copy.ts +105 -0
- package/src/components/history-view/css.ts +1078 -0
- package/src/components/history-view/grouping.test.ts +142 -0
- package/src/components/history-view/grouping.ts +142 -0
- package/src/components/history-view/icons.ts +73 -0
- package/src/components/history-view/rows.ts +341 -0
- package/src/components/history-view/state.ts +309 -0
- package/src/components/history-view.test.ts +2660 -0
- package/src/components/history-view.ts +1891 -0
- package/src/components/launcher-surface.test.ts +133 -0
- package/src/components/launcher.test.ts +18 -0
- package/src/components/launcher.ts +7 -7
- package/src/components/panel.ts +17 -7
- package/src/components/welcome.ts +72 -2
- package/src/defaults.ts +36 -3
- package/src/event-stream-view-bundle.test.ts +69 -0
- package/src/event-stream-view-entry.ts +19 -0
- package/src/event-stream-view-loader.ts +31 -0
- package/src/event-stream-view.ts +19 -0
- package/src/generated/runtype-openapi-contract.ts +1 -0
- package/src/history-render.ts +325 -0
- package/src/history-view-bundle.test.ts +78 -0
- package/src/history-view-entry.ts +41 -0
- package/src/history-view-loader.test.ts +30 -0
- package/src/history-view-loader.ts +32 -0
- package/src/history-view.ts +28 -0
- package/src/index-core.ts +50 -2
- package/src/index-global.ts +55 -0
- package/src/internal/demo-history-provider.test.ts +402 -0
- package/src/internal/demo-history-provider.ts +689 -0
- package/src/internal/history-provider-registry.ts +25 -0
- package/src/internal/history-provider.ts +150 -0
- package/src/internal/runtype-history-provider.test.ts +313 -0
- package/src/internal/runtype-history-provider.ts +240 -0
- package/src/plugin-kit.test.ts +16 -0
- package/src/plugin-kit.ts +13 -5
- package/src/plugins/types.ts +177 -1
- package/src/session-history.test.ts +1397 -0
- package/src/session.ts +1200 -26
- package/src/styles/widget-styles.test.ts +133 -0
- package/src/styles/widget.css +227 -8
- package/src/theme-editor/preview-utils.test.ts +19 -0
- package/src/theme-editor/preview-utils.ts +15 -1
- package/src/theme-editor/preview.ts +7 -1
- package/src/theme-editor/role-mappings.ts +4 -1
- package/src/theme-editor/sections.test.ts +168 -1
- package/src/theme-editor/sections.ts +136 -7
- package/src/theme-editor/webmcp/summary.ts +16 -2
- package/src/theme-reference.ts +20 -6
- package/src/types/theme.ts +213 -7
- package/src/types.ts +927 -7
- package/src/ui.artifact-rail-resize.test.ts +235 -0
- package/src/ui.attachments-drop.test.ts +1 -0
- package/src/ui.header-update-stability.test.ts +2 -2
- package/src/ui.history-chunk.test.ts +72 -0
- package/src/ui.history-config-provider.test.ts +318 -0
- package/src/ui.history-controller.test.ts +711 -0
- package/src/ui.history-lifecycle.test.ts +456 -0
- package/src/ui.history-render-hooks.test.ts +904 -0
- package/src/ui.history-shell.test.ts +1887 -0
- package/src/ui.rail-overlay.test.ts +573 -0
- package/src/ui.rail-resize.test.ts +284 -0
- package/src/ui.rebuild-composer.test.ts +21 -0
- package/src/ui.render-welcome.test.ts +42 -0
- package/src/ui.shortcuts.test.ts +166 -0
- package/src/ui.ts +3071 -240
- package/src/utils/__fixtures__/client-conversation-history-wire.json +108 -0
- package/src/utils/artifact-file.ts +15 -0
- package/src/utils/artifact-gate.test.ts +4 -0
- package/src/utils/artifact-gate.ts +13 -17
- package/src/utils/artifact-resize.test.ts +28 -0
- package/src/utils/artifact-resize.ts +9 -2
- package/src/utils/buttons.ts +188 -1
- package/src/utils/config-merge.test.ts +91 -0
- package/src/utils/dropdown.ts +10 -1
- package/src/utils/history-messages.test.ts +284 -0
- package/src/utils/history-messages.ts +284 -0
- package/src/utils/icons.ts +3 -0
- package/src/utils/shortcuts.test.ts +288 -0
- package/src/utils/shortcuts.ts +221 -0
- package/src/utils/theme.test.ts +501 -4
- package/src/utils/theme.ts +42 -3
- package/src/utils/tokens.ts +153 -15
- package/src/utils/tooltip-timing.ts +4 -0
- package/src/utils/tooltip.test.ts +249 -7
- package/src/utils/tooltip.ts +146 -4
- package/src/utils/visitor-store.test.ts +425 -0
- package/src/utils/visitor-store.ts +337 -0
- package/dist/chunk-QJ4LV2KW.js +0 -1
package/src/utils/theme.test.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
3
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
applyThemeVariables,
|
|
6
|
+
createDarkTheme,
|
|
7
|
+
createLightTheme,
|
|
8
|
+
createTheme,
|
|
9
|
+
getActiveTheme,
|
|
10
|
+
themeToCssVariables,
|
|
11
|
+
} from './theme';
|
|
5
12
|
|
|
6
13
|
describe('theme utils', () => {
|
|
7
14
|
afterEach(() => {
|
|
@@ -137,6 +144,61 @@ describe('theme utils', () => {
|
|
|
137
144
|
expect(cssVars['--persona-button-radius']).toBe('6px');
|
|
138
145
|
});
|
|
139
146
|
|
|
147
|
+
it('emits history motion vars with ms appended to bare durations', () => {
|
|
148
|
+
const cssVars = themeToCssVariables(
|
|
149
|
+
createTheme({
|
|
150
|
+
components: {
|
|
151
|
+
history: {
|
|
152
|
+
motion: {
|
|
153
|
+
enterDurationMs: 320,
|
|
154
|
+
enterEasing: 'ease-out',
|
|
155
|
+
exitDurationMs: 0,
|
|
156
|
+
exitEasing: 'linear',
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
} as any)
|
|
161
|
+
);
|
|
162
|
+
expect(cssVars['--persona-history-enter-ms']).toBe('320ms');
|
|
163
|
+
expect(cssVars['--persona-history-enter-easing']).toBe('ease-out');
|
|
164
|
+
expect(cssVars['--persona-history-exit-ms']).toBe('0ms');
|
|
165
|
+
expect(cssVars['--persona-history-exit-easing']).toBe('linear');
|
|
166
|
+
|
|
167
|
+
// Unset motion emits nothing: the chunk's fallbacks stay in charge.
|
|
168
|
+
const bare = themeToCssVariables(createTheme({} as any));
|
|
169
|
+
expect(bare['--persona-history-enter-ms']).toBeUndefined();
|
|
170
|
+
expect(bare['--persona-history-exit-easing']).toBeUndefined();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('keys launcher pill vars to shared tokens by default and launcher tokens when customized', () => {
|
|
174
|
+
const defaults = themeToCssVariables(createTheme());
|
|
175
|
+
expect(defaults['--persona-launcher-bg']).toBe(defaults['--persona-surface']);
|
|
176
|
+
expect(defaults['--persona-launcher-fg']).toBe(defaults['--persona-primary']);
|
|
177
|
+
expect(defaults['--persona-launcher-fg-muted']).toBe(defaults['--persona-muted']);
|
|
178
|
+
expect(defaults['--persona-launcher-border']).toBe(defaults['--persona-border']);
|
|
179
|
+
|
|
180
|
+
const custom = themeToCssVariables(
|
|
181
|
+
createTheme({
|
|
182
|
+
components: {
|
|
183
|
+
launcher: {
|
|
184
|
+
background: '#1e293b',
|
|
185
|
+
foreground: '#f8fafc',
|
|
186
|
+
border: '#334155',
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
} as any)
|
|
190
|
+
);
|
|
191
|
+
expect(custom['--persona-launcher-bg']).toBe('#1e293b');
|
|
192
|
+
expect(custom['--persona-launcher-fg']).toBe('#f8fafc');
|
|
193
|
+
expect(custom['--persona-launcher-fg-muted']).toBe(
|
|
194
|
+
'color-mix(in srgb, #f8fafc 70%, transparent)'
|
|
195
|
+
);
|
|
196
|
+
expect(custom['--persona-launcher-border']).toBe('#334155');
|
|
197
|
+
// Launcher tokens must not leak into the shared tokens the panel reads.
|
|
198
|
+
expect(custom['--persona-surface']).toBe(defaults['--persona-surface']);
|
|
199
|
+
expect(custom['--persona-primary']).toBe(defaults['--persona-primary']);
|
|
200
|
+
});
|
|
201
|
+
|
|
140
202
|
it('zeroes horizontal intro-card padding when the card resolves flat', () => {
|
|
141
203
|
const cssVars = themeToCssVariables(createTheme({} as any));
|
|
142
204
|
expect(cssVars['--persona-intro-card-padding']).toBe('1.5rem 0');
|
|
@@ -294,9 +356,15 @@ describe('theme utils', () => {
|
|
|
294
356
|
// Default header uses solid primary role: icon-bg=primary.600, icon-fg=primary.50, etc.
|
|
295
357
|
expect(cssVars['--persona-header-icon-bg']).toBe('#0f0f0f'); // primary.600
|
|
296
358
|
expect(cssVars['--persona-header-icon-fg']).toBe('#ffffff'); // primary.50
|
|
297
|
-
expect(cssVars['--persona-header-title-fg']).toBe('#ffffff'); // primary.50
|
|
298
|
-
|
|
299
|
-
|
|
359
|
+
expect(cssVars['--persona-header-title-fg']).toBe('#ffffff'); // primary.50, via header.foreground
|
|
360
|
+
// Subtitle and action icons now derive from the background/foreground
|
|
361
|
+
// pair; the mix replaces the primary.200 they used to name directly.
|
|
362
|
+
expect(cssVars['--persona-header-subtitle-fg']).toBe(
|
|
363
|
+
'color-mix(in srgb, #ffffff 72%, #171717)'
|
|
364
|
+
);
|
|
365
|
+
expect(cssVars['--persona-header-action-icon-fg']).toBe(
|
|
366
|
+
'color-mix(in srgb, #ffffff 72%, #171717)'
|
|
367
|
+
);
|
|
300
368
|
|
|
301
369
|
const custom = createTheme({
|
|
302
370
|
components: {
|
|
@@ -317,6 +385,278 @@ describe('theme utils', () => {
|
|
|
317
385
|
expect(customVars['--persona-header-action-icon-fg']).toBe('#9ca3af');
|
|
318
386
|
});
|
|
319
387
|
|
|
388
|
+
it('renders the same default header chrome in light and dark schemes', () => {
|
|
389
|
+
// Golden values for the shipped header band. The dark preset overrides no
|
|
390
|
+
// header token, so both schemes must resolve identically.
|
|
391
|
+
const expected = {
|
|
392
|
+
'--persona-header-bg': '#171717', // primary.500
|
|
393
|
+
'--persona-header-border': 'color-mix(in srgb, #ffffff 14%, #171717)',
|
|
394
|
+
'--persona-header-title-fg': '#ffffff', // primary.50
|
|
395
|
+
'--persona-header-subtitle-fg': 'color-mix(in srgb, #ffffff 72%, #171717)',
|
|
396
|
+
'--persona-header-action-icon-fg': 'color-mix(in srgb, #ffffff 72%, #171717)',
|
|
397
|
+
'--persona-header-icon-bg': '#0f0f0f', // primary.600
|
|
398
|
+
'--persona-header-icon-fg': '#ffffff', // primary.50
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const light = themeToCssVariables(createLightTheme());
|
|
402
|
+
const dark = themeToCssVariables(createDarkTheme());
|
|
403
|
+
|
|
404
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
405
|
+
expect(light[key]).toBe(value);
|
|
406
|
+
expect(dark[key]).toBe(value);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('derives header text and border from the background/foreground pair', () => {
|
|
411
|
+
const cssVars = themeToCssVariables(
|
|
412
|
+
createTheme({
|
|
413
|
+
components: {
|
|
414
|
+
header: { background: '#0b3d2e', foreground: '#f4f3ee' },
|
|
415
|
+
},
|
|
416
|
+
} as any)
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
expect(cssVars['--persona-header-bg']).toBe('#0b3d2e');
|
|
420
|
+
expect(cssVars['--persona-header-title-fg']).toBe('#f4f3ee');
|
|
421
|
+
expect(cssVars['--persona-header-subtitle-fg']).toBe(
|
|
422
|
+
'color-mix(in srgb, #f4f3ee 72%, #0b3d2e)'
|
|
423
|
+
);
|
|
424
|
+
expect(cssVars['--persona-header-action-icon-fg']).toBe(
|
|
425
|
+
'color-mix(in srgb, #f4f3ee 72%, #0b3d2e)'
|
|
426
|
+
);
|
|
427
|
+
expect(cssVars['--persona-header-border']).toBe(
|
|
428
|
+
'color-mix(in srgb, #f4f3ee 14%, #0b3d2e)'
|
|
429
|
+
);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('lets explicit header keys win over the foreground derivation', () => {
|
|
433
|
+
const cssVars = themeToCssVariables(
|
|
434
|
+
createTheme({
|
|
435
|
+
components: {
|
|
436
|
+
header: {
|
|
437
|
+
background: '#0b3d2e',
|
|
438
|
+
foreground: '#f4f3ee',
|
|
439
|
+
border: '#1f5c48',
|
|
440
|
+
titleForeground: '#ffe066',
|
|
441
|
+
actionIconForeground: '#cfd8cc',
|
|
442
|
+
subtitle: { color: '#b9c8bc' },
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
} as any)
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
expect(cssVars['--persona-header-border']).toBe('#1f5c48');
|
|
449
|
+
expect(cssVars['--persona-header-title-fg']).toBe('#ffe066');
|
|
450
|
+
expect(cssVars['--persona-header-action-icon-fg']).toBe('#cfd8cc');
|
|
451
|
+
expect(cssVars['--persona-header-subtitle-fg']).toBe('#b9c8bc');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('ranks header title color as title.color, then titleForeground, then foreground', () => {
|
|
455
|
+
const base = { background: '#0b3d2e', foreground: '#f4f3ee' };
|
|
456
|
+
|
|
457
|
+
const fromForeground = themeToCssVariables(
|
|
458
|
+
createTheme({ components: { header: base } } as any)
|
|
459
|
+
);
|
|
460
|
+
const fromAlias = themeToCssVariables(
|
|
461
|
+
createTheme({
|
|
462
|
+
components: { header: { ...base, titleForeground: '#ffe066' } },
|
|
463
|
+
} as any)
|
|
464
|
+
);
|
|
465
|
+
const fromTitleColor = themeToCssVariables(
|
|
466
|
+
createTheme({
|
|
467
|
+
components: {
|
|
468
|
+
header: { ...base, titleForeground: '#ffe066', title: { color: '#8b5cf6' } },
|
|
469
|
+
},
|
|
470
|
+
} as any)
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
expect(fromForeground['--persona-header-title-fg']).toBe('#f4f3ee');
|
|
474
|
+
expect(fromAlias['--persona-header-title-fg']).toBe('#ffe066');
|
|
475
|
+
expect(fromTitleColor['--persona-header-title-fg']).toBe('#8b5cf6');
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('emits the shared header control box, glyph, and stroke tokens', () => {
|
|
479
|
+
const defaults = themeToCssVariables(createTheme());
|
|
480
|
+
|
|
481
|
+
expect(defaults['--persona-header-control-size']).toBe('32px');
|
|
482
|
+
expect(defaults['--persona-header-control-icon-size']).toBe('20px');
|
|
483
|
+
// The stroke has no alias: widget.css reads the full-path token and owns
|
|
484
|
+
// the 1.5 default, which keeps the sparse close X on its 1.05.
|
|
485
|
+
expect(defaults['--persona-components-header-controlStrokeWidth']).toBeUndefined();
|
|
486
|
+
|
|
487
|
+
const custom = themeToCssVariables(
|
|
488
|
+
createTheme({
|
|
489
|
+
components: {
|
|
490
|
+
header: {
|
|
491
|
+
controlSize: '32px',
|
|
492
|
+
controlIconSize: '18px',
|
|
493
|
+
controlStrokeWidth: '2',
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
} as any)
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
expect(custom['--persona-header-control-size']).toBe('32px');
|
|
500
|
+
expect(custom['--persona-header-control-icon-size']).toBe('18px');
|
|
501
|
+
// Unitless, straight through: no px suffix, no token resolution.
|
|
502
|
+
expect(custom['--persona-components-header-controlStrokeWidth']).toBe('2');
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('emits the launcher icon stroke token only when set', () => {
|
|
506
|
+
const defaults = themeToCssVariables(createTheme());
|
|
507
|
+
|
|
508
|
+
// No alias and no default: widget.css owns the shipped 2 in its var()
|
|
509
|
+
// fallback, so an unset token must leave the variable undefined.
|
|
510
|
+
expect(defaults['--persona-components-launcher-iconStrokeWidth']).toBeUndefined();
|
|
511
|
+
|
|
512
|
+
const custom = themeToCssVariables(
|
|
513
|
+
createTheme({
|
|
514
|
+
components: {
|
|
515
|
+
launcher: {
|
|
516
|
+
iconStrokeWidth: '1.5',
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
} as any)
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
// Unitless, straight through: no px suffix, no token resolution.
|
|
523
|
+
expect(custom['--persona-components-launcher-iconStrokeWidth']).toBe('1.5');
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('emits the header and rail-header height/surface tokens only when set', () => {
|
|
527
|
+
const defaults = themeToCssVariables(createTheme());
|
|
528
|
+
expect(defaults['--persona-header-min-height']).toBeUndefined();
|
|
529
|
+
expect(defaults['--persona-history-rail-header-bg']).toBeUndefined();
|
|
530
|
+
expect(defaults['--persona-history-rail-header-border']).toBeUndefined();
|
|
531
|
+
expect(defaults['--persona-history-rail-header-min-height']).toBeUndefined();
|
|
532
|
+
|
|
533
|
+
const custom = themeToCssVariables(
|
|
534
|
+
createTheme({
|
|
535
|
+
components: {
|
|
536
|
+
header: { minHeight: '64px' },
|
|
537
|
+
history: {
|
|
538
|
+
railHeader: {
|
|
539
|
+
background: 'palette.colors.gray.100',
|
|
540
|
+
border: '1px solid #e5e7eb',
|
|
541
|
+
minHeight: '64px',
|
|
542
|
+
title: {
|
|
543
|
+
color: 'palette.colors.gray.900',
|
|
544
|
+
fontSize: '15px',
|
|
545
|
+
fontWeight: '700',
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
} as any)
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
expect(custom['--persona-header-min-height']).toBe('64px');
|
|
554
|
+
// The background goes through token resolution; the rest are raw CSS.
|
|
555
|
+
expect(custom['--persona-history-rail-header-bg']).toBe('#f3f4f6');
|
|
556
|
+
expect(custom['--persona-history-rail-header-border']).toBe('1px solid #e5e7eb');
|
|
557
|
+
expect(custom['--persona-history-rail-header-min-height']).toBe('64px');
|
|
558
|
+
// The rail title is styled straight off the auto-emitted component vars.
|
|
559
|
+
expect(custom['--persona-components-history-railHeader-title-color']).toBe('#111827');
|
|
560
|
+
expect(custom['--persona-components-history-railHeader-title-fontSize']).toBe('15px');
|
|
561
|
+
expect(custom['--persona-components-history-railHeader-title-fontWeight']).toBe('700');
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('emits the floating rail aliases only when set', () => {
|
|
565
|
+
const defaults = themeToCssVariables(createTheme());
|
|
566
|
+
expect(defaults['--persona-history-overlay-margin']).toBeUndefined();
|
|
567
|
+
expect(defaults['--persona-history-overlay-radius']).toBeUndefined();
|
|
568
|
+
expect(defaults['--persona-history-overlay-shadow']).toBeUndefined();
|
|
569
|
+
expect(defaults['--persona-history-overlay-bg']).toBeUndefined();
|
|
570
|
+
|
|
571
|
+
const custom = themeToCssVariables(
|
|
572
|
+
createTheme({
|
|
573
|
+
components: {
|
|
574
|
+
history: {
|
|
575
|
+
overlay: {
|
|
576
|
+
margin: '12px',
|
|
577
|
+
borderRadius: '20px',
|
|
578
|
+
shadow: '0 8px 24px rgba(0, 0, 0, 0.4)',
|
|
579
|
+
background: 'palette.colors.gray.100',
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
} as any)
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
expect(custom['--persona-history-overlay-margin']).toBe('12px');
|
|
587
|
+
expect(custom['--persona-history-overlay-radius']).toBe('20px');
|
|
588
|
+
expect(custom['--persona-history-overlay-shadow']).toBe(
|
|
589
|
+
'0 8px 24px rgba(0, 0, 0, 0.4)'
|
|
590
|
+
);
|
|
591
|
+
// The background goes through token resolution; the rest are raw CSS.
|
|
592
|
+
expect(custom['--persona-history-overlay-bg']).toBe('#f3f4f6');
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('emits the history menu aliases only when set', () => {
|
|
596
|
+
const defaults = themeToCssVariables(createTheme());
|
|
597
|
+
expect(defaults['--persona-history-menu-bg']).toBeUndefined();
|
|
598
|
+
expect(defaults['--persona-history-menu-radius']).toBeUndefined();
|
|
599
|
+
|
|
600
|
+
const custom = themeToCssVariables(
|
|
601
|
+
createTheme({
|
|
602
|
+
components: {
|
|
603
|
+
history: {
|
|
604
|
+
menu: {
|
|
605
|
+
background: 'palette.colors.gray.100',
|
|
606
|
+
borderRadius: '16px',
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
} as any)
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
expect(custom['--persona-history-menu-bg']).toBe('#f3f4f6');
|
|
614
|
+
expect(custom['--persona-history-menu-radius']).toBe('16px');
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it('emits the tooltip aliases only when set, including the arrow opt-out', () => {
|
|
618
|
+
const defaults = themeToCssVariables(createTheme());
|
|
619
|
+
expect(defaults['--persona-tooltip-background']).toBeUndefined();
|
|
620
|
+
expect(defaults['--persona-tooltip-hint-fg']).toBeUndefined();
|
|
621
|
+
expect(defaults['--persona-tooltip-radius']).toBeUndefined();
|
|
622
|
+
expect(defaults['--persona-tooltip-arrow-display']).toBeUndefined();
|
|
623
|
+
|
|
624
|
+
const custom = themeToCssVariables(
|
|
625
|
+
createTheme({
|
|
626
|
+
components: {
|
|
627
|
+
tooltip: {
|
|
628
|
+
background: 'palette.colors.gray.800',
|
|
629
|
+
foreground: 'palette.colors.gray.50',
|
|
630
|
+
hintForeground: 'palette.colors.gray.400',
|
|
631
|
+
borderRadius: 'palette.radius.md',
|
|
632
|
+
fontSize: '13px',
|
|
633
|
+
padding: '8px 14px',
|
|
634
|
+
maxWidth: '240px',
|
|
635
|
+
shadow: 'none',
|
|
636
|
+
arrow: false,
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
} as any)
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
// Colors and the radius resolve through the palette; the rest are raw CSS.
|
|
643
|
+
expect(custom['--persona-tooltip-background']).toBe('#1f2937');
|
|
644
|
+
expect(custom['--persona-tooltip-foreground']).toBe('#f9fafb');
|
|
645
|
+
expect(custom['--persona-tooltip-hint-fg']).toBe('#9ca3af');
|
|
646
|
+
expect(custom['--persona-tooltip-radius']).toBe('0.375rem');
|
|
647
|
+
expect(custom['--persona-tooltip-font-size']).toBe('13px');
|
|
648
|
+
expect(custom['--persona-tooltip-padding']).toBe('8px 14px');
|
|
649
|
+
expect(custom['--persona-tooltip-max-width']).toBe('240px');
|
|
650
|
+
expect(custom['--persona-tooltip-shadow']).toBe('none');
|
|
651
|
+
expect(custom['--persona-tooltip-arrow-display']).toBe('none');
|
|
652
|
+
|
|
653
|
+
// arrow: true is the built-in look, so it emits nothing.
|
|
654
|
+
const withArrow = themeToCssVariables(
|
|
655
|
+
createTheme({ components: { tooltip: { arrow: true } } } as any)
|
|
656
|
+
);
|
|
657
|
+
expect(withArrow['--persona-tooltip-arrow-display']).toBeUndefined();
|
|
658
|
+
});
|
|
659
|
+
|
|
320
660
|
it('emits full-path CSS variables for header and welcome text style tokens', () => {
|
|
321
661
|
const theme = createTheme({
|
|
322
662
|
components: {
|
|
@@ -397,6 +737,135 @@ describe('theme utils', () => {
|
|
|
397
737
|
expect(customVars['--persona-button-ghost-hover-bg']).toBe('#f3f4f6'); // gray.100
|
|
398
738
|
});
|
|
399
739
|
|
|
740
|
+
it('inverts the ghost hover wash for dark color schemes', () => {
|
|
741
|
+
// 5% black is invisible on a near-black surface, so every dark scheme —
|
|
742
|
+
// including a host theme that never mentions button.ghost — gets a light
|
|
743
|
+
// alpha wash instead. This drives the history rail's collapse toggle, its
|
|
744
|
+
// un-themed rows, and the per-message action buttons.
|
|
745
|
+
const light = themeToCssVariables(getActiveTheme({ colorScheme: 'light' }));
|
|
746
|
+
expect(light['--persona-button-ghost-hover-bg']).toBe('rgba(0, 0, 0, 0.05)');
|
|
747
|
+
|
|
748
|
+
const dark = themeToCssVariables(getActiveTheme({ colorScheme: 'dark' }));
|
|
749
|
+
expect(dark['--persona-button-ghost-hover-bg']).toBe('rgba(255, 255, 255, 0.08)');
|
|
750
|
+
|
|
751
|
+
// A host dark theme that overrides unrelated component tokens keeps it:
|
|
752
|
+
// the dark default is deep-merged underneath, not spread over.
|
|
753
|
+
const hostDark = themeToCssVariables(
|
|
754
|
+
getActiveTheme({
|
|
755
|
+
colorScheme: 'dark',
|
|
756
|
+
darkTheme: { components: { button: { primary: { background: '#22c55e' } } } },
|
|
757
|
+
} as any)
|
|
758
|
+
);
|
|
759
|
+
expect(hostDark['--persona-button-ghost-hover-bg']).toBe('rgba(255, 255, 255, 0.08)');
|
|
760
|
+
|
|
761
|
+
// An explicit hoverBackground always wins, in either scheme.
|
|
762
|
+
const explicitDark = themeToCssVariables(
|
|
763
|
+
getActiveTheme({
|
|
764
|
+
colorScheme: 'dark',
|
|
765
|
+
darkTheme: {
|
|
766
|
+
components: { button: { ghost: { hoverBackground: 'palette.colors.gray.800' } } },
|
|
767
|
+
},
|
|
768
|
+
} as any)
|
|
769
|
+
);
|
|
770
|
+
expect(explicitDark['--persona-button-ghost-hover-bg']).toBe('#1f2937'); // gray.800
|
|
771
|
+
|
|
772
|
+
// Set on `theme` alone it still reaches dark mode, which merges theme
|
|
773
|
+
// under darkTheme before the dark rebuild.
|
|
774
|
+
const explicitViaLight = themeToCssVariables(
|
|
775
|
+
getActiveTheme({
|
|
776
|
+
colorScheme: 'dark',
|
|
777
|
+
theme: { components: { button: { ghost: { hoverBackground: '#123456' } } } },
|
|
778
|
+
} as any)
|
|
779
|
+
);
|
|
780
|
+
expect(explicitViaLight['--persona-button-ghost-hover-bg']).toBe('#123456');
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
it('lightens the history danger red for dark color schemes', () => {
|
|
784
|
+
// error-600 lands ~3:1 on dark surfaces, under the AA floor for the 14px
|
|
785
|
+
// destructive labels; the built-in dark defaults swap in error-400.
|
|
786
|
+
const light = themeToCssVariables(getActiveTheme({ colorScheme: 'light' }));
|
|
787
|
+
expect(light['--persona-history-danger-fg']).toBeUndefined();
|
|
788
|
+
|
|
789
|
+
const dark = themeToCssVariables(getActiveTheme({ colorScheme: 'dark' }));
|
|
790
|
+
expect(dark['--persona-history-danger-fg']).toBe('#f87171');
|
|
791
|
+
|
|
792
|
+
// An explicit dangerForeground wins in either scheme.
|
|
793
|
+
const explicit = themeToCssVariables(
|
|
794
|
+
getActiveTheme({
|
|
795
|
+
colorScheme: 'dark',
|
|
796
|
+
darkTheme: {
|
|
797
|
+
components: { history: { dangerForeground: '#fecaca' } },
|
|
798
|
+
},
|
|
799
|
+
} as any)
|
|
800
|
+
);
|
|
801
|
+
expect(explicit['--persona-history-danger-fg']).toBe('#fecaca');
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it('emits the confirm dialog danger pair and forks the fill for dark', () => {
|
|
805
|
+
// Always emitted: the dialog's inline var() must resolve so the fill
|
|
806
|
+
// follows a customized error palette instead of its legacy fallback.
|
|
807
|
+
const light = themeToCssVariables(getActiveTheme({ colorScheme: 'light' }));
|
|
808
|
+
expect(light['--persona-danger']).toBe('#b91c1c'); // error.700
|
|
809
|
+
expect(light['--persona-danger-fg']).toBe('#ffffff');
|
|
810
|
+
expect(light['--persona-history-confirm-scrim']).toBeUndefined();
|
|
811
|
+
|
|
812
|
+
const dark = themeToCssVariables(getActiveTheme({ colorScheme: 'dark' }));
|
|
813
|
+
expect(dark['--persona-danger']).toBe('#dc2626'); // error.600
|
|
814
|
+
// The light scrim default barely dims near-black surfaces.
|
|
815
|
+
expect(dark['--persona-history-confirm-scrim']).toBe('rgba(0, 0, 0, 0.6)');
|
|
816
|
+
|
|
817
|
+
const explicit = themeToCssVariables(
|
|
818
|
+
getActiveTheme({
|
|
819
|
+
colorScheme: 'dark',
|
|
820
|
+
darkTheme: {
|
|
821
|
+
components: {
|
|
822
|
+
history: {
|
|
823
|
+
confirm: {
|
|
824
|
+
dangerBackground: '#7f1d1d',
|
|
825
|
+
dangerForeground: '#fee2e2',
|
|
826
|
+
scrim: 'rgba(0, 0, 0, 0.6)',
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
} as any)
|
|
832
|
+
);
|
|
833
|
+
expect(explicit['--persona-danger']).toBe('#7f1d1d');
|
|
834
|
+
expect(explicit['--persona-danger-fg']).toBe('#fee2e2');
|
|
835
|
+
expect(explicit['--persona-history-confirm-scrim']).toBe('rgba(0, 0, 0, 0.6)');
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it('emits messageActions aliases only when the group is configured', () => {
|
|
839
|
+
// widget.css chains to the ghost wash and semantic text on its own, so an
|
|
840
|
+
// unset group must leave every alias undefined.
|
|
841
|
+
const defaults = themeToCssVariables(createTheme());
|
|
842
|
+
expect(defaults['--persona-message-action-hover-bg']).toBeUndefined();
|
|
843
|
+
expect(defaults['--persona-message-action-hover-fg']).toBeUndefined();
|
|
844
|
+
expect(defaults['--persona-message-action-radius']).toBeUndefined();
|
|
845
|
+
|
|
846
|
+
const themed = themeToCssVariables(
|
|
847
|
+
createTheme({
|
|
848
|
+
components: {
|
|
849
|
+
messageActions: {
|
|
850
|
+
hoverBackground: 'palette.colors.gray.100',
|
|
851
|
+
hoverForeground: 'semantic.colors.text',
|
|
852
|
+
borderRadius: 'palette.radius.full',
|
|
853
|
+
},
|
|
854
|
+
},
|
|
855
|
+
} as any)
|
|
856
|
+
);
|
|
857
|
+
expect(themed['--persona-message-action-hover-bg']).toBe('#f3f4f6'); // gray.100
|
|
858
|
+
expect(themed['--persona-message-action-hover-fg']).toBe('#111827'); // semantic text
|
|
859
|
+
expect(themed['--persona-message-action-radius']).toBe('9999px');
|
|
860
|
+
|
|
861
|
+
// Partial groups emit only what they set.
|
|
862
|
+
const partial = themeToCssVariables(
|
|
863
|
+
createTheme({ components: { messageActions: { borderRadius: '2px' } } } as any)
|
|
864
|
+
);
|
|
865
|
+
expect(partial['--persona-message-action-radius']).toBe('2px');
|
|
866
|
+
expect(partial['--persona-message-action-hover-bg']).toBeUndefined();
|
|
867
|
+
});
|
|
868
|
+
|
|
400
869
|
it('defaults artifact pane fill from semantic container and resolves toolbar background token refs', () => {
|
|
401
870
|
const theme = createTheme();
|
|
402
871
|
const cssVars = themeToCssVariables(theme);
|
|
@@ -420,6 +889,34 @@ describe('theme utils', () => {
|
|
|
420
889
|
expect(surfaceVars['--persona-artifact-toolbar-bg']).toBe('#f9fafb');
|
|
421
890
|
});
|
|
422
891
|
|
|
892
|
+
it('coerces unitless zero toggle-group gap/padding to px (calc-safe)', () => {
|
|
893
|
+
const theme = createTheme({
|
|
894
|
+
components: {
|
|
895
|
+
artifact: {
|
|
896
|
+
toolbar: {
|
|
897
|
+
toggleGroupGap: '0',
|
|
898
|
+
toggleGroupPadding: '0',
|
|
899
|
+
},
|
|
900
|
+
},
|
|
901
|
+
},
|
|
902
|
+
} as any);
|
|
903
|
+
const cssVars = themeToCssVariables(theme);
|
|
904
|
+
// The selection thumb's width calc() mixes these with lengths; a bare
|
|
905
|
+
// "0" is a <number> there and would invalidate the whole expression.
|
|
906
|
+
expect(cssVars['--persona-artifact-toolbar-toggle-group-gap']).toBe('0px');
|
|
907
|
+
expect(cssVars['--persona-artifact-toolbar-toggle-group-padding']).toBe('0px');
|
|
908
|
+
|
|
909
|
+
const unitful = themeToCssVariables(
|
|
910
|
+
createTheme({
|
|
911
|
+
components: {
|
|
912
|
+
artifact: { toolbar: { toggleGroupGap: '4px', toggleGroupPadding: '0.25rem' } },
|
|
913
|
+
},
|
|
914
|
+
} as any)
|
|
915
|
+
);
|
|
916
|
+
expect(unitful['--persona-artifact-toolbar-toggle-group-gap']).toBe('4px');
|
|
917
|
+
expect(unitful['--persona-artifact-toolbar-toggle-group-padding']).toBe('0.25rem');
|
|
918
|
+
});
|
|
919
|
+
|
|
423
920
|
it('maps artifact inline chrome tokens to dedicated CSS variables (with semantic refs)', () => {
|
|
424
921
|
const theme = createTheme({
|
|
425
922
|
components: {
|
package/src/utils/theme.ts
CHANGED
|
@@ -106,6 +106,36 @@ const DARK_PALETTE = {
|
|
|
106
106
|
},
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Component defaults that only hold in a dark color scheme. Layered UNDER the
|
|
111
|
+
* host's theme, so any explicit value still wins.
|
|
112
|
+
*
|
|
113
|
+
* The ghost hover wash must invert with the scheme: 5% black is invisible on a
|
|
114
|
+
* near-black surface, so every dark scheme gets a light alpha instead.
|
|
115
|
+
*/
|
|
116
|
+
const DARK_COMPONENTS: DeepPartial<PersonaTheme> = {
|
|
117
|
+
components: {
|
|
118
|
+
button: {
|
|
119
|
+
ghost: {
|
|
120
|
+
hoverBackground: 'rgba(255, 255, 255, 0.08)',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
history: {
|
|
124
|
+
// error-400: the light-surface error-600 red lands ~3:1 on dark
|
|
125
|
+
// surfaces, under the 4.5:1 AA floor for the 14px destructive labels.
|
|
126
|
+
dangerForeground: '#f87171',
|
|
127
|
+
confirm: {
|
|
128
|
+
// error-600: bright enough to read as a control against a dark
|
|
129
|
+
// surface (3:1 non-text) while the white label keeps 4.8:1.
|
|
130
|
+
dangerBackground: 'palette.colors.error.600',
|
|
131
|
+
// 45% slate darkens near-black surfaces by almost nothing; dark
|
|
132
|
+
// schemes need a heavier dimmer for the modal to read as modal.
|
|
133
|
+
scrim: 'rgba(0, 0, 0, 0.6)',
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
109
139
|
/**
|
|
110
140
|
* Normalize theme config for merging; rejects non-objects.
|
|
111
141
|
*/
|
|
@@ -151,14 +181,23 @@ export const createDarkTheme = (userConfig?: DeepPartial<PersonaTheme>): Persona
|
|
|
151
181
|
// layered UNDER the user's colors here. Spreading a pre-built default
|
|
152
182
|
// palette instead would clobber the user's non-color palette overrides
|
|
153
183
|
// (radius/typography) in dark mode while light mode honored them.
|
|
184
|
+
//
|
|
185
|
+
// DARK_COMPONENTS goes underneath by deep merge, not spread: a shallow
|
|
186
|
+
// spread of `components` would drop the dark ghost hover the moment a host
|
|
187
|
+
// set any unrelated component token.
|
|
188
|
+
const config = (deepMerge(
|
|
189
|
+
DARK_COMPONENTS as Record<string, unknown>,
|
|
190
|
+
(userConfig ?? {}) as Record<string, unknown>
|
|
191
|
+
) ?? {}) as DeepPartial<PersonaTheme>;
|
|
192
|
+
|
|
154
193
|
return createTheme(
|
|
155
194
|
{
|
|
156
|
-
...
|
|
195
|
+
...config,
|
|
157
196
|
palette: {
|
|
158
|
-
...
|
|
197
|
+
...config.palette,
|
|
159
198
|
colors: {
|
|
160
199
|
...DARK_PALETTE.colors,
|
|
161
|
-
...
|
|
200
|
+
...config.palette?.colors,
|
|
162
201
|
},
|
|
163
202
|
},
|
|
164
203
|
},
|