fix: prevent sidebar expand flicker
This commit is contained in:
parent
23029b2fce
commit
c2fed764bb
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue