@runtypelabs/persona 4.23.1 → 4.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/dist/animations/glyph-cycle.d.cts +1 -1
  2. package/dist/animations/glyph-cycle.d.ts +1 -1
  3. package/dist/animations/{types-9Vxm-PPz.d.cts → types-Db61aI9J.d.cts} +25 -2
  4. package/dist/animations/{types-9Vxm-PPz.d.ts → types-Db61aI9J.d.ts} +25 -2
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/animations-extra.d.cts +25 -2
  8. package/dist/animations-extra.d.ts +25 -2
  9. package/dist/approval-ui-3NMYNXBD.js +1 -0
  10. package/dist/approval-ui.cjs +2 -2
  11. package/dist/approval-ui.d.cts +261 -40
  12. package/dist/approval-ui.d.ts +261 -40
  13. package/dist/approval-ui.js +2 -2
  14. package/dist/{artifacts-ui-F4N6VMKF.js → artifacts-ui-IGB5YKLO.js} +4 -4
  15. package/dist/artifacts-ui.d.cts +256 -28
  16. package/dist/artifacts-ui.d.ts +256 -28
  17. package/dist/{chunk-NIKYKSNZ.js → chunk-7CO4T5KB.js} +1 -1
  18. package/dist/codegen.cjs +11 -11
  19. package/dist/codegen.js +13 -13
  20. package/dist/context-mentions.d.cts +256 -28
  21. package/dist/context-mentions.d.ts +256 -28
  22. package/dist/event-stream-view.d.cts +256 -28
  23. package/dist/event-stream-view.d.ts +256 -28
  24. package/dist/forms-ui.d.cts +291 -30
  25. package/dist/forms-ui.d.ts +291 -30
  26. package/dist/history-view.d.cts +25 -2
  27. package/dist/history-view.d.ts +25 -2
  28. package/dist/index.cjs +70 -69
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +337 -51
  31. package/dist/index.d.ts +337 -51
  32. package/dist/index.global.js +57 -54
  33. package/dist/index.global.js.map +1 -1
  34. package/dist/index.js +70 -69
  35. package/dist/index.js.map +1 -1
  36. package/dist/install.global.js +1 -1
  37. package/dist/install.global.js.map +1 -1
  38. package/dist/launcher.global.js +2 -2
  39. package/dist/launcher.global.js.map +1 -1
  40. package/dist/session-reconnect.d.cts +256 -28
  41. package/dist/session-reconnect.d.ts +256 -28
  42. package/dist/smart-dom-reader.d.cts +256 -28
  43. package/dist/smart-dom-reader.d.ts +256 -28
  44. package/dist/theme-editor-preview.cjs +74 -73
  45. package/dist/theme-editor-preview.d.cts +263 -28
  46. package/dist/theme-editor-preview.d.ts +263 -28
  47. package/dist/theme-editor-preview.js +51 -50
  48. package/dist/theme-editor.cjs +10 -10
  49. package/dist/theme-editor.d.cts +276 -30
  50. package/dist/theme-editor.d.ts +276 -30
  51. package/dist/theme-editor.js +10 -10
  52. package/dist/theme-reference.cjs +1 -1
  53. package/dist/theme-reference.d.cts +1522 -11
  54. package/dist/theme-reference.d.ts +1522 -11
  55. package/dist/theme-reference.js +1 -1
  56. package/dist/widget.css +1 -1
  57. package/package.json +3 -2
  58. package/src/__fixtures__/defaults-parity.v4.json +776 -0
  59. package/src/approval-ui-entry.ts +0 -1
  60. package/src/approval-ui-loader.ts +5 -4
  61. package/src/approval-ui.ts +0 -1
  62. package/src/client-identity-proof.test.ts +193 -0
  63. package/src/client-session-warm.test.ts +216 -0
  64. package/src/client-visitor-history.test.ts +6 -0
  65. package/src/client.tool-pair-replay.test.ts +325 -0
  66. package/src/client.ts +154 -61
  67. package/src/codegen.test.ts +24 -0
  68. package/src/components/activity-row.test.ts +100 -0
  69. package/src/components/activity-row.ts +136 -0
  70. package/src/components/approval-actions.test.ts +8 -5
  71. package/src/components/approval-actions.ts +19 -6
  72. package/src/components/approval-bubble.test.ts +56 -9
  73. package/src/components/approval-bubble.ts +36 -41
  74. package/src/components/composer-builder.ts +5 -2
  75. package/src/components/composer-parts.ts +10 -2
  76. package/src/components/header-builder.test.ts +32 -0
  77. package/src/components/header-builder.ts +7 -4
  78. package/src/components/header-layouts.test.ts +12 -0
  79. package/src/components/header-layouts.ts +3 -2
  80. package/src/components/launcher.ts +11 -2
  81. package/src/components/message-bubble.test.ts +22 -0
  82. package/src/components/message-bubble.ts +7 -3
  83. package/src/components/panel.test.ts +52 -1
  84. package/src/components/panel.ts +47 -8
  85. package/src/components/pill-composer-builder.test.ts +2 -0
  86. package/src/components/pill-composer-builder.ts +0 -1
  87. package/src/components/reasoning-bubble.ts +29 -17
  88. package/src/components/suggestions.test.ts +1 -0
  89. package/src/components/suggestions.ts +0 -16
  90. package/src/components/tool-bubble.ts +131 -114
  91. package/src/components/tool-details.test.ts +87 -0
  92. package/src/components/tool-details.ts +102 -0
  93. package/src/components/welcome.test.ts +8 -0
  94. package/src/components/welcome.ts +4 -2
  95. package/src/defaults-parity.test.ts +158 -0
  96. package/src/defaults.test.ts +69 -0
  97. package/src/defaults.ts +128 -49
  98. package/src/generated/runtype-openapi-contract.ts +28 -13
  99. package/src/index-core.ts +2 -0
  100. package/src/install.test.ts +24 -0
  101. package/src/install.ts +3 -1
  102. package/src/launcher-global.ts +2 -1
  103. package/src/launcher-variants.test.ts +52 -0
  104. package/src/runtime/init-update-reset.test.ts +21 -0
  105. package/src/session.ts +61 -9
  106. package/src/styles/widget-styles.test.ts +19 -0
  107. package/src/styles/widget.css +295 -23
  108. package/src/theme-editor/index.ts +2 -0
  109. package/src/theme-editor/preview-utils.test.ts +8 -0
  110. package/src/theme-editor/preview-utils.ts +7 -3
  111. package/src/theme-editor/sections.test.ts +31 -0
  112. package/src/theme-editor/sections.ts +55 -1
  113. package/src/theme-editor/state-preferences.test.ts +49 -0
  114. package/src/theme-editor/state.ts +73 -15
  115. package/src/theme-reference.test.ts +20 -0
  116. package/src/theme-reference.ts +11 -5
  117. package/src/tool-call-display-defaults.test.ts +33 -1
  118. package/src/types/theme.ts +80 -3
  119. package/src/types.ts +179 -23
  120. package/src/ui.activity-row.test.ts +91 -0
  121. package/src/ui.approval-plugin.test.ts +63 -0
  122. package/src/ui.composer-compact.test.ts +24 -2
  123. package/src/ui.composer-placement.test.ts +23 -0
  124. package/src/ui.detached-panel.test.ts +33 -6
  125. package/src/ui.docked.test.ts +19 -0
  126. package/src/ui.header-update-stability.test.ts +25 -0
  127. package/src/ui.identity-proof.test.ts +58 -0
  128. package/src/ui.launcher-teaser.test.ts +25 -0
  129. package/src/ui.message-width.test.ts +17 -2
  130. package/src/ui.reasoning-display.test.ts +92 -0
  131. package/src/ui.rebuild-composer.test.ts +20 -3
  132. package/src/ui.scroll.test.ts +32 -0
  133. package/src/ui.session-init.test.ts +312 -0
  134. package/src/ui.status-indicator.test.ts +82 -0
  135. package/src/ui.suggest-replies.test.ts +11 -0
  136. package/src/ui.ts +363 -74
  137. package/src/ui.v5-defaults.test.ts +144 -0
  138. package/src/ui.welcome.test.ts +92 -3
  139. package/src/utils/__snapshots__/tokens-parity.test.ts.snap +811 -140
  140. package/src/utils/code-generators.ts +14 -0
  141. package/src/utils/composer-compact.test.ts +0 -2
  142. package/src/utils/composer-compact.ts +2 -4
  143. package/src/utils/composer-placement.ts +2 -3
  144. package/src/utils/config-merge.test.ts +81 -5
  145. package/src/utils/config-merge.ts +13 -3
  146. package/src/utils/defaults-provenance.ts +67 -0
  147. package/src/utils/dock.ts +7 -1
  148. package/src/utils/launcher-variant.ts +16 -0
  149. package/src/utils/panel-config.ts +97 -0
  150. package/src/utils/status-indicator.ts +39 -0
  151. package/src/utils/theme.test.ts +182 -0
  152. package/src/utils/theme.ts +131 -18
  153. package/src/utils/tokens.test.ts +122 -1
  154. package/src/utils/tokens.ts +177 -17
  155. package/src/utils/tool-pair-replay.ts +100 -0
  156. package/src/welcome.test.ts +23 -0
  157. package/src/welcome.ts +16 -9
  158. package/dist/approval-ui-K7R5Q5OC.js +0 -1
@@ -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
+ });
@@ -1,6 +1,8 @@
1
- import type { DeepPartial, PersonaTheme } from '../types/theme';
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 { createTheme, resolveTokens, themeToCssVariables } from './tokens';
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 DARK_PALETTE = {
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 DARK_COMPONENTS: DeepPartial<PersonaTheme> = {
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 = (userConfig?: DeepPartial<PersonaTheme>): PersonaTheme => {
199
- return createTheme(userConfig);
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 = (userConfig?: DeepPartial<PersonaTheme>): PersonaTheme => {
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
- DARK_COMPONENTS as Record<string, unknown>,
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
- colors: {
223
- ...DARK_PALETTE.colors,
224
- ...config.palette?.colors,
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 lightThemeConfig = normalizeThemeConfig(config?.theme);
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 = (
@@ -1,10 +1,131 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { createTheme, themeToCssVariables } from './tokens';
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
  });