feat(ui): route persistence, sidebar active fix, header spacing

- Persist last visited path via Zustand persist, restore on login/root
- Sidebar: exact match for active state (issue detail no longer highlights Issues)
- Sidebar header: increase vertical padding
- Inbox unread count: simplified to text-xs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-03-26 10:57:32 +08:00
parent 8983a9fefa
commit 459d9745b9
6 changed files with 64 additions and 12 deletions

View file

@ -4,6 +4,7 @@ import { Suspense, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { useAuthStore } from "@/features/auth";
import { useWorkspaceStore } from "@/features/workspace";
import { useNavigationStore } from "@/features/navigation";
import { api } from "@/shared/api";
import {
Card,
@ -40,7 +41,8 @@ function LoginPageContent() {
await login(email, name || undefined);
const wsList = await api.listWorkspaces();
await hydrateWorkspace(wsList);
router.push(searchParams.get("next") || "/issues");
const fallback = useNavigationStore.getState().lastPath;
router.push(searchParams.get("next") || fallback);
} catch (err) {
setError("Login failed. Make sure the server is running.");
setSubmitting(false);

View file

@ -73,7 +73,7 @@ export function AppSidebar() {
return (
<Sidebar variant="inset">
{/* Workspace Switcher */}
<SidebarHeader>
<SidebarHeader className="py-3">
<div className="flex items-center gap-4">
<SidebarMenu className="min-w-0 flex-1">
<SidebarMenuItem>
@ -181,9 +181,7 @@ export function AppSidebar() {
<SidebarGroupContent>
<SidebarMenu className="gap-0.5">
{navItems.map((item) => {
const isActive =
pathname === item.href ||
pathname.startsWith(item.href + "/");
const isActive = pathname === item.href;
return (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton
@ -194,7 +192,7 @@ export function AppSidebar() {
<item.icon />
<span>{item.label}</span>
{item.label === "Inbox" && unreadCount > 0 && (
<span className="ml-auto rounded-full bg-primary px-1.5 py-0.5 text-[10px] font-medium text-primary-foreground">
<span className="ml-auto text-xs">
{unreadCount > 99 ? "99+" : unreadCount}
</span>
)}
@ -213,7 +211,7 @@ export function AppSidebar() {
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="sm">
<div className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-[9px] font-medium">
<div className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-xs leading-none font-medium">
{user.name
.split(" ")
.map((w) => w[0])

View file

@ -1,8 +1,9 @@
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useRouter, usePathname } from "next/navigation";
import { MulticaIcon } from "@/components/multica-icon";
import { useNavigationStore } from "@/features/navigation";
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
import { useAuthStore } from "@/features/auth";
import { useWorkspaceStore } from "@/features/workspace";
@ -14,6 +15,7 @@ export default function DashboardLayout({
children: React.ReactNode;
}) {
const router = useRouter();
const pathname = usePathname();
const user = useAuthStore((s) => s.user);
const isLoading = useAuthStore((s) => s.isLoading);
const workspace = useWorkspaceStore((s) => s.workspace);
@ -24,6 +26,10 @@ export default function DashboardLayout({
}
}, [user, isLoading, router]);
useEffect(() => {
useNavigationStore.getState().onPathChange(pathname);
}, [pathname]);
if (isLoading) {
return (
<div className="flex h-screen items-center justify-center">
@ -35,9 +41,9 @@ export default function DashboardLayout({
if (!user || !workspace) return null;
return (
<SidebarProvider>
<SidebarProvider className="max-h-svh">
<AppSidebar />
<SidebarInset>{children}</SidebarInset>
<SidebarInset className="overflow-hidden">{children}</SidebarInset>
</SidebarProvider>
);
}

View file

@ -1,5 +1,21 @@
import { redirect } from "next/navigation";
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useNavigationStore } from "@/features/navigation";
import { MulticaIcon } from "@/components/multica-icon";
export default function Home() {
redirect("/issues");
const router = useRouter();
useEffect(() => {
const lastPath = useNavigationStore.getState().lastPath;
router.replace(lastPath);
}, [router]);
return (
<div className="flex h-screen items-center justify-center">
<MulticaIcon className="size-6" />
</div>
);
}

View file

@ -0,0 +1 @@
export { useNavigationStore } from "./store";

View file

@ -0,0 +1,29 @@
"use client";
import { create } from "zustand";
import { persist } from "zustand/middleware";
const EXCLUDED_PREFIXES = ["/login", "/pair/"];
interface NavigationState {
lastPath: string;
onPathChange: (path: string) => void;
}
export const useNavigationStore = create<NavigationState>()(
persist(
(set) => ({
lastPath: "/issues",
onPathChange: (path: string) => {
if (!EXCLUDED_PREFIXES.some((prefix) => path.startsWith(prefix))) {
set({ lastPath: path });
}
},
}),
{
name: "multica_navigation",
partialize: (state) => ({ lastPath: state.lastPath }),
}
)
);