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

98 lines
3.7 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.

// 左导航条目数据 + 激活判定(纯逻辑,供桌面静态侧栏 / 移动抽屉共用UX §5.1/§3.1)。
// 组件只负责渲染;条目与高亮规则在此集中,避免侧栏与抽屉漂移。
export type ActiveNav =
| "write"
| "outline"
| "foreshadow"
| "review"
| "style"
| "codex"
| "rules"
| "skills"
| "toolbox"
| "chains";
export interface NavItem {
href: string;
label: string;
// 项目级入口的激活键(与 activeNav 比对高亮);全局项无 key。
key?: ActiveNav;
}
export const GLOBAL_NAV_ITEMS: readonly NavItem[] = [
{ href: "/", label: "作品库" },
{ href: "/templates", label: "模板库" },
{ href: "/settings/providers", label: "设置" },
];
// 项目内页展开的项目级入口UX §3.1)。
export function projectNavItems(projectId: string): NavItem[] {
return [
{ href: `/projects/${projectId}/write`, label: "写作", key: "write" },
{ href: `/projects/${projectId}/outline`, label: "大纲", key: "outline" },
{
href: `/projects/${projectId}/foreshadow`,
label: "伏笔",
key: "foreshadow",
},
{ href: `/projects/${projectId}/review`, label: "审稿", key: "review" },
{ href: `/projects/${projectId}/chains`, label: "多章链", key: "chains" },
{ href: `/projects/${projectId}/style`, label: "文风", key: "style" },
{ href: `/projects/${projectId}/codex`, label: "设定库", key: "codex" },
{ href: `/projects/${projectId}/toolbox`, label: "工具箱", key: "toolbox" },
{ href: `/projects/${projectId}/rules`, label: "规则", key: "rules" },
{ href: `/projects/${projectId}/skills`, label: "技能库", key: "skills" },
];
}
// 左栏一个分组:可选小标题 + 若干条目P2-1。全局组 label=null顶部直显、无小标题
// 项目组复用现有"本作"式小标题样式。分组只重排 + 加标题,链接/路由/激活键均不变(行为保持)。
export interface NavSection {
label: string | null;
items: readonly NavItem[];
}
// 项目级入口的语义分组P2-1把 10 个平铺项归为 写作/资料库/审阅/更多,便于扫读。
// 工具箱归入「更多」——原分组清单只列了 规则·技能库,收进"更多"以不丢任何入口
// (见 memory/decisions.md 2026-07-10 条)。
const PROJECT_GROUPS: readonly { label: string; keys: readonly ActiveNav[] }[] =
[
{ label: "写作", keys: ["write"] },
{ label: "资料库", keys: ["outline", "codex", "foreshadow", "style"] },
{ label: "审阅", keys: ["review", "chains"] },
{ label: "更多", keys: ["toolbox", "rules", "skills"] },
];
// 左导航分组(桌面侧栏 / 移动抽屉共用):全局组恒在顶部,项目内页再展开四个项目组。
export function navSections(projectId?: string): NavSection[] {
const global: NavSection = { label: null, items: GLOBAL_NAV_ITEMS };
if (projectId === undefined) return [global];
const byKey = new Map(projectNavItems(projectId).map((item) => [item.key, item]));
const projectSections = PROJECT_GROUPS.map((group) => ({
label: group.label,
items: group.keys.map((key) => {
const item = byKey.get(key);
if (item === undefined) {
throw new Error(`navSections: 缺少项目入口 ${key}`);
}
return item;
}),
}));
return [global, ...projectSections];
}
export interface NavActiveContext {
pathname: string;
activeNav?: ActiveNav;
}
// 全局项按精确路径匹配;项目项按 activeNav 键匹配(或路径相等兜底)。
export function isNavItemActive(item: NavItem, ctx: NavActiveContext): boolean {
if (item.key !== undefined) {
return item.key === ctx.activeNav || ctx.pathname === item.href;
}
return ctx.pathname === item.href;
}