@runtypelabs/persona 2.3.0 → 3.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 (41) hide show
  1. package/README.md +221 -4
  2. package/dist/index.cjs +42 -42
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +832 -571
  5. package/dist/index.d.ts +832 -571
  6. package/dist/index.global.js +87 -87
  7. package/dist/index.global.js.map +1 -1
  8. package/dist/index.js +42 -42
  9. package/dist/index.js.map +1 -1
  10. package/dist/widget.css +205 -15
  11. package/package.json +2 -2
  12. package/src/components/artifact-card.ts +39 -5
  13. package/src/components/artifact-pane.ts +67 -126
  14. package/src/components/composer-builder.ts +3 -23
  15. package/src/components/header-builder.ts +29 -34
  16. package/src/components/header-layouts.ts +109 -41
  17. package/src/components/launcher.ts +10 -7
  18. package/src/components/message-bubble.ts +7 -11
  19. package/src/components/panel.ts +4 -4
  20. package/src/defaults.ts +22 -93
  21. package/src/index.ts +20 -7
  22. package/src/presets.ts +66 -51
  23. package/src/runtime/host-layout.test.ts +196 -0
  24. package/src/runtime/host-layout.ts +265 -27
  25. package/src/runtime/init.test.ts +77 -7
  26. package/src/styles/widget.css +205 -15
  27. package/src/types/theme.ts +76 -0
  28. package/src/types.ts +86 -97
  29. package/src/ui.docked.test.ts +203 -7
  30. package/src/ui.ts +129 -88
  31. package/src/utils/buttons.ts +417 -0
  32. package/src/utils/code-generators.test.ts +43 -7
  33. package/src/utils/code-generators.ts +9 -25
  34. package/src/utils/deep-merge.ts +26 -0
  35. package/src/utils/dock.ts +18 -5
  36. package/src/utils/dropdown.ts +178 -0
  37. package/src/utils/sanitize.ts +1 -1
  38. package/src/utils/theme.test.ts +90 -15
  39. package/src/utils/theme.ts +20 -46
  40. package/src/utils/tokens.ts +108 -11
  41. package/src/utils/migration.ts +0 -220
package/src/presets.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { AgentWidgetConfig } from './types';
1
+ import type { AgentWidgetConfig } from "./types";
2
+ import type { DeepPartial, PersonaTheme } from "./types/theme";
2
3
 
3
4
  /**
4
5
  * A named preset containing partial widget configuration.
@@ -11,51 +12,71 @@ export interface WidgetPreset {
11
12
  config: Partial<AgentWidgetConfig>;
12
13
  }
13
14
 
15
+ /** Shopping palette + semantic roles (matches prior shop preset visuals). */
16
+ const SHOP_THEME: DeepPartial<PersonaTheme> = {
17
+ palette: {
18
+ colors: {
19
+ primary: { 500: "#111827" },
20
+ accent: { 600: "#1d4ed8" },
21
+ gray: {
22
+ 50: "#ffffff",
23
+ 100: "#f8fafc",
24
+ 200: "#f1f5f9",
25
+ 500: "#6b7280",
26
+ 900: "#000000",
27
+ },
28
+ },
29
+ radius: {
30
+ sm: "0.75rem",
31
+ md: "1rem",
32
+ lg: "1.5rem",
33
+ launcher: "9999px",
34
+ button: "9999px",
35
+ },
36
+ },
37
+ semantic: {
38
+ colors: {
39
+ primary: "palette.colors.primary.500",
40
+ textInverse: "palette.colors.gray.50",
41
+ },
42
+ },
43
+ };
44
+
45
+ const PANEL_EDGELESS_THEME: DeepPartial<PersonaTheme> = {
46
+ components: {
47
+ panel: {
48
+ borderRadius: "0",
49
+ shadow: "none",
50
+ },
51
+ },
52
+ };
53
+
14
54
  /**
15
55
  * Shopping / e-commerce preset.
16
56
  * Dark header, rounded launchers, shopping-oriented copy.
17
57
  */
18
58
  export const PRESET_SHOP: WidgetPreset = {
19
- id: 'shop',
20
- label: 'Shopping Assistant',
59
+ id: "shop",
60
+ label: "Shopping Assistant",
21
61
  config: {
22
- theme: {
23
- primary: '#111827',
24
- accent: '#1d4ed8',
25
- surface: '#ffffff',
26
- muted: '#6b7280',
27
- container: '#f8fafc',
28
- border: '#f1f5f9',
29
- divider: '#f1f5f9',
30
- messageBorder: '#f1f5f9',
31
- inputBackground: '#ffffff',
32
- callToAction: '#000000',
33
- callToActionBackground: '#ffffff',
34
- sendButtonBackgroundColor: '#111827',
35
- sendButtonTextColor: '#ffffff',
36
- radiusSm: '0.75rem',
37
- radiusMd: '1rem',
38
- radiusLg: '1.5rem',
39
- launcherRadius: '9999px',
40
- buttonRadius: '9999px',
41
- },
62
+ theme: SHOP_THEME,
42
63
  launcher: {
43
- title: 'Shopping Assistant',
44
- subtitle: 'Here to help you find what you need',
45
- agentIconText: '🛍️',
46
- position: 'bottom-right',
47
- width: 'min(400px, calc(100vw - 24px))',
64
+ title: "Shopping Assistant",
65
+ subtitle: "Here to help you find what you need",
66
+ agentIconText: "🛍️",
67
+ position: "bottom-right",
68
+ width: "min(400px, calc(100vw - 24px))",
48
69
  },
49
70
  copy: {
50
- welcomeTitle: 'Welcome to our shop!',
51
- welcomeSubtitle: 'I can help you find products and answer questions',
52
- inputPlaceholder: 'Ask me anything...',
53
- sendButtonLabel: 'Send',
71
+ welcomeTitle: "Welcome to our shop!",
72
+ welcomeSubtitle: "I can help you find products and answer questions",
73
+ inputPlaceholder: "Ask me anything...",
74
+ sendButtonLabel: "Send",
54
75
  },
55
76
  suggestionChips: [
56
- 'What can you help me with?',
57
- 'Tell me about your features',
58
- 'How does this work?',
77
+ "What can you help me with?",
78
+ "Tell me about your features",
79
+ "How does this work?",
59
80
  ],
60
81
  },
61
82
  };
@@ -65,8 +86,8 @@ export const PRESET_SHOP: WidgetPreset = {
65
86
  * Stripped-down header, no launcher button, suitable for inline embeds.
66
87
  */
67
88
  export const PRESET_MINIMAL: WidgetPreset = {
68
- id: 'minimal',
69
- label: 'Minimal',
89
+ id: "minimal",
90
+ label: "Minimal",
70
91
  config: {
71
92
  launcher: {
72
93
  enabled: false,
@@ -74,17 +95,14 @@ export const PRESET_MINIMAL: WidgetPreset = {
74
95
  },
75
96
  layout: {
76
97
  header: {
77
- layout: 'minimal',
98
+ layout: "minimal",
78
99
  showCloseButton: false,
79
100
  },
80
101
  messages: {
81
- layout: 'minimal',
102
+ layout: "minimal",
82
103
  },
83
104
  },
84
- theme: {
85
- panelBorderRadius: '0',
86
- panelShadow: 'none',
87
- },
105
+ theme: PANEL_EDGELESS_THEME,
88
106
  },
89
107
  };
90
108
 
@@ -93,8 +111,8 @@ export const PRESET_MINIMAL: WidgetPreset = {
93
111
  * No launcher, content-max-width constrained, minimal header.
94
112
  */
95
113
  export const PRESET_FULLSCREEN: WidgetPreset = {
96
- id: 'fullscreen',
97
- label: 'Fullscreen Assistant',
114
+ id: "fullscreen",
115
+ label: "Fullscreen Assistant",
98
116
  config: {
99
117
  launcher: {
100
118
  enabled: false,
@@ -102,15 +120,12 @@ export const PRESET_FULLSCREEN: WidgetPreset = {
102
120
  },
103
121
  layout: {
104
122
  header: {
105
- layout: 'minimal',
123
+ layout: "minimal",
106
124
  showCloseButton: false,
107
125
  },
108
- contentMaxWidth: '72ch',
109
- },
110
- theme: {
111
- panelBorderRadius: '0',
112
- panelShadow: 'none',
126
+ contentMaxWidth: "72ch",
113
127
  },
128
+ theme: PANEL_EDGELESS_THEME,
114
129
  },
115
130
  };
116
131
 
@@ -0,0 +1,196 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, describe, expect, it } from "vitest";
4
+
5
+ import { createWidgetHostLayout } from "./host-layout";
6
+
7
+ describe("createWidgetHostLayout docked", () => {
8
+ afterEach(() => {
9
+ document.body.innerHTML = "";
10
+ });
11
+
12
+ it("reserves no dock column when panel is closed (always 0px)", () => {
13
+ const parent = document.createElement("div");
14
+ document.body.appendChild(parent);
15
+ const target = document.createElement("div");
16
+ parent.appendChild(target);
17
+
18
+ const layout = createWidgetHostLayout(target, {
19
+ launcher: {
20
+ mountMode: "docked",
21
+ autoExpand: false,
22
+ dock: { width: "320px" },
23
+ },
24
+ });
25
+
26
+ const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
27
+ expect(dockSlot).not.toBeNull();
28
+ expect(dockSlot?.style.minWidth).toBe("0px");
29
+ expect(dockSlot?.style.overflow).toBe("hidden");
30
+
31
+ layout.syncWidgetState({ open: true, launcherEnabled: true });
32
+ expect(dockSlot?.style.width).toBe("320px");
33
+ expect(dockSlot?.style.overflow).toBe("visible");
34
+
35
+ layout.syncWidgetState({ open: false, launcherEnabled: true });
36
+ expect(dockSlot?.style.minWidth).toBe("0px");
37
+ expect(dockSlot?.style.overflow).toBe("hidden");
38
+
39
+ layout.destroy();
40
+ });
41
+
42
+ it("disables dock width transition when dock.animate is false", () => {
43
+ const parent = document.createElement("div");
44
+ document.body.appendChild(parent);
45
+ const target = document.createElement("div");
46
+ parent.appendChild(target);
47
+
48
+ const layout = createWidgetHostLayout(target, {
49
+ launcher: {
50
+ mountMode: "docked",
51
+ autoExpand: false,
52
+ dock: { width: "320px", animate: false },
53
+ },
54
+ });
55
+
56
+ const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
57
+ expect(dockSlot?.style.transition).toBe("none");
58
+
59
+ layout.destroy();
60
+ });
61
+
62
+ it("overlay reveal keeps panel width and translates off-screen when closed", () => {
63
+ const parent = document.createElement("div");
64
+ document.body.appendChild(parent);
65
+ const target = document.createElement("div");
66
+ parent.appendChild(target);
67
+
68
+ const layout = createWidgetHostLayout(target, {
69
+ launcher: {
70
+ mountMode: "docked",
71
+ autoExpand: false,
72
+ dock: { width: "320px", reveal: "overlay" },
73
+ },
74
+ });
75
+
76
+ const shell = layout.shell;
77
+ const dockSlot = shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
78
+ expect(shell?.dataset.personaDockReveal).toBe("overlay");
79
+ expect(shell?.style.overflow).toBe("hidden");
80
+ expect(dockSlot?.style.width).toBe("320px");
81
+ expect(dockSlot?.style.transform).toBe("translateX(100%)");
82
+
83
+ layout.syncWidgetState({ open: true, launcherEnabled: true });
84
+ expect(dockSlot?.style.width).toBe("320px");
85
+ expect(dockSlot?.style.transform).toBe("translateX(0)");
86
+
87
+ layout.destroy();
88
+ });
89
+
90
+ it("overlay reveal uses translateX(-100%) on the left side when closed", () => {
91
+ const parent = document.createElement("div");
92
+ document.body.appendChild(parent);
93
+ const target = document.createElement("div");
94
+ parent.appendChild(target);
95
+
96
+ const layout = createWidgetHostLayout(target, {
97
+ launcher: {
98
+ mountMode: "docked",
99
+ autoExpand: false,
100
+ dock: { width: "300px", side: "left", reveal: "overlay" },
101
+ },
102
+ });
103
+
104
+ const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
105
+ expect(dockSlot?.style.transform).toBe("translateX(-100%)");
106
+ expect(dockSlot?.style.left).toBe("0px");
107
+
108
+ layout.destroy();
109
+ });
110
+
111
+ it("push reveal translates a track; main column width stays fixed in px", () => {
112
+ const parent = document.createElement("div");
113
+ parent.style.width = "800px";
114
+ document.body.appendChild(parent);
115
+ const target = document.createElement("div");
116
+ parent.appendChild(target);
117
+
118
+ const dockConfig = {
119
+ mountMode: "docked" as const,
120
+ autoExpand: false,
121
+ dock: { width: "320px", reveal: "push" as const },
122
+ };
123
+
124
+ const layout = createWidgetHostLayout(target, { launcher: dockConfig });
125
+ const shell = layout.shell!;
126
+ Object.defineProperty(shell, "clientWidth", { get: () => 800, configurable: true });
127
+ layout.updateConfig({ launcher: dockConfig });
128
+
129
+ const pushTrack = shell.querySelector<HTMLElement>('[data-persona-dock-role="push-track"]');
130
+ const contentSlot = shell.querySelector<HTMLElement>('[data-persona-dock-role="content"]');
131
+ expect(pushTrack).not.toBeNull();
132
+ expect(shell.dataset.personaDockReveal).toBe("push");
133
+ expect(contentSlot?.style.width).toBe("800px");
134
+ expect(pushTrack?.style.width).toBe("1120px");
135
+ expect(pushTrack?.style.transform).toBe("translateX(0)");
136
+
137
+ layout.syncWidgetState({ open: true, launcherEnabled: true });
138
+ expect(pushTrack?.style.transform).toBe("translateX(-320px)");
139
+
140
+ layout.destroy();
141
+ });
142
+
143
+ it("push reveal on the left uses negative translate when closed", () => {
144
+ const parent = document.createElement("div");
145
+ parent.style.width = "600px";
146
+ document.body.appendChild(parent);
147
+ const target = document.createElement("div");
148
+ parent.appendChild(target);
149
+
150
+ const dockConfig = {
151
+ mountMode: "docked" as const,
152
+ autoExpand: false,
153
+ dock: { width: "200px", side: "left" as const, reveal: "push" as const },
154
+ };
155
+
156
+ const layout = createWidgetHostLayout(target, { launcher: dockConfig });
157
+ const shell = layout.shell!;
158
+ Object.defineProperty(shell, "clientWidth", { get: () => 600, configurable: true });
159
+ layout.updateConfig({ launcher: dockConfig });
160
+
161
+ const pushTrack = shell.querySelector<HTMLElement>('[data-persona-dock-role="push-track"]');
162
+ expect(pushTrack?.style.transform).toBe("translateX(-200px)");
163
+
164
+ layout.syncWidgetState({ open: true, launcherEnabled: true });
165
+ expect(pushTrack?.style.transform).toBe("translateX(0)");
166
+
167
+ layout.destroy();
168
+ });
169
+
170
+ it("emerge reveal keeps host width at dock.width while the column animates like resize", () => {
171
+ const parent = document.createElement("div");
172
+ document.body.appendChild(parent);
173
+ const target = document.createElement("div");
174
+ parent.appendChild(target);
175
+
176
+ const layout = createWidgetHostLayout(target, {
177
+ launcher: {
178
+ mountMode: "docked",
179
+ autoExpand: false,
180
+ dock: { width: "320px", reveal: "emerge" },
181
+ },
182
+ });
183
+
184
+ const host = layout.host;
185
+ const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
186
+ expect(layout.shell?.dataset.personaDockReveal).toBe("emerge");
187
+ expect(dockSlot?.style.minWidth).toBe("0px");
188
+ expect(host.style.width).toBe("320px");
189
+
190
+ layout.syncWidgetState({ open: true, launcherEnabled: true });
191
+ expect(dockSlot?.style.minWidth).toBe("320px");
192
+ expect(host.style.width).toBe("320px");
193
+
194
+ layout.destroy();
195
+ });
196
+ });