@runtypelabs/persona 1.47.0 → 2.0.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 (73) hide show
  1. package/README.md +140 -8
  2. package/dist/index.cjs +90 -39
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1093 -25
  5. package/dist/index.d.ts +1093 -25
  6. package/dist/index.global.js +111 -60
  7. package/dist/index.global.js.map +1 -1
  8. package/dist/index.js +90 -39
  9. package/dist/index.js.map +1 -1
  10. package/dist/install.global.js +1 -1
  11. package/dist/install.global.js.map +1 -1
  12. package/dist/widget.css +852 -505
  13. package/package.json +1 -1
  14. package/src/artifacts-session.test.ts +80 -0
  15. package/src/client.test.ts +20 -21
  16. package/src/client.ts +153 -4
  17. package/src/components/approval-bubble.ts +45 -42
  18. package/src/components/artifact-card.ts +91 -0
  19. package/src/components/artifact-pane.ts +501 -0
  20. package/src/components/composer-builder.ts +32 -27
  21. package/src/components/event-stream-view.ts +40 -40
  22. package/src/components/feedback.ts +36 -36
  23. package/src/components/forms.ts +11 -11
  24. package/src/components/header-builder.test.ts +32 -0
  25. package/src/components/header-builder.ts +55 -36
  26. package/src/components/header-layouts.ts +58 -125
  27. package/src/components/launcher.ts +36 -21
  28. package/src/components/message-bubble.ts +92 -65
  29. package/src/components/messages.ts +2 -2
  30. package/src/components/panel.ts +42 -11
  31. package/src/components/reasoning-bubble.ts +23 -23
  32. package/src/components/registry.ts +4 -0
  33. package/src/components/suggestions.ts +1 -1
  34. package/src/components/tool-bubble.ts +32 -32
  35. package/src/defaults.ts +30 -4
  36. package/src/index.ts +80 -2
  37. package/src/install.ts +22 -0
  38. package/src/plugins/types.ts +23 -0
  39. package/src/postprocessors.ts +2 -2
  40. package/src/runtime/host-layout.ts +174 -0
  41. package/src/runtime/init.test.ts +236 -0
  42. package/src/runtime/init.ts +114 -55
  43. package/src/session.ts +173 -7
  44. package/src/styles/tailwind.css +1 -1
  45. package/src/styles/widget.css +852 -505
  46. package/src/types/theme.ts +354 -0
  47. package/src/types.ts +348 -16
  48. package/src/ui.docked.test.ts +104 -0
  49. package/src/ui.ts +1093 -244
  50. package/src/utils/artifact-gate.test.ts +255 -0
  51. package/src/utils/artifact-gate.ts +142 -0
  52. package/src/utils/artifact-resize.test.ts +64 -0
  53. package/src/utils/artifact-resize.ts +67 -0
  54. package/src/utils/attachment-manager.ts +10 -10
  55. package/src/utils/code-generators.test.ts +52 -0
  56. package/src/utils/code-generators.ts +40 -36
  57. package/src/utils/dock.ts +17 -0
  58. package/src/utils/dom-context.test.ts +504 -0
  59. package/src/utils/dom-context.ts +896 -0
  60. package/src/utils/dom.ts +12 -1
  61. package/src/utils/message-fingerprint.test.ts +187 -0
  62. package/src/utils/message-fingerprint.ts +105 -0
  63. package/src/utils/migration.ts +179 -0
  64. package/src/utils/morph.ts +1 -1
  65. package/src/utils/plugins.ts +175 -0
  66. package/src/utils/positioning.ts +4 -4
  67. package/src/utils/theme.test.ts +125 -0
  68. package/src/utils/theme.ts +216 -60
  69. package/src/utils/tokens.ts +682 -0
  70. package/src/voice/audio-playback-manager.ts +187 -0
  71. package/src/voice/runtype-voice-provider.ts +305 -69
  72. package/src/voice/voice-activity-detector.ts +90 -0
  73. package/src/voice/voice.test.ts +6 -5
@@ -0,0 +1,125 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, describe, expect, it } from 'vitest';
4
+ import { createTheme, getActiveTheme, themeToCssVariables } from './theme';
5
+
6
+ describe('theme utils', () => {
7
+ afterEach(() => {
8
+ document.documentElement.classList.remove('dark');
9
+ });
10
+
11
+ it('uses darkTheme overrides when dark mode is active', () => {
12
+ const lightAndDarkThemeConfig = {
13
+ colorScheme: 'dark' as const,
14
+ theme: {
15
+ primary: '#111111',
16
+ },
17
+ darkTheme: {
18
+ primary: '#22c55e',
19
+ },
20
+ };
21
+
22
+ const activeTheme = getActiveTheme(lightAndDarkThemeConfig as any);
23
+ const cssVars = themeToCssVariables(activeTheme);
24
+
25
+ expect(cssVars['--persona-palette-colors-primary-500']).toBe('#22c55e');
26
+ });
27
+
28
+ it('uses darkTheme overrides after auto-detecting dark mode', () => {
29
+ document.documentElement.classList.add('dark');
30
+
31
+ const lightAndDarkThemeConfig = {
32
+ colorScheme: 'auto' as const,
33
+ theme: {
34
+ primary: '#111111',
35
+ },
36
+ darkTheme: {
37
+ primary: '#22c55e',
38
+ },
39
+ };
40
+
41
+ const activeTheme = getActiveTheme(lightAndDarkThemeConfig as any);
42
+ const cssVars = themeToCssVariables(activeTheme);
43
+
44
+ expect(cssVars['--persona-palette-colors-primary-500']).toBe('#22c55e');
45
+ });
46
+
47
+ it('maps radius tokens into the legacy widget radius aliases', () => {
48
+ const theme = createTheme({
49
+ palette: {
50
+ radius: {
51
+ none: '0px',
52
+ sm: '2px',
53
+ md: '6px',
54
+ lg: '10px',
55
+ xl: '18px',
56
+ full: '9999px',
57
+ },
58
+ },
59
+ components: {
60
+ panel: {
61
+ borderRadius: 'palette.radius.xl',
62
+ },
63
+ input: {
64
+ borderRadius: 'palette.radius.md',
65
+ },
66
+ launcher: {
67
+ borderRadius: 'palette.radius.full',
68
+ },
69
+ button: {
70
+ primary: {
71
+ borderRadius: 'palette.radius.md',
72
+ },
73
+ },
74
+ message: {
75
+ user: {
76
+ borderRadius: 'palette.radius.sm',
77
+ },
78
+ assistant: {
79
+ borderRadius: 'palette.radius.lg',
80
+ },
81
+ },
82
+ },
83
+ } as any);
84
+
85
+ const cssVars = themeToCssVariables(theme);
86
+
87
+ expect(cssVars['--persona-radius-sm']).toBe('2px');
88
+ expect(cssVars['--persona-radius-md']).toBe('6px');
89
+ expect(cssVars['--persona-radius-lg']).toBe('10px');
90
+ expect(cssVars['--persona-panel-radius']).toBe('18px');
91
+ expect(cssVars['--persona-input-radius']).toBe('6px');
92
+ expect(cssVars['--persona-message-user-radius']).toBe('2px');
93
+ expect(cssVars['--persona-message-assistant-radius']).toBe('10px');
94
+ expect(cssVars['--persona-launcher-radius']).toBe('9999px');
95
+ expect(cssVars['--persona-button-radius']).toBe('6px');
96
+ });
97
+
98
+ it('maps markdown link and optional heading tokens to consumer CSS vars', () => {
99
+ const theme = createTheme({
100
+ components: {
101
+ markdown: {
102
+ link: {
103
+ foreground: '#60a5fa',
104
+ },
105
+ prose: {
106
+ fontFamily: 'Georgia, serif',
107
+ },
108
+ heading: {
109
+ h1: { fontSize: '1.375rem', fontWeight: '650' },
110
+ h2: { fontSize: '1.125rem', fontWeight: '600' },
111
+ },
112
+ },
113
+ },
114
+ } as any);
115
+
116
+ const cssVars = themeToCssVariables(theme);
117
+
118
+ expect(cssVars['--persona-md-link-color']).toBe('#60a5fa');
119
+ expect(cssVars['--persona-md-h1-size']).toBe('1.375rem');
120
+ expect(cssVars['--persona-md-h1-weight']).toBe('650');
121
+ expect(cssVars['--persona-md-h2-size']).toBe('1.125rem');
122
+ expect(cssVars['--persona-md-h2-weight']).toBe('600');
123
+ expect(cssVars['--persona-md-prose-font-family']).toBe('Georgia, serif');
124
+ });
125
+ });
@@ -1,105 +1,261 @@
1
- import { AgentWidgetConfig, AgentWidgetTheme } from "../types";
1
+ import type { PersonaTheme } from '../types/theme';
2
+ import type { AgentWidgetConfig, AgentWidgetTheme } from '../types';
3
+ import { createTheme, resolveTokens, themeToCssVariables } from './tokens';
4
+ import { migrateV1Theme } from './migration';
5
+
6
+ export type ColorScheme = 'light' | 'dark' | 'auto';
7
+
8
+ export interface PersonaWidgetConfig {
9
+ theme?: Partial<PersonaTheme>;
10
+ darkTheme?: Partial<PersonaTheme>;
11
+ colorScheme?: ColorScheme;
12
+ }
13
+
14
+ type WidgetConfig = PersonaWidgetConfig | AgentWidgetConfig;
15
+
16
+ const DARK_PALETTE = {
17
+ colors: {
18
+ primary: {
19
+ 50: '#eff6ff',
20
+ 100: '#dbeafe',
21
+ 200: '#bfdbfe',
22
+ 300: '#93c5fd',
23
+ 400: '#60a5fa',
24
+ 500: '#3b82f6',
25
+ 600: '#2563eb',
26
+ 700: '#1d4ed8',
27
+ 800: '#1e40af',
28
+ 900: '#1e3a8a',
29
+ 950: '#172554',
30
+ },
31
+ secondary: {
32
+ 50: '#f5f3ff',
33
+ 100: '#ede9fe',
34
+ 200: '#ddd6fe',
35
+ 300: '#c4b5fd',
36
+ 400: '#a78bfa',
37
+ 500: '#8b5cf6',
38
+ 600: '#7c3aed',
39
+ 700: '#6d28d9',
40
+ 800: '#5b21b6',
41
+ 900: '#4c1d95',
42
+ 950: '#2e1065',
43
+ },
44
+ accent: {
45
+ 50: '#ecfeff',
46
+ 100: '#cffafe',
47
+ 200: '#a5f3fc',
48
+ 300: '#67e8f9',
49
+ 400: '#22d3ee',
50
+ 500: '#06b6d4',
51
+ 600: '#0891b2',
52
+ 700: '#0e7490',
53
+ 800: '#155e75',
54
+ 900: '#164e63',
55
+ 950: '#083344',
56
+ },
57
+ gray: {
58
+ 50: '#f9fafb',
59
+ 100: '#f3f4f6',
60
+ 200: '#e5e7eb',
61
+ 300: '#d1d5db',
62
+ 400: '#9ca3af',
63
+ 500: '#6b7280',
64
+ 600: '#4b5563',
65
+ 700: '#374151',
66
+ 800: '#1f2937',
67
+ 900: '#111827',
68
+ 950: '#030712',
69
+ },
70
+ success: {
71
+ 50: '#f0fdf4',
72
+ 100: '#dcfce7',
73
+ 200: '#bbf7d0',
74
+ 300: '#86efac',
75
+ 400: '#4ade80',
76
+ 500: '#22c55e',
77
+ 600: '#16a34a',
78
+ 700: '#15803d',
79
+ 800: '#166534',
80
+ 900: '#14532d',
81
+ },
82
+ warning: {
83
+ 50: '#fefce8',
84
+ 100: '#fef9c3',
85
+ 200: '#fef08a',
86
+ 300: '#fde047',
87
+ 400: '#facc15',
88
+ 500: '#eab308',
89
+ 600: '#ca8a04',
90
+ 700: '#a16207',
91
+ 800: '#854d0e',
92
+ 900: '#713f12',
93
+ },
94
+ error: {
95
+ 50: '#fef2f2',
96
+ 100: '#fee2e2',
97
+ 200: '#fecaca',
98
+ 300: '#fca5a5',
99
+ 400: '#f87171',
100
+ 500: '#ef4444',
101
+ 600: '#dc2626',
102
+ 700: '#b91c1c',
103
+ 800: '#991b1b',
104
+ 900: '#7f1d1d',
105
+ },
106
+ },
107
+ };
108
+
109
+ const isObject = (value: unknown): value is Record<string, unknown> =>
110
+ typeof value === 'object' && value !== null && !Array.isArray(value);
111
+
112
+ const deepMerge = <T extends Record<string, unknown>>(
113
+ base: T | undefined,
114
+ override: Record<string, unknown> | undefined
115
+ ): T | Record<string, unknown> | undefined => {
116
+ if (!base) return override;
117
+ if (!override) return base;
118
+
119
+ const merged: Record<string, unknown> = { ...base };
120
+
121
+ for (const [key, value] of Object.entries(override)) {
122
+ const existing = merged[key];
123
+ if (isObject(existing) && isObject(value)) {
124
+ merged[key] = deepMerge(existing, value);
125
+ } else {
126
+ merged[key] = value;
127
+ }
128
+ }
129
+
130
+ return merged;
131
+ };
132
+
133
+ const isTokenTheme = (theme: unknown): theme is Partial<PersonaTheme> => {
134
+ return isObject(theme) && ('palette' in theme || 'semantic' in theme || 'components' in theme);
135
+ };
136
+
137
+ const normalizeThemeConfig = (
138
+ theme: Partial<PersonaTheme> | AgentWidgetTheme | undefined
139
+ ): Partial<PersonaTheme> | undefined => {
140
+ if (!theme) return undefined;
141
+ const migratedTheme = migrateV1Theme(theme as AgentWidgetTheme, { warn: false });
142
+ if (isTokenTheme(theme)) {
143
+ return deepMerge(migratedTheme, theme) as Partial<PersonaTheme>;
144
+ }
145
+ return migratedTheme;
146
+ };
2
147
 
3
- /**
4
- * Detects the current color scheme from the page.
5
- * 1. Checks if <html> element has 'dark' class
6
- * 2. Falls back to prefers-color-scheme media query
7
- */
8
148
  export const detectColorScheme = (): 'light' | 'dark' => {
9
- // Check for 'dark' class on <html> element
10
149
  if (typeof document !== 'undefined' && document.documentElement.classList.contains('dark')) {
11
150
  return 'dark';
12
151
  }
13
-
14
- // Fall back to media query
152
+
15
153
  if (typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
16
154
  return 'dark';
17
155
  }
18
-
156
+
19
157
  return 'light';
20
158
  };
21
159
 
22
- /**
23
- * Gets the active theme based on colorScheme setting and current detection.
24
- */
25
- export const getActiveTheme = (config?: AgentWidgetConfig): AgentWidgetTheme => {
160
+ const getColorSchemeFromConfig = (config?: WidgetConfig): 'light' | 'dark' => {
26
161
  const colorScheme = config?.colorScheme ?? 'light';
27
- const lightTheme = config?.theme ?? {};
28
- const darkTheme = config?.darkTheme ?? lightTheme;
162
+
163
+ if (colorScheme === 'light') return 'light';
164
+ if (colorScheme === 'dark') return 'dark';
165
+
166
+ return detectColorScheme();
167
+ };
168
+
169
+ export const getColorScheme = (config?: WidgetConfig): 'light' | 'dark' => {
170
+ return getColorSchemeFromConfig(config);
171
+ };
172
+
173
+ export const createLightTheme = (userConfig?: Partial<PersonaTheme>): PersonaTheme => {
174
+ return createTheme(userConfig);
175
+ };
176
+
177
+ export const createDarkTheme = (userConfig?: Partial<PersonaTheme>): PersonaTheme => {
178
+ const baseTheme = createTheme(undefined, { validate: false });
29
179
 
30
- if (colorScheme === 'light') {
31
- return lightTheme;
180
+ return createTheme(
181
+ {
182
+ ...userConfig,
183
+ palette: {
184
+ ...baseTheme.palette,
185
+ colors: {
186
+ ...DARK_PALETTE.colors,
187
+ ...userConfig?.palette?.colors,
188
+ },
189
+ },
190
+ },
191
+ { validate: false }
192
+ );
193
+ };
194
+
195
+ export const getActiveTheme = (config?: WidgetConfig): PersonaTheme => {
196
+ const scheme = getColorScheme(config);
197
+ const lightThemeConfig = normalizeThemeConfig(config?.theme as Partial<PersonaTheme> | AgentWidgetTheme | undefined);
198
+ const darkThemeConfig = normalizeThemeConfig(config?.darkTheme as Partial<PersonaTheme> | AgentWidgetTheme | undefined);
199
+
200
+ if (scheme === 'dark') {
201
+ return createDarkTheme(
202
+ deepMerge(lightThemeConfig, darkThemeConfig) as Partial<PersonaTheme> | undefined
203
+ );
32
204
  }
33
-
34
- if (colorScheme === 'dark') {
35
- return darkTheme;
205
+
206
+ return createLightTheme(lightThemeConfig);
207
+ };
208
+
209
+ export const getCssVariables = (theme: PersonaTheme): Record<string, string> => {
210
+ return themeToCssVariables(theme);
211
+ };
212
+
213
+ export const applyThemeVariables = (
214
+ element: HTMLElement,
215
+ config?: WidgetConfig
216
+ ): void => {
217
+ const theme = getActiveTheme(config);
218
+ const cssVars = getCssVariables(theme);
219
+
220
+ for (const [name, value] of Object.entries(cssVars)) {
221
+ element.style.setProperty(name, value);
36
222
  }
37
-
38
- // colorScheme === 'auto'
39
- const detectedScheme = detectColorScheme();
40
- return detectedScheme === 'dark' ? darkTheme : lightTheme;
41
223
  };
42
224
 
43
- /**
44
- * Creates observers for theme changes (HTML class and media query).
45
- * Returns a cleanup function.
46
- */
47
225
  export const createThemeObserver = (
48
226
  callback: (scheme: 'light' | 'dark') => void
49
227
  ): (() => void) => {
50
228
  const cleanupFns: Array<() => void> = [];
51
-
52
- // Observe HTML class changes
229
+
53
230
  if (typeof document !== 'undefined' && typeof MutationObserver !== 'undefined') {
54
231
  const observer = new MutationObserver(() => {
55
232
  callback(detectColorScheme());
56
233
  });
57
-
234
+
58
235
  observer.observe(document.documentElement, {
59
236
  attributes: true,
60
- attributeFilter: ['class']
237
+ attributeFilter: ['class'],
61
238
  });
62
-
239
+
63
240
  cleanupFns.push(() => observer.disconnect());
64
241
  }
65
-
66
- // Observe media query changes
242
+
67
243
  if (typeof window !== 'undefined' && window.matchMedia) {
68
244
  const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
69
245
  const handleChange = () => callback(detectColorScheme());
70
-
71
- // Use addEventListener if available (modern browsers), otherwise addListener
246
+
72
247
  if (mediaQuery.addEventListener) {
73
248
  mediaQuery.addEventListener('change', handleChange);
74
249
  cleanupFns.push(() => mediaQuery.removeEventListener('change', handleChange));
75
250
  } else if (mediaQuery.addListener) {
76
- // Legacy Safari
77
251
  mediaQuery.addListener(handleChange);
78
252
  cleanupFns.push(() => mediaQuery.removeListener(handleChange));
79
253
  }
80
254
  }
81
-
255
+
82
256
  return () => {
83
- cleanupFns.forEach(fn => fn());
257
+ cleanupFns.forEach((fn) => fn());
84
258
  };
85
259
  };
86
260
 
87
- export const applyThemeVariables = (
88
- element: HTMLElement,
89
- config?: AgentWidgetConfig
90
- ) => {
91
- const theme = getActiveTheme(config);
92
- Object.entries(theme).forEach(([key, value]) => {
93
- // Skip undefined or empty values
94
- if (value === undefined || value === null || value === "") {
95
- return;
96
- }
97
- // Convert camelCase to kebab-case (e.g., radiusSm → radius-sm)
98
- const kebabKey = key.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
99
- element.style.setProperty(`--cw-${kebabKey}`, String(value));
100
- });
101
- };
102
-
103
-
104
-
105
-
261
+ export { createTheme, resolveTokens, themeToCssVariables };