From 191ec01d11186db7ab4840b41d6fe008e3729909 Mon Sep 17 00:00:00 2001 From: johnnyjoygh Date: Tue, 1 Sep 2026 23:24:50 +0800 Subject: [PATCH] fix(sidebar): unify layout rails and spacing Keep the primary navigator compact and horizontal while aligning header, content, and footer controls on shared artwork and label rails. Add regression coverage for symmetric popovers, compact hit targets, and mobile drawer accessibility. --- web/src/components/AppSidebar/AppSidebar.tsx | 120 ++++++++++-------- web/src/components/AppSidebar/SidebarRow.tsx | 27 ++-- .../components/AppSidebar/SidebarSection.tsx | 4 +- .../AppSidebar/SidebarSectionHeader.tsx | 2 +- .../components/AppSidebar/SpaceSwitcher.tsx | 37 ++++-- .../components/AppSidebar/sidebar-layout.ts | 35 +++++ web/src/components/MemosLogo.tsx | 21 ++- web/src/components/TagTree.tsx | 6 +- web/src/components/UserMenu.tsx | 60 +++++---- web/src/components/ui/sheet.tsx | 7 +- web/tests/app-sidebar-logo.test.tsx | 64 ++++++++-- web/tests/sidebar-row-grammar.test.tsx | 18 ++- web/tests/space-switcher.test.tsx | 54 +++++++- web/tests/user-menu.test.tsx | 4 +- 14 files changed, 326 insertions(+), 133 deletions(-) create mode 100644 web/src/components/AppSidebar/sidebar-layout.ts diff --git a/web/src/components/AppSidebar/AppSidebar.tsx b/web/src/components/AppSidebar/AppSidebar.tsx index b31d964f..c44a0110 100644 --- a/web/src/components/AppSidebar/AppSidebar.tsx +++ b/web/src/components/AppSidebar/AppSidebar.tsx @@ -54,11 +54,20 @@ import { getSidebarRouteKind, routeSupportsCollectionScope } from "./routes"; import SidebarRow, { SIDEBAR_ROW_CLASSES, SIDEBAR_ROW_FOCUS_CLASSES, SidebarRowIconSlot, sidebarRowStateClasses } from "./SidebarRow"; import SidebarSection, { SIDEBAR_SECTION_STACK_CLASSES } from "./SidebarSection"; import SpaceSwitcher from "./SpaceSwitcher"; +import { + SIDEBAR_FOOTER_CLASSES, + SIDEBAR_LEADING_SLOT_CLASSES, + SIDEBAR_NAV_LEADING_SLOT_CLASSES, + SIDEBAR_RAIL_CLASSES, + sidebarSurfaceVariants, +} from "./sidebar-layout"; import TagsSection from "./TagsSection"; import ViewsSection from "./ViewsSection"; -const SIDEBAR_HORIZONTAL_PADDING = "px-3"; -const SIDEBAR_HEADER_ACTION_CLASSES = "size-7 shrink-0 rounded-md text-muted-foreground hover:text-foreground"; +const SIDEBAR_HEADER_ACTION_CLASSES = + "size-7 shrink-0 rounded-md text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50"; +const SIDEBAR_HEADER_PRIMARY_ACTION_CLASSES = + "size-7 shrink-0 text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50"; const NewMemoAction = ({ onClick }: { onClick: () => void }) => { const t = useTranslate(); @@ -69,9 +78,9 @@ const NewMemoAction = ({ onClick }: { onClick: () => void }) => { 1fr. That - * keeps the expand/collapse a single smooth motion with no padding jump. + * The compact navigator is intentionally horizontal. Its 16px glyph plus 8px padding + * on each side makes the collapsed control an exact 32px square. Expanding the + * label only opens the text track, so the artwork and surface never jump. */ const navPillClasses = (active: boolean) => - cn( - "relative flex h-[30px] min-w-0 items-center rounded-md px-[7px] transition-colors", - SIDEBAR_ROW_FOCUS_CLASSES, - sidebarRowStateClasses(active), - ); + cn(sidebarSurfaceVariants({ role: "navPill" }), SIDEBAR_ROW_FOCUS_CLASSES, sidebarRowStateClasses(active)); const NavPillLabel = ({ expanded, label, children }: { expanded: boolean; label: ReactNode; children?: ReactNode }) => ( - {/* Content is shrink-0 so the collapsing track clips it in place — a plain - left-to-right reveal instead of re-truncating the label on every frame. */} - - {label} + + + {label} + {children} @@ -401,9 +403,9 @@ const GlobalNavigation = () => { }, ]; - // Keep one textual anchor in the compact navigator. A real active destination - // takes precedence; routes outside this navigation default to its first item - // without presenting that fallback as the current page. + // Keep exactly one textual anchor in the compact horizontal navigator. The active + // destination expands; routes outside this navigator fall back to its first control + // without incorrectly marking that fallback as the current page. const activeNavigatorItemId = currentUser && scopeRouteActive ? "scope" : items.find((item) => item.active)?.id; const expandedNavigatorItemId = activeNavigatorItemId ?? (currentUser ? "scope" : items[0]?.id); const scopeExpanded = expandedNavigatorItemId === "scope"; @@ -416,10 +418,7 @@ const GlobalNavigation = () => { navigateToScope(item.id)} > @@ -432,12 +431,11 @@ const GlobalNavigation = () => { return ( -