fix(webui): position sidebar highlight on mount

This commit is contained in:
Xubin Ren
2026-08-01 23:01:43 +08:00
parent 0cb7dd5cc9
commit db6c9effc3
5 changed files with 77 additions and 56 deletions
+5 -10
View File
@@ -107,7 +107,6 @@ export const ChatList = memo(function ChatList({
}: ChatListProps) {
const { t } = useTranslation();
const [visibleLimit, setVisibleLimit] = useState(INITIAL_VISIBLE_SESSIONS);
const listContentRef = useRef<HTMLDivElement>(null);
const activeRowRef = useRef<HTMLDivElement>(null);
const labels = useMemo<ChatGroupLabels>(() => ({
pinned: t("chat.groups.pinned"),
@@ -188,8 +187,10 @@ export const ChatList = memo(function ChatList({
return (
<div className="h-full min-h-0 min-w-0 overflow-x-hidden overflow-y-auto overscroll-contain scrollbar-thin scrollbar-track-transparent">
<div
ref={listContentRef}
<SidebarSelectionHighlight
targetRef={activeRowRef}
activeId={activeKey}
scope="sessions"
data-chat-list-content
className="relative min-w-0 space-y-3 px-2 py-1.5"
>
@@ -408,13 +409,7 @@ export const ChatList = memo(function ChatList({
</button>
</div>
) : null}
<SidebarSelectionHighlight
containerRef={listContentRef}
targetRef={activeRowRef}
activeId={activeKey}
scope="sessions"
/>
</div>
</SidebarSelectionHighlight>
</div>
);
});
+5 -10
View File
@@ -92,7 +92,6 @@ export function Sidebar(props: SidebarProps) {
const collapsed = Boolean(props.collapsed);
const toggleLabel = t("thread.header.toggleSidebar");
const newChatShortcut = newChatShortcutLabel();
const actionListRef = useRef<HTMLDivElement>(null);
const activeActionRef = useRef<HTMLButtonElement>(null);
const activeActionId = props.newChatActive
? "new-chat"
@@ -150,8 +149,10 @@ export function Sidebar(props: SidebarProps) {
)}
</div>
<div
ref={actionListRef}
<SidebarSelectionHighlight
targetRef={activeActionRef}
activeId={activeActionId}
scope="actions"
className={cn(
"relative space-y-1.5 px-2 pb-2",
collapsed && "flex w-14 flex-col items-center px-0",
@@ -208,13 +209,7 @@ export function Sidebar(props: SidebarProps) {
icon={<Archive className="h-4 w-4" />}
/>
) : null}
<SidebarSelectionHighlight
containerRef={actionListRef}
targetRef={activeActionRef}
activeId={activeActionId}
scope="actions"
/>
</div>
</SidebarSelectionHighlight>
<div
className={cn(
"flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden transition-opacity duration-200",
@@ -1,11 +1,11 @@
import {
type HTMLAttributes,
type RefObject,
useLayoutEffect,
useRef,
} from "react";
interface SidebarSelectionHighlightProps {
containerRef: RefObject<HTMLElement>;
interface SidebarSelectionHighlightProps extends HTMLAttributes<HTMLDivElement> {
targetRef: RefObject<HTMLElement>;
activeId: string | null;
scope: string;
@@ -18,11 +18,13 @@ export const SIDEBAR_SELECTION_ACTION_ITEM_CLASS =
"relative z-[1] transition-[width,padding,color] [transition-duration:300ms,300ms,150ms] ease-out motion-reduce:transition-none";
export function SidebarSelectionHighlight({
containerRef,
targetRef,
activeId,
scope,
children,
...containerProps
}: SidebarSelectionHighlightProps) {
const containerRef = useRef<HTMLDivElement>(null);
const highlightRef = useRef<HTMLDivElement>(null);
const positionedRef = useRef(false);
@@ -30,11 +32,19 @@ export function SidebarSelectionHighlight({
const highlight = highlightRef.current;
const container = containerRef.current;
const target = targetRef.current;
if (!highlight) return;
if (!activeId || !container || !target) {
highlight.style.opacity = "0";
positionedRef.current = false;
return;
}
let restoreTransitionFrame: number | null = null;
const position = () => {
if (!highlight) return;
if (!activeId || !container || !target) {
const containerRect = container.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
if (targetRect.width === 0 || targetRect.height === 0) {
highlight.style.opacity = "0";
positionedRef.current = false;
return;
@@ -43,8 +53,6 @@ export function SidebarSelectionHighlight({
const firstPosition = !positionedRef.current;
if (firstPosition) highlight.style.transitionProperty = "none";
const containerRect = container.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
highlight.style.width = `${targetRect.width}px`;
highlight.style.height = `${targetRect.height}px`;
highlight.style.transform = `translate3d(${targetRect.left - containerRect.left}px, ${
@@ -64,8 +72,8 @@ export function SidebarSelectionHighlight({
position();
const resizeObserver =
typeof ResizeObserver === "undefined" ? null : new ResizeObserver(position);
if (container) resizeObserver?.observe(container);
if (target) resizeObserver?.observe(target);
resizeObserver?.observe(container);
resizeObserver?.observe(target);
window.addEventListener("resize", position);
return () => {
@@ -79,12 +87,15 @@ export function SidebarSelectionHighlight({
});
return (
<div
ref={highlightRef}
data-testid={`${scope}-selection-highlight`}
data-active-id={activeId ?? undefined}
aria-hidden="true"
className="pointer-events-none absolute left-0 top-0 z-0 !mt-0 rounded-xl bg-sidebar-foreground/[0.055] opacity-0 transition-[transform,width,height] duration-300 ease-out will-change-transform motion-reduce:transition-none dark:bg-white/[0.07]"
/>
<div {...containerProps} ref={containerRef}>
{children}
<div
ref={highlightRef}
data-testid={`${scope}-selection-highlight`}
data-active-id={activeId ?? undefined}
aria-hidden="true"
className="pointer-events-none absolute left-0 top-0 z-0 !mt-0 rounded-xl bg-sidebar-foreground/[0.055] opacity-0 transition-[transform,width,height] duration-300 ease-out will-change-transform motion-reduce:transition-none dark:bg-white/[0.07]"
/>
</div>
);
}
@@ -2501,7 +2501,6 @@ function SettingsSidebar({
hostChromeInset?: boolean;
}) {
const { t } = useTranslation();
const desktopNavRef = useRef<HTMLDivElement>(null);
const activeNavItemRef = useRef<HTMLButtonElement>(null);
const activeItem = SETTINGS_NAV_ITEMS.find((item) => item.key === activeSection)
?? SETTINGS_NAV_ITEMS[0];
@@ -2575,7 +2574,12 @@ function SettingsSidebar({
</DropdownMenuContent>
</DropdownMenu>
<div ref={desktopNavRef} className="relative hidden space-y-1 lg:block">
<SidebarSelectionHighlight
targetRef={activeNavItemRef}
activeId={activeSection}
scope="settings"
className="relative hidden space-y-1 lg:block"
>
{SETTINGS_NAV_ITEMS.map(({ key, icon: Icon, fallback }) => {
const active = key === activeSection;
return (
@@ -2600,13 +2604,7 @@ function SettingsSidebar({
</button>
);
})}
<SidebarSelectionHighlight
containerRef={desktopNavRef}
targetRef={activeNavItemRef}
activeId={activeSection}
scope="settings"
/>
</div>
</SidebarSelectionHighlight>
</nav>
<div className="hidden lg:mt-auto lg:block lg:pt-4">