From ec689c3a038b4496d64ef15d6621e867461be04a Mon Sep 17 00:00:00 2001 From: johnnyjoygh Date: Fri, 7 Aug 2026 00:04:04 +0800 Subject: [PATCH] chore(sidebar): replace static mode with a library-content fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rail had two navigation grammars for the same destinations: a compact icon strip on recognized routes, and StaticNavigation — full labeled rows — on everything else (/about, /403, /404, wildcard). Crossing onto one of those pages rearranged the entire rail, and any route nobody remembered to classify silently inherited the alternate layout. Make the frame invariant instead: header, global strip, contextual content, footer — on every route. Pages without a specific sidebar tenant now fall back to the default library content (the home collection for signed-in users, explore for guests), following the convention that error and utility pages render inside the persistent shell rather than degrading it. The memo-detail route gets the same fallback while the page has not yet published its descriptor, instead of a blank scroll area. Off the collection routes the library is display-plus-navigation, so StatisticsView and TagsSection accept a navigationTarget: calendar and tag clicks land on / or /explore with the filter in the query string, which RootLayout's route-change effect would otherwise wipe. StaticNavigation is deleted, About returns to living only in the user menu (the showAbout prop is gone), and the guest strip now marks About active on /about. --- web/src/components/AppSidebar/AppSidebar.tsx | 111 ++++-------------- web/src/components/AppSidebar/TagsSection.tsx | 17 ++- .../StatisticsView/StatisticsView.tsx | 4 +- web/src/components/UserMenu.tsx | 13 +- web/tests/app-sidebar-logo.test.tsx | 15 ++- 5 files changed, 52 insertions(+), 108 deletions(-) diff --git a/web/src/components/AppSidebar/AppSidebar.tsx b/web/src/components/AppSidebar/AppSidebar.tsx index 3dd4a841..c750b54a 100644 --- a/web/src/components/AppSidebar/AppSidebar.tsx +++ b/web/src/components/AppSidebar/AppSidebar.tsx @@ -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_COUNT_CLASSES, SIDEBAR_ROW_ICON_CLASSES } from "./SidebarRow"; +import SidebarRow, { SIDEBAR_ROW_CLASSES, SIDEBAR_ROW_ICON_CLASSES } from "./SidebarRow"; import SidebarSectionHeader from "./SidebarSectionHeader"; import TagsSection from "./TagsSection"; @@ -237,14 +237,19 @@ const CollectionSidebarContent = ({ context }: { context: MemoStatsContext }) => const showViews = !!currentUser && (context === "home" || context === "archived" || context === "explore"); + // Off the collection routes (the library shown as fallback content), calendar and tag + // clicks must land somewhere that renders the filtered feed. + const onCollectionRoute = isMemoScopeRoute(location.pathname) || !!profileMatch; + const filterTarget = onCollectionRoute ? undefined : context === "explore" ? ROUTES.EXPLORE : ROUTES.HOME; + return (
{context === "profile" && }
- setMobileOpen(false)} /> + setMobileOpen(false)} />
{showViews && } - setMobileOpen(false)} /> + setMobileOpen(false)} />
); }; @@ -380,6 +385,8 @@ const MemoDetailSidebarContent = () => { const RouteSidebarContent = () => { const location = useLocation(); + const currentUser = useCurrentUser(); + const { memoDetail } = useAppSidebar(); const kind = getSidebarRouteKind(location.pathname); if (kind === "home" || kind === "archived" || kind === "explore" || kind === "profile") { return ; @@ -388,8 +395,10 @@ const RouteSidebarContent = () => { if (kind === "attachments") return ; if (kind === "inbox") return ; if (kind === "settings") return ; - if (kind === "memo") return ; - return null; + if (kind === "memo" && memoDetail) return ; + // Routes without a specific tenant (about, error pages, unknown paths, memo detail + // before the page publishes its descriptor) fall back to the default library content. + return ; }; interface GlobalNavItem { @@ -467,7 +476,7 @@ const GlobalNavigation = () => { icon: EarthIcon, active: routeKind === "explore" || routeKind === "profile" || routeKind === "memo", }, - { id: "about", label: t("common.about"), path: ROUTES.ABOUT, icon: InfoIcon, active: false }, + { id: "about", label: t("common.about"), path: ROUTES.ABOUT, icon: InfoIcon, active: location.pathname === ROUTES.ABOUT }, ]; const scopeTrigger = ( @@ -584,80 +593,10 @@ const GlobalNavigation = () => { ); }; -const StaticNavigation = () => { - const t = useTranslate(); - const location = useLocation(); - const currentUser = useCurrentUser(); - const { data: notifications = [] } = useNotifications(); - const { setMobileOpen } = useAppSidebar(); - const unreadCount = notifications.filter((notification) => notification.status === UserNotification_Status.UNREAD).length; - const primaryItems: Array<{ id: string; label: string; path: string; icon: LucideIcon }> = currentUser - ? [ - { id: "home", label: t("common.home"), path: ROUTES.HOME, icon: HouseIcon }, - { id: "explore", label: t("common.explore"), path: ROUTES.EXPLORE, icon: EarthIcon }, - ] - : [{ id: "explore", label: t("common.explore"), path: ROUTES.EXPLORE, icon: EarthIcon }]; - const personalItems: Array<{ id: string; label: string; path: string; icon: LucideIcon; count?: number }> = currentUser - ? [ - { id: "attachments", label: t("common.attachments"), path: ROUTES.ATTACHMENTS, icon: PaperclipIcon }, - { id: "inbox", label: t("common.inbox"), path: ROUTES.INBOX, icon: BellIcon, count: unreadCount }, - ] - : []; - - const renderLink = (item: (typeof primaryItems)[number] | (typeof personalItems)[number]) => { - const Icon = item.icon; - const count = "count" in item ? item.count : undefined; - return ( - setMobileOpen(false)} - className={cn(SIDEBAR_ROW_CLASSES, "text-muted-foreground hover:bg-sidebar-accent/65 hover:text-foreground")} - > - - {item.label} - {count != null && count > 0 && {count}} - - ); - }; - - return ( -
- - -
- ); -}; - const AppSidebar = ({ className }: { className?: string }) => { const t = useTranslate(); - const location = useLocation(); const currentUser = useCurrentUser(); const { setMobileOpen, setQuickFindOpen } = useAppSidebar(); - const staticMode = getSidebarRouteKind(location.pathname) === "empty"; return (