@runtypelabs/persona 4.23.2 → 4.25.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/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CI7OckWI.d.cts → types-Db61aI9J.d.cts} +2 -0
- package/dist/animations/{types-CI7OckWI.d.ts → types-Db61aI9J.d.ts} +2 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +2 -0
- package/dist/animations-extra.d.ts +2 -0
- package/dist/approval-ui-3NMYNXBD.js +1 -0
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +278 -37
- package/dist/approval-ui.d.ts +278 -37
- package/dist/approval-ui.js +2 -2
- package/dist/{artifacts-ui-F4N6VMKF.js → artifacts-ui-IGB5YKLO.js} +4 -4
- package/dist/artifacts-ui.d.cts +273 -25
- package/dist/artifacts-ui.d.ts +273 -25
- package/dist/{chunk-NIKYKSNZ.js → chunk-7CO4T5KB.js} +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +13 -13
- package/dist/context-mentions.d.cts +273 -25
- package/dist/context-mentions.d.ts +273 -25
- package/dist/event-stream-view.d.cts +273 -25
- package/dist/event-stream-view.d.ts +273 -25
- package/dist/forms-ui.d.cts +319 -27
- package/dist/forms-ui.d.ts +319 -27
- package/dist/history-view.d.cts +2 -0
- package/dist/history-view.d.ts +2 -0
- package/dist/index.cjs +70 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +344 -39
- package/dist/index.d.ts +344 -39
- package/dist/index.global.js +57 -54
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +70 -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/session-reconnect.d.cts +273 -25
- package/dist/session-reconnect.d.ts +273 -25
- package/dist/smart-dom-reader.d.cts +273 -25
- package/dist/smart-dom-reader.d.ts +273 -25
- package/dist/theme-editor-preview.cjs +74 -73
- package/dist/theme-editor-preview.d.cts +280 -25
- package/dist/theme-editor-preview.d.ts +280 -25
- package/dist/theme-editor-preview.js +51 -50
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +293 -27
- package/dist/theme-editor.d.ts +293 -27
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1522 -11
- package/dist/theme-reference.d.ts +1522 -11
- package/dist/theme-reference.js +1 -1
- package/dist/voice-runtime-7DA7MJH4.js +1 -0
- package/dist/voice-runtime.cjs +1 -1
- package/dist/voice-runtime.d.cts +33 -0
- package/dist/voice-runtime.d.ts +33 -0
- package/dist/voice-runtime.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +3 -2
- package/src/__fixtures__/defaults-parity.v4.json +776 -0
- package/src/approval-ui-entry.ts +0 -1
- package/src/approval-ui-loader.ts +5 -4
- package/src/approval-ui.ts +0 -1
- package/src/client-identity-proof.test.ts +193 -0
- package/src/client-session-warm.test.ts +216 -0
- package/src/client-visitor-history.test.ts +6 -0
- package/src/client.ts +122 -30
- package/src/codegen.test.ts +24 -0
- package/src/components/activity-row.test.ts +100 -0
- package/src/components/activity-row.ts +136 -0
- package/src/components/approval-actions.test.ts +8 -5
- package/src/components/approval-actions.ts +19 -6
- package/src/components/approval-bubble.test.ts +56 -9
- package/src/components/approval-bubble.ts +36 -41
- package/src/components/composer-builder.ts +5 -2
- package/src/components/composer-parts.ts +10 -2
- package/src/components/header-builder.test.ts +32 -0
- package/src/components/header-builder.ts +7 -4
- package/src/components/header-layouts.test.ts +12 -0
- package/src/components/header-layouts.ts +3 -2
- package/src/components/launcher.ts +11 -2
- package/src/components/message-bubble.test.ts +22 -0
- package/src/components/message-bubble.ts +7 -3
- package/src/components/panel.test.ts +52 -1
- package/src/components/panel.ts +47 -8
- package/src/components/pill-composer-builder.test.ts +2 -0
- package/src/components/pill-composer-builder.ts +0 -1
- package/src/components/reasoning-bubble.ts +29 -17
- package/src/components/suggestions.test.ts +1 -0
- package/src/components/suggestions.ts +0 -16
- package/src/components/tool-bubble.ts +131 -114
- package/src/components/tool-details.test.ts +87 -0
- package/src/components/tool-details.ts +102 -0
- package/src/components/welcome.test.ts +8 -0
- package/src/components/welcome.ts +4 -2
- package/src/defaults-parity.test.ts +158 -0
- package/src/defaults.test.ts +69 -0
- package/src/defaults.ts +128 -49
- package/src/generated/runtype-openapi-contract.ts +2 -2
- package/src/index-core.ts +4 -0
- package/src/install.test.ts +24 -0
- package/src/install.ts +3 -1
- package/src/launcher-global.ts +2 -1
- package/src/launcher-variants.test.ts +52 -0
- package/src/runtime/init-update-reset.test.ts +21 -0
- package/src/session.ts +59 -7
- package/src/session.voice.test.ts +72 -1
- package/src/styles/widget-styles.test.ts +19 -0
- package/src/styles/widget.css +295 -23
- package/src/theme-editor/index.ts +2 -0
- package/src/theme-editor/preview-utils.test.ts +8 -0
- package/src/theme-editor/preview-utils.ts +7 -3
- package/src/theme-editor/sections.test.ts +31 -0
- package/src/theme-editor/sections.ts +55 -1
- package/src/theme-editor/state-preferences.test.ts +49 -0
- package/src/theme-editor/state.ts +73 -15
- package/src/theme-reference.test.ts +20 -0
- package/src/theme-reference.ts +11 -5
- package/src/tool-call-display-defaults.test.ts +33 -1
- package/src/types/theme.ts +80 -3
- package/src/types.ts +207 -22
- package/src/ui.activity-row.test.ts +91 -0
- package/src/ui.approval-plugin.test.ts +63 -0
- package/src/ui.composer-compact.test.ts +24 -2
- package/src/ui.composer-placement.test.ts +23 -0
- package/src/ui.detached-panel.test.ts +33 -6
- package/src/ui.docked.test.ts +19 -0
- package/src/ui.header-update-stability.test.ts +25 -0
- package/src/ui.identity-proof.test.ts +58 -0
- package/src/ui.launcher-teaser.test.ts +25 -0
- package/src/ui.message-width.test.ts +17 -2
- package/src/ui.reasoning-display.test.ts +92 -0
- package/src/ui.rebuild-composer.test.ts +20 -3
- package/src/ui.scroll.test.ts +32 -0
- package/src/ui.session-init.test.ts +312 -0
- package/src/ui.status-indicator.test.ts +82 -0
- package/src/ui.suggest-replies.test.ts +11 -0
- package/src/ui.ts +422 -74
- package/src/ui.v5-defaults.test.ts +144 -0
- package/src/ui.voice-prewarm.test.ts +222 -0
- package/src/ui.welcome.test.ts +92 -3
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +811 -140
- package/src/utils/code-generators.ts +14 -0
- package/src/utils/composer-compact.test.ts +0 -2
- package/src/utils/composer-compact.ts +2 -4
- package/src/utils/composer-placement.ts +2 -3
- package/src/utils/config-merge.test.ts +81 -5
- package/src/utils/config-merge.ts +13 -3
- package/src/utils/defaults-provenance.ts +67 -0
- package/src/utils/dock.ts +7 -1
- package/src/utils/launcher-variant.ts +16 -0
- package/src/utils/panel-config.ts +97 -0
- package/src/utils/status-indicator.ts +39 -0
- package/src/utils/theme.test.ts +182 -0
- package/src/utils/theme.ts +131 -18
- package/src/utils/tokens.test.ts +122 -1
- package/src/utils/tokens.ts +177 -17
- package/src/voice/runtype-voice-provider.ts +238 -27
- package/src/voice/voice.test.ts +272 -2
- package/src/welcome.test.ts +23 -0
- package/src/welcome.ts +16 -9
- package/dist/approval-ui-K7R5Q5OC.js +0 -1
- package/dist/voice-runtime-EFR3R5ES.js +0 -1
package/src/utils/theme.test.ts
CHANGED
|
@@ -1249,6 +1249,24 @@ describe('theme utils', () => {
|
|
|
1249
1249
|
expect(el.getAttribute('data-persona-color-scheme')).toBe('dark');
|
|
1250
1250
|
});
|
|
1251
1251
|
|
|
1252
|
+
it.each([{ v5Defaults: false }, { v5Defaults: true }])(
|
|
1253
|
+
'threads future.v5Defaults=%s through active theme resolution and root state',
|
|
1254
|
+
(future) => {
|
|
1255
|
+
const config = {
|
|
1256
|
+
colorScheme: 'dark' as const,
|
|
1257
|
+
future,
|
|
1258
|
+
theme: { palette: { colors: { gray: { 500: '#123456' } } } },
|
|
1259
|
+
};
|
|
1260
|
+
expect(getActiveTheme(config).palette.colors.gray[500]).toBe('#123456');
|
|
1261
|
+
|
|
1262
|
+
const el = document.createElement('div');
|
|
1263
|
+
applyThemeVariables(el, config);
|
|
1264
|
+
expect(el.getAttribute('data-persona-defaults')).toBe(
|
|
1265
|
+
future.v5Defaults ? 'v5' : 'v4'
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
);
|
|
1269
|
+
|
|
1252
1270
|
it('maps components.code.background to --persona-code-bg', () => {
|
|
1253
1271
|
const theme = createTheme({
|
|
1254
1272
|
components: { code: { background: '#fafafa' } },
|
|
@@ -1309,3 +1327,167 @@ describe('theme utils', () => {
|
|
|
1309
1327
|
expect(explicit['--persona-md-code-block-border-radius']).toBe('2px');
|
|
1310
1328
|
});
|
|
1311
1329
|
});
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
describe.each([false, true])('versioned panel mode widths (v5=%s)', (v5Defaults) => {
|
|
1333
|
+
it.each(['docked', 'sidebar'] as const)('uses version defaults and explicit widths in %s', (mode) => {
|
|
1334
|
+
const config = { apiUrl: '/api', future: { v5Defaults }, launcher: mode === 'docked' ? { mountMode: 'docked' as const } : { sidebarMode: true } };
|
|
1335
|
+
expect(getActiveTheme(config).components.panel.width).toBe(v5Defaults ? '400px' : '420px');
|
|
1336
|
+
expect(getActiveTheme({ ...config, theme: { components: { panel: { width: '510px' } } } }).components.panel.width).toBe('510px');
|
|
1337
|
+
expect(getActiveTheme({ ...config, colorScheme: 'dark', darkTheme: { components: { panel: { width: '530px' } } } }).components.panel.width).toBe('530px');
|
|
1338
|
+
});
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
describe.each([false, true])('versioned dark palette (v5=%s)', (v5Defaults) => {
|
|
1342
|
+
const future = { v5Defaults };
|
|
1343
|
+
const variables = (colorScheme: 'light' | 'dark' | 'auto') =>
|
|
1344
|
+
themeToCssVariables(getActiveTheme({ future, colorScheme }));
|
|
1345
|
+
|
|
1346
|
+
afterEach(() => document.documentElement.classList.remove('dark'));
|
|
1347
|
+
|
|
1348
|
+
it('changes neutral and markdown defaults only in V5', () => {
|
|
1349
|
+
const light = variables('light');
|
|
1350
|
+
const dark = variables('dark');
|
|
1351
|
+
for (const token of [
|
|
1352
|
+
'surface', 'background', 'text', 'border', 'md-inline-code-bg', 'md-inline-code-color',
|
|
1353
|
+
'md-code-block-bg', 'md-table-header-bg', 'md-blockquote-bg', 'md-blockquote-text-color',
|
|
1354
|
+
]) {
|
|
1355
|
+
expect(dark[`--persona-${token}`], token).toBeDefined();
|
|
1356
|
+
if (v5Defaults) expect(dark[`--persona-${token}`], token).not.toBe(light[`--persona-${token}`]);
|
|
1357
|
+
else expect(dark[`--persona-${token}`], token).toBe(light[`--persona-${token}`]);
|
|
1358
|
+
}
|
|
1359
|
+
if (v5Defaults) {
|
|
1360
|
+
expect(dark['--persona-surface']).toBe('#1a1a1b');
|
|
1361
|
+
expect(dark['--persona-background']).toBe('#0f0f10');
|
|
1362
|
+
expect(dark['--persona-text']).toBe('#f4f4f5');
|
|
1363
|
+
expect(dark['--persona-border']).toBe('#2a2a2d');
|
|
1364
|
+
expect(dark['--persona-message-user-bg']).toBe('#27272a');
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
it('resolves auto mode through the same versioned dark defaults', () => {
|
|
1369
|
+
expect(variables('auto')).toEqual(variables('light'));
|
|
1370
|
+
document.documentElement.classList.add('dark');
|
|
1371
|
+
expect(variables('auto')).toEqual(variables('dark'));
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
it.each(['light', 'dark', 'auto'] as const)('honors explicit palette, semantic and component tokens in %s', (colorScheme) => {
|
|
1375
|
+
document.documentElement.classList.add('dark');
|
|
1376
|
+
const css = themeToCssVariables(getActiveTheme({
|
|
1377
|
+
future, colorScheme,
|
|
1378
|
+
theme: {
|
|
1379
|
+
palette: { colors: { primary: { 500: '#123456' }, gray: { 500: '#abcdef' } } },
|
|
1380
|
+
semantic: { colors: { surface: '#223344' } },
|
|
1381
|
+
components: { markdown: { inlineCode: { background: '#334455' } } },
|
|
1382
|
+
},
|
|
1383
|
+
darkTheme: { components: { markdown: { table: { headerBackground: '#445566' } } } },
|
|
1384
|
+
}));
|
|
1385
|
+
expect(css['--persona-button-primary-bg']).toBe('#123456');
|
|
1386
|
+
expect(css['--persona-text-muted']).toBe('#abcdef');
|
|
1387
|
+
expect(css['--persona-surface']).toBe('#223344');
|
|
1388
|
+
expect(css['--persona-md-inline-code-bg']).toBe('#334455');
|
|
1389
|
+
if (colorScheme !== 'light') {
|
|
1390
|
+
expect(css['--persona-md-table-header-bg']).toBe('#445566');
|
|
1391
|
+
// A single custom gray stop must not reset the rest of V5's dark ramp.
|
|
1392
|
+
if (v5Defaults) expect(css['--persona-text']).toBe('#f4f4f5');
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
it('keeps V5 dark body and markdown text above AA contrast', () => {
|
|
1398
|
+
const css = themeToCssVariables(getActiveTheme({ colorScheme: 'dark', future: { v5Defaults: true } }));
|
|
1399
|
+
const luminance = (hex: string) => {
|
|
1400
|
+
const channels = hex.slice(1).match(/.{2}/g)!.map((channel) => {
|
|
1401
|
+
const value = parseInt(channel, 16) / 255;
|
|
1402
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
1403
|
+
});
|
|
1404
|
+
return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
|
|
1405
|
+
};
|
|
1406
|
+
for (const [foreground, background] of [
|
|
1407
|
+
['text', 'surface'], ['text', 'background'], ['text', 'container'],
|
|
1408
|
+
['text-muted', 'surface'], ['text-muted', 'container'],
|
|
1409
|
+
['text', 'message-user-bg'], ['md-inline-code-color', 'md-inline-code-bg'],
|
|
1410
|
+
['md-code-block-text-color', 'md-code-block-bg'], ['text', 'md-table-header-bg'],
|
|
1411
|
+
['md-blockquote-text-color', 'md-blockquote-bg'], ['md-link-color', 'surface'],
|
|
1412
|
+
['button-primary-fg', 'button-primary-bg'],
|
|
1413
|
+
]) {
|
|
1414
|
+
const values = [foreground, background].map((token) => luminance(css[`--persona-${token}`]));
|
|
1415
|
+
expect((Math.max(...values) + 0.05) / (Math.min(...values) + 0.05), `${foreground} on ${background}`).toBeGreaterThanOrEqual(4.5);
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
describe.each([false, true])('welcome typography defaults (V5: %s)', (v5Defaults) => {
|
|
1420
|
+
it('resolves lighter V5 typography and preserves V4 fallbacks', () => {
|
|
1421
|
+
const css = themeToCssVariables(getActiveTheme({ future: { v5Defaults } }));
|
|
1422
|
+
expect(css['--persona-components-introCard-title-fontSize']).toBe(v5Defaults ? '22px' : undefined);
|
|
1423
|
+
expect(css['--persona-components-introCard-title-fontWeight']).toBe(v5Defaults ? '500' : undefined);
|
|
1424
|
+
expect(css['--persona-components-header-title-fontWeight']).toBe(v5Defaults ? '500' : undefined);
|
|
1425
|
+
for (const variant of ['chip', 'card', 'list']) {
|
|
1426
|
+
expect(css[`--persona-components-suggestion-${variant}-fontWeight`]).toBe(v5Defaults ? '400' : undefined);
|
|
1427
|
+
}
|
|
1428
|
+
if (v5Defaults) expect(css['--persona-input-radius']).toBe('9999px');
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
it('honors explicit typography and composer corners', () => {
|
|
1432
|
+
const css = themeToCssVariables(getActiveTheme({ future: { v5Defaults }, theme: { components: {
|
|
1433
|
+
introCard: { title: { fontWeight: '400', fontSize: '30px' } },
|
|
1434
|
+
header: { title: { fontWeight: '600' } },
|
|
1435
|
+
suggestion: { card: { fontWeight: '600' } },
|
|
1436
|
+
input: { borderRadius: '20px' },
|
|
1437
|
+
} } }));
|
|
1438
|
+
expect(css['--persona-components-introCard-title-fontWeight']).toBe('400');
|
|
1439
|
+
expect(css['--persona-components-introCard-title-fontSize']).toBe('30px');
|
|
1440
|
+
expect(css['--persona-components-header-title-fontWeight']).toBe('600');
|
|
1441
|
+
expect(css['--persona-components-suggestion-card-fontWeight']).toBe('600');
|
|
1442
|
+
expect(css['--persona-input-radius']).toBe('20px');
|
|
1443
|
+
});
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
describe.each([false, true])('activity aliases (V5 %s)', v5Defaults => {
|
|
1447
|
+
it('emits versioned spacing and preserves independent spacing overrides', () => {
|
|
1448
|
+
const defaults = themeToCssVariables(getActiveTheme({ future: { v5Defaults } }));
|
|
1449
|
+
const expected = { indent: '0px', groupIndent: '0px', groupGap: '0px', groupPadding: '0px', transcriptGap: '0px', responseGap: '12px', bodyPadding: '4px 0', bodySize: '13px', bodyLineHeight: '1.5', bodyFontWeight: '400', sectionGap: '8px' };
|
|
1450
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
1451
|
+
expect(defaults[`--persona-components-activity-${key}`]).toBe(v5Defaults ? value : undefined);
|
|
1452
|
+
}
|
|
1453
|
+
const activity = { indent: '12px', groupIndent: '6px', groupGap: '3px', groupPadding: '2px 0', transcriptGap: '6px', responseGap: '16px', bodyPadding: '10px 0', bodySize: '15px', bodyLineHeight: '1.7', bodyFontWeight: '500', sectionGap: '14px' };
|
|
1454
|
+
const overridden = themeToCssVariables(getActiveTheme({ future: { v5Defaults }, theme: { components: { activity } } }));
|
|
1455
|
+
for (const [key, value] of Object.entries(activity)) {
|
|
1456
|
+
expect(overridden[`--persona-components-activity-${key}`]).toBe(value);
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
it('keeps legacy values usable while preferring explicit activity tokens', () => {
|
|
1460
|
+
const vars = (activity?: { labelSize?: string; bodySurface?: string }) => themeToCssVariables(getActiveTheme({ future: { v5Defaults }, theme: { components: {
|
|
1461
|
+
toolBubble: { labelSize: '17px', shadow: 'none' }, collapsibleWidget: { surface: '#123456' }, activity,
|
|
1462
|
+
} } }));
|
|
1463
|
+
expect(vars()['--persona-components-toolBubble-labelSize']).toBe('17px');
|
|
1464
|
+
expect(vars()['--persona-components-activity-bodySurface']).toBe('#123456');
|
|
1465
|
+
expect(vars({ labelSize: '15px', bodySurface: '#abcdef' })['--persona-components-toolBubble-labelSize']).toBe('15px');
|
|
1466
|
+
expect(vars({ bodySurface: '#abcdef' })['--persona-components-activity-bodySurface']).toBe('#abcdef');
|
|
1467
|
+
});
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
describe.each(['light', 'dark'] as const)('V5 header surface (%s)', colorScheme => {
|
|
1471
|
+
it('matches the transcript container while preserving explicit header colors', () => {
|
|
1472
|
+
const config = { colorScheme, future: { v5Defaults: true } };
|
|
1473
|
+
const css = themeToCssVariables(getActiveTheme(config));
|
|
1474
|
+
expect(css['--persona-header-bg']).toBe(css['--persona-container']);
|
|
1475
|
+
const custom = themeToCssVariables(getActiveTheme({ ...config, theme: { components: { header: { background: '#123456' } } } }));
|
|
1476
|
+
expect(custom['--persona-header-bg']).toBe('#123456');
|
|
1477
|
+
});
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
describe('V5 header control surfaces', () => {
|
|
1481
|
+
it('keeps V4 tokens unset and preserves explicit V5 overrides', () => {
|
|
1482
|
+
const legacy = themeToCssVariables(createTheme());
|
|
1483
|
+
const v5 = themeToCssVariables(createTheme(undefined, { future: { v5Defaults: true } }));
|
|
1484
|
+
expect(legacy['--persona-components-header-controlBorderRadius']).toBeUndefined();
|
|
1485
|
+
expect(v5['--persona-components-header-controlBorderRadius']).toBe('8px');
|
|
1486
|
+
expect(v5['--persona-components-header-controlHoverBackground']).toContain('8%');
|
|
1487
|
+
const custom = themeToCssVariables(createTheme({
|
|
1488
|
+
components: { header: { controlBorderRadius: '4px', controlHoverBackground: 'red' } },
|
|
1489
|
+
}, { future: { v5Defaults: true } }));
|
|
1490
|
+
expect(custom['--persona-components-header-controlBorderRadius']).toBe('4px');
|
|
1491
|
+
expect(custom['--persona-components-header-controlHoverBackground']).toBe('red');
|
|
1492
|
+
});
|
|
1493
|
+
});
|
package/src/utils/theme.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { CIRCLE_LAUNCHER_TOKENS, resolveLauncherVariant } from "./launcher-variant";
|
|
2
|
+
import type { CreateThemeOptions, DeepPartial, PersonaTheme } from '../types/theme';
|
|
2
3
|
import type { AgentWidgetConfig } from '../types';
|
|
3
|
-
import {
|
|
4
|
+
import { resolveDefaultsVersion } from '../defaults';
|
|
5
|
+
import { createTheme, resolveTokens, themeToCssVariables, DEFAULT_COMPONENTS_V4 } from './tokens';
|
|
4
6
|
import { deepMerge } from './deep-merge';
|
|
5
7
|
|
|
6
8
|
export type ColorScheme = 'light' | 'dark' | 'auto';
|
|
@@ -9,11 +11,32 @@ export interface PersonaWidgetConfig {
|
|
|
9
11
|
theme?: DeepPartial<PersonaTheme>;
|
|
10
12
|
darkTheme?: DeepPartial<PersonaTheme>;
|
|
11
13
|
colorScheme?: ColorScheme;
|
|
14
|
+
future?: { v5Defaults?: boolean };
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
type WidgetConfig = PersonaWidgetConfig | AgentWidgetConfig;
|
|
15
18
|
|
|
16
|
-
const
|
|
19
|
+
const isPanelWidthMode = (config?: WidgetConfig): boolean =>
|
|
20
|
+
!!config &&
|
|
21
|
+
"launcher" in config &&
|
|
22
|
+
(config.launcher?.sidebarMode === true || config.launcher?.mountMode === "docked");
|
|
23
|
+
|
|
24
|
+
const applyPanelModeWidth = (theme: PersonaTheme, config?: WidgetConfig): PersonaTheme => {
|
|
25
|
+
if (!config || !isPanelWidthMode(config)) return theme;
|
|
26
|
+
const lightWidth = config.theme?.components?.panel?.width;
|
|
27
|
+
const darkWidth = config.darkTheme?.components?.panel?.width;
|
|
28
|
+
const configuredWidth = getColorScheme(config) === "dark" ? darkWidth ?? lightWidth : lightWidth;
|
|
29
|
+
if (configuredWidth != null) return theme;
|
|
30
|
+
return {
|
|
31
|
+
...theme,
|
|
32
|
+
components: {
|
|
33
|
+
...theme.components,
|
|
34
|
+
panel: { ...theme.components.panel, width: resolveDefaultsVersion(config) === "v5" ? "400px" : "420px" },
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const DARK_PALETTE_BASE = {
|
|
17
40
|
colors: {
|
|
18
41
|
primary: {
|
|
19
42
|
50: '#ffffff',
|
|
@@ -113,7 +136,7 @@ const DARK_PALETTE = {
|
|
|
113
136
|
* The ghost hover wash must invert with the scheme: 5% black is invisible on a
|
|
114
137
|
* near-black surface, so every dark scheme gets a light alpha instead.
|
|
115
138
|
*/
|
|
116
|
-
const
|
|
139
|
+
const DARK_COMPONENTS_BASE: DeepPartial<PersonaTheme> = {
|
|
117
140
|
components: {
|
|
118
141
|
button: {
|
|
119
142
|
ghost: {
|
|
@@ -160,6 +183,79 @@ const DARK_COMPONENTS: DeepPartial<PersonaTheme> = {
|
|
|
160
183
|
},
|
|
161
184
|
};
|
|
162
185
|
|
|
186
|
+
// V4 stays frozen; V5 provides the dark neutral ramp and semantic roles.
|
|
187
|
+
export const DARK_PALETTE_V4 = DARK_PALETTE_BASE;
|
|
188
|
+
export const DARK_PALETTE_V5 = {
|
|
189
|
+
...DARK_PALETTE_V4,
|
|
190
|
+
colors: {
|
|
191
|
+
...DARK_PALETTE_V4.colors,
|
|
192
|
+
primary: {
|
|
193
|
+
50: '#0f0f10', 100: '#27272a', 200: '#3f3f46', 300: '#52525b',
|
|
194
|
+
400: '#71717a', 500: '#f4f4f5', 600: '#e4e4e7', 700: '#d4d4d8',
|
|
195
|
+
800: '#a1a1aa', 900: '#fafafa', 950: '#ffffff',
|
|
196
|
+
},
|
|
197
|
+
gray: {
|
|
198
|
+
50: '#1a1a1b', 100: '#27272a', 200: '#2a2a2d', 300: '#52525b',
|
|
199
|
+
400: '#71717a', 500: '#a1a1aa', 600: '#b4b4bd', 700: '#d4d4d8',
|
|
200
|
+
800: '#e4e4e7', 900: '#f4f4f5', 950: '#fafafa',
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
export const DARK_COMPONENTS_V4 = DARK_COMPONENTS_BASE;
|
|
205
|
+
export const DARK_COMPONENTS_V5: DeepPartial<PersonaTheme> = {
|
|
206
|
+
semantic: {
|
|
207
|
+
colors: {
|
|
208
|
+
primary: 'palette.colors.primary.500',
|
|
209
|
+
accent: 'palette.colors.primary.600',
|
|
210
|
+
background: '#0f0f10',
|
|
211
|
+
surface: 'palette.colors.gray.50',
|
|
212
|
+
container: '#1c1c20',
|
|
213
|
+
text: 'palette.colors.gray.900',
|
|
214
|
+
textMuted: 'palette.colors.gray.500',
|
|
215
|
+
textInverse: '#0f0f10',
|
|
216
|
+
border: 'palette.colors.gray.200',
|
|
217
|
+
divider: 'palette.colors.gray.200',
|
|
218
|
+
interactive: {
|
|
219
|
+
default: 'palette.colors.primary.600', hover: 'palette.colors.primary.700',
|
|
220
|
+
focus: 'palette.colors.primary.600', active: 'palette.colors.primary.600',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
components: {
|
|
225
|
+
...DARK_COMPONENTS_BASE.components,
|
|
226
|
+
button: {
|
|
227
|
+
...DARK_COMPONENTS_BASE.components?.button,
|
|
228
|
+
primary: { background: 'semantic.colors.primary', foreground: 'palette.colors.primary.50' },
|
|
229
|
+
},
|
|
230
|
+
markdown: {
|
|
231
|
+
inlineCode: { background: 'palette.colors.gray.100', foreground: 'semantic.colors.text' },
|
|
232
|
+
link: { foreground: 'semantic.colors.accent' },
|
|
233
|
+
codeBlock: {
|
|
234
|
+
background: 'semantic.colors.container', borderColor: 'semantic.colors.border',
|
|
235
|
+
textColor: 'semantic.colors.text',
|
|
236
|
+
},
|
|
237
|
+
table: { headerBackground: 'palette.colors.gray.100', borderColor: 'semantic.colors.border' },
|
|
238
|
+
blockquote: {
|
|
239
|
+
background: 'semantic.colors.container', borderColor: 'palette.colors.gray.300',
|
|
240
|
+
textColor: 'semantic.colors.textMuted',
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
eventStream: {
|
|
244
|
+
...DARK_COMPONENTS_BASE.components?.eventStream,
|
|
245
|
+
badge: {
|
|
246
|
+
...DARK_COMPONENTS_BASE.components?.eventStream?.badge,
|
|
247
|
+
step: { background: 'palette.colors.gray.100', foreground: 'palette.colors.gray.700' },
|
|
248
|
+
default: { background: 'palette.colors.gray.100', foreground: 'palette.colors.gray.700' },
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const resolveDarkDefaults = (future?: { v5Defaults?: boolean }) =>
|
|
255
|
+
resolveDefaultsVersion({ future }) === 'v5'
|
|
256
|
+
? { palette: DARK_PALETTE_V5, components: DARK_COMPONENTS_V5 }
|
|
257
|
+
: { palette: DARK_PALETTE_V4, components: DARK_COMPONENTS_V4 };
|
|
258
|
+
|
|
163
259
|
/**
|
|
164
260
|
* Normalize theme config for merging; rejects non-objects.
|
|
165
261
|
*/
|
|
@@ -195,11 +291,17 @@ export const getColorScheme = (config?: WidgetConfig): 'light' | 'dark' => {
|
|
|
195
291
|
return getColorSchemeFromConfig(config);
|
|
196
292
|
};
|
|
197
293
|
|
|
198
|
-
export const createLightTheme = (
|
|
199
|
-
|
|
294
|
+
export const createLightTheme = (
|
|
295
|
+
userConfig?: DeepPartial<PersonaTheme>,
|
|
296
|
+
options: Pick<CreateThemeOptions, 'future'> = {}
|
|
297
|
+
): PersonaTheme => {
|
|
298
|
+
return createTheme(userConfig, options);
|
|
200
299
|
};
|
|
201
300
|
|
|
202
|
-
export const createDarkTheme = (
|
|
301
|
+
export const createDarkTheme = (
|
|
302
|
+
userConfig?: DeepPartial<PersonaTheme>,
|
|
303
|
+
options: Pick<CreateThemeOptions, 'future'> = {}
|
|
304
|
+
): PersonaTheme => {
|
|
203
305
|
// createTheme() already merges every palette sub-object (radius, typography,
|
|
204
306
|
// shadows, …) over the defaults, so only the dark color scales need to be
|
|
205
307
|
// layered UNDER the user's colors here. Spreading a pre-built default
|
|
@@ -209,8 +311,9 @@ export const createDarkTheme = (userConfig?: DeepPartial<PersonaTheme>): Persona
|
|
|
209
311
|
// DARK_COMPONENTS goes underneath by deep merge, not spread: a shallow
|
|
210
312
|
// spread of `components` would drop the dark ghost hover the moment a host
|
|
211
313
|
// set any unrelated component token.
|
|
314
|
+
const darkDefaults = resolveDarkDefaults(options.future);
|
|
212
315
|
const config = (deepMerge(
|
|
213
|
-
|
|
316
|
+
darkDefaults.components as Record<string, unknown>,
|
|
214
317
|
(userConfig ?? {}) as Record<string, unknown>
|
|
215
318
|
) ?? {}) as DeepPartial<PersonaTheme>;
|
|
216
319
|
|
|
@@ -219,31 +322,40 @@ export const createDarkTheme = (userConfig?: DeepPartial<PersonaTheme>): Persona
|
|
|
219
322
|
...config,
|
|
220
323
|
palette: {
|
|
221
324
|
...config.palette,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
325
|
+
// Preserve every dark stop when a host overrides just one shade.
|
|
326
|
+
// Keep V4's historical scale replacement behavior unchanged.
|
|
327
|
+
colors: resolveDefaultsVersion(options) === 'v5'
|
|
328
|
+
? deepMerge(darkDefaults.palette.colors, config.palette?.colors) as PersonaTheme['palette']['colors']
|
|
329
|
+
: { ...darkDefaults.palette.colors, ...config.palette?.colors },
|
|
226
330
|
},
|
|
227
331
|
},
|
|
228
|
-
{ validate: false }
|
|
332
|
+
{ validate: false, future: options.future }
|
|
229
333
|
);
|
|
230
334
|
};
|
|
231
335
|
|
|
232
336
|
export const getActiveTheme = (config?: WidgetConfig): PersonaTheme => {
|
|
233
337
|
const scheme = getColorScheme(config);
|
|
234
|
-
const
|
|
338
|
+
const explicitTheme = normalizeThemeConfig(config?.theme);
|
|
339
|
+
// Variant defaults sit under explicit theme tokens in either defaults state.
|
|
340
|
+
const launcherDefaults = resolveLauncherVariant(config) === 'circle'
|
|
341
|
+
? CIRCLE_LAUNCHER_TOKENS
|
|
342
|
+
: { ...DEFAULT_COMPONENTS_V4.launcher, ...(config?.future?.v5Defaults ? { offset: '1.5rem' } : {}) };
|
|
343
|
+
const lightThemeConfig = deepMerge(
|
|
344
|
+
{ components: { launcher: launcherDefaults } }, explicitTheme
|
|
345
|
+
) as DeepPartial<PersonaTheme>;
|
|
235
346
|
const darkThemeConfig = normalizeThemeConfig(config?.darkTheme);
|
|
236
347
|
|
|
237
348
|
if (scheme === 'dark') {
|
|
238
|
-
return createDarkTheme(
|
|
349
|
+
return applyPanelModeWidth(createDarkTheme(
|
|
239
350
|
deepMerge(
|
|
240
351
|
(lightThemeConfig ?? {}) as Record<string, unknown>,
|
|
241
352
|
(darkThemeConfig ?? {}) as Record<string, unknown>
|
|
242
|
-
) as DeepPartial<PersonaTheme
|
|
243
|
-
|
|
353
|
+
) as DeepPartial<PersonaTheme>,
|
|
354
|
+
{ future: config?.future }
|
|
355
|
+
), config);
|
|
244
356
|
}
|
|
245
357
|
|
|
246
|
-
return createLightTheme(lightThemeConfig);
|
|
358
|
+
return applyPanelModeWidth(createLightTheme(lightThemeConfig, { future: config?.future }), config);
|
|
247
359
|
};
|
|
248
360
|
|
|
249
361
|
export const getCssVariables = (theme: PersonaTheme): Record<string, string> => {
|
|
@@ -267,6 +379,7 @@ export const applyThemeVariables = (
|
|
|
267
379
|
// host pins colorScheme. Re-applied on every theme application, so live
|
|
268
380
|
// config updates and theme-observer callbacks keep it current.
|
|
269
381
|
element.setAttribute("data-persona-color-scheme", getColorScheme(config));
|
|
382
|
+
element.setAttribute("data-persona-defaults", resolveDefaultsVersion(config));
|
|
270
383
|
};
|
|
271
384
|
|
|
272
385
|
export const createThemeObserver = (
|
package/src/utils/tokens.test.ts
CHANGED
|
@@ -1,10 +1,131 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createTheme,
|
|
4
|
+
resolveThemeDefaults,
|
|
5
|
+
resolveThemeDefaultsVersion,
|
|
6
|
+
themeToCssVariables,
|
|
7
|
+
} from './tokens';
|
|
3
8
|
import type { DeepPartial, PersonaTheme } from '../types/theme';
|
|
4
9
|
|
|
5
10
|
const vars = (override: DeepPartial<PersonaTheme>): Record<string, string> =>
|
|
6
11
|
themeToCssVariables(createTheme(override, { validate: false }));
|
|
7
12
|
|
|
13
|
+
describe.each([{ v5Defaults: false }, { v5Defaults: true }])(
|
|
14
|
+
'theme defaults ($v5Defaults)',
|
|
15
|
+
({ v5Defaults }) => {
|
|
16
|
+
it('resolves an explicit overlay version while preserving explicit token precedence', () => {
|
|
17
|
+
const future = { v5Defaults };
|
|
18
|
+
expect(resolveThemeDefaultsVersion(future)).toBe(v5Defaults ? 'v5' : 'v4');
|
|
19
|
+
expect(resolveThemeDefaults(future).palette.colors.gray[500]).toBe(
|
|
20
|
+
v5Defaults ? '#737373' : '#6b7280'
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const theme = createTheme(
|
|
24
|
+
{ palette: { colors: { gray: { 500: '#123456' } } } },
|
|
25
|
+
{ future, validate: false }
|
|
26
|
+
);
|
|
27
|
+
expect(theme.palette.colors.gray[500]).toBe('#123456');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('materializes the selected version geometry defaults', () => {
|
|
31
|
+
const css = themeToCssVariables(createTheme(undefined, { future: { v5Defaults }, validate: false }));
|
|
32
|
+
expect(css['--persona-components-header-padding']).toBe(v5Defaults ? '8px 8px 8px 16px' : '20px 24px');
|
|
33
|
+
expect(css['--persona-components-header-minimalPadding']).toBe(v5Defaults ? '8px 8px 8px 16px' : '16px 24px');
|
|
34
|
+
expect(css['--persona-components-message-gap']).toBe(v5Defaults ? '20px' : '12px');
|
|
35
|
+
expect(css['--persona-components-panel-width']).toBe(v5Defaults ? 'min(400px, calc(100vw - 24px))' : 'min(440px, calc(100vw - 24px))');
|
|
36
|
+
expect(css['--persona-components-panel-height']).toBe(
|
|
37
|
+
v5Defaults ? 'min(704px, calc(100dvh - 104px))' : 'min(640px, max(200px, calc(100vh - 64px)))'
|
|
38
|
+
);
|
|
39
|
+
expect(css['--persona-components-panel-modes-mobile-borderRadius']).toBe('0');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('keeps explicit geometry and typography above the version overlay', () => {
|
|
43
|
+
const css = themeToCssVariables(createTheme({ components: {
|
|
44
|
+
message: { user: { padding: '4px', borderRadius: '7px' }, assistant: { borderWidth: '2px', lineHeight: '1.8' } },
|
|
45
|
+
composer: { fontSize: '17px', controlSize: '36px' },
|
|
46
|
+
input: { borderRadius: '9px' },
|
|
47
|
+
header: { controlSize: '34px', iconScale: '0.75' },
|
|
48
|
+
} }, { future: { v5Defaults }, validate: false }));
|
|
49
|
+
expect(css['--persona-message-user-padding']).toBe('4px');
|
|
50
|
+
expect(css['--persona-message-user-radius']).toBe('7px');
|
|
51
|
+
expect(css['--persona-message-assistant-border-width']).toBe('2px');
|
|
52
|
+
expect(css['--persona-message-assistant-line-height']).toBe('1.8');
|
|
53
|
+
expect(css['--persona-composer-font-size']).toBe('17px');
|
|
54
|
+
expect(css['--persona-composer-control-size']).toBe('36px');
|
|
55
|
+
expect(css['--persona-input-radius']).toBe('9px');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('uses an explicit shared turn gap in fullscreen unless separately overridden', () => {
|
|
59
|
+
const shared = createTheme({ components: { message: { gap: '13px' } } }, { future: { v5Defaults }, validate: false });
|
|
60
|
+
expect(shared.components.message.fullscreenGap).toBe('13px');
|
|
61
|
+
const specific = createTheme({ components: { message: { gap: '13px', fullscreenGap: '31px' } } }, { future: { v5Defaults }, validate: false });
|
|
62
|
+
expect(specific.components.message.fullscreenGap).toBe('31px');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('keeps the transcript surface separate from the tinted user bubble', () => {
|
|
66
|
+
const css = themeToCssVariables(createTheme(undefined, { future: { v5Defaults }, validate: false }));
|
|
67
|
+
expect(css['--persona-container']).toBe(v5Defaults ? '#ffffff' : '#f9fafb');
|
|
68
|
+
expect(css['--persona-message-user-bg']).toBe(v5Defaults ? '#f5f5f5' : '#171717');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('keeps palette radius overrides connected to panel, user bubble, and composer', () => {
|
|
72
|
+
const css = themeToCssVariables(createTheme({ palette: { radius: { lg: '3px', xl: '7px', '2xl': '11px', full: '99px' } } }, { future: { v5Defaults }, validate: false }));
|
|
73
|
+
expect(css['--persona-panel-radius']).toBe('7px');
|
|
74
|
+
expect(css['--persona-message-user-radius']).toBe(v5Defaults ? '7px' : '3px');
|
|
75
|
+
expect(css['--persona-input-radius']).toBe(v5Defaults ? '99px' : '3px');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('derives minimal header padding from an explicit shared padding unless overridden', () => {
|
|
79
|
+
const shared = createTheme(
|
|
80
|
+
{ components: { header: { padding: '10px 12px' } } },
|
|
81
|
+
{ future: { v5Defaults }, validate: false }
|
|
82
|
+
);
|
|
83
|
+
expect(shared.components.header.minimalPadding).toBe('10px 12px');
|
|
84
|
+
|
|
85
|
+
const specific = createTheme(
|
|
86
|
+
{ components: { header: { padding: '10px 12px', minimalPadding: '8px 10px' } } },
|
|
87
|
+
{ future: { v5Defaults }, validate: false }
|
|
88
|
+
);
|
|
89
|
+
expect(specific.components.header.minimalPadding).toBe('8px 10px');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('emits prefixed collapsible-widget chrome aliases and legacy aliases', () => {
|
|
93
|
+
const css = themeToCssVariables(createTheme(undefined, { future: { v5Defaults }, validate: false }));
|
|
94
|
+
|
|
95
|
+
expect(css['--persona-cw-container']).toBe(v5Defaults ? '#fafafa' : '#f9fafb');
|
|
96
|
+
expect(css['--persona-cw-surface']).toBe(v5Defaults ? '#ffffff' : '#f9fafb');
|
|
97
|
+
expect(css['--persona-cw-border']).toBe(v5Defaults ? '#e5e5e5' : '#e5e7eb');
|
|
98
|
+
expect(css['--cw-container']).toBe(css['--persona-cw-container']);
|
|
99
|
+
expect(css['--cw-surface']).toBe(css['--persona-cw-surface']);
|
|
100
|
+
expect(css['--cw-border']).toBe(css['--persona-cw-border']);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('applies explicit collapsible-widget tokens to both alias families', () => {
|
|
104
|
+
const css = themeToCssVariables(
|
|
105
|
+
createTheme(
|
|
106
|
+
{
|
|
107
|
+
components: {
|
|
108
|
+
collapsibleWidget: {
|
|
109
|
+
container: '#123456',
|
|
110
|
+
surface: '#234567',
|
|
111
|
+
border: '#345678',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{ future: { v5Defaults }, validate: false }
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(css['--persona-cw-container']).toBe('#123456');
|
|
120
|
+
expect(css['--persona-cw-surface']).toBe('#234567');
|
|
121
|
+
expect(css['--persona-cw-border']).toBe('#345678');
|
|
122
|
+
expect(css['--cw-container']).toBe('#123456');
|
|
123
|
+
expect(css['--cw-surface']).toBe('#234567');
|
|
124
|
+
expect(css['--cw-border']).toBe('#345678');
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
8
129
|
const flatSurface = (surface: string): DeepPartial<PersonaTheme> => ({
|
|
9
130
|
semantic: { colors: { surface, background: surface, container: surface } },
|
|
10
131
|
});
|