@runtypelabs/persona 3.6.0 → 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 +44 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.global.js +67 -67
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +44 -44
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +514 -227
- package/dist/theme-editor.d.cts +32 -1
- package/dist/theme-editor.d.ts +32 -1
- package/dist/theme-editor.js +513 -227
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +19 -0
- package/dist/theme-reference.d.ts +19 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +40 -0
- package/package.json +1 -1
- package/src/components/demo-carousel.ts +1 -1
- package/src/components/event-stream-view.test.ts +142 -0
- package/src/components/event-stream-view.ts +67 -28
- package/src/defaults.ts +15 -0
- package/src/scroll-to-bottom-defaults.test.ts +13 -0
- package/src/styles/widget.css +40 -0
- package/src/theme-editor/index.ts +1 -0
- package/src/theme-editor/role-mappings.ts +12 -0
- package/src/theme-editor/sections.test.ts +43 -0
- package/src/theme-editor/sections.ts +42 -0
- package/src/theme-reference.ts +8 -0
- package/src/types/theme.ts +10 -0
- package/src/types.ts +22 -0
- package/src/ui.scroll.test.ts +554 -0
- package/src/ui.ts +178 -83
- package/src/utils/auto-follow.test.ts +110 -0
- package/src/utils/auto-follow.ts +112 -0
- package/src/utils/theme.test.ts +34 -0
- package/src/utils/tokens.ts +49 -0
|
@@ -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,
|
|
@@ -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
|
};
|
package/src/defaults.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/styles/widget.css
CHANGED
|
@@ -2320,6 +2320,46 @@
|
|
|
2320
2320
|
background: var(--persona-label-btn-hover-bg, var(--persona-container, #f3f4f6));
|
|
2321
2321
|
}
|
|
2322
2322
|
|
|
2323
|
+
[data-persona-root] .persona-scroll-to-bottom-indicator {
|
|
2324
|
+
display: inline-flex;
|
|
2325
|
+
align-items: center;
|
|
2326
|
+
justify-content: center;
|
|
2327
|
+
gap: var(--persona-scroll-to-bottom-gap, 0.5rem);
|
|
2328
|
+
min-height: var(--persona-scroll-to-bottom-size, 40px);
|
|
2329
|
+
border-radius: var(--persona-scroll-to-bottom-radius, var(--persona-radius-full, 9999px));
|
|
2330
|
+
border: 1px solid var(--persona-scroll-to-bottom-border, var(--persona-primary, #111827));
|
|
2331
|
+
background: var(--persona-scroll-to-bottom-bg, var(--persona-button-primary-bg, var(--persona-accent, #0f0f0f)));
|
|
2332
|
+
color: var(--persona-scroll-to-bottom-fg, var(--persona-button-primary-fg, var(--persona-text-inverse, #ffffff)));
|
|
2333
|
+
box-shadow: var(--persona-scroll-to-bottom-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1));
|
|
2334
|
+
font-size: var(--persona-scroll-to-bottom-font-size, 0.875rem);
|
|
2335
|
+
line-height: 1;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
[data-persona-root] .persona-scroll-to-bottom-indicator[data-persona-scroll-to-bottom-has-label="true"] {
|
|
2339
|
+
padding: var(--persona-scroll-to-bottom-padding, 0.5rem 0.875rem);
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
[data-persona-root] .persona-scroll-to-bottom-indicator[data-persona-scroll-to-bottom-has-label="false"] {
|
|
2343
|
+
width: var(--persona-scroll-to-bottom-size, 40px);
|
|
2344
|
+
height: var(--persona-scroll-to-bottom-size, 40px);
|
|
2345
|
+
padding: 0;
|
|
2346
|
+
gap: 0;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
[data-persona-root] .persona-scroll-to-bottom-indicator svg {
|
|
2350
|
+
width: var(--persona-scroll-to-bottom-icon-size, 14px);
|
|
2351
|
+
height: var(--persona-scroll-to-bottom-icon-size, 14px);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
[data-persona-root] .persona-scroll-to-bottom-indicator:hover {
|
|
2355
|
+
opacity: 0.92;
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
[data-persona-root] .persona-scroll-to-bottom-indicator:focus-visible {
|
|
2359
|
+
outline: 2px solid var(--persona-accent, #171717);
|
|
2360
|
+
outline-offset: 2px;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2323
2363
|
/* Toggle group — mutually exclusive button set created by createToggleGroup() */
|
|
2324
2364
|
[data-persona-root] .persona-toggle-group {
|
|
2325
2365
|
display: inline-flex;
|
|
@@ -95,6 +95,17 @@ export const ROLE_PRIMARY_ACTIONS: RoleAssignmentOptions = {
|
|
|
95
95
|
],
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
+
export const ROLE_SCROLL_TO_BOTTOM: RoleAssignmentOptions = {
|
|
99
|
+
roleId: 'role-scroll-to-bottom',
|
|
100
|
+
helper: 'Scroll-to-bottom affordances in transcript and event stream',
|
|
101
|
+
intensities: ROLE_INTENSITIES,
|
|
102
|
+
targets: [
|
|
103
|
+
{ path: 'components.scrollToBottom.background', kind: 'background' },
|
|
104
|
+
{ path: 'components.scrollToBottom.foreground', kind: 'foreground' },
|
|
105
|
+
{ path: 'components.scrollToBottom.border', kind: 'border' },
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
|
|
98
109
|
export const ROLE_INPUT: RoleAssignmentOptions = {
|
|
99
110
|
roleId: 'role-input',
|
|
100
111
|
helper: 'Message input field',
|
|
@@ -137,6 +148,7 @@ export const ALL_ROLES: RoleAssignmentOptions[] = [
|
|
|
137
148
|
ROLE_USER_MESSAGES,
|
|
138
149
|
ROLE_ASSISTANT_MESSAGES,
|
|
139
150
|
ROLE_PRIMARY_ACTIONS,
|
|
151
|
+
ROLE_SCROLL_TO_BOTTOM,
|
|
140
152
|
ROLE_INPUT,
|
|
141
153
|
ROLE_LINKS_FOCUS,
|
|
142
154
|
ROLE_BORDERS,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { COMPONENTS_SECTIONS, CONFIGURE_SECTIONS, INTERFACE_ROLES_SECTION } from "./sections";
|
|
4
|
+
import { ALL_ROLES } from "./role-mappings";
|
|
5
|
+
|
|
6
|
+
describe("theme editor scroll-to-bottom controls", () => {
|
|
7
|
+
it("exposes scroll-to-bottom config controls", () => {
|
|
8
|
+
const featureSection = CONFIGURE_SECTIONS.find((section) => section.id === "features");
|
|
9
|
+
|
|
10
|
+
expect(featureSection?.fields.some((field) => field.path === "features.scrollToBottom.enabled")).toBe(true);
|
|
11
|
+
expect(featureSection?.fields.some((field) => field.path === "features.scrollToBottom.iconName")).toBe(true);
|
|
12
|
+
expect(featureSection?.fields.some((field) => field.path === "features.scrollToBottom.label")).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("exposes scroll-to-bottom component token controls", () => {
|
|
16
|
+
const fieldPaths = COMPONENTS_SECTIONS.flatMap((section) => section.fields.map((field) => field.path));
|
|
17
|
+
|
|
18
|
+
expect(fieldPaths).toContain("theme.components.scrollToBottom.background");
|
|
19
|
+
expect(fieldPaths).toContain("theme.components.scrollToBottom.foreground");
|
|
20
|
+
expect(fieldPaths).toContain("theme.components.scrollToBottom.border");
|
|
21
|
+
expect(fieldPaths).toContain("theme.components.scrollToBottom.size");
|
|
22
|
+
expect(fieldPaths).toContain("theme.components.scrollToBottom.borderRadius");
|
|
23
|
+
expect(fieldPaths).toContain("theme.components.scrollToBottom.shadow");
|
|
24
|
+
expect(fieldPaths).toContain("theme.components.scrollToBottom.padding");
|
|
25
|
+
expect(fieldPaths).toContain("theme.components.scrollToBottom.gap");
|
|
26
|
+
expect(fieldPaths).toContain("theme.components.scrollToBottom.fontSize");
|
|
27
|
+
expect(fieldPaths).toContain("theme.components.scrollToBottom.iconSize");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("adds a scroll-to-bottom interface role mapping", () => {
|
|
31
|
+
const role = ALL_ROLES.find((entry) => entry.roleId === "role-scroll-to-bottom");
|
|
32
|
+
|
|
33
|
+
expect(role).toBeDefined();
|
|
34
|
+
expect(role?.targets.map((target) => target.path)).toEqual(
|
|
35
|
+
expect.arrayContaining([
|
|
36
|
+
"components.scrollToBottom.background",
|
|
37
|
+
"components.scrollToBottom.foreground",
|
|
38
|
+
"components.scrollToBottom.border",
|
|
39
|
+
])
|
|
40
|
+
);
|
|
41
|
+
expect(INTERFACE_ROLES_SECTION.fields.some((field) => field.id === "role-scroll-to-bottom")).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
ROLE_USER_MESSAGES,
|
|
9
9
|
ROLE_ASSISTANT_MESSAGES,
|
|
10
10
|
ROLE_PRIMARY_ACTIONS,
|
|
11
|
+
ROLE_SCROLL_TO_BOTTOM,
|
|
11
12
|
ROLE_INPUT,
|
|
12
13
|
ROLE_LINKS_FOCUS,
|
|
13
14
|
ROLE_BORDERS,
|
|
@@ -420,6 +421,36 @@ const buttonColorsSectionDef: SectionDef = {
|
|
|
420
421
|
],
|
|
421
422
|
};
|
|
422
423
|
|
|
424
|
+
const scrollToBottomSectionDef: SectionDef = {
|
|
425
|
+
id: 'scroll-to-bottom-style',
|
|
426
|
+
title: 'Scroll To Bottom',
|
|
427
|
+
description: 'Style the floating jump-to-latest affordance.',
|
|
428
|
+
collapsed: true,
|
|
429
|
+
fields: [
|
|
430
|
+
{ id: 'scroll-bottom-bg', label: 'Background', type: 'token-ref', path: 'theme.components.scrollToBottom.background', defaultValue: 'components.button.primary.background', tokenRef: { tokenType: 'color' } },
|
|
431
|
+
{ id: 'scroll-bottom-fg', label: 'Foreground', type: 'token-ref', path: 'theme.components.scrollToBottom.foreground', defaultValue: 'components.button.primary.foreground', tokenRef: { tokenType: 'color' } },
|
|
432
|
+
{ id: 'scroll-bottom-border', label: 'Border', type: 'token-ref', path: 'theme.components.scrollToBottom.border', defaultValue: 'semantic.colors.primary', tokenRef: { tokenType: 'color' } },
|
|
433
|
+
{ id: 'scroll-bottom-size', label: 'Size', type: 'text', path: 'theme.components.scrollToBottom.size', defaultValue: '40px' },
|
|
434
|
+
{ id: 'scroll-bottom-radius', label: 'Border Radius', type: 'select', path: 'theme.components.scrollToBottom.borderRadius', defaultValue: 'palette.radius.full', options: [
|
|
435
|
+
{ value: 'palette.radius.md', label: 'Medium' },
|
|
436
|
+
{ value: 'palette.radius.lg', label: 'Large' },
|
|
437
|
+
{ value: 'palette.radius.xl', label: 'Extra Large' },
|
|
438
|
+
{ value: 'palette.radius.full', label: 'Full' },
|
|
439
|
+
] },
|
|
440
|
+
{ id: 'scroll-bottom-shadow', label: 'Shadow', type: 'select', path: 'theme.components.scrollToBottom.shadow', defaultValue: 'palette.shadows.sm', options: [
|
|
441
|
+
{ value: 'palette.shadows.none', label: 'None' },
|
|
442
|
+
{ value: 'palette.shadows.sm', label: 'Small' },
|
|
443
|
+
{ value: 'palette.shadows.md', label: 'Medium' },
|
|
444
|
+
{ value: 'palette.shadows.lg', label: 'Large' },
|
|
445
|
+
{ value: 'palette.shadows.xl', label: 'Extra Large' },
|
|
446
|
+
] },
|
|
447
|
+
{ id: 'scroll-bottom-padding', label: 'Padding', type: 'text', path: 'theme.components.scrollToBottom.padding', defaultValue: '0.5rem 0.875rem' },
|
|
448
|
+
{ id: 'scroll-bottom-gap', label: 'Gap', type: 'text', path: 'theme.components.scrollToBottom.gap', defaultValue: '0.5rem' },
|
|
449
|
+
{ id: 'scroll-bottom-font-size', label: 'Font Size', type: 'text', path: 'theme.components.scrollToBottom.fontSize', defaultValue: '0.875rem' },
|
|
450
|
+
{ id: 'scroll-bottom-icon-size', label: 'Icon Size', type: 'text', path: 'theme.components.scrollToBottom.iconSize', defaultValue: '14px' },
|
|
451
|
+
],
|
|
452
|
+
};
|
|
453
|
+
|
|
423
454
|
/** Shared shape sections (not scoped to light/dark) */
|
|
424
455
|
export const COMPONENT_SHAPE_SECTIONS: SectionDef[] = [
|
|
425
456
|
panelLayoutSectionDef,
|
|
@@ -435,6 +466,7 @@ export const COMPONENT_COLOR_SECTIONS: SectionDef[] = [
|
|
|
435
466
|
messageColorsSectionDef,
|
|
436
467
|
inputColorsSectionDef,
|
|
437
468
|
buttonColorsSectionDef,
|
|
469
|
+
scrollToBottomSectionDef,
|
|
438
470
|
];
|
|
439
471
|
|
|
440
472
|
export const COMPONENTS_SECTIONS: SectionDef[] = [
|
|
@@ -655,6 +687,9 @@ const featuresSectionDef: SectionDef = {
|
|
|
655
687
|
fields: [
|
|
656
688
|
{ id: 'feat-voice', label: 'Voice Recognition', description: 'Enable voice input', type: 'toggle', path: 'voiceRecognition.enabled', defaultValue: false },
|
|
657
689
|
{ id: 'feat-auto-focus', label: 'Auto Focus Input', description: 'Focus input after panel opens', type: 'toggle', path: 'autoFocusInput', defaultValue: false },
|
|
690
|
+
{ id: 'feat-scroll-bottom-enabled', label: 'Scroll To Bottom', description: 'Show a jump-to-latest affordance when the user scrolls away from new content', type: 'toggle', path: 'features.scrollToBottom.enabled', defaultValue: true },
|
|
691
|
+
{ id: 'feat-scroll-bottom-icon', label: 'Scroll To Bottom Icon', type: 'text', path: 'features.scrollToBottom.iconName', defaultValue: 'arrow-down' },
|
|
692
|
+
{ id: 'feat-scroll-bottom-label', label: 'Scroll To Bottom Label', description: 'Leave empty for icon-only mode', type: 'text', path: 'features.scrollToBottom.label', defaultValue: '' },
|
|
658
693
|
],
|
|
659
694
|
};
|
|
660
695
|
|
|
@@ -826,6 +861,13 @@ export const INTERFACE_ROLES_SECTION: SectionDef = {
|
|
|
826
861
|
path: 'theme.components.button.primary.background',
|
|
827
862
|
roleAssignment: ROLE_PRIMARY_ACTIONS,
|
|
828
863
|
},
|
|
864
|
+
{
|
|
865
|
+
id: 'role-scroll-to-bottom',
|
|
866
|
+
label: 'Scroll To Bottom',
|
|
867
|
+
type: 'role-assignment',
|
|
868
|
+
path: 'theme.components.scrollToBottom.background',
|
|
869
|
+
roleAssignment: ROLE_SCROLL_TO_BOTTOM,
|
|
870
|
+
},
|
|
829
871
|
{
|
|
830
872
|
id: 'role-input',
|
|
831
873
|
label: 'Input Field',
|
package/src/theme-reference.ts
CHANGED
|
@@ -138,6 +138,8 @@ export const THEME_TOKEN_DOCS = {
|
|
|
138
138
|
approval:
|
|
139
139
|
'requested (background, border, text), approve (background, foreground), deny (background, foreground).',
|
|
140
140
|
attachment: 'image (background, border).',
|
|
141
|
+
scrollToBottom:
|
|
142
|
+
'Floating scroll-to-bottom affordance shared by transcript and event stream: background, foreground, border, size, borderRadius, shadow, padding, gap, fontSize, iconSize.',
|
|
141
143
|
toolBubble: 'shadow — tool call row box-shadow.',
|
|
142
144
|
reasoningBubble: 'shadow — reasoning/thinking row box-shadow.',
|
|
143
145
|
composer: 'shadow — message input form box-shadow.',
|
|
@@ -201,6 +203,12 @@ export const THEME_TOKEN_DOCS = {
|
|
|
201
203
|
properties:
|
|
202
204
|
'enabled, iconColor, backgroundColor, borderWidth, borderColor, borderRadius, size.',
|
|
203
205
|
},
|
|
206
|
+
scrollToBottom: {
|
|
207
|
+
description:
|
|
208
|
+
'Shared transcript + event-stream jump-to-latest affordance.',
|
|
209
|
+
properties:
|
|
210
|
+
'features.scrollToBottom.enabled, features.scrollToBottom.iconName, features.scrollToBottom.label (empty string renders icon-only). Defaults: enabled=true, iconName="arrow-down", label="".',
|
|
211
|
+
},
|
|
204
212
|
toolCall: {
|
|
205
213
|
description: 'Tool call display styling.',
|
|
206
214
|
properties:
|
package/src/types/theme.ts
CHANGED
|
@@ -393,6 +393,14 @@ export interface LabelButtonTokens {
|
|
|
393
393
|
gap?: string;
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
/** Scroll-to-bottom pill chrome shared by transcript + event stream. */
|
|
397
|
+
export interface ScrollToBottomTokens extends ComponentTokenSet {
|
|
398
|
+
size?: string;
|
|
399
|
+
gap?: string;
|
|
400
|
+
fontSize?: string;
|
|
401
|
+
iconSize?: string;
|
|
402
|
+
}
|
|
403
|
+
|
|
396
404
|
/** Toggle group chrome (used by createToggleGroup). */
|
|
397
405
|
export interface ToggleGroupTokens {
|
|
398
406
|
/** Gap between toggle buttons. Default: 0 (connected). */
|
|
@@ -420,6 +428,8 @@ export interface ComponentTokens {
|
|
|
420
428
|
iconButton?: IconButtonTokens;
|
|
421
429
|
/** Label button styling tokens. */
|
|
422
430
|
labelButton?: LabelButtonTokens;
|
|
431
|
+
/** Scroll-to-bottom indicator styling tokens. */
|
|
432
|
+
scrollToBottom?: ScrollToBottomTokens;
|
|
423
433
|
/** Toggle group styling tokens. */
|
|
424
434
|
toggleGroup?: ToggleGroupTokens;
|
|
425
435
|
/** Artifact toolbar, tab strip, and pane chrome. */
|
package/src/types.ts
CHANGED
|
@@ -552,10 +552,32 @@ export type AgentWidgetArtifactsFeature = {
|
|
|
552
552
|
}) => HTMLElement | null;
|
|
553
553
|
};
|
|
554
554
|
|
|
555
|
+
export type AgentWidgetScrollToBottomFeature = {
|
|
556
|
+
/**
|
|
557
|
+
* When true, Persona shows a scroll-to-bottom affordance when the user breaks
|
|
558
|
+
* away from the latest transcript or event stream content.
|
|
559
|
+
* @default true
|
|
560
|
+
*/
|
|
561
|
+
enabled?: boolean;
|
|
562
|
+
/**
|
|
563
|
+
* Lucide icon name used for the affordance.
|
|
564
|
+
* @default "arrow-down"
|
|
565
|
+
*/
|
|
566
|
+
iconName?: string;
|
|
567
|
+
/**
|
|
568
|
+
* Optional label text shown next to the icon. Set to an empty string for an
|
|
569
|
+
* icon-only affordance.
|
|
570
|
+
* @default ""
|
|
571
|
+
*/
|
|
572
|
+
label?: string;
|
|
573
|
+
};
|
|
574
|
+
|
|
555
575
|
export type AgentWidgetFeatureFlags = {
|
|
556
576
|
showReasoning?: boolean;
|
|
557
577
|
showToolCalls?: boolean;
|
|
558
578
|
showEventStreamToggle?: boolean;
|
|
579
|
+
/** Shared transcript + event stream scroll-to-bottom affordance. */
|
|
580
|
+
scrollToBottom?: AgentWidgetScrollToBottomFeature;
|
|
559
581
|
/** Configuration for the Event Stream inspector view */
|
|
560
582
|
eventStream?: EventStreamConfig;
|
|
561
583
|
/** Optional artifact sidebar (split pane / mobile drawer) */
|