Files
writer-work-flow/apps/web/components/workbench/ChapterList.tsx
Yaojia Wang 5674158707 feat(ui): 工作台精修 + 拆分(P3-3..7)
- Workbench.tsx 937→467 行,抽出 AiToolbar/WorkbenchToolbar/MobileContextBar
- P3-3 AI 工具条分组留白 + 眉题 + 主 CTA 凸显
- P3-4 专注写作模式(useFocusMode hook+vitest,隐藏侧栏加宽正文)+ 左双栏面色区分
- P3-5 右栏本章参考小标题去衬线(font-sans) + 空态引导(无标题保 h2→h3 大纲)
- P3-6 底栏信息/操作/状态三区 + TOC 当前章高亮;TOC 状态点因无每章状态数据跳过
- P3-7 展示层流式跟随滚动(不触碰 SSE 逻辑,尊重 reduced-motion)
- AppShell 加可选 hideNav(加法式,默认 false)
2026-07-11 08:04:56 +02:00

134 lines
4.8 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-surface-soft 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-md border border-line bg-panel text-ink-soft transition-colors duration-fast ease-standard hover:border-cinnabar 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-surface-soft 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="font-sans text-eyebrow uppercase tracking-wide text-muted-soft">
</h2>
{onCollapse ? (
<button
type="button"
onClick={onCollapse}
aria-label="收起目录"
title="收起目录"
className={`flex h-7 w-7 items-center justify-center rounded-md border border-line bg-panel text-ink-soft transition-colors duration-fast ease-standard hover:border-cinnabar 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 font-medium text-cinnabar transition-colors duration-fast ease-standard ${focusRing}`
: `flex flex-col gap-0.5 border-l-2 border-transparent px-4 py-2 text-sm text-ink transition-colors duration-fast ease-standard hover:border-line-soft hover:bg-panel/60 hover:text-cinnabar ${focusRing}`
}
>
<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}
</>
);
}