@runtypelabs/persona 3.5.2 → 3.7.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 (53) hide show
  1. package/dist/index.cjs +46 -46
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +44 -0
  4. package/dist/index.d.ts +44 -0
  5. package/dist/index.global.js +70 -70
  6. package/dist/index.global.js.map +1 -1
  7. package/dist/index.js +46 -46
  8. package/dist/index.js.map +1 -1
  9. package/dist/theme-editor.cjs +18015 -0
  10. package/dist/theme-editor.d.cts +3888 -0
  11. package/dist/theme-editor.d.ts +3888 -0
  12. package/dist/theme-editor.js +17909 -0
  13. package/dist/theme-reference.cjs +1 -1
  14. package/dist/theme-reference.d.cts +33 -0
  15. package/dist/theme-reference.d.ts +33 -0
  16. package/dist/theme-reference.js +1 -1
  17. package/dist/widget.css +69 -25
  18. package/package.json +9 -7
  19. package/src/components/artifact-card.ts +1 -1
  20. package/src/components/composer-builder.ts +16 -29
  21. package/src/components/demo-carousel.ts +5 -5
  22. package/src/components/event-stream-view.test.ts +142 -0
  23. package/src/components/event-stream-view.ts +68 -29
  24. package/src/components/header-builder.ts +2 -2
  25. package/src/components/launcher.ts +9 -0
  26. package/src/components/message-bubble.ts +9 -3
  27. package/src/components/suggestions.ts +1 -1
  28. package/src/defaults.ts +24 -9
  29. package/src/scroll-to-bottom-defaults.test.ts +13 -0
  30. package/src/styles/widget.css +69 -25
  31. package/src/theme-editor/color-utils.ts +252 -0
  32. package/src/theme-editor/index.ts +131 -0
  33. package/src/theme-editor/presets.ts +144 -0
  34. package/src/theme-editor/preview-utils.ts +265 -0
  35. package/src/theme-editor/preview.ts +445 -0
  36. package/src/theme-editor/role-mappings.ts +343 -0
  37. package/src/theme-editor/sections.test.ts +43 -0
  38. package/src/theme-editor/sections.ts +994 -0
  39. package/src/theme-editor/state.ts +298 -0
  40. package/src/theme-editor/types.ts +177 -0
  41. package/src/theme-editor.ts +2 -0
  42. package/src/theme-reference.ts +8 -0
  43. package/src/types/theme.ts +11 -0
  44. package/src/types.ts +22 -0
  45. package/src/ui.scroll.test.ts +554 -0
  46. package/src/ui.ts +223 -133
  47. package/src/utils/auto-follow.test.ts +110 -0
  48. package/src/utils/auto-follow.ts +112 -0
  49. package/src/utils/plugins.ts +1 -1
  50. package/src/utils/theme.test.ts +44 -8
  51. package/src/utils/theme.ts +11 -11
  52. package/src/utils/tokens.ts +137 -41
  53. package/widget.css +0 -1
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Interface Role → Token mapping layer.
3
+ *
4
+ * Maps high-level editor choices (family + intensity) to concrete palette
5
+ * token references across multiple component/semantic paths. This is the
6
+ * core of the "Interface Roles" editor section — one picker writes to
7
+ * many tokens atomically.
8
+ *
9
+ * All functions are pure and headless (no DOM).
10
+ */
11
+
12
+ import type { RoleTarget, RoleIntensity, RoleAssignmentOptions } from './types';
13
+
14
+ // ─── Intensities ────────────────────────────────────────────────
15
+
16
+ export const ROLE_INTENSITIES: RoleIntensity[] = [
17
+ { id: 'solid', label: 'Solid' },
18
+ { id: 'soft', label: 'Soft' },
19
+ ];
20
+
21
+ // ─── Palette families available for role assignment ─────────────
22
+
23
+ export const ROLE_FAMILIES = ['primary', 'secondary', 'accent', 'gray'] as const;
24
+ export type RoleFamily = (typeof ROLE_FAMILIES)[number];
25
+
26
+ /** Display labels for palette families in the editor */
27
+ export const ROLE_FAMILY_LABELS: Record<RoleFamily, string> = {
28
+ primary: 'Primary',
29
+ secondary: 'Secondary',
30
+ accent: 'Accent',
31
+ gray: 'Neutral',
32
+ };
33
+
34
+ // ─── Role Definitions ───────────────────────────────────────────
35
+
36
+ export const ROLE_SURFACES: RoleAssignmentOptions = {
37
+ roleId: 'role-surfaces',
38
+ helper: 'Page and panel backgrounds',
39
+ previewZone: 'container',
40
+ intensities: ROLE_INTENSITIES,
41
+ targets: [
42
+ { path: 'semantic.colors.background', kind: 'background' },
43
+ { path: 'semantic.colors.surface', kind: 'background' },
44
+ { path: 'semantic.colors.container', kind: 'background' },
45
+ ],
46
+ };
47
+
48
+ export const ROLE_HEADER: RoleAssignmentOptions = {
49
+ roleId: 'role-header',
50
+ helper: 'Widget header bar',
51
+ previewZone: 'header',
52
+ intensities: ROLE_INTENSITIES,
53
+ targets: [
54
+ { path: 'components.header.background', kind: 'background' },
55
+ { path: 'components.header.border', kind: 'border' },
56
+ { path: 'components.header.iconBackground', kind: 'accent' },
57
+ { path: 'components.header.iconForeground', kind: 'foreground' },
58
+ { path: 'components.header.titleForeground', kind: 'accent' },
59
+ { path: 'components.header.subtitleForeground', kind: 'foreground' },
60
+ { path: 'components.header.actionIconForeground', kind: 'foreground' },
61
+ ],
62
+ };
63
+
64
+ export const ROLE_USER_MESSAGES: RoleAssignmentOptions = {
65
+ roleId: 'role-user-messages',
66
+ helper: 'User chat bubbles',
67
+ previewZone: 'user-message',
68
+ intensities: ROLE_INTENSITIES,
69
+ targets: [
70
+ { path: 'components.message.user.background', kind: 'background' },
71
+ { path: 'components.message.user.text', kind: 'foreground' },
72
+ ],
73
+ };
74
+
75
+ export const ROLE_ASSISTANT_MESSAGES: RoleAssignmentOptions = {
76
+ roleId: 'role-assistant-messages',
77
+ helper: 'Assistant chat bubbles',
78
+ previewZone: 'assistant-message',
79
+ intensities: ROLE_INTENSITIES,
80
+ targets: [
81
+ { path: 'components.message.assistant.background', kind: 'background' },
82
+ { path: 'components.message.assistant.text', kind: 'foreground' },
83
+ ],
84
+ };
85
+
86
+ export const ROLE_PRIMARY_ACTIONS: RoleAssignmentOptions = {
87
+ roleId: 'role-primary-actions',
88
+ helper: 'Send button and primary buttons',
89
+ intensities: ROLE_INTENSITIES,
90
+ targets: [
91
+ { path: 'components.button.primary.background', kind: 'background' },
92
+ { path: 'components.button.primary.foreground', kind: 'foreground' },
93
+ { path: 'semantic.colors.interactive.default', kind: 'accent' },
94
+ { path: 'semantic.colors.interactive.hover', kind: 'accent' },
95
+ ],
96
+ };
97
+
98
+ export const ROLE_SCROLL_TO_BOTTOM: RoleAssignmentOptions = {
99
+ roleId: 'role-scroll-to-bottom',
100
+ helper: 'Scroll-to-bottom affordances in transcript and event stream',
101
+ intensities: ROLE_INTENSITIES,
102
+ targets: [
103
+ { path: 'components.scrollToBottom.background', kind: 'background' },
104
+ { path: 'components.scrollToBottom.foreground', kind: 'foreground' },
105
+ { path: 'components.scrollToBottom.border', kind: 'border' },
106
+ ],
107
+ };
108
+
109
+ export const ROLE_INPUT: RoleAssignmentOptions = {
110
+ roleId: 'role-input',
111
+ helper: 'Message input field',
112
+ previewZone: 'composer',
113
+ intensities: ROLE_INTENSITIES,
114
+ targets: [
115
+ { path: 'components.input.background', kind: 'background' },
116
+ { path: 'components.input.placeholder', kind: 'foreground' },
117
+ { path: 'components.input.focus.border', kind: 'accent' },
118
+ { path: 'components.input.focus.ring', kind: 'accent' },
119
+ ],
120
+ };
121
+
122
+ export const ROLE_LINKS_FOCUS: RoleAssignmentOptions = {
123
+ roleId: 'role-links-focus',
124
+ helper: 'Links, focus rings, and interactive highlights',
125
+ intensities: ROLE_INTENSITIES,
126
+ targets: [
127
+ { path: 'semantic.colors.accent', kind: 'accent' },
128
+ { path: 'semantic.colors.interactive.focus', kind: 'accent' },
129
+ { path: 'semantic.colors.interactive.active', kind: 'accent' },
130
+ { path: 'components.markdown.link.foreground', kind: 'accent' },
131
+ ],
132
+ };
133
+
134
+ export const ROLE_BORDERS: RoleAssignmentOptions = {
135
+ roleId: 'role-borders',
136
+ helper: 'Borders, dividers, and separators',
137
+ intensities: ROLE_INTENSITIES,
138
+ targets: [
139
+ { path: 'semantic.colors.border', kind: 'border' },
140
+ { path: 'semantic.colors.divider', kind: 'border' },
141
+ ],
142
+ };
143
+
144
+ /** All interface role definitions in display order */
145
+ export const ALL_ROLES: RoleAssignmentOptions[] = [
146
+ ROLE_SURFACES,
147
+ ROLE_HEADER,
148
+ ROLE_USER_MESSAGES,
149
+ ROLE_ASSISTANT_MESSAGES,
150
+ ROLE_PRIMARY_ACTIONS,
151
+ ROLE_SCROLL_TO_BOTTOM,
152
+ ROLE_INPUT,
153
+ ROLE_LINKS_FOCUS,
154
+ ROLE_BORDERS,
155
+ ];
156
+
157
+ // ─── Resolution ─────────────────────────────────────────────────
158
+
159
+ /**
160
+ * Resolve a role assignment (family + intensity) into concrete token writes.
161
+ *
162
+ * Returns a map of `{ "theme.{path}": "palette.colors.{family}.{shade}" }`.
163
+ * The `theme.` prefix is added so callers can pass the result directly to
164
+ * `state.setBatch()`.
165
+ */
166
+ export function resolveRoleAssignment(
167
+ family: string,
168
+ intensity: string,
169
+ role: RoleAssignmentOptions
170
+ ): Record<string, string> {
171
+ const writes: Record<string, string> = {};
172
+ const f = family === 'neutral' ? 'gray' : family;
173
+
174
+ for (const target of role.targets) {
175
+ const value = resolveTarget(f, intensity, target, role.roleId);
176
+ writes[`theme.${target.path}`] = value;
177
+ writes[`darkTheme.${target.path}`] = value;
178
+ }
179
+
180
+ // For primary-actions, also write the hover shade (one step darker than default)
181
+ if (role.roleId === 'role-primary-actions') {
182
+ const hoverValue = intensity === 'solid'
183
+ ? `palette.colors.${f}.700`
184
+ : `palette.colors.${f}.200`;
185
+ writes['theme.semantic.colors.interactive.hover'] = hoverValue;
186
+ writes['darkTheme.semantic.colors.interactive.hover'] = hoverValue;
187
+ }
188
+
189
+ return writes;
190
+ }
191
+
192
+ function resolveTarget(
193
+ family: string,
194
+ intensity: string,
195
+ target: RoleTarget,
196
+ roleId: string
197
+ ): string {
198
+ const solid = intensity === 'solid';
199
+
200
+ // Header has nuanced per-sub-target shading
201
+ if (roleId === 'role-header') {
202
+ return resolveHeaderTarget(family, solid, target);
203
+ }
204
+
205
+ // Input has special handling — foreground target is the placeholder
206
+ if (roleId === 'role-input') {
207
+ return resolveInputTarget(family, solid, target);
208
+ }
209
+
210
+ switch (target.kind) {
211
+ case 'background':
212
+ return solid
213
+ ? `palette.colors.${family}.500`
214
+ : `palette.colors.${family}.${family === 'gray' ? '50' : '100'}`;
215
+ case 'foreground':
216
+ return solid
217
+ ? `palette.colors.${family === 'gray' ? 'gray' : family}.50`
218
+ : `palette.colors.${family === 'gray' ? 'gray' : family}.900`;
219
+ case 'border':
220
+ return solid
221
+ ? `palette.colors.${family}.600`
222
+ : `palette.colors.${family}.200`;
223
+ case 'accent':
224
+ return solid
225
+ ? `palette.colors.${family}.600`
226
+ : `palette.colors.${family}.400`;
227
+ }
228
+ }
229
+
230
+ function resolveHeaderTarget(family: string, solid: boolean, target: RoleTarget): string {
231
+ const path = target.path;
232
+
233
+ if (path.endsWith('.background')) {
234
+ return solid
235
+ ? `palette.colors.${family}.500`
236
+ : `palette.colors.${family}.${family === 'gray' ? '50' : '100'}`;
237
+ }
238
+ if (path.endsWith('.border')) {
239
+ return solid
240
+ ? `palette.colors.${family}.600`
241
+ : `palette.colors.${family}.200`;
242
+ }
243
+ if (path.endsWith('.iconBackground')) {
244
+ return solid
245
+ ? `palette.colors.${family}.${family === 'gray' ? '700' : '600'}`
246
+ : `palette.colors.${family}.500`;
247
+ }
248
+ if (path.endsWith('.iconForeground')) {
249
+ return solid
250
+ ? `palette.colors.${family}.50`
251
+ : `palette.colors.${family}.50`;
252
+ }
253
+ if (path.endsWith('.titleForeground')) {
254
+ return solid
255
+ ? `palette.colors.${family}.50`
256
+ : `palette.colors.${family}.${family === 'gray' ? '900' : '700'}`;
257
+ }
258
+ if (path.endsWith('.subtitleForeground')) {
259
+ return solid
260
+ ? `palette.colors.${family}.200`
261
+ : `palette.colors.${family}.500`;
262
+ }
263
+ if (path.endsWith('.actionIconForeground')) {
264
+ return solid
265
+ ? `palette.colors.${family}.200`
266
+ : `palette.colors.${family}.500`;
267
+ }
268
+
269
+ // Fallback
270
+ return `palette.colors.${family}.500`;
271
+ }
272
+
273
+ function resolveInputTarget(family: string, solid: boolean, target: RoleTarget): string {
274
+ const path = target.path;
275
+
276
+ if (path.endsWith('.background')) {
277
+ return solid
278
+ ? `palette.colors.${family}.${family === 'gray' ? '100' : '50'}`
279
+ : `palette.colors.${family}.${family === 'gray' ? '50' : '50'}`;
280
+ }
281
+ if (path.endsWith('.placeholder')) {
282
+ return `palette.colors.${family}.${solid ? '400' : '400'}`;
283
+ }
284
+ if (path.endsWith('.border') || path.endsWith('.ring')) {
285
+ return solid
286
+ ? `palette.colors.${family}.500`
287
+ : `palette.colors.${family}.400`;
288
+ }
289
+
290
+ return `palette.colors.${family}.500`;
291
+ }
292
+
293
+ // ─── Detection (reverse mapping) ────────────────────────────────
294
+
295
+ /** Result of detecting a role assignment from current state */
296
+ export interface DetectedRoleAssignment {
297
+ family: RoleFamily;
298
+ intensity: string;
299
+ }
300
+
301
+ /** Pattern: palette.colors.{family}.{shade} */
302
+ const PALETTE_REF_RE = /^palette\.colors\.(\w+)\.(\d+)$/;
303
+
304
+ /**
305
+ * Detect the current role assignment by reading token values and matching
306
+ * against known palette reference patterns.
307
+ *
308
+ * @param getValue - Function to read a theme token value (e.g., `(p) => state.get('theme.' + p)`)
309
+ * @param role - The role definition to detect against
310
+ * @returns Detected assignment or null if tokens don't match a known pattern
311
+ */
312
+ export function detectRoleAssignment(
313
+ getValue: (path: string) => unknown,
314
+ role: RoleAssignmentOptions
315
+ ): DetectedRoleAssignment | null {
316
+ // Read the first background target (or first target of any kind) to determine the family
317
+ const probeTarget = role.targets.find((t) => t.kind === 'background') ?? role.targets[0];
318
+ if (!probeTarget) return null;
319
+
320
+ const bgValue = String(getValue(probeTarget.path) ?? '');
321
+ const bgMatch = bgValue.match(PALETTE_REF_RE);
322
+ if (!bgMatch) return null;
323
+
324
+ const detectedFamily = bgMatch[1] as string;
325
+
326
+ // Normalize gray → gray (it's already the canonical name)
327
+ const family = ROLE_FAMILIES.includes(detectedFamily as RoleFamily)
328
+ ? (detectedFamily as RoleFamily)
329
+ : null;
330
+ if (!family) return null;
331
+
332
+ // Try both intensities — shade-based guessing doesn't work for all target kinds
333
+ for (const intensity of ['solid', 'soft'] as const) {
334
+ const expected = resolveRoleAssignment(family, intensity, role);
335
+ const allMatch = role.targets.every((t) => {
336
+ const actual = String(getValue(t.path) ?? '');
337
+ return actual === expected[`theme.${t.path}`];
338
+ });
339
+ if (allMatch) return { family, intensity };
340
+ }
341
+
342
+ return null;
343
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { COMPONENTS_SECTIONS, CONFIGURE_SECTIONS, INTERFACE_ROLES_SECTION } from "./sections";
4
+ import { ALL_ROLES } from "./role-mappings";
5
+
6
+ describe("theme editor scroll-to-bottom controls", () => {
7
+ it("exposes scroll-to-bottom config controls", () => {
8
+ const featureSection = CONFIGURE_SECTIONS.find((section) => section.id === "features");
9
+
10
+ expect(featureSection?.fields.some((field) => field.path === "features.scrollToBottom.enabled")).toBe(true);
11
+ expect(featureSection?.fields.some((field) => field.path === "features.scrollToBottom.iconName")).toBe(true);
12
+ expect(featureSection?.fields.some((field) => field.path === "features.scrollToBottom.label")).toBe(true);
13
+ });
14
+
15
+ it("exposes scroll-to-bottom component token controls", () => {
16
+ const fieldPaths = COMPONENTS_SECTIONS.flatMap((section) => section.fields.map((field) => field.path));
17
+
18
+ expect(fieldPaths).toContain("theme.components.scrollToBottom.background");
19
+ expect(fieldPaths).toContain("theme.components.scrollToBottom.foreground");
20
+ expect(fieldPaths).toContain("theme.components.scrollToBottom.border");
21
+ expect(fieldPaths).toContain("theme.components.scrollToBottom.size");
22
+ expect(fieldPaths).toContain("theme.components.scrollToBottom.borderRadius");
23
+ expect(fieldPaths).toContain("theme.components.scrollToBottom.shadow");
24
+ expect(fieldPaths).toContain("theme.components.scrollToBottom.padding");
25
+ expect(fieldPaths).toContain("theme.components.scrollToBottom.gap");
26
+ expect(fieldPaths).toContain("theme.components.scrollToBottom.fontSize");
27
+ expect(fieldPaths).toContain("theme.components.scrollToBottom.iconSize");
28
+ });
29
+
30
+ it("adds a scroll-to-bottom interface role mapping", () => {
31
+ const role = ALL_ROLES.find((entry) => entry.roleId === "role-scroll-to-bottom");
32
+
33
+ expect(role).toBeDefined();
34
+ expect(role?.targets.map((target) => target.path)).toEqual(
35
+ expect.arrayContaining([
36
+ "components.scrollToBottom.background",
37
+ "components.scrollToBottom.foreground",
38
+ "components.scrollToBottom.border",
39
+ ])
40
+ );
41
+ expect(INTERFACE_ROLES_SECTION.fields.some((field) => field.id === "role-scroll-to-bottom")).toBe(true);
42
+ });
43
+ });