@runtypelabs/persona 1.36.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 (61) hide show
  1. package/README.md +1080 -0
  2. package/dist/index.cjs +140 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +2626 -0
  5. package/dist/index.d.ts +2626 -0
  6. package/dist/index.global.js +1843 -0
  7. package/dist/index.global.js.map +1 -0
  8. package/dist/index.js +140 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/install.global.js +2 -0
  11. package/dist/install.global.js.map +1 -0
  12. package/dist/widget.css +1627 -0
  13. package/package.json +79 -0
  14. package/src/@types/idiomorph.d.ts +37 -0
  15. package/src/client.test.ts +387 -0
  16. package/src/client.ts +1589 -0
  17. package/src/components/composer-builder.ts +530 -0
  18. package/src/components/feedback.ts +379 -0
  19. package/src/components/forms.ts +170 -0
  20. package/src/components/header-builder.ts +455 -0
  21. package/src/components/header-layouts.ts +303 -0
  22. package/src/components/launcher.ts +193 -0
  23. package/src/components/message-bubble.ts +528 -0
  24. package/src/components/messages.ts +54 -0
  25. package/src/components/panel.ts +204 -0
  26. package/src/components/reasoning-bubble.ts +144 -0
  27. package/src/components/registry.ts +87 -0
  28. package/src/components/suggestions.ts +97 -0
  29. package/src/components/tool-bubble.ts +288 -0
  30. package/src/defaults.ts +321 -0
  31. package/src/index.ts +175 -0
  32. package/src/install.ts +284 -0
  33. package/src/plugins/registry.ts +77 -0
  34. package/src/plugins/types.ts +95 -0
  35. package/src/postprocessors.ts +194 -0
  36. package/src/runtime/init.ts +162 -0
  37. package/src/session.ts +376 -0
  38. package/src/styles/tailwind.css +20 -0
  39. package/src/styles/widget.css +1627 -0
  40. package/src/types.ts +1635 -0
  41. package/src/ui.ts +3341 -0
  42. package/src/utils/actions.ts +227 -0
  43. package/src/utils/attachment-manager.ts +384 -0
  44. package/src/utils/code-generators.test.ts +500 -0
  45. package/src/utils/code-generators.ts +1806 -0
  46. package/src/utils/component-middleware.ts +137 -0
  47. package/src/utils/component-parser.ts +119 -0
  48. package/src/utils/constants.ts +16 -0
  49. package/src/utils/content.ts +306 -0
  50. package/src/utils/dom.ts +25 -0
  51. package/src/utils/events.ts +41 -0
  52. package/src/utils/formatting.test.ts +166 -0
  53. package/src/utils/formatting.ts +470 -0
  54. package/src/utils/icons.ts +92 -0
  55. package/src/utils/message-id.ts +37 -0
  56. package/src/utils/morph.ts +36 -0
  57. package/src/utils/positioning.ts +17 -0
  58. package/src/utils/storage.ts +72 -0
  59. package/src/utils/theme.ts +105 -0
  60. package/src/widget.css +1 -0
  61. package/widget.css +1 -0
@@ -0,0 +1,303 @@
1
+ import { createElement } from "../utils/dom";
2
+ import { renderLucideIcon } from "../utils/icons";
3
+ import { AgentWidgetConfig, AgentWidgetHeaderLayoutConfig } from "../types";
4
+ import { buildHeader, HeaderElements, attachHeaderToContainer } from "./header-builder";
5
+
6
+ export interface HeaderLayoutContext {
7
+ config: AgentWidgetConfig;
8
+ showClose?: boolean;
9
+ onClose?: () => void;
10
+ onClearChat?: () => void;
11
+ }
12
+
13
+ export type HeaderLayoutRenderer = (context: HeaderLayoutContext) => HeaderElements;
14
+
15
+ /**
16
+ * Build default header layout
17
+ * Full header with icon, title, subtitle, clear chat, and close button
18
+ */
19
+ export const buildDefaultHeader: HeaderLayoutRenderer = (context) => {
20
+ return buildHeader({
21
+ config: context.config,
22
+ showClose: context.showClose,
23
+ onClose: context.onClose,
24
+ onClearChat: context.onClearChat
25
+ });
26
+ };
27
+
28
+ /**
29
+ * Build minimal header layout
30
+ * Simplified layout with just title and close button
31
+ */
32
+ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
33
+ const { config, showClose = true, onClose } = context;
34
+ const launcher = config?.launcher ?? {};
35
+
36
+ const header = createElement(
37
+ "div",
38
+ "tvw-flex tvw-items-center tvw-justify-between tvw-bg-cw-surface tvw-px-6 tvw-py-4 tvw-border-b-cw-divider"
39
+ );
40
+
41
+ // Title only (no icon, no subtitle)
42
+ const title = createElement("span", "tvw-text-base tvw-font-semibold");
43
+ title.textContent = launcher.title ?? "Chat Assistant";
44
+
45
+ header.appendChild(title);
46
+
47
+ // Close button
48
+ const closeButtonSize = launcher.closeButtonSize ?? "32px";
49
+ const closeButtonWrapper = createElement("div", "");
50
+
51
+ const closeButton = createElement(
52
+ "button",
53
+ "tvw-inline-flex tvw-items-center tvw-justify-center tvw-rounded-full tvw-text-cw-muted hover:tvw-bg-gray-100 tvw-cursor-pointer tvw-border-none"
54
+ ) as HTMLButtonElement;
55
+ closeButton.style.height = closeButtonSize;
56
+ closeButton.style.width = closeButtonSize;
57
+ closeButton.type = "button";
58
+ closeButton.setAttribute("aria-label", "Close chat");
59
+ closeButton.style.display = showClose ? "" : "none";
60
+
61
+ const closeButtonIconName = launcher.closeButtonIconName ?? "x";
62
+ const closeIconSvg = renderLucideIcon(
63
+ closeButtonIconName,
64
+ "20px",
65
+ launcher.closeButtonColor || "",
66
+ 2
67
+ );
68
+ if (closeIconSvg) {
69
+ closeButton.appendChild(closeIconSvg);
70
+ } else {
71
+ closeButton.textContent = "×";
72
+ }
73
+
74
+ if (onClose) {
75
+ closeButton.addEventListener("click", onClose);
76
+ }
77
+
78
+ closeButtonWrapper.appendChild(closeButton);
79
+ header.appendChild(closeButtonWrapper);
80
+
81
+ // Create placeholder elements for compatibility
82
+ const iconHolder = createElement("div");
83
+ iconHolder.style.display = "none";
84
+ const headerSubtitle = createElement("span");
85
+ headerSubtitle.style.display = "none";
86
+
87
+ return {
88
+ header,
89
+ iconHolder,
90
+ headerTitle: title,
91
+ headerSubtitle,
92
+ closeButton,
93
+ closeButtonWrapper,
94
+ clearChatButton: null,
95
+ clearChatButtonWrapper: null
96
+ };
97
+ };
98
+
99
+ /**
100
+ * Build expanded header layout
101
+ * Full branding area with additional space for custom content
102
+ */
103
+ export const buildExpandedHeader: HeaderLayoutRenderer = (context) => {
104
+ const { config, showClose = true, onClose, onClearChat } = context;
105
+ const launcher = config?.launcher ?? {};
106
+
107
+ const header = createElement(
108
+ "div",
109
+ "tvw-flex tvw-flex-col tvw-bg-cw-surface tvw-px-6 tvw-py-5 tvw-border-b-cw-divider"
110
+ );
111
+
112
+ // Top row: icon + text + buttons
113
+ const topRow = createElement(
114
+ "div",
115
+ "tvw-flex tvw-items-center tvw-gap-3"
116
+ );
117
+
118
+ // Icon
119
+ const headerIconSize = launcher.headerIconSize ?? "56px";
120
+ const iconHolder = createElement(
121
+ "div",
122
+ "tvw-flex tvw-items-center tvw-justify-center tvw-rounded-xl tvw-bg-cw-primary tvw-text-white tvw-text-2xl"
123
+ );
124
+ iconHolder.style.height = headerIconSize;
125
+ iconHolder.style.width = headerIconSize;
126
+
127
+ const headerIconName = launcher.headerIconName;
128
+ if (headerIconName) {
129
+ const iconSize = parseFloat(headerIconSize) || 24;
130
+ const iconSvg = renderLucideIcon(headerIconName, iconSize * 0.5, "#ffffff", 2);
131
+ if (iconSvg) {
132
+ iconHolder.replaceChildren(iconSvg);
133
+ } else {
134
+ iconHolder.textContent = launcher.agentIconText ?? "💬";
135
+ }
136
+ } else if (launcher.iconUrl) {
137
+ const img = createElement("img") as HTMLImageElement;
138
+ img.src = launcher.iconUrl;
139
+ img.alt = "";
140
+ img.className = "tvw-rounded-xl tvw-object-cover";
141
+ img.style.height = headerIconSize;
142
+ img.style.width = headerIconSize;
143
+ iconHolder.replaceChildren(img);
144
+ } else {
145
+ iconHolder.textContent = launcher.agentIconText ?? "💬";
146
+ }
147
+
148
+ // Title and subtitle
149
+ const headerCopy = createElement("div", "tvw-flex tvw-flex-col tvw-flex-1");
150
+ const title = createElement("span", "tvw-text-lg tvw-font-semibold");
151
+ title.textContent = launcher.title ?? "Chat Assistant";
152
+ const subtitle = createElement("span", "tvw-text-sm tvw-text-cw-muted");
153
+ subtitle.textContent = launcher.subtitle ?? "Here to help you get answers fast";
154
+ headerCopy.append(title, subtitle);
155
+
156
+ topRow.append(iconHolder, headerCopy);
157
+
158
+ // Close button
159
+ const closeButtonSize = launcher.closeButtonSize ?? "32px";
160
+ const closeButtonWrapper = createElement("div", "");
161
+
162
+ const closeButton = createElement(
163
+ "button",
164
+ "tvw-inline-flex tvw-items-center tvw-justify-center tvw-rounded-full tvw-text-cw-muted hover:tvw-bg-gray-100 tvw-cursor-pointer tvw-border-none"
165
+ ) as HTMLButtonElement;
166
+ closeButton.style.height = closeButtonSize;
167
+ closeButton.style.width = closeButtonSize;
168
+ closeButton.type = "button";
169
+ closeButton.setAttribute("aria-label", "Close chat");
170
+ closeButton.style.display = showClose ? "" : "none";
171
+
172
+ const closeButtonIconName = launcher.closeButtonIconName ?? "x";
173
+ const closeIconSvg = renderLucideIcon(
174
+ closeButtonIconName,
175
+ "20px",
176
+ launcher.closeButtonColor || "",
177
+ 2
178
+ );
179
+ if (closeIconSvg) {
180
+ closeButton.appendChild(closeIconSvg);
181
+ } else {
182
+ closeButton.textContent = "×";
183
+ }
184
+
185
+ if (onClose) {
186
+ closeButton.addEventListener("click", onClose);
187
+ }
188
+
189
+ closeButtonWrapper.appendChild(closeButton);
190
+ topRow.appendChild(closeButtonWrapper);
191
+
192
+ header.appendChild(topRow);
193
+
194
+ // Bottom row: additional space for status or branding
195
+ const bottomRow = createElement(
196
+ "div",
197
+ "tvw-mt-3 tvw-pt-3 tvw-border-t tvw-border-gray-100 tvw-text-xs tvw-text-cw-muted"
198
+ );
199
+ bottomRow.textContent = "Online and ready to help";
200
+ header.appendChild(bottomRow);
201
+
202
+ return {
203
+ header,
204
+ iconHolder,
205
+ headerTitle: title,
206
+ headerSubtitle: subtitle,
207
+ closeButton,
208
+ closeButtonWrapper,
209
+ clearChatButton: null,
210
+ clearChatButtonWrapper: null
211
+ };
212
+ };
213
+
214
+ /**
215
+ * Header layout registry
216
+ * Maps layout names to their renderer functions
217
+ */
218
+ export const headerLayouts: Record<string, HeaderLayoutRenderer> = {
219
+ default: buildDefaultHeader,
220
+ minimal: buildMinimalHeader,
221
+ expanded: buildExpandedHeader
222
+ };
223
+
224
+ /**
225
+ * Get header layout renderer by name
226
+ */
227
+ export const getHeaderLayout = (layoutName: string): HeaderLayoutRenderer => {
228
+ return headerLayouts[layoutName] ?? headerLayouts.default;
229
+ };
230
+
231
+ /**
232
+ * Build header based on layout configuration
233
+ * Applies layout config settings to determine which layout to use
234
+ */
235
+ export const buildHeaderWithLayout = (
236
+ config: AgentWidgetConfig,
237
+ layoutConfig?: AgentWidgetHeaderLayoutConfig,
238
+ context?: Partial<HeaderLayoutContext>
239
+ ): HeaderElements => {
240
+ // If custom render is provided, use it
241
+ if (layoutConfig?.render) {
242
+ const customHeader = layoutConfig.render({
243
+ config,
244
+ onClose: context?.onClose,
245
+ onClearChat: context?.onClearChat
246
+ });
247
+
248
+ // Wrap in HeaderElements structure
249
+ const iconHolder = createElement("div");
250
+ iconHolder.style.display = "none";
251
+ const headerTitle = createElement("span");
252
+ const headerSubtitle = createElement("span");
253
+ const closeButton = createElement("button") as HTMLButtonElement;
254
+ closeButton.style.display = "none";
255
+ const closeButtonWrapper = createElement("div");
256
+ closeButtonWrapper.style.display = "none";
257
+
258
+ return {
259
+ header: customHeader,
260
+ iconHolder,
261
+ headerTitle,
262
+ headerSubtitle,
263
+ closeButton,
264
+ closeButtonWrapper,
265
+ clearChatButton: null,
266
+ clearChatButtonWrapper: null
267
+ };
268
+ }
269
+
270
+ // Get layout renderer
271
+ const layoutName = layoutConfig?.layout ?? "default";
272
+ const layoutRenderer = getHeaderLayout(layoutName);
273
+
274
+ // Build header with layout
275
+ const headerElements = layoutRenderer({
276
+ config,
277
+ showClose: layoutConfig?.showCloseButton ?? context?.showClose ?? true,
278
+ onClose: context?.onClose,
279
+ onClearChat: context?.onClearChat
280
+ });
281
+
282
+ // Apply visibility settings from layout config
283
+ if (layoutConfig) {
284
+ if (layoutConfig.showIcon === false) {
285
+ headerElements.iconHolder.style.display = "none";
286
+ }
287
+ if (layoutConfig.showTitle === false) {
288
+ headerElements.headerTitle.style.display = "none";
289
+ }
290
+ if (layoutConfig.showSubtitle === false) {
291
+ headerElements.headerSubtitle.style.display = "none";
292
+ }
293
+ if (layoutConfig.showCloseButton === false) {
294
+ headerElements.closeButton.style.display = "none";
295
+ }
296
+ if (layoutConfig.showClearChat === false && headerElements.clearChatButtonWrapper) {
297
+ headerElements.clearChatButtonWrapper.style.display = "none";
298
+ }
299
+ }
300
+
301
+ return headerElements;
302
+ };
303
+
@@ -0,0 +1,193 @@
1
+ import { createElement } from "../utils/dom";
2
+ import { AgentWidgetConfig } from "../types";
3
+ import { positionMap } from "../utils/positioning";
4
+ import { renderLucideIcon } from "../utils/icons";
5
+
6
+ export interface LauncherButton {
7
+ element: HTMLButtonElement;
8
+ update: (config: AgentWidgetConfig) => void;
9
+ destroy: () => void;
10
+ }
11
+
12
+ export const createLauncherButton = (
13
+ config: AgentWidgetConfig | undefined,
14
+ onToggle: () => void
15
+ ): LauncherButton => {
16
+ const button = createElement("button") as HTMLButtonElement;
17
+ button.type = "button";
18
+ button.innerHTML = `
19
+ <span class="tvw-inline-flex tvw-items-center tvw-justify-center tvw-rounded-full tvw-bg-cw-primary tvw-text-white" data-role="launcher-icon">💬</span>
20
+ <img data-role="launcher-image" class="tvw-rounded-full tvw-object-cover" alt="" style="display:none" />
21
+ <span class="tvw-flex tvw-flex-col tvw-items-start tvw-text-left">
22
+ <span class="tvw-text-sm tvw-font-semibold tvw-text-cw-primary" data-role="launcher-title"></span>
23
+ <span class="tvw-text-xs tvw-text-cw-muted" data-role="launcher-subtitle"></span>
24
+ </span>
25
+ <span class="tvw-ml-2 tvw-grid tvw-place-items-center tvw-rounded-full tvw-bg-cw-primary tvw-text-cw-call-to-action" data-role="launcher-call-to-action-icon">↗</span>
26
+ `;
27
+ button.addEventListener("click", onToggle);
28
+
29
+ const update = (newConfig: AgentWidgetConfig) => {
30
+ const launcher = newConfig.launcher ?? {};
31
+
32
+ const titleEl = button.querySelector("[data-role='launcher-title']");
33
+ if (titleEl) {
34
+ titleEl.textContent = launcher.title ?? "Chat Assistant";
35
+ }
36
+
37
+ const subtitleEl = button.querySelector("[data-role='launcher-subtitle']");
38
+ if (subtitleEl) {
39
+ subtitleEl.textContent = launcher.subtitle ?? "Get answers fast";
40
+ }
41
+
42
+ // Hide/show text container
43
+ const textContainer = button.querySelector(".tvw-flex-col");
44
+ if (textContainer) {
45
+ if (launcher.textHidden) {
46
+ (textContainer as HTMLElement).style.display = "none";
47
+ } else {
48
+ (textContainer as HTMLElement).style.display = "";
49
+ }
50
+ }
51
+
52
+ const icon = button.querySelector<HTMLSpanElement>("[data-role='launcher-icon']");
53
+ if (icon) {
54
+ if (launcher.agentIconHidden) {
55
+ icon.style.display = "none";
56
+ } else {
57
+ const iconSize = launcher.agentIconSize ?? "40px";
58
+ icon.style.height = iconSize;
59
+ icon.style.width = iconSize;
60
+
61
+ // Clear existing content
62
+ icon.innerHTML = "";
63
+
64
+ // Render icon based on priority: Lucide icon > iconUrl > agentIconText
65
+ if (launcher.agentIconName) {
66
+ // Use Lucide icon
67
+ const iconSizeNum = parseFloat(iconSize) || 24;
68
+ const iconSvg = renderLucideIcon(launcher.agentIconName, iconSizeNum * 0.6, "#ffffff", 2);
69
+ if (iconSvg) {
70
+ icon.appendChild(iconSvg);
71
+ icon.style.display = "";
72
+ } else {
73
+ // Fallback to agentIconText if Lucide icon fails
74
+ icon.textContent = launcher.agentIconText ?? "💬";
75
+ icon.style.display = "";
76
+ }
77
+ } else if (launcher.iconUrl) {
78
+ // Use image URL - hide icon span and show img
79
+ icon.style.display = "none";
80
+ } else {
81
+ // Use text/emoji
82
+ icon.textContent = launcher.agentIconText ?? "💬";
83
+ icon.style.display = "";
84
+ }
85
+ }
86
+ }
87
+
88
+ const img = button.querySelector<HTMLImageElement>("[data-role='launcher-image']");
89
+ if (img) {
90
+ const iconSize = launcher.agentIconSize ?? "40px";
91
+ img.style.height = iconSize;
92
+ img.style.width = iconSize;
93
+ if (launcher.iconUrl && !launcher.agentIconName && !launcher.agentIconHidden) {
94
+ // Only show image if not using Lucide icon and not hidden
95
+ img.src = launcher.iconUrl;
96
+ img.style.display = "block";
97
+ } else {
98
+ img.style.display = "none";
99
+ }
100
+ }
101
+
102
+ const callToActionIconEl = button.querySelector<HTMLSpanElement>("[data-role='launcher-call-to-action-icon']");
103
+ if (callToActionIconEl) {
104
+ const callToActionIconSize = launcher.callToActionIconSize ?? "32px";
105
+ callToActionIconEl.style.height = callToActionIconSize;
106
+ callToActionIconEl.style.width = callToActionIconSize;
107
+
108
+ // Apply background color if configured
109
+ if (launcher.callToActionIconBackgroundColor) {
110
+ callToActionIconEl.style.backgroundColor = launcher.callToActionIconBackgroundColor;
111
+ callToActionIconEl.classList.remove("tvw-bg-cw-primary");
112
+ } else {
113
+ callToActionIconEl.style.backgroundColor = "";
114
+ callToActionIconEl.classList.add("tvw-bg-cw-primary");
115
+ }
116
+
117
+ // Calculate padding to adjust icon size
118
+ let paddingTotal = 0;
119
+ if (launcher.callToActionIconPadding) {
120
+ callToActionIconEl.style.boxSizing = "border-box";
121
+ callToActionIconEl.style.padding = launcher.callToActionIconPadding;
122
+ // Parse padding value to calculate total padding (padding applies to both sides)
123
+ const paddingValue = parseFloat(launcher.callToActionIconPadding) || 0;
124
+ paddingTotal = paddingValue * 2; // padding on both sides
125
+ } else {
126
+ callToActionIconEl.style.boxSizing = "";
127
+ callToActionIconEl.style.padding = "";
128
+ }
129
+
130
+ if (launcher.callToActionIconHidden) {
131
+ callToActionIconEl.style.display = "none";
132
+ } else {
133
+ callToActionIconEl.style.display = "";
134
+
135
+ // Clear existing content
136
+ callToActionIconEl.innerHTML = "";
137
+
138
+ // Use Lucide icon if provided, otherwise fall back to text
139
+ if (launcher.callToActionIconName) {
140
+ // Calculate actual icon size by subtracting padding
141
+ const containerSize = parseFloat(callToActionIconSize) || 24;
142
+ const iconSize = Math.max(containerSize - paddingTotal, 8); // Ensure minimum size of 8px
143
+ const iconSvg = renderLucideIcon(launcher.callToActionIconName, iconSize, "currentColor", 2);
144
+ if (iconSvg) {
145
+ callToActionIconEl.appendChild(iconSvg);
146
+ } else {
147
+ // Fallback to text if icon fails to render
148
+ callToActionIconEl.textContent = launcher.callToActionIconText ?? "↗";
149
+ }
150
+ } else {
151
+ callToActionIconEl.textContent = launcher.callToActionIconText ?? "↗";
152
+ }
153
+ }
154
+ }
155
+
156
+ const positionClass =
157
+ launcher.position && positionMap[launcher.position]
158
+ ? positionMap[launcher.position]
159
+ : positionMap["bottom-right"];
160
+
161
+ // Removed hardcoded border/shadow classes (tvw-shadow-lg, tvw-border, tvw-border-gray-200)
162
+ // These are now applied via inline styles from config
163
+ const base =
164
+ "tvw-fixed tvw-flex tvw-items-center tvw-gap-3 tvw-rounded-launcher tvw-bg-cw-surface tvw-py-2.5 tvw-pl-3 tvw-pr-3 tvw-transition hover:tvw-translate-y-[-2px] tvw-cursor-pointer tvw-z-50";
165
+
166
+ button.className = `${base} ${positionClass}`;
167
+
168
+ // Apply launcher border and shadow from config (with defaults matching previous Tailwind classes)
169
+ const defaultBorder = "1px solid #e5e7eb";
170
+ const defaultShadow = "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)";
171
+
172
+ button.style.border = launcher.border ?? defaultBorder;
173
+ button.style.boxShadow = launcher.shadow ?? defaultShadow;
174
+ };
175
+
176
+ const destroy = () => {
177
+ button.removeEventListener("click", onToggle);
178
+ button.remove();
179
+ };
180
+
181
+ // Initial update
182
+ if (config) {
183
+ update(config);
184
+ }
185
+
186
+ return {
187
+ element: button,
188
+ update,
189
+ destroy
190
+ };
191
+ };
192
+
193
+