diff --git a/web/src/components/AppSidebar/AppSidebar.tsx b/web/src/components/AppSidebar/AppSidebar.tsx index 574c9750..446cd5d9 100644 --- a/web/src/components/AppSidebar/AppSidebar.tsx +++ b/web/src/components/AppSidebar/AppSidebar.tsx @@ -22,7 +22,7 @@ import { Trash2Icon, UserRoundIcon, } from "lucide-react"; -import { useEffect, useState } from "react"; +import { type ReactNode, useEffect, useState } from "react"; import toast from "react-hot-toast"; import { Link, matchPath, useLocation, useNavigate, useSearchParams } from "react-router-dom"; import ConfirmDialog from "@/components/ConfirmDialog"; @@ -62,7 +62,7 @@ import { User_Role, UserNotification_Status } from "@/types/proto/api/v1/user_se import { useTranslate } from "@/utils/i18n"; import MemosLogo from "../MemosLogo"; import { getSidebarRouteKind } from "./routes"; -import SidebarRow, { SIDEBAR_ROW_CLASSES, SIDEBAR_ROW_ICON_CLASSES, sidebarRowStateClasses } from "./SidebarRow"; +import SidebarRow, { SIDEBAR_ROW_CLASSES, SIDEBAR_ROW_FOCUS_CLASSES, SIDEBAR_ROW_ICON_CLASSES, sidebarRowStateClasses } from "./SidebarRow"; import SidebarSection, { SIDEBAR_SECTION_ACTION_BUTTON_CLASSES, SIDEBAR_SECTION_ACTION_ICON_CLASSES, @@ -391,8 +391,41 @@ interface GlobalNavItem { icon: LucideIcon; active: boolean; count?: number; + alwaysExpanded?: boolean; } +/** + * Pills keep a constant px so the icon sits exactly where it does in the collapsed 30px + * square; all width change comes from the label column, which animates 0fr -> 1fr. That + * keeps the expand/collapse a single smooth motion with no padding 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), + ); + +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} + {children} + + +); + const GlobalNavigation = () => { const t = useTranslate(); const location = useLocation(); @@ -458,27 +491,11 @@ const GlobalNavigation = () => { path: ROUTES.EXPLORE, icon: EarthIcon, active: routeKind === "explore" || routeKind === "profile" || routeKind === "memo", + alwaysExpanded: true, }, { id: "about", label: t("common.about"), path: ROUTES.ABOUT, icon: InfoIcon, active: location.pathname === ROUTES.ABOUT }, ]; - const scopeTrigger = ( - - } - > - - {activeScopeItem.label} - - - ); - const scopeMenuContent = ( {scopeItems.map((item) => { @@ -505,68 +522,74 @@ const GlobalNavigation = () => {