@runtypelabs/persona 1.40.0 → 1.41.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.
package/dist/widget.css CHANGED
@@ -687,6 +687,13 @@
687
687
  box-shadow: none !important;
688
688
  }
689
689
 
690
+ /* Prevent iOS Safari from zooming on input focus (requires 16px minimum) */
691
+ @media (hover: none) and (pointer: coarse) {
692
+ .tvw-composer-textarea {
693
+ font-size: 1rem !important;
694
+ }
695
+ }
696
+
690
697
  /* Prevent form container from showing focus styles */
691
698
  .tvw-widget-composer:focus-within {
692
699
  outline: none !important;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "1.40.0",
3
+ "version": "1.41.0",
4
4
  "description": "Themeable, plugable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -85,7 +85,7 @@ export const buildComposer = (context: ComposerBuildContext): ComposerElements =
85
85
  const textarea = createElement("textarea") as HTMLTextAreaElement;
86
86
  textarea.placeholder = config?.copy?.inputPlaceholder ?? "Type your message…";
87
87
  textarea.className =
88
- "tvw-w-full tvw-min-h-[24px] tvw-resize-none tvw-border-none tvw-bg-transparent tvw-text-sm tvw-text-cw-primary focus:tvw-outline-none focus:tvw-border-none";
88
+ "tvw-w-full tvw-min-h-[24px] tvw-resize-none tvw-border-none tvw-bg-transparent tvw-text-sm tvw-text-cw-primary focus:tvw-outline-none focus:tvw-border-none tvw-composer-textarea";
89
89
  textarea.rows = 1;
90
90
 
91
91
  // Apply font family and weight from config
@@ -1,14 +1,17 @@
1
1
  import { createElement } from "../utils/dom";
2
- import {
3
- AgentWidgetMessage,
2
+ import {
3
+ AgentWidgetMessage,
4
4
  AgentWidgetMessageLayoutConfig,
5
5
  AgentWidgetAvatarConfig,
6
6
  AgentWidgetTimestampConfig,
7
7
  AgentWidgetMessageActionsConfig,
8
- AgentWidgetMessageFeedback
8
+ AgentWidgetMessageFeedback,
9
+ LoadingIndicatorRenderContext
9
10
  } from "../types";
10
11
  import { renderLucideIcon } from "../utils/icons";
11
12
 
13
+ export type LoadingIndicatorRenderer = (context: LoadingIndicatorRenderContext) => HTMLElement | null;
14
+
12
15
  export type MessageTransform = (context: {
13
16
  text: string;
14
17
  message: AgentWidgetMessage;
@@ -50,6 +53,35 @@ export const createTypingIndicator = (): HTMLElement => {
50
53
  return container;
51
54
  };
52
55
 
56
+ /**
57
+ * Render loading indicator with fallback chain:
58
+ * 1. Custom renderer (if provided and returns non-null)
59
+ * 2. Default typing indicator
60
+ */
61
+ export const renderLoadingIndicatorWithFallback = (
62
+ location: 'inline' | 'standalone',
63
+ customRenderer?: LoadingIndicatorRenderer,
64
+ widgetConfig?: import("../types").AgentWidgetConfig
65
+ ): HTMLElement | null => {
66
+ const context: LoadingIndicatorRenderContext = {
67
+ config: widgetConfig ?? ({} as import("../types").AgentWidgetConfig),
68
+ streaming: true,
69
+ location,
70
+ defaultRenderer: createTypingIndicator
71
+ };
72
+
73
+ // Try custom renderer first
74
+ if (customRenderer) {
75
+ const result = customRenderer(context);
76
+ if (result !== null) {
77
+ return result;
78
+ }
79
+ }
80
+
81
+ // Fall back to default
82
+ return createTypingIndicator();
83
+ };
84
+
53
85
  /**
54
86
  * Create an avatar element
55
87
  */
@@ -394,6 +426,20 @@ export const createMessageActions = (
394
426
  return container;
395
427
  };
396
428
 
429
+ /**
430
+ * Options for creating a standard message bubble
431
+ */
432
+ export type CreateStandardBubbleOptions = {
433
+ /**
434
+ * Custom loading indicator renderer for inline location
435
+ */
436
+ loadingIndicatorRenderer?: LoadingIndicatorRenderer;
437
+ /**
438
+ * Full widget config (needed for loading indicator context)
439
+ */
440
+ widgetConfig?: import("../types").AgentWidgetConfig;
441
+ };
442
+
397
443
  /**
398
444
  * Create standard message bubble
399
445
  * Supports layout configuration for avatars, timestamps, and visual presets
@@ -403,7 +449,8 @@ export const createStandardBubble = (
403
449
  transform: MessageTransform,
404
450
  layoutConfig?: AgentWidgetMessageLayoutConfig,
405
451
  actionsConfig?: AgentWidgetMessageActionsConfig,
406
- actionCallbacks?: MessageActionCallbacks
452
+ actionCallbacks?: MessageActionCallbacks,
453
+ options?: CreateStandardBubbleOptions
407
454
  ): HTMLElement => {
408
455
  const config = layoutConfig ?? {};
409
456
  const layout = config.layout ?? "bubble";
@@ -449,8 +496,15 @@ export const createStandardBubble = (
449
496
  // Add typing indicator if this is a streaming assistant message
450
497
  if (message.streaming && message.role === "assistant") {
451
498
  if (!message.content || !message.content.trim()) {
452
- const typingIndicator = createTypingIndicator();
453
- bubble.appendChild(typingIndicator);
499
+ // Use custom renderer if provided, otherwise default
500
+ const indicator = renderLoadingIndicatorWithFallback(
501
+ 'inline',
502
+ options?.loadingIndicatorRenderer,
503
+ options?.widgetConfig
504
+ );
505
+ if (indicator) {
506
+ bubble.appendChild(indicator);
507
+ }
454
508
  }
455
509
  }
456
510
 
@@ -502,7 +556,8 @@ export const createBubbleWithLayout = (
502
556
  transform: MessageTransform,
503
557
  layoutConfig?: AgentWidgetMessageLayoutConfig,
504
558
  actionsConfig?: AgentWidgetMessageActionsConfig,
505
- actionCallbacks?: MessageActionCallbacks
559
+ actionCallbacks?: MessageActionCallbacks,
560
+ options?: CreateStandardBubbleOptions
506
561
  ): HTMLElement => {
507
562
  const config = layoutConfig ?? {};
508
563
 
@@ -524,5 +579,5 @@ export const createBubbleWithLayout = (
524
579
  }
525
580
 
526
581
  // Fall back to standard bubble
527
- return createStandardBubble(message, transform, layoutConfig, actionsConfig, actionCallbacks);
582
+ return createStandardBubble(message, transform, layoutConfig, actionsConfig, actionCallbacks, options);
528
583
  };
package/src/index.ts CHANGED
@@ -53,7 +53,12 @@ export type {
53
53
  InjectMessageOptions,
54
54
  InjectAssistantMessageOptions,
55
55
  InjectUserMessageOptions,
56
- InjectSystemMessageOptions
56
+ InjectSystemMessageOptions,
57
+ // Loading indicator types
58
+ LoadingIndicatorRenderContext,
59
+ AgentWidgetLoadingIndicatorConfig,
60
+ // Idle indicator types
61
+ IdleIndicatorRenderContext
57
62
  } from "./types";
58
63
 
59
64
  export { initAgentWidgetFn as initAgentWidget };
@@ -169,9 +174,15 @@ export {
169
174
  createStandardBubble,
170
175
  createBubbleWithLayout,
171
176
  createTypingIndicator,
172
- createMessageActions
177
+ createMessageActions,
178
+ renderLoadingIndicatorWithFallback
179
+ } from "./components/message-bubble";
180
+ export type {
181
+ MessageTransform,
182
+ MessageActionCallbacks,
183
+ LoadingIndicatorRenderer,
184
+ CreateStandardBubbleOptions
173
185
  } from "./components/message-bubble";
174
- export type { MessageTransform, MessageActionCallbacks } from "./components/message-bubble";
175
186
  export {
176
187
  createCSATFeedback,
177
188
  createNPSFeedback
@@ -1,4 +1,4 @@
1
- import { AgentWidgetMessage, AgentWidgetConfig } from "../types";
1
+ import { AgentWidgetMessage, AgentWidgetConfig, LoadingIndicatorRenderContext, IdleIndicatorRenderContext } from "../types";
2
2
 
3
3
  /**
4
4
  * Plugin interface for customizing widget components
@@ -75,6 +75,43 @@ export interface AgentWidgetPlugin {
75
75
  config: AgentWidgetConfig;
76
76
  }) => HTMLElement | null;
77
77
 
78
+ /**
79
+ * Custom renderer for loading indicator
80
+ * Return null to use default renderer (or config-based renderer)
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * renderLoadingIndicator: ({ location, defaultRenderer }) => {
85
+ * if (location === 'standalone') {
86
+ * const el = document.createElement('div');
87
+ * el.textContent = 'Thinking...';
88
+ * return el;
89
+ * }
90
+ * return defaultRenderer();
91
+ * }
92
+ * ```
93
+ */
94
+ renderLoadingIndicator?: (context: LoadingIndicatorRenderContext) => HTMLElement | null;
95
+
96
+ /**
97
+ * Custom renderer for idle state indicator.
98
+ * Called when the widget is idle (not streaming) and has at least one message.
99
+ * Return an HTMLElement to display, or null to hide (default).
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * renderIdleIndicator: ({ lastMessage, messageCount }) => {
104
+ * if (messageCount === 0) return null;
105
+ * if (lastMessage?.role !== 'assistant') return null;
106
+ * const el = document.createElement('div');
107
+ * el.className = 'idle-pulse';
108
+ * el.setAttribute('data-preserve-animation', 'true');
109
+ * return el;
110
+ * }
111
+ * ```
112
+ */
113
+ renderIdleIndicator?: (context: IdleIndicatorRenderContext) => HTMLElement | null;
114
+
78
115
  /**
79
116
  * Called when plugin is registered
80
117
  */
@@ -687,6 +687,13 @@
687
687
  box-shadow: none !important;
688
688
  }
689
689
 
690
+ /* Prevent iOS Safari from zooming on input focus (requires 16px minimum) */
691
+ @media (hover: none) and (pointer: coarse) {
692
+ .tvw-composer-textarea {
693
+ font-size: 1rem !important;
694
+ }
695
+ }
696
+
690
697
  /* Prevent form container from showing focus styles */
691
698
  .tvw-widget-composer:focus-within {
692
699
  outline: none !important;
package/src/types.ts CHANGED
@@ -1256,6 +1256,146 @@ export type AgentWidgetPersistStateConfig = {
1256
1256
  clearOnChatClear?: boolean;
1257
1257
  };
1258
1258
 
1259
+ // ============================================================================
1260
+ // Loading Indicator Types
1261
+ // ============================================================================
1262
+
1263
+ /**
1264
+ * Context provided to loading indicator render functions.
1265
+ * Used for customizing the loading indicator appearance.
1266
+ */
1267
+ export type LoadingIndicatorRenderContext = {
1268
+ /**
1269
+ * Full widget configuration for accessing theme, etc.
1270
+ */
1271
+ config: AgentWidgetConfig;
1272
+ /**
1273
+ * Current streaming state (always true when indicator is shown)
1274
+ */
1275
+ streaming: boolean;
1276
+ /**
1277
+ * Location where the indicator is rendered:
1278
+ * - 'inline': Inside a streaming assistant message bubble (when content is empty)
1279
+ * - 'standalone': Separate bubble while waiting for stream to start
1280
+ */
1281
+ location: 'inline' | 'standalone';
1282
+ /**
1283
+ * Function to render the default 3-dot bouncing indicator.
1284
+ * Call this if you want to use the default for certain cases.
1285
+ */
1286
+ defaultRenderer: () => HTMLElement;
1287
+ };
1288
+
1289
+ /**
1290
+ * Context provided to idle indicator render functions.
1291
+ * Used for customizing the idle state indicator appearance.
1292
+ */
1293
+ export type IdleIndicatorRenderContext = {
1294
+ /**
1295
+ * Full widget configuration for accessing theme, etc.
1296
+ */
1297
+ config: AgentWidgetConfig;
1298
+ /**
1299
+ * The last message in the conversation (if any).
1300
+ * Useful for conditional rendering based on who spoke last.
1301
+ */
1302
+ lastMessage: AgentWidgetMessage | undefined;
1303
+ /**
1304
+ * Total number of messages in the conversation.
1305
+ */
1306
+ messageCount: number;
1307
+ };
1308
+
1309
+ /**
1310
+ * Configuration for customizing the loading indicator.
1311
+ * The loading indicator is shown while waiting for a response or
1312
+ * when an assistant message is streaming but has no content yet.
1313
+ *
1314
+ * @example
1315
+ * ```typescript
1316
+ * // Custom animated spinner
1317
+ * config: {
1318
+ * loadingIndicator: {
1319
+ * render: ({ location }) => {
1320
+ * const el = document.createElement('div');
1321
+ * el.innerHTML = '<svg class="spinner">...</svg>';
1322
+ * el.setAttribute('data-preserve-animation', 'true');
1323
+ * return el;
1324
+ * }
1325
+ * }
1326
+ * }
1327
+ * ```
1328
+ *
1329
+ * @example
1330
+ * ```typescript
1331
+ * // Different indicators by location
1332
+ * config: {
1333
+ * loadingIndicator: {
1334
+ * render: ({ location, defaultRenderer }) => {
1335
+ * if (location === 'inline') {
1336
+ * return defaultRenderer(); // Use default for inline
1337
+ * }
1338
+ * // Custom for standalone
1339
+ * const el = document.createElement('div');
1340
+ * el.textContent = 'Thinking...';
1341
+ * return el;
1342
+ * }
1343
+ * }
1344
+ * }
1345
+ * ```
1346
+ *
1347
+ * @example
1348
+ * ```typescript
1349
+ * // Hide loading indicator entirely
1350
+ * config: {
1351
+ * loadingIndicator: {
1352
+ * render: () => null
1353
+ * }
1354
+ * }
1355
+ * ```
1356
+ */
1357
+ export type AgentWidgetLoadingIndicatorConfig = {
1358
+ /**
1359
+ * Whether to show the bubble background and border around the standalone loading indicator.
1360
+ * Set to false to render the loading indicator without any bubble styling.
1361
+ * @default true
1362
+ */
1363
+ showBubble?: boolean;
1364
+
1365
+ /**
1366
+ * Custom render function for the loading indicator.
1367
+ * Return an HTMLElement to display, or null to hide the indicator.
1368
+ *
1369
+ * For custom animations, add `data-preserve-animation="true"` attribute
1370
+ * to prevent the DOM morpher from interrupting the animation.
1371
+ */
1372
+ render?: (context: LoadingIndicatorRenderContext) => HTMLElement | null;
1373
+
1374
+ /**
1375
+ * Render function for the idle state indicator.
1376
+ * Called when the widget is idle (not streaming) and has at least one message.
1377
+ * Return an HTMLElement to display, or null to hide (default).
1378
+ *
1379
+ * For animations, add `data-preserve-animation="true"` attribute
1380
+ * to prevent the DOM morpher from interrupting the animation.
1381
+ *
1382
+ * @example
1383
+ * ```typescript
1384
+ * loadingIndicator: {
1385
+ * renderIdle: ({ lastMessage }) => {
1386
+ * // Only show idle indicator after assistant messages
1387
+ * if (lastMessage?.role !== 'assistant') return null;
1388
+ * const el = document.createElement('div');
1389
+ * el.className = 'pulse-dot';
1390
+ * el.setAttribute('data-preserve-animation', 'true');
1391
+ * return el;
1392
+ * }
1393
+ * }
1394
+ * ```
1395
+ */
1396
+ renderIdle?: (context: IdleIndicatorRenderContext) => HTMLElement | null;
1397
+ };
1398
+
1259
1399
  export type AgentWidgetConfig = {
1260
1400
  apiUrl?: string;
1261
1401
  flowId?: string;
@@ -1693,6 +1833,29 @@ export type AgentWidgetConfig = {
1693
1833
  * ```
1694
1834
  */
1695
1835
  persistState?: boolean | AgentWidgetPersistStateConfig;
1836
+
1837
+ /**
1838
+ * Configuration for customizing the loading indicator.
1839
+ * The loading indicator is shown while waiting for a response or
1840
+ * when an assistant message is streaming but has no content yet.
1841
+ *
1842
+ * @example
1843
+ * ```typescript
1844
+ * config: {
1845
+ * loadingIndicator: {
1846
+ * render: ({ location, defaultRenderer }) => {
1847
+ * if (location === 'standalone') {
1848
+ * const el = document.createElement('div');
1849
+ * el.textContent = 'Thinking...';
1850
+ * return el;
1851
+ * }
1852
+ * return defaultRenderer();
1853
+ * }
1854
+ * }
1855
+ * }
1856
+ * ```
1857
+ */
1858
+ loadingIndicator?: AgentWidgetLoadingIndicatorConfig;
1696
1859
  };
1697
1860
 
1698
1861
  export type AgentWidgetMessageRole = "user" | "assistant" | "system";