@runtypelabs/persona 4.17.1 → 4.18.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/chunk-3I2S3I5P.js +1 -0
  2. package/dist/chunk-76IELU7F.js +1 -0
  3. package/dist/chunk-EFJBUH5F.js +1 -0
  4. package/dist/chunk-PDPVORUF.js +1 -0
  5. package/dist/chunk-Q735PSZL.js +1 -0
  6. package/dist/chunk-VK6JOIYJ.js +1 -0
  7. package/dist/codegen.cjs +4 -4
  8. package/dist/codegen.js +4 -4
  9. package/dist/{context-mentions-BTSTIBFC.js → context-mentions-MJL6S6P2.js} +1 -1
  10. package/dist/context-mentions.cjs +9 -8
  11. package/dist/context-mentions.d.cts +1212 -14
  12. package/dist/context-mentions.d.ts +1212 -14
  13. package/dist/context-mentions.js +9 -8
  14. package/dist/event-stream-view-4EZ7BGFK.js +1 -0
  15. package/dist/event-stream-view.cjs +129 -0
  16. package/dist/event-stream-view.d.cts +8384 -0
  17. package/dist/event-stream-view.d.ts +8384 -0
  18. package/dist/event-stream-view.js +129 -0
  19. package/dist/history-view-BD6OXLNA.js +898 -0
  20. package/dist/history-view.cjs +898 -0
  21. package/dist/history-view.d.cts +1461 -0
  22. package/dist/history-view.d.ts +1461 -0
  23. package/dist/history-view.js +898 -0
  24. package/dist/index.cjs +77 -77
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +1987 -17
  27. package/dist/index.d.ts +1987 -17
  28. package/dist/index.global.js +67 -66
  29. package/dist/index.global.js.map +1 -1
  30. package/dist/index.js +77 -77
  31. package/dist/index.js.map +1 -1
  32. package/dist/launcher.global.js +5 -4
  33. package/dist/launcher.global.js.map +1 -1
  34. package/dist/plugin-kit.cjs +1 -1
  35. package/dist/plugin-kit.d.cts +0 -8
  36. package/dist/plugin-kit.d.ts +0 -8
  37. package/dist/plugin-kit.js +1 -1
  38. package/dist/smart-dom-reader.d.cts +1212 -14
  39. package/dist/smart-dom-reader.d.ts +1212 -14
  40. package/dist/theme-editor-preview.cjs +970 -73
  41. package/dist/theme-editor-preview.d.cts +1331 -15
  42. package/dist/theme-editor-preview.d.ts +1331 -15
  43. package/dist/theme-editor-preview.js +81 -81
  44. package/dist/theme-editor.cjs +7 -7
  45. package/dist/theme-editor.d.cts +1215 -15
  46. package/dist/theme-editor.d.ts +1215 -15
  47. package/dist/theme-editor.js +9 -9
  48. package/dist/theme-reference.cjs +1 -1
  49. package/dist/theme-reference.d.cts +226 -7
  50. package/dist/theme-reference.d.ts +226 -7
  51. package/dist/theme-reference.js +1 -1
  52. package/dist/widget.css +1 -1
  53. package/package.json +16 -4
  54. package/src/client-history-rest.test.ts +1034 -0
  55. package/src/client-visitor-history.test.ts +596 -0
  56. package/src/client.ts +1040 -24
  57. package/src/components/artifact-inline.ts +8 -1
  58. package/src/components/artifact-pane.test.ts +278 -1
  59. package/src/components/artifact-pane.ts +135 -67
  60. package/src/components/artifact-preview.test.ts +14 -1
  61. package/src/components/artifact-preview.ts +9 -0
  62. package/src/components/header-builder.ts +3 -0
  63. package/src/components/header-layouts.test.ts +171 -7
  64. package/src/components/header-layouts.ts +83 -53
  65. package/src/components/header-parts.test.ts +154 -1
  66. package/src/components/header-parts.ts +266 -138
  67. package/src/components/history-confirm-dialog.ts +177 -0
  68. package/src/components/history-shell-copy.ts +99 -0
  69. package/src/components/history-view/announcer.ts +40 -0
  70. package/src/components/history-view/copy.ts +105 -0
  71. package/src/components/history-view/css.ts +1078 -0
  72. package/src/components/history-view/grouping.test.ts +142 -0
  73. package/src/components/history-view/grouping.ts +142 -0
  74. package/src/components/history-view/icons.ts +73 -0
  75. package/src/components/history-view/rows.ts +341 -0
  76. package/src/components/history-view/state.ts +309 -0
  77. package/src/components/history-view.test.ts +2660 -0
  78. package/src/components/history-view.ts +1891 -0
  79. package/src/components/launcher-surface.test.ts +133 -0
  80. package/src/components/launcher.test.ts +18 -0
  81. package/src/components/launcher.ts +7 -7
  82. package/src/components/panel.ts +17 -7
  83. package/src/components/welcome.ts +72 -2
  84. package/src/defaults.ts +36 -3
  85. package/src/event-stream-view-bundle.test.ts +69 -0
  86. package/src/event-stream-view-entry.ts +19 -0
  87. package/src/event-stream-view-loader.ts +31 -0
  88. package/src/event-stream-view.ts +19 -0
  89. package/src/generated/runtype-openapi-contract.ts +1 -0
  90. package/src/history-render.ts +325 -0
  91. package/src/history-view-bundle.test.ts +78 -0
  92. package/src/history-view-entry.ts +41 -0
  93. package/src/history-view-loader.test.ts +30 -0
  94. package/src/history-view-loader.ts +32 -0
  95. package/src/history-view.ts +28 -0
  96. package/src/index-core.ts +50 -2
  97. package/src/index-global.ts +55 -0
  98. package/src/internal/demo-history-provider.test.ts +402 -0
  99. package/src/internal/demo-history-provider.ts +689 -0
  100. package/src/internal/history-provider-registry.ts +25 -0
  101. package/src/internal/history-provider.ts +150 -0
  102. package/src/internal/runtype-history-provider.test.ts +313 -0
  103. package/src/internal/runtype-history-provider.ts +240 -0
  104. package/src/plugin-kit.test.ts +16 -0
  105. package/src/plugin-kit.ts +13 -5
  106. package/src/plugins/types.ts +177 -1
  107. package/src/session-history.test.ts +1397 -0
  108. package/src/session.ts +1200 -26
  109. package/src/styles/widget-styles.test.ts +133 -0
  110. package/src/styles/widget.css +227 -8
  111. package/src/theme-editor/preview-utils.test.ts +19 -0
  112. package/src/theme-editor/preview-utils.ts +15 -1
  113. package/src/theme-editor/preview.ts +7 -1
  114. package/src/theme-editor/role-mappings.ts +4 -1
  115. package/src/theme-editor/sections.test.ts +168 -1
  116. package/src/theme-editor/sections.ts +136 -7
  117. package/src/theme-editor/webmcp/summary.ts +16 -2
  118. package/src/theme-reference.ts +20 -6
  119. package/src/types/theme.ts +213 -7
  120. package/src/types.ts +927 -7
  121. package/src/ui.artifact-rail-resize.test.ts +235 -0
  122. package/src/ui.attachments-drop.test.ts +1 -0
  123. package/src/ui.header-update-stability.test.ts +2 -2
  124. package/src/ui.history-chunk.test.ts +72 -0
  125. package/src/ui.history-config-provider.test.ts +318 -0
  126. package/src/ui.history-controller.test.ts +711 -0
  127. package/src/ui.history-lifecycle.test.ts +456 -0
  128. package/src/ui.history-render-hooks.test.ts +904 -0
  129. package/src/ui.history-shell.test.ts +1887 -0
  130. package/src/ui.rail-overlay.test.ts +573 -0
  131. package/src/ui.rail-resize.test.ts +284 -0
  132. package/src/ui.rebuild-composer.test.ts +21 -0
  133. package/src/ui.render-welcome.test.ts +42 -0
  134. package/src/ui.shortcuts.test.ts +166 -0
  135. package/src/ui.ts +3071 -240
  136. package/src/utils/__fixtures__/client-conversation-history-wire.json +108 -0
  137. package/src/utils/artifact-file.ts +15 -0
  138. package/src/utils/artifact-gate.test.ts +4 -0
  139. package/src/utils/artifact-gate.ts +13 -17
  140. package/src/utils/artifact-resize.test.ts +28 -0
  141. package/src/utils/artifact-resize.ts +9 -2
  142. package/src/utils/buttons.ts +188 -1
  143. package/src/utils/config-merge.test.ts +91 -0
  144. package/src/utils/dropdown.ts +10 -1
  145. package/src/utils/history-messages.test.ts +284 -0
  146. package/src/utils/history-messages.ts +284 -0
  147. package/src/utils/icons.ts +3 -0
  148. package/src/utils/shortcuts.test.ts +288 -0
  149. package/src/utils/shortcuts.ts +221 -0
  150. package/src/utils/theme.test.ts +501 -4
  151. package/src/utils/theme.ts +42 -3
  152. package/src/utils/tokens.ts +153 -15
  153. package/src/utils/tooltip-timing.ts +4 -0
  154. package/src/utils/tooltip.test.ts +249 -7
  155. package/src/utils/tooltip.ts +146 -4
  156. package/src/utils/visitor-store.test.ts +425 -0
  157. package/src/utils/visitor-store.ts +337 -0
  158. package/dist/chunk-QJ4LV2KW.js +0 -1
@@ -1,6 +1,13 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
- import { COMPONENTS_SECTIONS, CONFIGURE_SECTIONS, INTERFACE_ROLES_SECTION, STYLE_SECTIONS } from "./sections";
3
+ import {
4
+ COMPONENT_COLOR_SECTIONS,
5
+ COMPONENT_SHAPE_SECTIONS,
6
+ COMPONENTS_SECTIONS,
7
+ CONFIGURE_SECTIONS,
8
+ INTERFACE_ROLES_SECTION,
9
+ STYLE_SECTIONS,
10
+ } from "./sections";
4
11
  import { ALL_ROLES } from "./role-mappings";
5
12
 
6
13
  describe("theme editor scroll-to-bottom controls", () => {
@@ -93,6 +100,166 @@ describe("theme editor scroll-to-bottom controls", () => {
93
100
  );
94
101
  });
95
102
 
103
+ it("exposes the launcher icon stroke as a unitless slider", () => {
104
+ const section = COMPONENT_SHAPE_SECTIONS.find(
105
+ (entry) => entry.id === "comp-launcher"
106
+ );
107
+ const stroke = section?.fields.find(
108
+ (field) => field.path === "theme.components.launcher.iconStrokeWidth"
109
+ );
110
+
111
+ expect(stroke?.defaultValue).toBe("1.5");
112
+ // Unitless slider: the stroke must never pick up a px suffix, and the
113
+ // WebMCP escape hatch coerces sliders to raw numbers while token
114
+ // resolution only walks string values.
115
+ expect(stroke?.slider?.unit).toBe("none");
116
+ expect(stroke?.parseValue?.(1.5)).toBe("1.5");
117
+ expect(stroke?.parseValue?.("1.75")).toBe("1.75");
118
+ });
119
+
120
+ it("exposes the three shared header control knobs as live shape fields", () => {
121
+ const section = COMPONENT_SHAPE_SECTIONS.find(
122
+ (entry) => entry.id === "comp-header-controls"
123
+ );
124
+ const fieldsByPath = new Map(
125
+ section?.fields.map((field) => [field.path, field]) ?? []
126
+ );
127
+
128
+ const size = fieldsByPath.get("theme.components.header.controlSize");
129
+ const iconSize = fieldsByPath.get("theme.components.header.controlIconSize");
130
+ const stroke = fieldsByPath.get("theme.components.header.controlStrokeWidth");
131
+
132
+ expect(size?.defaultValue).toBe("32px");
133
+ expect(iconSize?.defaultValue).toBe("20px");
134
+ expect(stroke?.defaultValue).toBe("1.5");
135
+
136
+ // Unitless slider: the stroke must never pick up a px suffix.
137
+ expect(stroke?.slider?.unit).toBe("none");
138
+ expect(size?.slider?.unit).toBeUndefined();
139
+
140
+ // The WebMCP escape hatch coerces sliders to raw numbers, and token
141
+ // resolution only walks string values.
142
+ expect(size?.parseValue?.(44)).toBe("44px");
143
+ expect(size?.parseValue?.("2.5rem")).toBe("2.5rem");
144
+ expect(stroke?.parseValue?.(2)).toBe("2");
145
+ expect(stroke?.parseValue?.("1.75")).toBe("1.75");
146
+
147
+ // Sizes are not colors: light/dark scoping must never fork them.
148
+ expect(
149
+ COMPONENT_COLOR_SECTIONS.some((entry) => entry.id === "comp-header-controls")
150
+ ).toBe(false);
151
+ });
152
+
153
+ it("exposes the header and rail header band controls", () => {
154
+ const headerControls = COMPONENT_SHAPE_SECTIONS.find(
155
+ (entry) => entry.id === "comp-header-controls"
156
+ );
157
+ const minHeight = headerControls?.fields.find(
158
+ (field) => field.path === "theme.components.header.minHeight"
159
+ );
160
+ expect(minHeight?.type).toBe("text");
161
+ // Blank keeps the built-in auto height instead of writing an empty token.
162
+ expect(minHeight?.parseValue?.(" 65px ")).toBe("65px");
163
+ expect(minHeight?.parseValue?.("")).toBeUndefined();
164
+
165
+ const railShape = COMPONENT_SHAPE_SECTIONS.find(
166
+ (entry) => entry.id === "comp-history-rail-header"
167
+ );
168
+ const railPaths = railShape?.fields.map((field) => field.path) ?? [];
169
+ expect(railPaths).toEqual([
170
+ "theme.components.history.railHeader.minHeight",
171
+ "theme.components.history.railHeader.border",
172
+ ]);
173
+
174
+ const railColors = COMPONENT_COLOR_SECTIONS.find(
175
+ (entry) => entry.id === "comp-history-rail-header-colors"
176
+ );
177
+ const background = railColors?.fields.find(
178
+ (field) => field.path === "theme.components.history.railHeader.background"
179
+ );
180
+ expect(background?.type).toBe("token-ref");
181
+ expect(background?.tokenRef?.tokenType).toBe("color");
182
+
183
+ // Sizes are not colors: light/dark scoping must never fork them.
184
+ expect(
185
+ COMPONENT_COLOR_SECTIONS.some(
186
+ (entry) => entry.id === "comp-history-rail-header"
187
+ )
188
+ ).toBe(false);
189
+ });
190
+
191
+ it("exposes the floating rail surface controls", () => {
192
+ const shape = COMPONENT_SHAPE_SECTIONS.find(
193
+ (entry) => entry.id === "comp-history-overlay"
194
+ );
195
+ expect(shape?.fields.map((field) => field.path)).toEqual([
196
+ "theme.components.history.overlay.margin",
197
+ "theme.components.history.overlay.borderRadius",
198
+ "theme.components.history.overlay.shadow",
199
+ ]);
200
+ // Blank keeps the built-in default instead of writing an empty token.
201
+ const margin = shape?.fields[0];
202
+ expect(margin?.type).toBe("text");
203
+ expect(margin?.parseValue?.(" 12px ")).toBe("12px");
204
+ expect(margin?.parseValue?.("")).toBeUndefined();
205
+
206
+ const colors = COMPONENT_COLOR_SECTIONS.find(
207
+ (entry) => entry.id === "comp-history-overlay-colors"
208
+ );
209
+ const background = colors?.fields.find(
210
+ (field) => field.path === "theme.components.history.overlay.background"
211
+ );
212
+ expect(background?.type).toBe("token-ref");
213
+ expect(background?.tokenRef?.tokenType).toBe("color");
214
+
215
+ // Geometry is not color: the shape section never forks per light/dark.
216
+ expect(
217
+ COMPONENT_COLOR_SECTIONS.some((entry) => entry.id === "comp-history-overlay")
218
+ ).toBe(false);
219
+ });
220
+
221
+ it("exposes the tooltip shape and color controls", () => {
222
+ const shape = COMPONENT_SHAPE_SECTIONS.find(
223
+ (entry) => entry.id === "comp-tooltip"
224
+ );
225
+ const shapePaths = shape?.fields.map((field) => field.path) ?? [];
226
+ expect(shapePaths).toEqual([
227
+ "theme.components.tooltip.borderRadius",
228
+ "theme.components.tooltip.fontSize",
229
+ "theme.components.tooltip.arrow",
230
+ ]);
231
+
232
+ const fontSize = shape?.fields.find(
233
+ (field) => field.path === "theme.components.tooltip.fontSize"
234
+ );
235
+ // Blank keeps the built-in 12px instead of writing an empty token.
236
+ expect(fontSize?.parseValue?.(" 13px ")).toBe("13px");
237
+ expect(fontSize?.parseValue?.("")).toBeUndefined();
238
+
239
+ const arrow = shape?.fields.find(
240
+ (field) => field.path === "theme.components.tooltip.arrow"
241
+ );
242
+ expect(arrow?.type).toBe("toggle");
243
+ expect(arrow?.defaultValue).toBe(true);
244
+
245
+ const colors = COMPONENT_COLOR_SECTIONS.find(
246
+ (entry) => entry.id === "comp-tooltip-colors"
247
+ );
248
+ expect(colors?.fields.map((field) => field.path)).toEqual([
249
+ "theme.components.tooltip.background",
250
+ "theme.components.tooltip.foreground",
251
+ "theme.components.tooltip.hintForeground",
252
+ ]);
253
+ expect(
254
+ colors?.fields.every((field) => field.tokenRef?.tokenType === "color")
255
+ ).toBe(true);
256
+
257
+ // Sizes are not colors: light/dark scoping must never fork them.
258
+ expect(
259
+ COMPONENT_COLOR_SECTIONS.some((entry) => entry.id === "comp-tooltip")
260
+ ).toBe(false);
261
+ });
262
+
96
263
  it("exposes a shadow control for every themeable component", () => {
97
264
  const fieldPaths = COMPONENTS_SECTIONS.flatMap((section) => section.fields.map((field) => field.path));
98
265
 
@@ -27,6 +27,13 @@ const parseOptionalCssValue = (value: unknown): string | undefined => {
27
27
  return normalized || undefined;
28
28
  };
29
29
 
30
+ // Slider controls emit CSS strings, but the WebMCP escape hatch coerces a
31
+ // slider to a raw number and token resolution only walks string values.
32
+ const parseSliderPxLength = (value: unknown): string =>
33
+ typeof value === 'number' ? `${value}px` : String(value ?? '').trim();
34
+
35
+ const parseSliderUnitless = (value: unknown): string => String(value ?? '').trim();
36
+
30
37
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
31
38
  // STYLE TAB: brand colors, chat colors, typography, shape, etc.
32
39
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -72,11 +79,12 @@ const chatColorsSectionDef: SectionDef = {
72
79
  collapsed: true,
73
80
  fields: [
74
81
  { id: 'chat-header-bg', label: 'Header Background', type: 'token-ref', path: 'theme.components.header.background', defaultValue: 'semantic.colors.surface', tokenRef: { tokenType: 'color' } },
82
+ { id: 'chat-header-fg', label: 'Header Foreground', description: 'Anchors the header text. The title takes it, the subtitle and button icons take a 72% mix of it over the background', type: 'token-ref', path: 'theme.components.header.foreground', defaultValue: '', tokenRef: { tokenType: 'color' } },
75
83
  { id: 'chat-header-icon-bg', label: 'Header Icon Background', type: 'token-ref', path: 'theme.components.header.iconBackground', defaultValue: 'semantic.colors.primary', tokenRef: { tokenType: 'color' } },
76
84
  { id: 'chat-header-icon-fg', label: 'Header Icon Color', type: 'token-ref', path: 'theme.components.header.iconForeground', defaultValue: 'semantic.colors.textInverse', tokenRef: { tokenType: 'color' } },
77
- { id: 'chat-header-title-fg', label: 'Header Title Color', type: 'token-ref', path: 'theme.components.header.titleForeground', defaultValue: 'semantic.colors.primary', tokenRef: { tokenType: 'color' } },
78
- { id: 'chat-header-subtitle-fg', label: 'Header Subtitle Color', type: 'token-ref', path: 'theme.components.header.subtitleForeground', defaultValue: 'semantic.colors.textMuted', tokenRef: { tokenType: 'color' } },
79
- { id: 'chat-header-action-icons-fg', label: 'Header Button Icons', type: 'token-ref', path: 'theme.components.header.actionIconForeground', defaultValue: 'semantic.colors.textMuted', tokenRef: { tokenType: 'color' } },
85
+ { id: 'chat-header-title-fg', label: 'Header Title Color', description: 'Blank follows the header foreground', type: 'token-ref', path: 'theme.components.header.titleForeground', defaultValue: '', tokenRef: { tokenType: 'color' } },
86
+ { id: 'chat-header-subtitle-fg', label: 'Header Subtitle Color', description: 'Blank follows the 72% foreground mix', type: 'token-ref', path: 'theme.components.header.subtitleForeground', defaultValue: '', tokenRef: { tokenType: 'color' } },
87
+ { id: 'chat-header-action-icons-fg', label: 'Header Button Icons', description: 'Blank follows the 72% foreground mix', type: 'token-ref', path: 'theme.components.header.actionIconForeground', defaultValue: '', tokenRef: { tokenType: 'color' } },
80
88
  { id: 'chat-msg-user-bg', label: 'User Message Background', type: 'token-ref', path: 'theme.components.message.user.background', defaultValue: 'semantic.colors.primary', tokenRef: { tokenType: 'color' } },
81
89
  { id: 'chat-msg-user-text', label: 'User Message Text', type: 'token-ref', path: 'theme.components.message.user.text', defaultValue: 'semantic.colors.textInverse', tokenRef: { tokenType: 'color' } },
82
90
  { id: 'chat-msg-assistant-bg', label: 'Assistant Message Background', type: 'token-ref', path: 'theme.components.message.assistant.background', defaultValue: 'semantic.colors.container', tokenRef: { tokenType: 'color' } },
@@ -316,6 +324,7 @@ const launcherLayoutSectionDef: SectionDef = {
316
324
  fields: [
317
325
  { id: 'launcher-size', label: 'Size', type: 'slider', path: 'theme.components.launcher.size', defaultValue: '60px', slider: { min: 32, max: 80, step: 2 } },
318
326
  { id: 'launcher-icon-size', label: 'Icon Size', type: 'slider', path: 'theme.components.launcher.iconSize', defaultValue: '28px', slider: { min: 16, max: 48, step: 2 } },
327
+ { id: 'launcher-icon-stroke-width', label: 'Icon Stroke Width', description: 'Unitless line weight for the agent glyph and the call-to-action arrow', type: 'slider', path: 'theme.components.launcher.iconStrokeWidth', defaultValue: '1.5', slider: { min: 1, max: 2.5, step: 0.25, unit: 'none' }, parseValue: parseSliderUnitless },
319
328
  { id: 'launcher-border-radius', label: 'Border Radius', type: 'select', path: 'theme.components.launcher.borderRadius', defaultValue: 'palette.radius.full', options: [
320
329
  { value: 'palette.radius.md', label: 'Medium' },
321
330
  { value: 'palette.radius.lg', label: 'Large' },
@@ -332,6 +341,115 @@ const launcherLayoutSectionDef: SectionDef = {
332
341
  ],
333
342
  };
334
343
 
344
+ const headerControlsSectionDef: SectionDef = {
345
+ id: 'comp-header-controls',
346
+ title: 'Header Controls',
347
+ description:
348
+ 'One box, glyph, and stroke shared by every header icon button: close, clear chat, trailing actions, and Messages, plus the strip height.',
349
+ collapsed: true,
350
+ fields: [
351
+ { id: 'header-min-height', label: 'Header Min Height', description: 'CSS length, e.g. 65px. Blank stays at the natural height', type: 'text', path: 'theme.components.header.minHeight', defaultValue: '', parseValue: parseOptionalCssValue },
352
+ { id: 'header-control-size', label: 'Control Size', description: 'The button box. Coarse pointers still floor the hit area at 40px', type: 'slider', path: 'theme.components.header.controlSize', defaultValue: '32px', slider: { min: 24, max: 64, step: 2 }, parseValue: parseSliderPxLength },
353
+ { id: 'header-control-icon-size', label: 'Icon Size', description: 'The glyph inside the button', type: 'slider', path: 'theme.components.header.controlIconSize', defaultValue: '20px', slider: { min: 12, max: 40, step: 1 }, parseValue: parseSliderPxLength },
354
+ { id: 'header-control-stroke-width', label: 'Icon Stroke Width', description: 'Unitless line weight. The close X renders at 0.7 of it so its sparser glyph matches', type: 'slider', path: 'theme.components.header.controlStrokeWidth', defaultValue: '1.5', slider: { min: 1, max: 2.5, step: 0.25, unit: 'none' }, parseValue: parseSliderUnitless },
355
+ ],
356
+ };
357
+
358
+ // The Messages rail's own top strip, beside the widget header in rail
359
+ // presentation. Its color lives in COMPONENT_COLOR_SECTIONS.
360
+ const historyRailHeaderShapeSectionDef: SectionDef = {
361
+ id: 'comp-history-rail-header',
362
+ title: 'Messages Rail Header',
363
+ description:
364
+ 'The Messages rail top strip. Match its height to the header so both read as one band.',
365
+ collapsed: true,
366
+ fields: [
367
+ { id: 'rail-header-min-height', label: 'Min Height', description: 'CSS length. Blank follows the header min height', type: 'text', path: 'theme.components.history.railHeader.minHeight', defaultValue: '', parseValue: parseOptionalCssValue },
368
+ { id: 'rail-header-border', label: 'Bottom Border', description: 'Full border-bottom shorthand, e.g. 1px solid #333', type: 'text', path: 'theme.components.history.railHeader.border', defaultValue: '', parseValue: parseOptionalCssValue },
369
+ ],
370
+ };
371
+
372
+ const historyRailHeaderColorsSectionDef: SectionDef = {
373
+ id: 'comp-history-rail-header-colors',
374
+ title: 'Messages Rail Header Colors',
375
+ collapsed: true,
376
+ fields: [
377
+ { id: 'rail-header-bg', label: 'Background', type: 'token-ref', path: 'theme.components.history.railHeader.background', defaultValue: 'semantic.colors.container', tokenRef: { tokenType: 'color' } },
378
+ ],
379
+ };
380
+
381
+ // The floating rail a collapsed overlay rail opens on hover. Its width and
382
+ // edge stay features.history.rail config; only the surface is themeable.
383
+ const historyOverlayShapeSectionDef: SectionDef = {
384
+ id: 'comp-history-overlay',
385
+ title: 'Messages Floating Rail',
386
+ description:
387
+ 'The Messages rail floating over the conversation, when a collapsed rail opens on hover.',
388
+ collapsed: true,
389
+ fields: [
390
+ { id: 'history-overlay-margin', label: 'Margin', description: 'Gap from the trigger, the docked edge and the bottom', type: 'text', path: 'theme.components.history.overlay.margin', defaultValue: '', parseValue: parseOptionalCssValue },
391
+ { id: 'history-overlay-radius', label: 'Corner Radius', description: 'CSS length on all four corners. Blank keeps 16px', type: 'text', path: 'theme.components.history.overlay.borderRadius', defaultValue: '', parseValue: parseOptionalCssValue },
392
+ { id: 'history-overlay-shadow', label: 'Shadow', description: 'Full box-shadow shorthand. Blank keeps the built-in elevation', type: 'text', path: 'theme.components.history.overlay.shadow', defaultValue: '', parseValue: parseOptionalCssValue },
393
+ ],
394
+ };
395
+
396
+ const historyOverlayColorsSectionDef: SectionDef = {
397
+ id: 'comp-history-overlay-colors',
398
+ title: 'Messages Floating Rail Colors',
399
+ collapsed: true,
400
+ fields: [
401
+ { id: 'history-overlay-bg', label: 'Background', type: 'token-ref', path: 'theme.components.history.overlay.background', defaultValue: 'semantic.colors.container', tokenRef: { tokenType: 'color' } },
402
+ ],
403
+ };
404
+
405
+ // Enter/exit motion of the Messages surface. Durations, not colors, so it
406
+ // stays a shape section and never forks per scheme.
407
+ const historyMotionSectionDef: SectionDef = {
408
+ id: 'comp-history-motion',
409
+ title: 'Messages Motion',
410
+ description:
411
+ 'Enter and exit animation of the Messages body. The bar is persistent chrome and never animates; reduced-motion preferences always win.',
412
+ collapsed: true,
413
+ fields: [
414
+ { id: 'history-motion-enter-ms', label: 'Enter Duration', description: 'Milliseconds, e.g. 180. 0 disables the entrance. Blank keeps 180', type: 'text', path: 'theme.components.history.motion.enterDurationMs', defaultValue: '', parseValue: parseOptionalCssValue },
415
+ { id: 'history-motion-enter-easing', label: 'Enter Easing', description: 'CSS easing, e.g. ease-out or cubic-bezier(0, 0, 0.2, 1)', type: 'text', path: 'theme.components.history.motion.enterEasing', defaultValue: '', parseValue: parseOptionalCssValue },
416
+ { id: 'history-motion-exit-ms', label: 'Exit Duration', description: 'Milliseconds, e.g. 160. 0 disables the exit. Blank keeps 160', type: 'text', path: 'theme.components.history.motion.exitDurationMs', defaultValue: '', parseValue: parseOptionalCssValue },
417
+ { id: 'history-motion-exit-easing', label: 'Exit Easing', description: 'CSS easing, e.g. ease-in or cubic-bezier(0.4, 0, 1, 1)', type: 'text', path: 'theme.components.history.motion.exitEasing', defaultValue: '', parseValue: parseOptionalCssValue },
418
+ ],
419
+ };
420
+
421
+ // The portaled tooltip on icon controls. Its colors live in
422
+ // COMPONENT_COLOR_SECTIONS.
423
+ const tooltipShapeSectionDef: SectionDef = {
424
+ id: 'comp-tooltip',
425
+ title: 'Tooltip',
426
+ description:
427
+ 'The hover and focus tooltip on icon controls, including its shortcut hint chip.',
428
+ collapsed: true,
429
+ fields: [
430
+ { id: 'tooltip-radius', label: 'Corner Radius', type: 'select', path: 'theme.components.tooltip.borderRadius', defaultValue: 'palette.radius.sm', options: [
431
+ { value: 'palette.radius.none', label: 'None' },
432
+ { value: 'palette.radius.sm', label: 'Small' },
433
+ { value: 'palette.radius.md', label: 'Medium' },
434
+ { value: 'palette.radius.lg', label: 'Large' },
435
+ { value: 'palette.radius.xl', label: 'Extra Large' },
436
+ ] },
437
+ { id: 'tooltip-font-size', label: 'Font Size', description: 'CSS length. Blank stays at 12px', type: 'text', path: 'theme.components.tooltip.fontSize', defaultValue: '', parseValue: parseOptionalCssValue },
438
+ { id: 'tooltip-arrow', label: 'Show Arrow', description: 'The caret pointing at the control', type: 'toggle', path: 'theme.components.tooltip.arrow', defaultValue: true },
439
+ ],
440
+ };
441
+
442
+ const tooltipColorsSectionDef: SectionDef = {
443
+ id: 'comp-tooltip-colors',
444
+ title: 'Tooltip Colors',
445
+ collapsed: true,
446
+ fields: [
447
+ { id: 'tooltip-bg', label: 'Background', type: 'token-ref', path: 'theme.components.tooltip.background', defaultValue: 'palette.colors.gray.900', tokenRef: { tokenType: 'color' } },
448
+ { id: 'tooltip-fg', label: 'Label', type: 'token-ref', path: 'theme.components.tooltip.foreground', defaultValue: 'palette.colors.primary.50', tokenRef: { tokenType: 'color' } },
449
+ { id: 'tooltip-hint-fg', label: 'Shortcut Hint', type: 'token-ref', path: 'theme.components.tooltip.hintForeground', defaultValue: 'palette.colors.gray.400', tokenRef: { tokenType: 'color' } },
450
+ ],
451
+ };
452
+
335
453
  const messageShapeSectionDef: SectionDef = {
336
454
  id: 'comp-message-shape',
337
455
  title: 'Message Shape',
@@ -401,12 +519,13 @@ const headerColorsSectionDef: SectionDef = {
401
519
  collapsed: true,
402
520
  fields: [
403
521
  { id: 'header-bg', label: 'Background', type: 'token-ref', path: 'theme.components.header.background', defaultValue: 'semantic.colors.surface', tokenRef: { tokenType: 'color' } },
522
+ { id: 'header-fg', label: 'Foreground', description: 'Anchors the header text. Background and foreground together derive every color left blank below', type: 'token-ref', path: 'theme.components.header.foreground', defaultValue: '', tokenRef: { tokenType: 'color' } },
404
523
  { id: 'header-icon-bg', label: 'Icon background', type: 'token-ref', path: 'theme.components.header.iconBackground', defaultValue: 'semantic.colors.primary', tokenRef: { tokenType: 'color' } },
405
524
  { id: 'header-icon-fg', label: 'Icon color', type: 'token-ref', path: 'theme.components.header.iconForeground', defaultValue: 'semantic.colors.textInverse', tokenRef: { tokenType: 'color' } },
406
- { id: 'header-title-fg', label: 'Title color', type: 'token-ref', path: 'theme.components.header.titleForeground', defaultValue: 'semantic.colors.primary', tokenRef: { tokenType: 'color' } },
407
- { id: 'header-subtitle-fg', label: 'Subtitle color', type: 'token-ref', path: 'theme.components.header.subtitleForeground', defaultValue: 'semantic.colors.textMuted', tokenRef: { tokenType: 'color' } },
408
- { id: 'header-action-icons-fg', label: 'Clear / close icons', type: 'token-ref', path: 'theme.components.header.actionIconForeground', defaultValue: 'semantic.colors.textMuted', tokenRef: { tokenType: 'color' } },
409
- { id: 'header-border', label: 'Border', type: 'token-ref', path: 'theme.components.header.border', defaultValue: 'semantic.colors.border', tokenRef: { tokenType: 'color' } },
525
+ { id: 'header-title-fg', label: 'Title color', description: 'Blank follows the header foreground', type: 'token-ref', path: 'theme.components.header.titleForeground', defaultValue: '', tokenRef: { tokenType: 'color' } },
526
+ { id: 'header-subtitle-fg', label: 'Subtitle color', description: 'Blank follows a 72% mix of the foreground over the background', type: 'token-ref', path: 'theme.components.header.subtitleForeground', defaultValue: '', tokenRef: { tokenType: 'color' } },
527
+ { id: 'header-action-icons-fg', label: 'Clear / close icons', description: 'Blank follows the same 72% foreground mix as the subtitle', type: 'token-ref', path: 'theme.components.header.actionIconForeground', defaultValue: '', tokenRef: { tokenType: 'color' } },
528
+ { id: 'header-border', label: 'Border', description: 'Blank follows a 14% mix of the foreground over the background', type: 'token-ref', path: 'theme.components.header.border', defaultValue: '', tokenRef: { tokenType: 'color' } },
410
529
  ],
411
530
  };
412
531
 
@@ -566,6 +685,13 @@ const textStylesSectionDef: SectionDef = {
566
685
  export const COMPONENT_SHAPE_SECTIONS: SectionDef[] = [
567
686
  panelLayoutSectionDef,
568
687
  launcherLayoutSectionDef,
688
+ // Sizes, not colors: kept out of COMPONENT_COLOR_SECTIONS so scopeSection
689
+ // never forks them per light/dark.
690
+ headerControlsSectionDef,
691
+ historyRailHeaderShapeSectionDef,
692
+ historyOverlayShapeSectionDef,
693
+ historyMotionSectionDef,
694
+ tooltipShapeSectionDef,
569
695
  messageShapeSectionDef,
570
696
  inputShapeSectionDef,
571
697
  composerSpacingSectionDef,
@@ -577,6 +703,9 @@ export const COMPONENT_SHAPE_SECTIONS: SectionDef[] = [
577
703
  /** Component color sections (can be scoped for light/dark) */
578
704
  export const COMPONENT_COLOR_SECTIONS: SectionDef[] = [
579
705
  headerColorsSectionDef,
706
+ historyRailHeaderColorsSectionDef,
707
+ historyOverlayColorsSectionDef,
708
+ tooltipColorsSectionDef,
580
709
  messageColorsSectionDef,
581
710
  inputColorsSectionDef,
582
711
  buttonColorsSectionDef,
@@ -163,13 +163,15 @@ export interface ContrastPair {
163
163
  key: string;
164
164
  label: string;
165
165
  fg: string;
166
+ /** Tried in order when `fg` is unset; suggestions still write to `fg`. */
167
+ fgFallbacks?: string[];
166
168
  bg: string;
167
169
  }
168
170
 
169
171
  export const CONTRAST_PAIRS: ContrastPair[] = [
170
172
  { key: 'user-message', label: 'User message text', fg: 'components.message.user.text', bg: 'components.message.user.background' },
171
173
  { key: 'assistant-message', label: 'Assistant message text', fg: 'components.message.assistant.text', bg: 'components.message.assistant.background' },
172
- { key: 'header', label: 'Header title', fg: 'components.header.titleForeground', bg: 'components.header.background' },
174
+ { key: 'header', label: 'Header title', fg: 'components.header.titleForeground', fgFallbacks: ['components.header.foreground'], bg: 'components.header.background' },
173
175
  { key: 'primary-button', label: 'Primary button label', fg: 'components.button.primary.foreground', bg: 'components.button.primary.background' },
174
176
  { key: 'input', label: 'Input placeholder', fg: 'components.input.placeholder', bg: 'components.input.background' },
175
177
  { key: 'link', label: 'Link text', fg: 'components.markdown.link.foreground', bg: 'semantic.colors.background' },
@@ -178,6 +180,18 @@ export const CONTRAST_PAIRS: ContrastPair[] = [
178
180
  { key: 'surface', label: 'Body text on surface', fg: 'semantic.colors.text', bg: 'semantic.colors.surface' },
179
181
  ];
180
182
 
183
+ function resolveContrastForeground(
184
+ state: ThemeEditorLike,
185
+ pair: ContrastPair,
186
+ prefix: 'theme' | 'darkTheme'
187
+ ): string | null {
188
+ for (const path of [pair.fg, ...(pair.fgFallbacks ?? [])]) {
189
+ const color = resolveColor(state, path, prefix);
190
+ if (color) return color;
191
+ }
192
+ return null;
193
+ }
194
+
181
195
  /**
182
196
  * The contrast-pair keys relevant to a role, derived by intersecting the role's
183
197
  * target token paths with the contrast pairs. This replaces a hand-maintained
@@ -275,7 +289,7 @@ export function runContrastChecks(
275
289
  for (const v of variants) {
276
290
  const prefix = v === 'light' ? 'theme' : 'darkTheme';
277
291
  for (const pair of pairs) {
278
- const fg = resolveColor(state, pair.fg, prefix);
292
+ const fg = resolveContrastForeground(state, pair, prefix);
279
293
  const bg = resolveColor(state, pair.bg, prefix);
280
294
  if (!fg || !bg) continue;
281
295
  const ratio = round2(wcagContrastRatio(fg, bg));
@@ -110,7 +110,8 @@ export const THEME_TOKEN_DOCS = {
110
110
  },
111
111
  launcher: {
112
112
  description: 'Floating launcher button.',
113
- properties: 'size (60px), iconSize (28px), borderRadius, shadow.',
113
+ properties:
114
+ 'size (60px), iconSize (28px), iconStrokeWidth (2, unitless glyph stroke weight; applies to registry-icon launchers, inert for emoji and image icons), borderRadius, shadow.',
114
115
  },
115
116
  panel: {
116
117
  description: 'Chat panel container.',
@@ -118,11 +119,24 @@ export const THEME_TOKEN_DOCS = {
118
119
  'width, maxWidth (440px), height (600px), maxHeight, borderRadius, shadow, inset (16px, gap around a detached panel), canvasBackground (transparent, fills the region behind a detached panel in docked and inline embed modes; a no-op in sidebar mode, where the gap stays click-through and the host page shows through like floating). inset and canvasBackground apply only when launcher.detachedPanel is true or artifacts.layout.paneAppearance is "detached".',
119
120
  },
120
121
  header: {
121
- description: 'Chat panel header.',
122
+ description:
123
+ 'Chat panel header. background and foreground are the pair that colors the band: set those two and the rest of the header text follows.',
122
124
  properties:
123
- 'background, border, borderRadius, padding, iconBackground, iconForeground, actionIconForeground, shadow, borderBottom, title and subtitle (each fontFamily, fontSize, fontWeight, lineHeight, letterSpacing, color).',
125
+ 'background, foreground (anchors the header text; the title takes it directly, the subtitle and action icons take color-mix(in srgb, foreground 72%, background), and the border takes color-mix(in srgb, foreground 14%, background)), border, borderRadius, padding, iconBackground, iconForeground (the avatar tile, which keeps its own defaults and never derives), actionIconForeground, controlSize (32px, the box of every header icon button), controlIconSize (20px, the glyph inside it), controlStrokeWidth (1.5, unitless glyph stroke weight; sparse-viewBox glyphs such as the close X scale it by 0.7), shadow, borderBottom, minHeight (height floor for the header strip), title and subtitle (each fontFamily, fontSize, fontWeight, lineHeight, letterSpacing, color). Derivation only fills keys the theme leaves unset, so any explicit border, titleForeground, subtitleForeground, actionIconForeground, title.color, or subtitle.color wins. With no foreground either, each key falls back as before: the primary color for the title, the muted text color for the subtitle and action icons, the divider color for the border.',
124
126
  legacy:
125
- 'titleForeground and subtitleForeground are legacy aliases of header.title.color and header.subtitle.color. Both still work; the title/subtitle color wins when both are set.',
127
+ 'titleForeground and subtitleForeground are legacy aliases of header.title.color and header.subtitle.color. Both still work; the title/subtitle color wins when both are set, and either alias still outranks the foreground derivation.',
128
+ },
129
+ history: {
130
+ description:
131
+ 'Messages (conversation history) chrome and motion. railHeader styles the rail sidebar top strip, which runs beside the widget header in rail presentation; overlay styles the floating rail a collapsed overlay rail opens on hover; motion times the surface entering and leaving.',
132
+ properties:
133
+ 'railHeader.background (defaults to the rail surface), railHeader.border (full border-bottom shorthand, default 0), railHeader.minHeight (defaults to components.header.minHeight, then 56px). Pin railHeader.minHeight and header.minHeight to the same value so both top strips read as one band. railHeader.title takes TextStyleTokens for the strip view title ("Messages"): color (defaults to the text color; the sidebar identity reads at full strength), fontFamily, fontSize (14px), fontWeight (600), lineHeight, letterSpacing. listHeading takes the same TextStyleTokens for the rail-only "Conversations" heading over the list block (14px/600 in the text color); groupHeading styles the muted sub-headers, both the date groups ("Today", "Yesterday") and rail nav section titles (rail 13px/500, muted text color). Every key of all three is pinned in the widget CSS, so host-page h2/h3 rules cannot restyle them. overlay.margin (8px, the gap from the trigger, the docked edge and the bottom), overlay.borderRadius (16px), overlay.shadow (0 12px 40px rgba(0, 0, 0, 0.25)), overlay.background (defaults to the rail surface). The floating rail keeps features.history.rail width and side, which are config, not tokens. motion.enterDurationMs (180) and motion.exitDurationMs (160) are bare millisecond numbers, 0 disables that leg; motion.enterEasing (cubic-bezier(0, 0, 0.2, 1)) and motion.exitEasing (cubic-bezier(0.4, 0, 1, 1)) take any CSS easing. Only the body animates, and prefers-reduced-motion always wins.',
134
+ },
135
+ tooltip: {
136
+ description:
137
+ 'The portaled tooltip on icon controls, plus the muted shortcut hint chip after its label.',
138
+ properties:
139
+ 'background (#111827), foreground (#ffffff), hintForeground (rgba(255, 255, 255, 0.55)), borderRadius (palette.radius.sm), fontSize (12px), padding (6px 12px), shadow, maxWidth (min(320px, calc(100vw - 16px))), arrow (boolean, default true; false drops the caret). Unset keys keep the built-in dark look.',
126
140
  },
127
141
  message: {
128
142
  description: 'Chat message bubbles.',
@@ -211,12 +225,12 @@ export const THEME_TOKEN_DOCS = {
211
225
  'backgroundColor, textColor, borderWidth, borderColor, paddingX, paddingY, iconText, iconName, size.',
212
226
  },
213
227
  closeButton: {
214
- description: 'Close button (on launcher config).',
228
+ description: 'Close button (on launcher config). Unset closeButtonSize means theme.components.header.controlSize owns the box.',
215
229
  properties:
216
230
  'closeButtonSize, closeButtonColor, closeButtonBackgroundColor, closeButtonBorderWidth, closeButtonBorderColor, closeButtonBorderRadius.',
217
231
  },
218
232
  clearChat: {
219
- description: 'Clear chat button (on launcher.clearChat config).',
233
+ description: 'Clear chat button (on launcher.clearChat config). Unset size means theme.components.header.controlSize owns the box.',
220
234
  properties:
221
235
  'enabled, iconColor, backgroundColor, borderWidth, borderColor, borderRadius, size.',
222
236
  },