Files
writer-work-flow/apps/web/components/workbench/ChapterList.tsx
Yaojia Wang dbece64d4d feat(frontend): 「目录」列可折叠、记忆折叠状态(UX P2-2)
useChapterListCollapse 用 localStorage 记忆折叠态(SSR 首帧恒展开避免 hydration 不一致);收起降为带展开按钮的窄轨、让宽给正文,展开时标题旁加收起按钮;三栏栅格据折叠态切列宽,章节列表/高亮/切章导航全保留。
2026-07-10 18:14:49 +02:00

134 lines
4.5 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.

import Link from "next/link";
import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { buildChapterEntries, type ChapterEntry } from "@/lib/workbench/chapter";
import { focusRing } from "@/lib/ui/variants";
interface ChapterListProps {
projectId: string;
// 大纲章节(来自 GET .../outline无大纲时为空当前章会被并入并去重。
chapters: ChapterEntry[];
currentChapterNo: number;
}
interface DesktopChapterListProps extends ChapterListProps {
// 目录列收起态P2-2收起时只留一条带「展开」按钮的窄轨让宽给正文。
collapsed: boolean;
onToggle: () => void;
}
// 左栏目录UX §6.3 / P2-2列出大纲全部章节 + 当前章,点击切换写作章号(`?chapter=N`)。
// 桌面 aside 包裹,可收起为窄轨;移动端经 Workbench 抽屉复用 ChapterListContent不带折叠
export function ChapterList({
projectId,
chapters,
currentChapterNo,
collapsed,
onToggle,
}: DesktopChapterListProps) {
if (collapsed) {
return (
<aside
className="hidden shrink-0 border-r border-line bg-panel py-4 xl:block"
aria-label="目录(已收起)"
>
<button
type="button"
onClick={onToggle}
aria-expanded={false}
aria-label="展开目录"
title="展开目录"
className={`mx-auto flex h-8 w-8 items-center justify-center rounded text-ink-soft transition-colors hover:bg-[var(--color-cinnabar-wash)] hover:text-cinnabar ${focusRing}`}
>
<PanelLeftOpen className="h-4 w-4" aria-hidden="true" />
</button>
</aside>
);
}
return (
<aside className="hidden border-r border-line bg-panel py-4 xl:block">
<ChapterListContent
projectId={projectId}
chapters={chapters}
currentChapterNo={currentChapterNo}
onCollapse={onToggle}
/>
</aside>
);
}
interface ChapterListContentProps extends ChapterListProps {
// 桌面列传入 → 目录标题旁显示「收起」按钮;移动抽屉不传(抽屉自带关闭)。
onCollapse?: () => void;
}
export function ChapterListContent({
projectId,
chapters,
currentChapterNo,
onCollapse,
}: ChapterListContentProps) {
const entries = buildChapterEntries(chapters, currentChapterNo);
const hasOutline = chapters.length > 0;
return (
<>
<div className="flex items-center justify-between px-4">
<h2 className="text-xs font-semibold uppercase tracking-wide text-ink-soft">
</h2>
{onCollapse ? (
<button
type="button"
onClick={onCollapse}
aria-label="收起目录"
title="收起目录"
className={`flex h-6 w-6 items-center justify-center rounded text-ink-soft transition-colors hover:bg-[var(--color-cinnabar-wash)] hover:text-cinnabar ${focusRing}`}
>
<PanelLeftClose className="h-4 w-4" aria-hidden="true" />
</button>
) : null}
</div>
<ul className="mt-2">
{entries.map((entry) => {
const active = entry.no === currentChapterNo;
return (
<li key={entry.no}>
<Link
href={`/projects/${projectId}/write?chapter=${entry.no}`}
aria-current={active ? "page" : undefined}
className={
active
? "flex flex-col gap-0.5 border-l-2 border-cinnabar bg-[var(--color-cinnabar-wash)] px-4 py-2 text-sm text-cinnabar"
: "flex flex-col gap-0.5 border-l-2 border-transparent px-4 py-2 text-sm text-ink hover:border-cinnabar hover:text-cinnabar"
}
>
<span className="flex items-center gap-2">
{active ? <span aria-hidden="true"></span> : null}{" "}
{entry.no}
</span>
{entry.title ? (
<span className="truncate text-xs text-ink-soft">
{entry.title}
</span>
) : null}
</Link>
</li>
);
})}
</ul>
{!hasOutline ? (
<p className="mt-4 px-4 text-xs text-ink-soft/70">
<Link
href={`/projects/${projectId}/outline`}
className="text-cinnabar hover:underline"
>
</Link>
</p>
) : null}
</>
);
}