@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.
- package/dist/index.cjs +46 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.global.js +70 -70
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -46
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +18015 -0
- package/dist/theme-editor.d.cts +3888 -0
- package/dist/theme-editor.d.ts +3888 -0
- package/dist/theme-editor.js +17909 -0
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +33 -0
- package/dist/theme-reference.d.ts +33 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +69 -25
- package/package.json +9 -7
- package/src/components/artifact-card.ts +1 -1
- package/src/components/composer-builder.ts +16 -29
- package/src/components/demo-carousel.ts +5 -5
- package/src/components/event-stream-view.test.ts +142 -0
- package/src/components/event-stream-view.ts +68 -29
- package/src/components/header-builder.ts +2 -2
- package/src/components/launcher.ts +9 -0
- package/src/components/message-bubble.ts +9 -3
- package/src/components/suggestions.ts +1 -1
- package/src/defaults.ts +24 -9
- package/src/scroll-to-bottom-defaults.test.ts +13 -0
- package/src/styles/widget.css +69 -25
- package/src/theme-editor/color-utils.ts +252 -0
- package/src/theme-editor/index.ts +131 -0
- package/src/theme-editor/presets.ts +144 -0
- package/src/theme-editor/preview-utils.ts +265 -0
- package/src/theme-editor/preview.ts +445 -0
- package/src/theme-editor/role-mappings.ts +343 -0
- package/src/theme-editor/sections.test.ts +43 -0
- package/src/theme-editor/sections.ts +994 -0
- package/src/theme-editor/state.ts +298 -0
- package/src/theme-editor/types.ts +177 -0
- package/src/theme-editor.ts +2 -0
- package/src/theme-reference.ts +8 -0
- package/src/types/theme.ts +11 -0
- package/src/types.ts +22 -0
- package/src/ui.scroll.test.ts +554 -0
- package/src/ui.ts +223 -133
- package/src/utils/auto-follow.test.ts +110 -0
- package/src/utils/auto-follow.ts +112 -0
- package/src/utils/plugins.ts +1 -1
- package/src/utils/theme.test.ts +44 -8
- package/src/utils/theme.ts +11 -11
- package/src/utils/tokens.ts +137 -41
- 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
|
-
//
|
|
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: #
|
|
189
|
-
color: #
|
|
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: #
|
|
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 #
|
|
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, #
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
647
|
-
"
|
|
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
|
-
|
|
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
|
|
784
|
+
const wasAutoFollowing = autoFollow.isFollowing();
|
|
770
785
|
suppressScrollHandler = true;
|
|
771
|
-
|
|
786
|
+
autoFollow.pause(); // prevent auto-scroll during re-render
|
|
772
787
|
updateNow();
|
|
773
788
|
eventsList.scrollTop = savedScrollTop;
|
|
774
|
-
|
|
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
|
-
|
|
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 (
|
|
847
|
+
if (scrollToBottomEnabled && !autoFollow.isFollowing() && newCount > lastFilteredCount) {
|
|
837
848
|
newEventsSincePause += newCount - lastFilteredCount;
|
|
838
|
-
indicatorText.textContent =
|
|
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 (
|
|
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
|
|
1068
|
-
|
|
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 (
|
|
1071
|
-
|
|
1072
|
-
userScrolledUp = false;
|
|
1090
|
+
if (action === "resume") {
|
|
1091
|
+
autoFollow.resume();
|
|
1073
1092
|
newEventsSincePause = 0;
|
|
1074
1093
|
scrollIndicator.style.display = "none";
|
|
1075
|
-
} else if (
|
|
1076
|
-
|
|
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
|
-
|
|
1084
|
-
|
|
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
|
-
|
|
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, #
|
|
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, #
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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:
|
|
39
|
-
callToActionIconBackgroundColor:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
93
|
+
iconColor: undefined,
|
|
94
94
|
backgroundColor: "transparent",
|
|
95
95
|
borderColor: "transparent",
|
|
96
|
-
recordingIconColor:
|
|
97
|
-
recordingBackgroundColor:
|
|
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
|
+
});
|