@runtypelabs/persona 3.6.0 → 3.8.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 +40 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -4
- package/dist/index.d.ts +73 -4
- package/dist/index.global.js +69 -69
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +40 -40
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +704 -243
- package/dist/theme-editor.d.cts +75 -5
- package/dist/theme-editor.d.ts +75 -5
- package/dist/theme-editor.js +703 -243
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +53 -0
- package/dist/theme-reference.d.ts +53 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +44 -0
- package/package.json +1 -1
- package/src/components/artifact-card.ts +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/components/header-builder.ts +3 -0
- package/src/components/launcher.ts +7 -2
- package/src/components/panel.ts +3 -1
- package/src/defaults.ts +15 -0
- package/src/runtime/host-layout.test.ts +1 -1
- package/src/runtime/host-layout.ts +2 -1
- package/src/scroll-to-bottom-defaults.test.ts +13 -0
- package/src/styles/widget.css +44 -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 +45 -0
- package/src/types.ts +31 -4
- package/src/ui.overlay-z-index.test.ts +34 -2
- package/src/ui.scroll.test.ts +554 -0
- package/src/ui.ts +264 -90
- package/src/utils/auto-follow.test.ts +110 -0
- package/src/utils/auto-follow.ts +112 -0
- package/src/utils/constants.ts +13 -0
- package/src/utils/dropdown.ts +2 -1
- package/src/utils/overlay-host-stacking.test.ts +61 -0
- package/src/utils/overlay-host-stacking.ts +38 -0
- package/src/utils/scroll-lock.test.ts +64 -0
- package/src/utils/scroll-lock.ts +62 -0
- package/src/utils/theme.test.ts +34 -0
- package/src/utils/tokens.ts +112 -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
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createElement, createElementInDocument } from "../utils/dom";
|
|
2
2
|
import { renderLucideIcon } from "../utils/icons";
|
|
3
3
|
import { AgentWidgetConfig } from "../types";
|
|
4
|
+
import { PORTALED_OVERLAY_Z_INDEX } from "../utils/constants";
|
|
4
5
|
|
|
5
6
|
/** CSS `color` values; variables are set on `[data-persona-root]` from `theme.components.header`. */
|
|
6
7
|
export const HEADER_THEME_CSS = {
|
|
@@ -233,6 +234,7 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
|
|
|
233
234
|
|
|
234
235
|
// Position tooltip above button
|
|
235
236
|
portaledTooltip.style.position = "fixed";
|
|
237
|
+
portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
|
|
236
238
|
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
237
239
|
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
238
240
|
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
@@ -389,6 +391,7 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
|
|
|
389
391
|
|
|
390
392
|
// Position tooltip above button
|
|
391
393
|
portaledTooltip.style.position = "fixed";
|
|
394
|
+
portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
|
|
392
395
|
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
393
396
|
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
394
397
|
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
@@ -3,6 +3,7 @@ import { AgentWidgetConfig } from "../types";
|
|
|
3
3
|
import { positionMap } from "../utils/positioning";
|
|
4
4
|
import { isDockedMountMode } from "../utils/dock";
|
|
5
5
|
import { renderLucideIcon } from "../utils/icons";
|
|
6
|
+
import { DEFAULT_OVERLAY_Z_INDEX } from "../utils/constants";
|
|
6
7
|
|
|
7
8
|
export interface LauncherButton {
|
|
8
9
|
element: HTMLButtonElement;
|
|
@@ -174,12 +175,16 @@ export const createLauncherButton = (
|
|
|
174
175
|
: positionMap["bottom-right"];
|
|
175
176
|
|
|
176
177
|
const floatingBase =
|
|
177
|
-
"persona-fixed persona-flex persona-items-center persona-gap-3 persona-rounded-launcher persona-bg-persona-surface persona-py-2.5 persona-pl-3 persona-pr-3 persona-transition hover:persona-translate-y-[-2px] persona-cursor-pointer
|
|
178
|
+
"persona-fixed persona-flex persona-items-center persona-gap-3 persona-rounded-launcher persona-bg-persona-surface persona-py-2.5 persona-pl-3 persona-pr-3 persona-transition hover:persona-translate-y-[-2px] persona-cursor-pointer";
|
|
178
179
|
const dockedBase =
|
|
179
180
|
"persona-relative persona-mt-4 persona-mb-4 persona-mx-auto persona-flex persona-items-center persona-justify-center persona-rounded-launcher persona-bg-persona-surface persona-transition hover:persona-translate-y-[-2px] persona-cursor-pointer";
|
|
180
181
|
|
|
181
182
|
button.className = dockedMode ? dockedBase : `${floatingBase} ${positionClass}`;
|
|
182
|
-
|
|
183
|
+
|
|
184
|
+
if (!dockedMode) {
|
|
185
|
+
button.style.zIndex = String(launcher.zIndex ?? DEFAULT_OVERLAY_Z_INDEX);
|
|
186
|
+
}
|
|
187
|
+
|
|
183
188
|
// Apply launcher border and shadow from config (with defaults matching previous Tailwind classes)
|
|
184
189
|
const defaultBorder = "1px solid var(--persona-border, #e5e7eb)";
|
|
185
190
|
const defaultShadow = "var(--persona-shadow, 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1))";
|
package/src/components/panel.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createElement } from "../utils/dom";
|
|
|
2
2
|
import { AgentWidgetConfig } from "../types";
|
|
3
3
|
import { positionMap } from "../utils/positioning";
|
|
4
4
|
import { isDockedMountMode } from "../utils/dock";
|
|
5
|
+
import { DEFAULT_OVERLAY_Z_INDEX } from "../utils/constants";
|
|
5
6
|
import { buildHeader, attachHeaderToContainer, HeaderElements } from "./header-builder";
|
|
6
7
|
import { buildHeaderWithLayout } from "./header-layouts";
|
|
7
8
|
import { buildComposer, ComposerElements } from "./composer-builder";
|
|
@@ -58,8 +59,9 @@ export const createWrapper = (config?: AgentWidgetConfig): PanelWrapper => {
|
|
|
58
59
|
|
|
59
60
|
const wrapper = createElement(
|
|
60
61
|
"div",
|
|
61
|
-
`persona-widget-wrapper persona-fixed ${position} persona-
|
|
62
|
+
`persona-widget-wrapper persona-fixed ${position} persona-transition`
|
|
62
63
|
);
|
|
64
|
+
wrapper.style.zIndex = String(config?.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX);
|
|
63
65
|
|
|
64
66
|
const panel = createElement(
|
|
65
67
|
"div",
|
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
|
})(),
|
|
@@ -228,7 +228,7 @@ describe("createWidgetHostLayout docked", () => {
|
|
|
228
228
|
const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
|
|
229
229
|
layout.syncWidgetState({ open: true, launcherEnabled: true });
|
|
230
230
|
expect(dockSlot?.style.position).toBe("fixed");
|
|
231
|
-
expect(dockSlot?.style.zIndex).toBe("
|
|
231
|
+
expect(dockSlot?.style.zIndex).toBe("100000");
|
|
232
232
|
expect(layout.shell?.dataset.personaDockMobileFullscreen).toBe("true");
|
|
233
233
|
|
|
234
234
|
layout.destroy();
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
AgentWidgetStateSnapshot,
|
|
4
4
|
} from "../types";
|
|
5
5
|
import { isDockedMountMode, resolveDockConfig } from "../utils/dock";
|
|
6
|
+
import { DEFAULT_OVERLAY_Z_INDEX } from "../utils/constants";
|
|
6
7
|
|
|
7
8
|
export type WidgetHostLayoutMode = "direct" | "docked";
|
|
8
9
|
|
|
@@ -198,7 +199,7 @@ const applyDockStyles = (
|
|
|
198
199
|
dockSlot.style.minWidth = "0";
|
|
199
200
|
dockSlot.style.minHeight = "0";
|
|
200
201
|
dockSlot.style.overflow = "hidden";
|
|
201
|
-
dockSlot.style.zIndex = String(config?.launcher?.zIndex ??
|
|
202
|
+
dockSlot.style.zIndex = String(config?.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX);
|
|
202
203
|
dockSlot.style.transform = "none";
|
|
203
204
|
dockSlot.style.transition = "none";
|
|
204
205
|
dockSlot.style.pointerEvents = "auto";
|
|
@@ -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
|
@@ -834,6 +834,10 @@
|
|
|
834
834
|
overflow-y: auto;
|
|
835
835
|
}
|
|
836
836
|
|
|
837
|
+
.persona-widget-body {
|
|
838
|
+
overscroll-behavior: contain;
|
|
839
|
+
}
|
|
840
|
+
|
|
837
841
|
.persona-overflow-hidden {
|
|
838
842
|
overflow: hidden;
|
|
839
843
|
}
|
|
@@ -2320,6 +2324,46 @@
|
|
|
2320
2324
|
background: var(--persona-label-btn-hover-bg, var(--persona-container, #f3f4f6));
|
|
2321
2325
|
}
|
|
2322
2326
|
|
|
2327
|
+
[data-persona-root] .persona-scroll-to-bottom-indicator {
|
|
2328
|
+
display: inline-flex;
|
|
2329
|
+
align-items: center;
|
|
2330
|
+
justify-content: center;
|
|
2331
|
+
gap: var(--persona-scroll-to-bottom-gap, 0.5rem);
|
|
2332
|
+
min-height: var(--persona-scroll-to-bottom-size, 40px);
|
|
2333
|
+
border-radius: var(--persona-scroll-to-bottom-radius, var(--persona-radius-full, 9999px));
|
|
2334
|
+
border: 1px solid var(--persona-scroll-to-bottom-border, var(--persona-primary, #111827));
|
|
2335
|
+
background: var(--persona-scroll-to-bottom-bg, var(--persona-button-primary-bg, var(--persona-accent, #0f0f0f)));
|
|
2336
|
+
color: var(--persona-scroll-to-bottom-fg, var(--persona-button-primary-fg, var(--persona-text-inverse, #ffffff)));
|
|
2337
|
+
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));
|
|
2338
|
+
font-size: var(--persona-scroll-to-bottom-font-size, 0.875rem);
|
|
2339
|
+
line-height: 1;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
[data-persona-root] .persona-scroll-to-bottom-indicator[data-persona-scroll-to-bottom-has-label="true"] {
|
|
2343
|
+
padding: var(--persona-scroll-to-bottom-padding, 0.5rem 0.875rem);
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
[data-persona-root] .persona-scroll-to-bottom-indicator[data-persona-scroll-to-bottom-has-label="false"] {
|
|
2347
|
+
width: var(--persona-scroll-to-bottom-size, 40px);
|
|
2348
|
+
height: var(--persona-scroll-to-bottom-size, 40px);
|
|
2349
|
+
padding: 0;
|
|
2350
|
+
gap: 0;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
[data-persona-root] .persona-scroll-to-bottom-indicator svg {
|
|
2354
|
+
width: var(--persona-scroll-to-bottom-icon-size, 14px);
|
|
2355
|
+
height: var(--persona-scroll-to-bottom-icon-size, 14px);
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
[data-persona-root] .persona-scroll-to-bottom-indicator:hover {
|
|
2359
|
+
opacity: 0.92;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
[data-persona-root] .persona-scroll-to-bottom-indicator:focus-visible {
|
|
2363
|
+
outline: 2px solid var(--persona-accent, #171717);
|
|
2364
|
+
outline-offset: 2px;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2323
2367
|
/* Toggle group — mutually exclusive button set created by createToggleGroup() */
|
|
2324
2368
|
[data-persona-root] .persona-toggle-group {
|
|
2325
2369
|
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
|
@@ -233,6 +233,18 @@ export interface MessageTokens {
|
|
|
233
233
|
/** Assistant bubble box-shadow (token ref or raw CSS, e.g. `none`). */
|
|
234
234
|
shadow?: string;
|
|
235
235
|
};
|
|
236
|
+
/** Border color between messages in the thread. */
|
|
237
|
+
border?: TokenReference<'color'>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Collapsible widget chrome (tool bubbles, reasoning bubbles, approval bubbles). */
|
|
241
|
+
export interface CollapsibleWidgetTokens {
|
|
242
|
+
/** Background for content areas. */
|
|
243
|
+
container?: TokenReference<'color'>;
|
|
244
|
+
/** Background for code blocks inside collapsible sections. */
|
|
245
|
+
surface?: TokenReference<'color'>;
|
|
246
|
+
/** Border color for collapsible sections. */
|
|
247
|
+
border?: TokenReference<'color'>;
|
|
236
248
|
}
|
|
237
249
|
|
|
238
250
|
export interface MarkdownTokens {
|
|
@@ -262,6 +274,27 @@ export interface MarkdownTokens {
|
|
|
262
274
|
fontWeight?: string;
|
|
263
275
|
};
|
|
264
276
|
};
|
|
277
|
+
/** Fenced code block styling. */
|
|
278
|
+
codeBlock?: {
|
|
279
|
+
background?: TokenReference<'color'>;
|
|
280
|
+
borderColor?: TokenReference<'color'>;
|
|
281
|
+
textColor?: TokenReference<'color'>;
|
|
282
|
+
};
|
|
283
|
+
/** Table styling. */
|
|
284
|
+
table?: {
|
|
285
|
+
headerBackground?: TokenReference<'color'>;
|
|
286
|
+
borderColor?: TokenReference<'color'>;
|
|
287
|
+
};
|
|
288
|
+
/** Horizontal rule styling. */
|
|
289
|
+
hr?: {
|
|
290
|
+
color?: TokenReference<'color'>;
|
|
291
|
+
};
|
|
292
|
+
/** Blockquote styling. */
|
|
293
|
+
blockquote?: {
|
|
294
|
+
borderColor?: TokenReference<'color'>;
|
|
295
|
+
background?: TokenReference<'color'>;
|
|
296
|
+
textColor?: TokenReference<'color'>;
|
|
297
|
+
};
|
|
265
298
|
}
|
|
266
299
|
|
|
267
300
|
export interface VoiceTokens {
|
|
@@ -393,6 +426,14 @@ export interface LabelButtonTokens {
|
|
|
393
426
|
gap?: string;
|
|
394
427
|
}
|
|
395
428
|
|
|
429
|
+
/** Scroll-to-bottom pill chrome shared by transcript + event stream. */
|
|
430
|
+
export interface ScrollToBottomTokens extends ComponentTokenSet {
|
|
431
|
+
size?: string;
|
|
432
|
+
gap?: string;
|
|
433
|
+
fontSize?: string;
|
|
434
|
+
iconSize?: string;
|
|
435
|
+
}
|
|
436
|
+
|
|
396
437
|
/** Toggle group chrome (used by createToggleGroup). */
|
|
397
438
|
export interface ToggleGroupTokens {
|
|
398
439
|
/** Gap between toggle buttons. Default: 0 (connected). */
|
|
@@ -420,6 +461,8 @@ export interface ComponentTokens {
|
|
|
420
461
|
iconButton?: IconButtonTokens;
|
|
421
462
|
/** Label button styling tokens. */
|
|
422
463
|
labelButton?: LabelButtonTokens;
|
|
464
|
+
/** Scroll-to-bottom indicator styling tokens. */
|
|
465
|
+
scrollToBottom?: ScrollToBottomTokens;
|
|
423
466
|
/** Toggle group styling tokens. */
|
|
424
467
|
toggleGroup?: ToggleGroupTokens;
|
|
425
468
|
/** Artifact toolbar, tab strip, and pane chrome. */
|
|
@@ -428,6 +471,8 @@ export interface ComponentTokens {
|
|
|
428
471
|
tab?: ArtifactTabTokens;
|
|
429
472
|
pane?: ArtifactPaneTokens;
|
|
430
473
|
};
|
|
474
|
+
/** Collapsible widget chrome (tool/reasoning/approval bubbles). */
|
|
475
|
+
collapsibleWidget?: CollapsibleWidgetTokens;
|
|
431
476
|
}
|
|
432
477
|
|
|
433
478
|
export interface PaletteExtras {
|