@runtypelabs/persona 3.21.2 → 3.22.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 (59) hide show
  1. package/README.md +67 -0
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-CWPIj66R.d.cts → types-BZVr1YOV.d.cts} +10 -0
  5. package/dist/animations/{types-CWPIj66R.d.ts → types-BZVr1YOV.d.ts} +10 -0
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/index.cjs +50 -43
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +474 -6
  11. package/dist/index.d.ts +474 -6
  12. package/dist/index.global.js +98 -88
  13. package/dist/index.global.js.map +1 -1
  14. package/dist/index.js +48 -41
  15. package/dist/index.js.map +1 -1
  16. package/dist/smart-dom-reader.cjs +1875 -0
  17. package/dist/smart-dom-reader.d.cts +4521 -0
  18. package/dist/smart-dom-reader.d.ts +4521 -0
  19. package/dist/smart-dom-reader.js +1848 -0
  20. package/dist/theme-editor.cjs +2282 -90
  21. package/dist/theme-editor.d.cts +348 -1
  22. package/dist/theme-editor.d.ts +348 -1
  23. package/dist/theme-editor.js +2267 -90
  24. package/package.json +9 -2
  25. package/src/client.test.ts +165 -0
  26. package/src/client.ts +144 -23
  27. package/src/components/composer-parts.test.ts +34 -0
  28. package/src/components/composer-parts.ts +9 -6
  29. package/src/index.ts +26 -0
  30. package/src/session.test.ts +258 -0
  31. package/src/session.ts +886 -30
  32. package/src/session.webmcp.test.ts +815 -0
  33. package/src/smart-dom-reader.test.ts +135 -0
  34. package/src/smart-dom-reader.ts +135 -0
  35. package/src/theme-editor/color-utils.test.ts +59 -0
  36. package/src/theme-editor/color-utils.ts +38 -2
  37. package/src/theme-editor/index.ts +35 -0
  38. package/src/theme-editor/webmcp/coerce.test.ts +86 -0
  39. package/src/theme-editor/webmcp/coerce.ts +286 -0
  40. package/src/theme-editor/webmcp/index.ts +45 -0
  41. package/src/theme-editor/webmcp/summary.ts +324 -0
  42. package/src/theme-editor/webmcp/tools.test.ts +205 -0
  43. package/src/theme-editor/webmcp/tools.ts +795 -0
  44. package/src/theme-editor/webmcp/types.ts +87 -0
  45. package/src/types.ts +186 -0
  46. package/src/ui.composer-keyboard.test.ts +229 -0
  47. package/src/ui.ts +127 -5
  48. package/src/utils/composer-history.test.ts +128 -0
  49. package/src/utils/composer-history.ts +113 -0
  50. package/src/utils/message-fingerprint.test.ts +20 -0
  51. package/src/utils/message-fingerprint.ts +2 -0
  52. package/src/utils/smart-dom-adapter.test.ts +257 -0
  53. package/src/utils/smart-dom-adapter.ts +217 -0
  54. package/{LICENSE → src/vendor/smart-dom-reader/LICENSE} +2 -2
  55. package/src/vendor/smart-dom-reader/README.md +61 -0
  56. package/src/vendor/smart-dom-reader/index.d.ts +476 -0
  57. package/src/vendor/smart-dom-reader/index.js +1618 -0
  58. package/src/webmcp-bridge.test.ts +429 -0
  59. package/src/webmcp-bridge.ts +547 -0
@@ -0,0 +1,205 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { ThemeEditorState } from '../state';
3
+ import { createThemeEditorTools } from './tools';
4
+ import type { WebMcpTool } from './types';
5
+
6
+ function toolMap(state: ThemeEditorState, opts?: Parameters<typeof createThemeEditorTools>[1]) {
7
+ const tools = createThemeEditorTools(state, opts);
8
+ const map = new Map<string, WebMcpTool>();
9
+ for (const t of tools) map.set(t.name, t);
10
+ return map;
11
+ }
12
+
13
+ async function call(tool: WebMcpTool, input: unknown): Promise<any> {
14
+ const res = await tool.execute(input);
15
+ return res.structuredContent;
16
+ }
17
+
18
+ describe('createThemeEditorTools', () => {
19
+ let state: ThemeEditorState;
20
+ let tools: Map<string, WebMcpTool>;
21
+
22
+ beforeEach(() => {
23
+ state = new ThemeEditorState();
24
+ tools = toolMap(state);
25
+ });
26
+
27
+ it('exposes the expected catalog', () => {
28
+ expect([...tools.keys()].sort()).toEqual(
29
+ [
30
+ 'apply_preset',
31
+ 'assign_color_role',
32
+ 'check_contrast',
33
+ 'configure_widget',
34
+ 'get_theme_overview',
35
+ 'manage_session',
36
+ 'set_brand_colors',
37
+ 'set_color_scheme',
38
+ 'set_copy_and_suggestions',
39
+ 'set_roundness',
40
+ 'set_theme_fields',
41
+ 'set_typography',
42
+ ].sort()
43
+ );
44
+ });
45
+
46
+ it('get_theme_overview returns summary + presets and is read-only', async () => {
47
+ const overview = tools.get('get_theme_overview')!;
48
+ expect(overview.annotations?.readOnlyHint).toBe(true);
49
+ const out = await call(overview, {});
50
+ expect(out.summary.brand.primary).toMatch(/^#/);
51
+ expect(out.presets.map((p: any) => p.id)).toContain('default-dark');
52
+ expect(out.availableRoles.some((r: any) => r.role === 'header')).toBe(true);
53
+ });
54
+
55
+ it('set_brand_colors generates a full scale and applies to light + dark', async () => {
56
+ const out = await call(tools.get('set_brand_colors')!, { primary: 'blue' });
57
+ expect(out.ok).toBe(true);
58
+ expect(out.applied.primary).toBe('#0000ff');
59
+ // Whole scale written, not just 500.
60
+ expect(state.get('theme.palette.colors.primary.50')).toMatch(/^#/);
61
+ expect(state.get('theme.palette.colors.primary.700')).toMatch(/^#/);
62
+ expect(state.get('darkTheme.palette.colors.primary.500')).toMatch(/^#/);
63
+ });
64
+
65
+ it('set_brand_colors accepts rgb() input without corrupting the scale', async () => {
66
+ const out = await call(tools.get('set_brand_colors')!, { primary: 'rgb(37, 99, 235)' });
67
+ expect(out.ok).toBe(true);
68
+ for (const shade of ['50', '500', '700', '950']) {
69
+ expect(state.get(`theme.palette.colors.primary.${shade}`)).toMatch(/^#[0-9a-f]{6}$/);
70
+ expect(state.get(`darkTheme.palette.colors.primary.${shade}`)).not.toContain('NaN');
71
+ }
72
+ });
73
+
74
+ it('assign_color_role writes role tokens to both variants', async () => {
75
+ const out = await call(tools.get('assign_color_role')!, {
76
+ role: 'header',
77
+ family: 'primary',
78
+ intensity: 'solid',
79
+ });
80
+ expect(out.applied.role).toBe('header');
81
+ expect(out.applied.tokensWritten).toBeGreaterThan(0);
82
+ expect(state.get('theme.components.header.background')).toBe('palette.colors.primary.500');
83
+ expect(state.get('darkTheme.components.header.background')).toBe('palette.colors.primary.500');
84
+ });
85
+
86
+ it('coerces neutral family alias', async () => {
87
+ await call(tools.get('assign_color_role')!, { role: 'borders', family: 'gray' });
88
+ expect(state.get('theme.semantic.colors.border')).toBe('palette.colors.gray.600');
89
+ });
90
+
91
+ it('set_typography maps keywords to token refs', async () => {
92
+ await call(tools.get('set_typography')!, { fontFamily: 'serif', fontWeight: 600 });
93
+ expect(state.get('theme.semantic.typography.fontFamily')).toBe(
94
+ 'palette.typography.fontFamily.serif'
95
+ );
96
+ expect(state.get('theme.semantic.typography.fontWeight')).toBe(
97
+ 'palette.typography.fontWeight.semibold'
98
+ );
99
+ });
100
+
101
+ it('set_roundness applies a keyword preset', async () => {
102
+ await call(tools.get('set_roundness')!, { style: 'pill' });
103
+ expect(state.get('theme.palette.radius.md')).toBe('9999px');
104
+ const out = await call(tools.get('get_theme_overview')!, {});
105
+ expect(out.summary.roundness.style).toBe('pill');
106
+ });
107
+
108
+ it('set_color_scheme sets scheme and editTarget scopes later writes', async () => {
109
+ await call(tools.get('set_color_scheme')!, { scheme: 'dark', editTarget: 'light' });
110
+ expect(state.get('colorScheme')).toBe('dark');
111
+ // editTarget=light → brand write should not touch darkTheme.
112
+ const before = state.get('darkTheme.palette.colors.accent.500');
113
+ await call(tools.get('set_brand_colors')!, { accent: 'red' });
114
+ expect(state.get('theme.palette.colors.accent.500')).toBe('#ff0000');
115
+ expect(state.get('darkTheme.palette.colors.accent.500')).toBe(before);
116
+ });
117
+
118
+ it('apply_preset validates id and applies', async () => {
119
+ const out = await call(tools.get('apply_preset')!, { presetId: 'default-dark' });
120
+ expect(out.applied.appliedPreset.id).toBe('default-dark');
121
+ expect(() => tools.get('apply_preset')!.execute({ presetId: 'nope' })).toThrow(
122
+ /Valid presets/
123
+ );
124
+ });
125
+
126
+ it('configure_widget writes config paths', async () => {
127
+ await call(tools.get('configure_widget')!, {
128
+ launcherPosition: 'bottom-left',
129
+ features: { voice: true, attachments: true },
130
+ layout: { avatars: true, messageStyle: 'flat' },
131
+ });
132
+ expect(state.get('launcher.position')).toBe('bottom-left');
133
+ expect(state.get('voiceRecognition.enabled')).toBe(true);
134
+ expect(state.get('attachments.enabled')).toBe(true);
135
+ expect(state.get('layout.messages.avatar.show')).toBe(true);
136
+ expect(state.get('layout.messages.layout')).toBe('flat');
137
+ });
138
+
139
+ it('set_copy_and_suggestions sets copy + chips', async () => {
140
+ await call(tools.get('set_copy_and_suggestions')!, {
141
+ title: 'Hi there',
142
+ suggestions: ['a', 'b'],
143
+ });
144
+ expect(state.get('copy.welcomeTitle')).toBe('Hi there');
145
+ expect(state.get('suggestionChips')).toEqual(['a', 'b']);
146
+ });
147
+
148
+ it('set_theme_fields resolves field ids and reports per-update status', async () => {
149
+ const out = await call(tools.get('set_theme_fields')!, {
150
+ updates: [
151
+ { field: 'launch-enabled', value: false },
152
+ { field: 'theme.palette.radius.md', value: '20px' },
153
+ { field: 'totally-unknown', value: 'x' },
154
+ ],
155
+ });
156
+ expect(state.get('launcher.enabled')).toBe(false);
157
+ expect(state.get('theme.palette.radius.md')).toBe('20px');
158
+ const reports = out.applied.updates;
159
+ expect(reports.find((r: any) => r.field === 'launch-enabled').ok).toBe(true);
160
+ expect(reports.find((r: any) => r.field === 'totally-unknown').ok).toBe(false);
161
+ });
162
+
163
+ it('set_theme_fields field-ids honor the edit target (both → light + dark)', async () => {
164
+ await call(tools.get('set_theme_fields')!, {
165
+ updates: [{ field: 'brand-primary', value: '#123456' }],
166
+ });
167
+ // 'brand-primary' resolves to theme.palette.colors.primary.500; default
168
+ // editTarget is 'both', so it must scope to dark as well.
169
+ expect(state.get('theme.palette.colors.primary.500')).toBe('#123456');
170
+ expect(state.get('darkTheme.palette.colors.primary.500')).toBe('#123456');
171
+ });
172
+
173
+ it('assign_color_role on input is covered by contrast checks (no silent no-op)', async () => {
174
+ // ROLE_INPUT used to map to no contrast pairs; derived keys now include it.
175
+ const out = await call(tools.get('assign_color_role')!, {
176
+ role: 'input',
177
+ family: 'primary',
178
+ intensity: 'soft',
179
+ });
180
+ expect(out.applied.role).toBe('input');
181
+ expect(Array.isArray(out.warnings)).toBe(true);
182
+ });
183
+
184
+ it('check_contrast returns checks and is read-only', async () => {
185
+ const tool = tools.get('check_contrast')!;
186
+ expect(tool.annotations?.readOnlyHint).toBe(true);
187
+ const out = await call(tool, { level: 'AA', variant: 'light' });
188
+ expect(out.total).toBeGreaterThan(0);
189
+ expect(Array.isArray(out.checks)).toBe(true);
190
+ });
191
+
192
+ it('manage_session undo reverts the last change', async () => {
193
+ await call(tools.get('set_brand_colors')!, { primary: 'red' });
194
+ expect(state.get('theme.palette.colors.primary.500')).toBe('#ff0000');
195
+ const out = await call(tools.get('manage_session')!, { action: 'undo' });
196
+ expect(out.ok).toBe(true);
197
+ expect(state.get('theme.palette.colors.primary.500')).not.toBe('#ff0000');
198
+ });
199
+
200
+ it('manage_session export returns a snapshot', async () => {
201
+ const out = await call(tools.get('manage_session')!, { action: 'export' });
202
+ expect(out.snapshot.version).toBe(2);
203
+ expect(out.snapshot.theme).toBeDefined();
204
+ });
205
+ });