@runtypelabs/persona 3.5.2 → 3.7.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 (53) hide show
  1. package/dist/index.cjs +46 -46
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +44 -0
  4. package/dist/index.d.ts +44 -0
  5. package/dist/index.global.js +70 -70
  6. package/dist/index.global.js.map +1 -1
  7. package/dist/index.js +46 -46
  8. package/dist/index.js.map +1 -1
  9. package/dist/theme-editor.cjs +18015 -0
  10. package/dist/theme-editor.d.cts +3888 -0
  11. package/dist/theme-editor.d.ts +3888 -0
  12. package/dist/theme-editor.js +17909 -0
  13. package/dist/theme-reference.cjs +1 -1
  14. package/dist/theme-reference.d.cts +33 -0
  15. package/dist/theme-reference.d.ts +33 -0
  16. package/dist/theme-reference.js +1 -1
  17. package/dist/widget.css +69 -25
  18. package/package.json +9 -7
  19. package/src/components/artifact-card.ts +1 -1
  20. package/src/components/composer-builder.ts +16 -29
  21. package/src/components/demo-carousel.ts +5 -5
  22. package/src/components/event-stream-view.test.ts +142 -0
  23. package/src/components/event-stream-view.ts +68 -29
  24. package/src/components/header-builder.ts +2 -2
  25. package/src/components/launcher.ts +9 -0
  26. package/src/components/message-bubble.ts +9 -3
  27. package/src/components/suggestions.ts +1 -1
  28. package/src/defaults.ts +24 -9
  29. package/src/scroll-to-bottom-defaults.test.ts +13 -0
  30. package/src/styles/widget.css +69 -25
  31. package/src/theme-editor/color-utils.ts +252 -0
  32. package/src/theme-editor/index.ts +131 -0
  33. package/src/theme-editor/presets.ts +144 -0
  34. package/src/theme-editor/preview-utils.ts +265 -0
  35. package/src/theme-editor/preview.ts +445 -0
  36. package/src/theme-editor/role-mappings.ts +343 -0
  37. package/src/theme-editor/sections.test.ts +43 -0
  38. package/src/theme-editor/sections.ts +994 -0
  39. package/src/theme-editor/state.ts +298 -0
  40. package/src/theme-editor/types.ts +177 -0
  41. package/src/theme-editor.ts +2 -0
  42. package/src/theme-reference.ts +8 -0
  43. package/src/types/theme.ts +11 -0
  44. package/src/types.ts +22 -0
  45. package/src/ui.scroll.test.ts +554 -0
  46. package/src/ui.ts +223 -133
  47. package/src/utils/auto-follow.test.ts +110 -0
  48. package/src/utils/auto-follow.ts +112 -0
  49. package/src/utils/plugins.ts +1 -1
  50. package/src/utils/theme.test.ts +44 -8
  51. package/src/utils/theme.ts +11 -11
  52. package/src/utils/tokens.ts +137 -41
  53. package/widget.css +0 -1
@@ -153,33 +153,25 @@ export const buildComposer = (context: ComposerBuildContext): ComposerElements =
153
153
  // Clear any existing content
154
154
  sendButton.innerHTML = "";
155
155
 
156
+ // Set button foreground color from config or theme token
157
+ if (textColor) {
158
+ sendButton.style.color = textColor;
159
+ } else {
160
+ sendButton.style.color = "var(--persona-button-primary-fg, #ffffff)";
161
+ }
162
+
156
163
  // Use Lucide icon if iconName is provided, otherwise fall back to iconText
157
164
  if (iconName) {
158
165
  const iconSize = parseFloat(buttonSize) || 24;
159
- const iconColor =
160
- textColor && typeof textColor === "string" && textColor.trim()
161
- ? textColor.trim()
162
- : "currentColor";
166
+ const iconColor = textColor?.trim() || "currentColor";
163
167
  const iconSvg = renderLucideIcon(iconName, iconSize, iconColor, 2);
164
168
  if (iconSvg) {
165
169
  sendButton.appendChild(iconSvg);
166
- sendButton.style.color = iconColor;
167
170
  } else {
168
- // Fallback to text if icon fails to render
169
171
  sendButton.textContent = iconText;
170
- if (textColor) {
171
- sendButton.style.color = textColor;
172
- } else {
173
- sendButton.classList.add("persona-text-white");
174
- }
175
172
  }
176
173
  } else {
177
174
  sendButton.textContent = iconText;
178
- if (textColor) {
179
- sendButton.style.color = textColor;
180
- } else {
181
- sendButton.classList.add("persona-text-white");
182
- }
183
175
  }
184
176
 
185
177
  if (backgroundColor) {
@@ -273,7 +265,14 @@ export const buildComposer = (context: ComposerBuildContext): ComposerElements =
273
265
  micButton.style.fontSize = "18px";
274
266
  micButton.style.lineHeight = "1";
275
267
 
276
- // Use Lucide mic icon with configured color (stroke width 1.5 for minimalist outline style)
268
+ // Set mic button foreground from config or theme token
269
+ if (micIconColor) {
270
+ micButton.style.color = micIconColor;
271
+ } else {
272
+ micButton.style.color = "var(--persona-text, #111827)";
273
+ }
274
+
275
+ // Use Lucide mic icon (stroke width 1.5 for minimalist outline style)
277
276
  const iconColorValue = micIconColor || "currentColor";
278
277
  const micIconSvg = renderLucideIcon(
279
278
  micIconName,
@@ -283,25 +282,13 @@ export const buildComposer = (context: ComposerBuildContext): ComposerElements =
283
282
  );
284
283
  if (micIconSvg) {
285
284
  micButton.appendChild(micIconSvg);
286
- micButton.style.color = iconColorValue;
287
285
  } else {
288
- // Fallback to text if icon fails
289
286
  micButton.textContent = "🎤";
290
- micButton.style.color = iconColorValue;
291
287
  }
292
288
 
293
289
  // Apply background color
294
290
  if (micBackgroundColor) {
295
291
  micButton.style.backgroundColor = micBackgroundColor;
296
- } else {
297
- micButton.classList.add("persona-bg-persona-primary");
298
- }
299
-
300
- // Apply icon/text color
301
- if (micIconColor) {
302
- micButton.style.color = micIconColor;
303
- } else if (!micIconColor && !textColor) {
304
- micButton.classList.add("persona-text-white");
305
292
  }
306
293
 
307
294
  // Apply border styling
@@ -185,8 +185,8 @@ const CAROUSEL_CSS = /* css */ `
185
185
  background: #f3f4f6;
186
186
  }
187
187
  .persona-dc-root .persona-dc-dropdown button.persona-dc-dropdown-item[aria-current="true"] {
188
- background: #eff6ff;
189
- color: #2563eb;
188
+ background: #f5f5f5;
189
+ color: #0f0f0f;
190
190
  }
191
191
  .persona-dc-root .persona-dc-dropdown-desc {
192
192
  font-weight: 400;
@@ -196,7 +196,7 @@ const CAROUSEL_CSS = /* css */ `
196
196
  text-align: left;
197
197
  }
198
198
  .persona-dc-root .persona-dc-dropdown button.persona-dc-dropdown-item[aria-current="true"] .persona-dc-dropdown-desc {
199
- color: #60a5fa;
199
+ color: #737373;
200
200
  }
201
201
  .persona-dc-counter {
202
202
  font-size: 12px;
@@ -280,7 +280,7 @@ const CAROUSEL_CSS = /* css */ `
280
280
  background: #f3f4f6;
281
281
  }
282
282
  .persona-dc-root .persona-icon-btn:focus-visible {
283
- outline: 2px solid #3b82f6;
283
+ outline: 2px solid #171717;
284
284
  outline-offset: 2px;
285
285
  }
286
286
  .persona-dc-root .persona-icon-btn[aria-pressed="true"] {
@@ -591,7 +591,7 @@ export function createDemoCarousel(
591
591
  wrapper.dataset.colorScheme = currentScheme;
592
592
 
593
593
  const iframe = createElement("iframe", "persona-dc-iframe");
594
- iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
594
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
595
595
  iframe.setAttribute("loading", "lazy");
596
596
  iframe.title = items[currentIndex].title;
597
597
 
@@ -263,6 +263,9 @@ function getEventsList(element: any) {
263
263
  function getNoResultsMsg(element: any) {
264
264
  return getEventsWrapper(element).children[1]; // noResultsMsg
265
265
  }
266
+ function getScrollIndicator(element: any) {
267
+ return getEventsWrapper(element).children[2]; // scrollIndicator
268
+ }
266
269
 
267
270
  describe("createEventStreamView", () => {
268
271
  it("should create a container element with expected children", async () => {
@@ -904,6 +907,145 @@ describe("createEventStreamView", () => {
904
907
  });
905
908
  });
906
909
 
910
+ describe("scroll-to-bottom affordance", () => {
911
+ it("uses icon-only arrow-down defaults when paused and new events arrive", async () => {
912
+ vi.useFakeTimers();
913
+ const { createEventStreamView } = await loadModule();
914
+ const events = [makeEvent("step_chunk", 1)];
915
+ const buffer = createMockBuffer(events);
916
+ const { element, update } = createEventStreamView({
917
+ buffer: buffer as any
918
+ });
919
+
920
+ update();
921
+
922
+ const eventsList = getEventsList(element);
923
+ eventsList.scrollTop = 0;
924
+ eventsList.scrollHeight = 600;
925
+ eventsList.clientHeight = 300;
926
+ eventsList.__fireEvent("wheel", { deltaY: -24 });
927
+
928
+ vi.advanceTimersByTime(150);
929
+ buffer.push(makeEvent("step_chunk", 2));
930
+ update();
931
+
932
+ const indicator = getScrollIndicator(element);
933
+ expect(indicator.style.display).toBe("");
934
+ expect(indicator.children[1]?.textContent).toBe("");
935
+ expect(indicator.children[0]?.__iconName).toBe("arrow-down");
936
+ vi.useRealTimers();
937
+ });
938
+
939
+ it("hides the event stream affordance when disabled", async () => {
940
+ vi.useFakeTimers();
941
+ const { createEventStreamView } = await loadModule();
942
+ const events = [makeEvent("step_chunk", 1)];
943
+ const buffer = createMockBuffer(events);
944
+ const { element, update } = createEventStreamView({
945
+ buffer: buffer as any,
946
+ config: {
947
+ features: {
948
+ eventStream: {},
949
+ scrollToBottom: {
950
+ enabled: false
951
+ }
952
+ }
953
+ } as any
954
+ });
955
+
956
+ update();
957
+
958
+ const eventsList = getEventsList(element);
959
+ eventsList.scrollTop = 0;
960
+ eventsList.scrollHeight = 600;
961
+ eventsList.clientHeight = 300;
962
+ eventsList.__fireEvent("wheel", { deltaY: -24 });
963
+
964
+ vi.advanceTimersByTime(150);
965
+ buffer.push(makeEvent("step_chunk", 2));
966
+ update();
967
+
968
+ expect(getScrollIndicator(element).style.display).toBe("none");
969
+ vi.useRealTimers();
970
+ });
971
+
972
+ it("renders the event stream affordance as icon-only when label is empty", async () => {
973
+ vi.useFakeTimers();
974
+ const { createEventStreamView } = await loadModule();
975
+ const events = [makeEvent("step_chunk", 1)];
976
+ const buffer = createMockBuffer(events);
977
+ const { element, update } = createEventStreamView({
978
+ buffer: buffer as any,
979
+ config: {
980
+ features: {
981
+ eventStream: {},
982
+ scrollToBottom: {
983
+ enabled: true,
984
+ iconName: "arrow-down",
985
+ label: ""
986
+ }
987
+ }
988
+ } as any
989
+ });
990
+
991
+ update();
992
+
993
+ const eventsList = getEventsList(element);
994
+ eventsList.scrollTop = 0;
995
+ eventsList.scrollHeight = 600;
996
+ eventsList.clientHeight = 300;
997
+ eventsList.__fireEvent("wheel", { deltaY: -24 });
998
+
999
+ vi.advanceTimersByTime(150);
1000
+ buffer.push(makeEvent("step_chunk", 2));
1001
+ update();
1002
+
1003
+ const indicator = getScrollIndicator(element);
1004
+ expect(indicator.style.display).toBe("");
1005
+ expect(indicator.children[1]?.textContent).toBe("");
1006
+ expect(indicator.children[0]?.__iconName).toBe("arrow-down");
1007
+ vi.useRealTimers();
1008
+ });
1009
+
1010
+ it("supports a configured label and icon override", async () => {
1011
+ vi.useFakeTimers();
1012
+ const { createEventStreamView } = await loadModule();
1013
+ const events = [makeEvent("step_chunk", 1)];
1014
+ const buffer = createMockBuffer(events);
1015
+ const { element, update } = createEventStreamView({
1016
+ buffer: buffer as any,
1017
+ config: {
1018
+ features: {
1019
+ eventStream: {},
1020
+ scrollToBottom: {
1021
+ enabled: true,
1022
+ iconName: "arrow-down",
1023
+ label: "Jump to latest"
1024
+ }
1025
+ }
1026
+ } as any
1027
+ });
1028
+
1029
+ update();
1030
+
1031
+ const eventsList = getEventsList(element);
1032
+ eventsList.scrollTop = 0;
1033
+ eventsList.scrollHeight = 600;
1034
+ eventsList.clientHeight = 300;
1035
+ eventsList.__fireEvent("wheel", { deltaY: -24 });
1036
+
1037
+ vi.advanceTimersByTime(150);
1038
+ buffer.push(makeEvent("step_chunk", 2));
1039
+ update();
1040
+
1041
+ const indicator = getScrollIndicator(element);
1042
+ expect(indicator.style.display).toBe("");
1043
+ expect(indicator.children[1]?.textContent).toContain("Jump to latest");
1044
+ expect(indicator.children[0]?.__iconName).toBe("arrow-down");
1045
+ vi.useRealTimers();
1046
+ });
1047
+ });
1048
+
907
1049
  describe("individual event copy", () => {
908
1050
  it("should format event as structured JSON with parsed payload", async () => {
909
1051
  const { createEventStreamView } = await loadModule();
@@ -1,5 +1,11 @@
1
1
  import { createElement } from "../utils/dom";
2
2
  import { renderLucideIcon } from "../utils/icons";
3
+ import {
4
+ createFollowStateController,
5
+ isElementNearBottom,
6
+ resolveFollowStateFromScroll,
7
+ resolveFollowStateFromWheel
8
+ } from "../utils/auto-follow";
3
9
  import type { EventStreamBuffer } from "../utils/event-stream-buffer";
4
10
  import type {
5
11
  SSEEventRecord,
@@ -26,7 +32,7 @@ function applyCustomClasses(el: HTMLElement, classes?: string): void {
26
32
 
27
33
  const DEFAULT_BADGE_COLORS: Record<string, EventStreamBadgeColor> = {
28
34
  flow_: { bg: "var(--persona-palette-colors-success-100, #dcfce7)", text: "var(--persona-palette-colors-success-700, #166534)" },
29
- step_: { bg: "var(--persona-palette-colors-primary-100, #dbeafe)", text: "var(--persona-palette-colors-primary-700, #1e40af)" },
35
+ step_: { bg: "var(--persona-palette-colors-primary-100, #f5f5f5)", text: "var(--persona-palette-colors-primary-700, #0a0a0a)" },
30
36
  reason_: { bg: "var(--persona-palette-colors-warning-100, #ffedd5)", text: "var(--persona-palette-colors-warning-700, #9a3412)" },
31
37
  tool_: { bg: "var(--persona-palette-colors-purple-100, #f3e8ff)", text: "var(--persona-palette-colors-purple-700, #6b21a8)" },
32
38
  agent_: { bg: "var(--persona-palette-colors-teal-100, #ccfbf1)", text: "var(--persona-palette-colors-teal-700, #115e59)" },
@@ -391,6 +397,10 @@ export function createEventStreamView(
391
397
  config,
392
398
  plugins = [],
393
399
  } = options;
400
+ const scrollToBottomConfig = config?.features?.scrollToBottom;
401
+ const scrollToBottomEnabled = scrollToBottomConfig?.enabled !== false;
402
+ const scrollToBottomIconName = scrollToBottomConfig?.iconName ?? "arrow-down";
403
+ const scrollToBottomLabel = scrollToBottomConfig?.label ?? "";
394
404
 
395
405
  const esConfig: EventStreamConfig = config?.features?.eventStream ?? {};
396
406
 
@@ -438,7 +448,7 @@ export function createEventStreamView(
438
448
  let lastKnownTypes: string[] = [];
439
449
  let lastTypeCounts: Record<string, number> = {};
440
450
  let lastFilteredCount = 0;
441
- let userScrolledUp = false;
451
+ const autoFollow = createFollowStateController();
442
452
  let newEventsSincePause = 0;
443
453
  let lastRenderTime = 0;
444
454
  let pendingUpdate = false;
@@ -638,18 +648,23 @@ export function createEventStreamView(
638
648
  // Scroll-to-bottom indicator
639
649
  const scrollIndicator = createElement(
640
650
  "div",
641
- "persona-absolute persona-bottom-3 persona-left-1/2 persona-transform persona--translate-x-1/2 persona-bg-persona-accent persona-text-white persona-text-xs persona-px-3 persona-py-1.5 persona-rounded-full persona-cursor-pointer persona-shadow-md persona-z-10 persona-flex persona-items-center persona-gap-1"
651
+ "persona-scroll-to-bottom-indicator persona-absolute persona-bottom-3 persona-left-1/2 persona-transform persona--translate-x-1/2 persona-cursor-pointer persona-z-10 persona-text-xs"
642
652
  );
643
653
  applyCustomClasses(scrollIndicator, customClasses?.scrollIndicator);
644
654
  scrollIndicator.style.display = "none";
655
+ scrollIndicator.setAttribute(
656
+ "data-persona-scroll-to-bottom-has-label",
657
+ scrollToBottomLabel ? "true" : "false"
658
+ );
645
659
  const arrowIcon = renderLucideIcon(
646
- "arrow-down",
647
- "12px",
660
+ scrollToBottomIconName,
661
+ "14px",
648
662
  "currentColor",
649
663
  2
650
664
  );
651
665
  if (arrowIcon) scrollIndicator.appendChild(arrowIcon);
652
666
  const indicatorText = createElement("span", "");
667
+ indicatorText.textContent = scrollToBottomLabel;
653
668
  scrollIndicator.appendChild(indicatorText);
654
669
 
655
670
  // No matching events message
@@ -753,7 +768,7 @@ export function createEventStreamView(
753
768
  function resetScrollState() {
754
769
  lastFilteredCount = 0;
755
770
  newEventsSincePause = 0;
756
- userScrolledUp = false;
771
+ autoFollow.resume();
757
772
  scrollIndicator.style.display = "none";
758
773
  }
759
774
 
@@ -766,12 +781,14 @@ export function createEventStreamView(
766
781
  dirtyExpandId = eventId;
767
782
  // Save scroll position — user-initiated expand/collapse should not auto-scroll
768
783
  const savedScrollTop = eventsList.scrollTop;
769
- const wasUserScrolledUp = userScrolledUp;
784
+ const wasAutoFollowing = autoFollow.isFollowing();
770
785
  suppressScrollHandler = true;
771
- userScrolledUp = true; // prevent auto-scroll during re-render
786
+ autoFollow.pause(); // prevent auto-scroll during re-render
772
787
  updateNow();
773
788
  eventsList.scrollTop = savedScrollTop;
774
- userScrolledUp = wasUserScrolledUp;
789
+ if (wasAutoFollowing) {
790
+ autoFollow.resume();
791
+ }
775
792
  suppressScrollHandler = false;
776
793
  }
777
794
 
@@ -780,13 +797,7 @@ export function createEventStreamView(
780
797
  // ========================================================================
781
798
 
782
799
  function isNearBottom(): boolean {
783
- const threshold = 50;
784
- return (
785
- eventsList.scrollHeight -
786
- eventsList.scrollTop -
787
- eventsList.clientHeight <=
788
- threshold
789
- );
800
+ return isElementNearBottom(eventsList, 50);
790
801
  }
791
802
 
792
803
  function updateNow() {
@@ -833,9 +844,11 @@ export function createEventStreamView(
833
844
  }
834
845
 
835
846
  // Track new events since user scrolled up
836
- if (userScrolledUp && newCount > lastFilteredCount) {
847
+ if (scrollToBottomEnabled && !autoFollow.isFollowing() && newCount > lastFilteredCount) {
837
848
  newEventsSincePause += newCount - lastFilteredCount;
838
- indicatorText.textContent = `${newEventsSincePause} new event${newEventsSincePause === 1 ? "" : "s"}`;
849
+ indicatorText.textContent = scrollToBottomLabel
850
+ ? `${scrollToBottomLabel}${newEventsSincePause > 0 ? ` (${newEventsSincePause})` : ""}`
851
+ : "";
839
852
  scrollIndicator.style.display = "";
840
853
  }
841
854
  lastFilteredCount = newCount;
@@ -939,7 +952,7 @@ export function createEventStreamView(
939
952
  }
940
953
 
941
954
  // Auto-scroll if user hasn't scrolled up
942
- if (!userScrolledUp) {
955
+ if (autoFollow.isFollowing()) {
943
956
  eventsList.scrollTop = eventsList.scrollHeight;
944
957
  }
945
958
  }
@@ -1064,30 +1077,56 @@ export function createEventStreamView(
1064
1077
  const handleListScroll = () => {
1065
1078
  if (suppressScrollHandler) return;
1066
1079
  const currentScrollTop = eventsList.scrollTop;
1067
- const scrollingDown = currentScrollTop > lastScrollTop;
1068
- lastScrollTop = currentScrollTop;
1080
+ const { action, nextLastScrollTop } = resolveFollowStateFromScroll({
1081
+ following: autoFollow.isFollowing(),
1082
+ currentScrollTop,
1083
+ lastScrollTop,
1084
+ nearBottom: isNearBottom(),
1085
+ userScrollThreshold: 1,
1086
+ resumeRequiresDownwardScroll: true
1087
+ });
1088
+ lastScrollTop = nextLastScrollTop;
1069
1089
 
1070
- if (isNearBottom() && scrollingDown) {
1071
- // User scrolled back down to bottom — re-enable auto-scroll
1072
- userScrolledUp = false;
1090
+ if (action === "resume") {
1091
+ autoFollow.resume();
1073
1092
  newEventsSincePause = 0;
1074
1093
  scrollIndicator.style.display = "none";
1075
- } else if (!isNearBottom()) {
1076
- userScrolledUp = true;
1094
+ } else if (action === "pause") {
1095
+ autoFollow.pause();
1096
+ if (scrollToBottomEnabled) {
1097
+ indicatorText.textContent = scrollToBottomLabel;
1098
+ scrollIndicator.style.display = "";
1099
+ }
1077
1100
  }
1078
1101
  };
1079
1102
 
1080
1103
  // Wheel events fire synchronously before rAF callbacks, so we can
1081
1104
  // detect upward scroll intent before the next updateNow() auto-scrolls.
1082
1105
  const handleWheel = (e: WheelEvent) => {
1083
- if (e.deltaY < 0) {
1084
- userScrolledUp = true;
1106
+ const action = resolveFollowStateFromWheel({
1107
+ following: autoFollow.isFollowing(),
1108
+ deltaY: e.deltaY,
1109
+ nearBottom: isNearBottom(),
1110
+ resumeWhenNearBottom: true
1111
+ });
1112
+
1113
+ if (action === "pause") {
1114
+ autoFollow.pause();
1115
+ if (scrollToBottomEnabled) {
1116
+ indicatorText.textContent = scrollToBottomLabel;
1117
+ scrollIndicator.style.display = "";
1118
+ }
1119
+ } else if (action === "resume") {
1120
+ autoFollow.resume();
1121
+ newEventsSincePause = 0;
1122
+ scrollIndicator.style.display = "none";
1085
1123
  }
1086
1124
  };
1087
1125
 
1088
1126
  const handleScrollIndicatorClick = () => {
1127
+ if (!scrollToBottomEnabled) return;
1089
1128
  eventsList.scrollTop = eventsList.scrollHeight;
1090
- userScrolledUp = false;
1129
+ autoFollow.resume();
1091
1130
  newEventsSincePause = 0;
1092
1131
  scrollIndicator.style.display = "none";
1093
1132
  };
@@ -5,7 +5,7 @@ import { AgentWidgetConfig } from "../types";
5
5
  /** CSS `color` values; variables are set on `[data-persona-root]` from `theme.components.header`. */
6
6
  export const HEADER_THEME_CSS = {
7
7
  titleColor:
8
- "var(--persona-header-title-fg, var(--persona-primary, #2563eb))",
8
+ "var(--persona-header-title-fg, var(--persona-primary, #0f0f0f))",
9
9
  subtitleColor:
10
10
  "var(--persona-header-subtitle-fg, var(--persona-text-muted, var(--persona-muted, #9ca3af)))",
11
11
  actionIconColor:
@@ -61,7 +61,7 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
61
61
  iconHolder.style.height = headerIconSize;
62
62
  iconHolder.style.width = headerIconSize;
63
63
  iconHolder.style.backgroundColor =
64
- "var(--persona-header-icon-bg, var(--persona-primary, #2563eb))";
64
+ "var(--persona-header-icon-bg, var(--persona-primary, #0f0f0f))";
65
65
  iconHolder.style.color =
66
66
  "var(--persona-header-icon-fg, var(--persona-text-inverse, #ffffff))";
67
67
 
@@ -119,6 +119,15 @@ export const createLauncherButton = (
119
119
  callToActionIconEl.style.backgroundColor = "";
120
120
  callToActionIconEl.classList.add("persona-bg-persona-primary");
121
121
  }
122
+
123
+ // Apply foreground/icon color if configured
124
+ if (launcher.callToActionIconColor) {
125
+ callToActionIconEl.style.color = launcher.callToActionIconColor;
126
+ callToActionIconEl.classList.remove("persona-text-persona-call-to-action");
127
+ } else {
128
+ callToActionIconEl.style.color = "";
129
+ callToActionIconEl.classList.add("persona-text-persona-call-to-action");
130
+ }
122
131
 
123
132
  // Calculate padding to adjust icon size
124
133
  let paddingTotal = 0;
@@ -140,15 +140,21 @@ export const createTypingIndicator = (): HTMLElement => {
140
140
  container.className = "persona-flex persona-items-center persona-space-x-1 persona-h-5 persona-mt-2";
141
141
 
142
142
  const dot1 = document.createElement("div");
143
- dot1.className = "persona-bg-persona-primary persona-animate-typing persona-rounded-full persona-h-1.5 persona-w-1.5";
143
+ dot1.className = "persona-animate-typing persona-rounded-full persona-h-1.5 persona-w-1.5";
144
+ dot1.style.backgroundColor = "currentColor";
145
+ dot1.style.opacity = "0.4";
144
146
  dot1.style.animationDelay = "0ms";
145
147
 
146
148
  const dot2 = document.createElement("div");
147
- dot2.className = "persona-bg-persona-primary persona-animate-typing persona-rounded-full persona-h-1.5 persona-w-1.5";
149
+ dot2.className = "persona-animate-typing persona-rounded-full persona-h-1.5 persona-w-1.5";
150
+ dot2.style.backgroundColor = "currentColor";
151
+ dot2.style.opacity = "0.4";
148
152
  dot2.style.animationDelay = "250ms";
149
153
 
150
154
  const dot3 = document.createElement("div");
151
- dot3.className = "persona-bg-persona-primary persona-animate-typing persona-rounded-full persona-h-1.5 persona-w-1.5";
155
+ dot3.className = "persona-animate-typing persona-rounded-full persona-h-1.5 persona-w-1.5";
156
+ dot3.style.backgroundColor = "currentColor";
157
+ dot3.style.opacity = "0.4";
152
158
  dot3.style.animationDelay = "500ms";
153
159
 
154
160
  const srOnly = document.createElement("span");
@@ -52,7 +52,7 @@ export const createSuggestions = (container: HTMLElement): SuggestionButtons =>
52
52
  chips.forEach((chip) => {
53
53
  const btn = createElement(
54
54
  "button",
55
- "persona-rounded-button persona-bg-persona-surface persona-px-3 persona-py-1.5 persona-text-xs persona-font-medium persona-text-persona-muted hover:persona-opacity-90 persona-cursor-pointer persona-border persona-border-gray-200"
55
+ "persona-rounded-button persona-bg-persona-surface persona-px-3 persona-py-1.5 persona-text-xs persona-font-medium persona-text-persona-primary hover:persona-opacity-80 persona-cursor-pointer persona-border persona-border-persona-border"
56
56
  ) as HTMLButtonElement;
57
57
  btn.type = "button";
58
58
  btn.textContent = chip;
package/src/defaults.ts CHANGED
@@ -23,6 +23,8 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
23
23
  title: "Chat Assistant",
24
24
  subtitle: "Here to help you get answers fast",
25
25
  agentIconText: "💬",
26
+ agentIconName: "bot",
27
+ headerIconName: "bot",
26
28
  position: "bottom-right",
27
29
  width: "min(400px, calc(100vw - 24px))",
28
30
  heightOffset: 0,
@@ -35,8 +37,8 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
35
37
  callToActionIconText: "",
36
38
  callToActionIconSize: "32px",
37
39
  callToActionIconPadding: "5px",
38
- callToActionIconColor: "#000000",
39
- callToActionIconBackgroundColor: "#ffffff",
40
+ callToActionIconColor: undefined,
41
+ callToActionIconBackgroundColor: undefined,
40
42
  // closeButtonColor / clearChat.iconColor omitted so theme.components.header.actionIconForeground applies.
41
43
  closeButtonBackgroundColor: "transparent",
42
44
  clearChat: {
@@ -52,7 +54,7 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
52
54
  paddingY: "0px",
53
55
  },
54
56
  headerIconHidden: false,
55
- border: "1px solid #e5e7eb",
57
+ border: undefined,
56
58
  shadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)",
57
59
  },
58
60
  copy: {
@@ -65,9 +67,7 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
65
67
  borderWidth: "0px",
66
68
  paddingX: "12px",
67
69
  paddingY: "10px",
68
- backgroundColor: "#111827",
69
- textColor: "#ffffff",
70
- borderColor: "#60a5fa",
70
+ borderColor: undefined,
71
71
  useIcon: true,
72
72
  iconText: "↑",
73
73
  size: "40px",
@@ -90,11 +90,11 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
90
90
  borderWidth: "0px",
91
91
  paddingX: "9px",
92
92
  paddingY: "14px",
93
- iconColor: "#111827",
93
+ iconColor: undefined,
94
94
  backgroundColor: "transparent",
95
95
  borderColor: "transparent",
96
- recordingIconColor: "#ffffff",
97
- recordingBackgroundColor: "#ef4444",
96
+ recordingIconColor: undefined,
97
+ recordingBackgroundColor: undefined,
98
98
  recordingBorderColor: "transparent",
99
99
  showTooltip: true,
100
100
  tooltipText: "Start voice recognition",
@@ -102,6 +102,11 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
102
102
  features: {
103
103
  showReasoning: true,
104
104
  showToolCalls: true,
105
+ scrollToBottom: {
106
+ enabled: true,
107
+ iconName: "arrow-down",
108
+ label: "",
109
+ },
105
110
  },
106
111
  suggestionChips: [
107
112
  "What can you help me with?",
@@ -214,6 +219,8 @@ export function mergeWithDefaults(
214
219
  features: (() => {
215
220
  const da = DEFAULT_WIDGET_CONFIG.features?.artifacts;
216
221
  const ca = config.features?.artifacts;
222
+ const dsb = DEFAULT_WIDGET_CONFIG.features?.scrollToBottom;
223
+ const csb = config.features?.scrollToBottom;
217
224
  const mergedArtifacts =
218
225
  da === undefined && ca === undefined
219
226
  ? undefined
@@ -225,9 +232,17 @@ export function mergeWithDefaults(
225
232
  ...ca?.layout,
226
233
  },
227
234
  };
235
+ const mergedScrollToBottom =
236
+ dsb === undefined && csb === undefined
237
+ ? undefined
238
+ : {
239
+ ...dsb,
240
+ ...csb,
241
+ };
228
242
  return {
229
243
  ...DEFAULT_WIDGET_CONFIG.features,
230
244
  ...config.features,
245
+ ...(mergedScrollToBottom !== undefined ? { scrollToBottom: mergedScrollToBottom } : {}),
231
246
  ...(mergedArtifacts !== undefined ? { artifacts: mergedArtifacts } : {}),
232
247
  };
233
248
  })(),
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { DEFAULT_WIDGET_CONFIG } from "./defaults";
4
+
5
+ describe("scroll-to-bottom defaults", () => {
6
+ it("defaults to an enabled icon-only circular control", () => {
7
+ expect(DEFAULT_WIDGET_CONFIG.features?.scrollToBottom).toEqual({
8
+ enabled: true,
9
+ iconName: "arrow-down",
10
+ label: "",
11
+ });
12
+ });
13
+ });