fix: prevent sidebar expand flicker

This commit is contained in:
maaz 2026-07-05 09:08:57 +05:30
parent 23029b2fce
commit c2fed764bb
3 changed files with 57 additions and 14 deletions

View File

@ -13,7 +13,7 @@ import {
Sun,
Trash2,
} from "lucide-react";
import { useRef, useState, type ReactNode } from "react";
import { useEffect, useRef, useState, type ReactNode } from "react";
import {
attentionZone,
newestActiveOrchestrator,
@ -145,8 +145,26 @@ export function Sidebar({
const eventsConnection = useEventsConnection();
const { state, setOpen } = useSidebar();
const isCollapsed = state === "collapsed";
const [expandedChromeVisible, setExpandedChromeVisible] = useState(!isCollapsed);
const theme = useUiStore((s) => s.theme);
const toggleTheme = useUiStore((s) => s.toggleTheme);
useEffect(() => {
if (isCollapsed) {
setExpandedChromeVisible(false);
return;
}
const reducedMotion =
typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reducedMotion) {
setExpandedChromeVisible(true);
return;
}
const timer = window.setTimeout(() => setExpandedChromeVisible(true), 160);
return () => window.clearTimeout(timer);
}, [isCollapsed]);
// Disclosure state: projects are expanded by default; a project id present in
// this set is collapsed (sessions hidden).
const [collapsedIds, setCollapsedIds] = useState<ReadonlySet<string>>(() => new Set());
@ -191,6 +209,7 @@ export function Sidebar({
// (same override as shadcn's header-above-sidebar block).
<SidebarRoot
collapsible="icon"
data-expanded-chrome={expandedChromeVisible ? "visible" : "hidden"}
className={cn("border-border", underTopbar ? "top-14 h-[calc(100svh-3.5rem)]!" : "top-0 h-svh!")}
>
<SidebarHeader className="gap-0 p-0 pl-2.5 pr-[7px] pt-3.5 group-data-[collapsible=icon]:px-1.5">
@ -229,12 +248,12 @@ export function Sidebar({
<TooltipContent side="right">Expand sidebar · B</TooltipContent>
</Tooltip>
)}
<span className="min-w-0 flex-1 truncate text-[14px] font-bold tracking-[-0.015em] text-foreground group-data-[collapsible=icon]:hidden">
<span className="sidebar-expanded-chrome min-w-0 flex-1 truncate text-[14px] font-bold tracking-[-0.015em] text-foreground group-data-[collapsible=icon]:hidden">
Agent Orchestrator
</span>
{isNightly && (
<span
className="shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-semibold leading-none group-data-[collapsible=icon]:hidden"
className="sidebar-expanded-chrome shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-semibold leading-none group-data-[collapsible=icon]:hidden"
style={{
color: "var(--purple)",
background: "color-mix(in srgb, var(--purple) 12%, transparent)",
@ -249,7 +268,7 @@ export function Sidebar({
<TooltipTrigger asChild>
<SidebarTrigger
aria-label="Collapse sidebar"
className="size-[18px] shrink-0 rounded-[4px] p-0 text-passive hover:bg-interactive-hover hover:text-foreground group-data-[collapsible=icon]:hidden [&_svg]:size-[15px]"
className="sidebar-expanded-chrome size-[18px] shrink-0 rounded-[4px] p-0 text-passive hover:bg-interactive-hover hover:text-foreground group-data-[collapsible=icon]:hidden [&_svg]:size-[15px]"
/>
</TooltipTrigger>
<TooltipContent>Collapse sidebar · B</TooltipContent>
@ -261,7 +280,7 @@ export function Sidebar({
<SidebarContent className="gap-0 pl-2.5 pr-[7px] group-data-[collapsible=icon]:items-center group-data-[collapsible=icon]:px-1.5">
<SidebarGroup className="p-0">
{/* Section label (project-sidebar__nav-label) */}
<div className="flex shrink-0 items-center justify-between px-2 pb-2 group-data-[collapsible=icon]:hidden">
<div className="sidebar-expanded-chrome flex shrink-0 items-center justify-between px-2 pb-2 group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel className="h-auto rounded-none p-0 text-[10.5px] font-semibold uppercase tracking-[0.09em] text-passive">
Projects
</SidebarGroupLabel>
@ -271,12 +290,12 @@ export function Sidebar({
{/* Tree (project-sidebar__tree) */}
<SidebarGroupContent>
{workspaceError ? (
<div className="px-2 py-3 group-data-[collapsible=icon]:hidden">
<div className="sidebar-expanded-chrome px-2 py-3 group-data-[collapsible=icon]:hidden">
<p className="text-[12px] text-foreground">Could not load projects.</p>
<p className="mt-1 text-[11px] text-passive">{workspaceError}</p>
</div>
) : workspaces.length === 0 ? (
<div className="px-2 py-3 group-data-[collapsible=icon]:hidden">
<div className="sidebar-expanded-chrome px-2 py-3 group-data-[collapsible=icon]:hidden">
<p className="text-[12px] text-passive">No projects yet.</p>
<p className="mt-1 text-[11px] text-passive">
Click <span className="text-foreground">+</span> above to register a git repo.
@ -307,7 +326,7 @@ export function Sidebar({
12px top, 0 bottom, content-hugging button). The icon rail keeps the
icon-only settings action plus expand toggle (off macOS). */}
<SidebarFooter className="relative mt-auto min-h-[51px] gap-0 overflow-hidden border-t border-border p-[7px] transition-[padding] duration-200 ease-linear group-data-[collapsible=icon]:items-center group-data-[collapsible=icon]:px-1.5">
<div className="relative flex min-h-[37px] w-full min-w-[186px] items-center transition-[opacity,transform] duration-150 ease-out group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:-translate-x-2 group-data-[collapsible=icon]:opacity-0">
<div className="sidebar-expanded-chrome relative flex min-h-[37px] w-full min-w-[186px] items-center transition-[opacity,transform] duration-150 ease-out group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:-translate-x-2 group-data-[collapsible=icon]:opacity-0">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
@ -541,7 +560,9 @@ function ProjectItem({
aria-hidden="true"
/>
<span className="hidden group-data-[collapsible=icon]:block">{workspace.name.charAt(0).toUpperCase()}</span>
<span className="min-w-0 flex-1 truncate group-data-[collapsible=icon]:hidden">{workspace.name}</span>
<span className="sidebar-expanded-chrome min-w-0 flex-1 truncate group-data-[collapsible=icon]:hidden">
{workspace.name}
</span>
<span className="hidden h-4 min-w-4 shrink-0 place-items-center rounded bg-interactive-hover px-1 font-mono text-[10px] leading-none text-passive">
{sessions.length}
</span>
@ -551,7 +572,7 @@ function ProjectItem({
propagation issues in Electron's Chromium. Hidden in the icon rail. */}
<div
className={cn(
"absolute top-0 right-1 z-10 flex h-9 items-center gap-px",
"sidebar-expanded-chrome absolute top-0 right-1 z-10 flex h-9 items-center gap-px",
"group-data-[collapsible=icon]:hidden",
)}
>
@ -615,7 +636,7 @@ function ProjectItem({
{/* project-sidebar__sessions: indented under the project parent so worker
sessions read as children without adding a persistent guide rail. */}
{expanded && sessions.length > 0 && (
<SidebarMenuSub className="mx-0 ml-[18px] translate-x-0 gap-0 border-l-0 px-0 py-1 pl-2.5">
<SidebarMenuSub className="sidebar-expanded-chrome mx-0 ml-[18px] translate-x-0 gap-0 border-l-0 px-0 py-1 pl-2.5">
{sessions.map((session) => (
<SessionRow
key={session.id}

View File

@ -225,7 +225,7 @@ function ShellLayout() {
call the store directly) stay in sync. --sidebar-width chains to
the drag-resizable --ao-sidebar-w set on :root by useResizable. */}
<SidebarProvider
className="min-h-0 flex-1"
className="min-h-0 flex-1 overflow-x-hidden"
onOpenChange={(open) => open !== isSidebarOpen && toggleSidebar()}
open={isSidebarOpen}
style={{ "--sidebar-width": "var(--ao-sidebar-w, 240px)", "--sidebar-width-icon": "48px" } as CSSProperties}
@ -238,8 +238,8 @@ function ShellLayout() {
workspaceError={workspaceQuery.isError ? errorMessage(workspaceQuery.error) : undefined}
workspaces={workspaces}
/>
<main className="flex min-w-0 flex-1 flex-col">
<div className="min-h-0 flex-1">
<main className="flex min-w-0 flex-1 flex-col overflow-x-hidden">
<div className="min-h-0 flex-1 overflow-x-hidden">
<Outlet />
</div>
</main>

View File

@ -422,6 +422,28 @@ body.is-resizing-x [data-slot="sidebar-container"] {
transition: none;
}
/* Expanded-only sidebar chrome waits for the rail width transition to finish
* before painting, so tap-to-expand does not flash labels inside the narrow
* collapsed rail. */
.sidebar-expanded-chrome {
transition: opacity 150ms ease-out;
}
[data-slot="sidebar-container"][data-expanded-chrome="hidden"] .sidebar-expanded-chrome {
opacity: 0;
pointer-events: none;
}
[data-slot="sidebar-container"][data-expanded-chrome="visible"] .sidebar-expanded-chrome {
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.sidebar-expanded-chrome {
transition: none;
}
}
/* Shell topbar (agent-orchestrator .dashboard-app-header)
* One header for every screen (rendered by the shell layout), so one padding:
* AO's session-topbar values. Upstream splits this into a 14px dashboard