Files
writer-work-flow/apps/web/components/NavItems.tsx
Yaojia Wang 3c87b01379 feat(frontend): 左栏 10 项按写作/资料库/审阅/更多分组(UX P2-1)
navSections(projectId?) 纯函数把平铺项目项归为四组,NavItems 复用「本作」小标题模式逐组渲染;链接/路由/激活键不变,工具箱收进「更多」不丢入口。
2026-07-10 18:14:41 +02:00

114 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { Fragment } from "react";
import Link from "next/link";
import {
Blocks,
BookOpen,
Boxes,
ClipboardCheck,
Flag,
LayoutTemplate,
ListTree,
Palette,
PenLine,
Settings,
Sparkles,
Workflow,
type LucideIcon,
} from "lucide-react";
import { usePathname } from "next/navigation";
import {
isNavItemActive,
navSections,
type ActiveNav,
type NavItem,
} from "@/lib/nav/items";
import { focusRing } from "@/lib/ui/variants";
interface NavItemsProps {
// 项目内页传 projectId → 展开项目级入口(大纲/伏笔/审稿UX §3.1)。
projectId?: string;
activeNav?: ActiveNav;
// 抽屉内点链接后关闭抽屉;静态侧栏不传。
onNavigate?: () => void;
}
// 导航条目列表(桌面静态侧栏与移动抽屉共用,避免渲染漂移)。
// 项目内页把 10 项按 写作/资料库/审阅/更多 分组P2-1每组一个小标题全局组无标题直显于顶部。
export function NavItems({ projectId, activeNav, onNavigate }: NavItemsProps) {
const pathname = usePathname() ?? "/";
const sections = navSections(projectId);
return (
<ul className="flex flex-col gap-1 px-2">
{sections.map((section) => (
<Fragment key={section.label ?? "__global"}>
{section.label !== null ? (
<li
className="mt-3 px-3 pb-1 text-xs font-semibold uppercase tracking-wide text-ink-soft/60"
aria-hidden="true"
>
{section.label}
</li>
) : null}
{section.items.map((item) => (
<NavLink
key={item.label}
item={item}
active={isNavItemActive(item, { pathname, activeNav })}
onNavigate={onNavigate}
/>
))}
</Fragment>
))}
</ul>
);
}
function navIcon(item: NavItem): LucideIcon {
if (item.key === "write") return PenLine;
if (item.key === "outline") return ListTree;
if (item.key === "foreshadow") return Flag;
if (item.key === "review") return ClipboardCheck;
if (item.key === "chains") return Workflow;
if (item.key === "style") return Palette;
if (item.key === "codex") return BookOpen;
if (item.key === "toolbox") return Sparkles;
if (item.key === "rules") return Boxes;
if (item.key === "skills") return Blocks;
if (item.href === "/templates") return LayoutTemplate;
if (item.href === "/settings/providers") return Settings;
return BookOpen;
}
function NavLink({
item,
active,
onNavigate,
}: {
item: NavItem;
active: boolean;
onNavigate?: () => void;
}) {
const Icon = navIcon(item);
return (
<li>
<Link
href={item.href}
onClick={onNavigate}
aria-current={active ? "page" : undefined}
className={`flex items-center gap-2 rounded px-3 py-2 text-sm transition-colors ${focusRing} ${
active
? "border-l-2 border-cinnabar bg-[var(--color-cinnabar-wash)] text-cinnabar"
: "border-l-2 border-transparent text-ink hover:bg-[var(--color-cinnabar-wash)] hover:text-cinnabar"
}`}
>
<Icon className="h-4 w-4 shrink-0" aria-hidden="true" />
{item.label}
</Link>
</li>
);
}