@runtypelabs/persona 3.1.0 → 3.2.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
@@ -349,6 +349,11 @@
349
349
  word-break: break-all;
350
350
  }
351
351
 
352
+ .persona-break-words {
353
+ overflow-wrap: break-word;
354
+ word-break: break-word;
355
+ }
356
+
352
357
  .persona-px-4 {
353
358
  padding-left: 1rem;
354
359
  padding-right: 1rem;
@@ -1299,6 +1304,7 @@
1299
1304
  /* Ensure user message paragraphs and lists have proper styling too */
1300
1305
  .vanilla-message-user-bubble p {
1301
1306
  margin: 0;
1307
+ color: inherit;
1302
1308
  }
1303
1309
 
1304
1310
  .vanilla-message-user-bubble p + p {
@@ -1322,6 +1328,12 @@
1322
1328
  .vanilla-message-user-bubble li {
1323
1329
  margin: 0.25rem 0;
1324
1330
  padding-left: 0.25rem;
1331
+ color: inherit;
1332
+ }
1333
+
1334
+ .vanilla-message-assistant-bubble p,
1335
+ .vanilla-message-assistant-bubble li {
1336
+ color: inherit;
1325
1337
  }
1326
1338
 
1327
1339
  /* ============================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Themeable, pluggable 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",
@@ -0,0 +1,58 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { createLauncherButton } from "./launcher";
5
+ import { DEFAULT_WIDGET_CONFIG } from "../defaults";
6
+
7
+ describe("createLauncherButton", () => {
8
+ it("applies collapsedMaxWidth when set", () => {
9
+ const { element, update } = createLauncherButton(undefined, () => {});
10
+ update({
11
+ ...DEFAULT_WIDGET_CONFIG,
12
+ launcher: {
13
+ ...DEFAULT_WIDGET_CONFIG.launcher,
14
+ collapsedMaxWidth: "min(380px, calc(100vw - 48px))",
15
+ },
16
+ });
17
+ expect(element.style.maxWidth).toBe("min(380px, calc(100vw - 48px))");
18
+ element.remove();
19
+ });
20
+
21
+ it("sets title tooltip on launcher title and subtitle for truncated text", () => {
22
+ const { element, update } = createLauncherButton(undefined, () => {});
23
+ update({
24
+ ...DEFAULT_WIDGET_CONFIG,
25
+ launcher: {
26
+ ...DEFAULT_WIDGET_CONFIG.launcher,
27
+ title: "Hello",
28
+ subtitle: "Long subtitle for tooltip",
29
+ },
30
+ });
31
+ const titleEl = element.querySelector("[data-role='launcher-title']");
32
+ const subtitleEl = element.querySelector("[data-role='launcher-subtitle']");
33
+ expect(titleEl?.getAttribute("title")).toBe("Hello");
34
+ expect(subtitleEl?.getAttribute("title")).toBe("Long subtitle for tooltip");
35
+ element.remove();
36
+ });
37
+
38
+ it("clears maxWidth when collapsedMaxWidth is unset", () => {
39
+ const { element, update } = createLauncherButton(undefined, () => {});
40
+ update({
41
+ ...DEFAULT_WIDGET_CONFIG,
42
+ launcher: {
43
+ ...DEFAULT_WIDGET_CONFIG.launcher,
44
+ collapsedMaxWidth: "320px",
45
+ },
46
+ });
47
+ expect(element.style.maxWidth).toBe("320px");
48
+ update({
49
+ ...DEFAULT_WIDGET_CONFIG,
50
+ launcher: {
51
+ ...DEFAULT_WIDGET_CONFIG.launcher,
52
+ collapsedMaxWidth: undefined,
53
+ },
54
+ });
55
+ expect(element.style.maxWidth).toBe("");
56
+ element.remove();
57
+ });
58
+ });
@@ -19,9 +19,9 @@ export const createLauncherButton = (
19
19
  button.innerHTML = `
20
20
  <span class="persona-inline-flex persona-items-center persona-justify-center persona-rounded-full persona-bg-persona-primary persona-text-white" data-role="launcher-icon">💬</span>
21
21
  <img data-role="launcher-image" class="persona-rounded-full persona-object-cover" alt="" style="display:none" />
22
- <span class="persona-flex persona-flex-col persona-items-start persona-text-left">
23
- <span class="persona-text-sm persona-font-semibold persona-text-persona-primary" data-role="launcher-title"></span>
24
- <span class="persona-text-xs persona-text-persona-muted" data-role="launcher-subtitle"></span>
22
+ <span class="persona-flex persona-min-w-0 persona-flex-1 persona-flex-col persona-items-start persona-text-left">
23
+ <span class="persona-block persona-w-full persona-truncate persona-text-sm persona-font-semibold persona-text-persona-primary" data-role="launcher-title"></span>
24
+ <span class="persona-block persona-w-full persona-truncate persona-text-xs persona-text-persona-muted" data-role="launcher-subtitle"></span>
25
25
  </span>
26
26
  <span class="persona-ml-2 persona-grid persona-place-items-center persona-rounded-full persona-bg-persona-primary persona-text-persona-call-to-action" data-role="launcher-call-to-action-icon">↗</span>
27
27
  `;
@@ -33,12 +33,16 @@ export const createLauncherButton = (
33
33
 
34
34
  const titleEl = button.querySelector("[data-role='launcher-title']");
35
35
  if (titleEl) {
36
- titleEl.textContent = launcher.title ?? "Chat Assistant";
36
+ const t = launcher.title ?? "Chat Assistant";
37
+ titleEl.textContent = t;
38
+ titleEl.setAttribute("title", t);
37
39
  }
38
40
 
39
41
  const subtitleEl = button.querySelector("[data-role='launcher-subtitle']");
40
42
  if (subtitleEl) {
41
- subtitleEl.textContent = launcher.subtitle ?? "Get answers fast";
43
+ const s = launcher.subtitle ?? "Get answers fast";
44
+ subtitleEl.textContent = s;
45
+ subtitleEl.setAttribute("title", s);
42
46
  }
43
47
 
44
48
  // Hide/show text container
@@ -186,7 +190,7 @@ export const createLauncherButton = (
186
190
  } else {
187
191
  button.style.width = "";
188
192
  button.style.minWidth = "";
189
- button.style.maxWidth = "";
193
+ button.style.maxWidth = launcher.collapsedMaxWidth ?? "";
190
194
  button.style.justifyContent = "";
191
195
  button.style.padding = "";
192
196
  button.style.overflow = "";
@@ -349,6 +349,11 @@
349
349
  word-break: break-all;
350
350
  }
351
351
 
352
+ .persona-break-words {
353
+ overflow-wrap: break-word;
354
+ word-break: break-word;
355
+ }
356
+
352
357
  .persona-px-4 {
353
358
  padding-left: 1rem;
354
359
  padding-right: 1rem;
@@ -1299,6 +1304,7 @@
1299
1304
  /* Ensure user message paragraphs and lists have proper styling too */
1300
1305
  .vanilla-message-user-bubble p {
1301
1306
  margin: 0;
1307
+ color: inherit;
1302
1308
  }
1303
1309
 
1304
1310
  .vanilla-message-user-bubble p + p {
@@ -1322,6 +1328,12 @@
1322
1328
  .vanilla-message-user-bubble li {
1323
1329
  margin: 0.25rem 0;
1324
1330
  padding-left: 0.25rem;
1331
+ color: inherit;
1332
+ }
1333
+
1334
+ .vanilla-message-assistant-bubble p,
1335
+ .vanilla-message-assistant-bubble li {
1336
+ color: inherit;
1325
1337
  }
1326
1338
 
1327
1339
  /* ============================================
package/src/types.ts CHANGED
@@ -827,6 +827,13 @@ export type AgentWidgetLauncherConfig = {
827
827
  * @default "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)"
828
828
  */
829
829
  shadow?: string;
830
+ /**
831
+ * CSS `max-width` for the floating launcher button when the panel is closed.
832
+ * Title and subtitle each truncate with an ellipsis when space is tight; full strings are available via the native `title` tooltip. Does not affect the open chat panel (`width` / `launcherWidth`).
833
+ *
834
+ * @example "min(380px, calc(100vw - 48px))"
835
+ */
836
+ collapsedMaxWidth?: string;
830
837
  };
831
838
 
832
839
  export type AgentWidgetSendButtonConfig = {
@@ -2346,11 +2353,15 @@ export type AgentWidgetConfig = {
2346
2353
  *
2347
2354
  * This hook runs synchronously and must return the (potentially modified) state.
2348
2355
  *
2356
+ * Returning `{ state, open: true }` also signals that the widget panel should
2357
+ * open after initialization — useful when injecting a post-navigation message
2358
+ * that the user should immediately see.
2359
+ *
2349
2360
  * @example
2350
2361
  * ```typescript
2362
+ * // Plain state transform (existing form, still supported)
2351
2363
  * config: {
2352
2364
  * onStateLoaded: (state) => {
2353
- * // Check for pending navigation message
2354
2365
  * const navMessage = consumeNavigationFlag();
2355
2366
  * if (navMessage) {
2356
2367
  * return {
@@ -2367,8 +2378,34 @@ export type AgentWidgetConfig = {
2367
2378
  * }
2368
2379
  * }
2369
2380
  * ```
2381
+ *
2382
+ * @example
2383
+ * ```typescript
2384
+ * // Return { state, open: true } to also open the panel
2385
+ * config: {
2386
+ * onStateLoaded: (state) => {
2387
+ * const navMessage = consumeNavigationFlag();
2388
+ * if (navMessage) {
2389
+ * return {
2390
+ * state: {
2391
+ * ...state,
2392
+ * messages: [...(state.messages || []), {
2393
+ * id: `nav-${Date.now()}`,
2394
+ * role: 'assistant',
2395
+ * content: navMessage,
2396
+ * createdAt: new Date().toISOString()
2397
+ * }]
2398
+ * },
2399
+ * open: true
2400
+ * };
2401
+ * }
2402
+ * return state;
2403
+ * }
2404
+ * }
2405
+ * ```
2370
2406
  */
2371
- onStateLoaded?: (state: AgentWidgetStoredState) => AgentWidgetStoredState;
2407
+ onStateLoaded?: (state: AgentWidgetStoredState) =>
2408
+ AgentWidgetStoredState | { state: AgentWidgetStoredState; open?: boolean };
2372
2409
  /**
2373
2410
  * Registry of custom components that can be rendered from JSON directives.
2374
2411
  * Components are registered by name and can be invoked via JSON responses
package/src/ui.ts CHANGED
@@ -412,11 +412,20 @@ export const createAgentExperience = (
412
412
  let persistentMetadata: Record<string, unknown> = {};
413
413
  let pendingStoredState: Promise<AgentWidgetStoredState | null> | null = null;
414
414
 
415
- // Helper to apply onStateLoaded hook and extract state
415
+ let shouldOpenAfterStateLoaded = false;
416
+
417
+ // Helper to apply onStateLoaded hook and extract state.
418
+ // Supports both the legacy plain-state return and the new { state, open? } return.
416
419
  const applyStateLoadedHook = (state: AgentWidgetStoredState): AgentWidgetStoredState => {
417
420
  if (config.onStateLoaded) {
418
421
  try {
419
- return config.onStateLoaded(state);
422
+ const result = config.onStateLoaded(state);
423
+ if (result && typeof result === 'object' && 'state' in result) {
424
+ const { state: processedState, open } = result as { state: AgentWidgetStoredState; open?: boolean };
425
+ if (open) shouldOpenAfterStateLoaded = true;
426
+ return processedState;
427
+ }
428
+ return result as AgentWidgetStoredState;
420
429
  } catch (error) {
421
430
  if (typeof console !== "undefined") {
422
431
  // eslint-disable-next-line no-console
@@ -5379,6 +5388,13 @@ export const createAgentExperience = (
5379
5388
  }
5380
5389
  }
5381
5390
 
5391
+ // If onStateLoaded signalled open: true, open the panel after init.
5392
+ // Mirrors the same setTimeout(0) pattern used by persistState restore so both
5393
+ // can fire independently without interfering with each other.
5394
+ if (shouldOpenAfterStateLoaded && launcherEnabled) {
5395
+ setTimeout(() => { controller.open(); }, 0);
5396
+ }
5397
+
5382
5398
  return controller;
5383
5399
  };
5384
5400