feat(frontend): 「目录」列可折叠、记忆折叠状态(UX P2-2)

useChapterListCollapse 用 localStorage 记忆折叠态(SSR 首帧恒展开避免 hydration 不一致);收起降为带展开按钮的窄轨、让宽给正文,展开时标题旁加收起按钮;三栏栅格据折叠态切列宽,章节列表/高亮/切章导航全保留。
This commit is contained in:
Yaojia Wang
2026-07-10 18:14:49 +02:00
parent 3c87b01379
commit dbece64d4d
4 changed files with 178 additions and 8 deletions

View File

@@ -1,6 +1,8 @@
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;
@@ -9,36 +11,83 @@ interface ChapterListProps {
currentChapterNo: number;
}
// 左栏目录UX §6.3):列出大纲全部章节 + 当前章,点击切换写作章号(`?chapter=N`)。
// 桌面 aside 包裹;移动端经 Workbench 抽屉复用 ChapterListContent
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,
}: ChapterListProps) {
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,
}: ChapterListProps) {
onCollapse,
}: ChapterListContentProps) {
const entries = buildChapterEntries(chapters, currentChapterNo);
const hasOutline = chapters.length > 0;
return (
<>
<h2 className="px-4 text-xs font-semibold uppercase tracking-wide text-ink-soft">
</h2>
<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;

View File

@@ -59,6 +59,7 @@ import { GenrePicker } from "./GenrePicker";
import { GENRES } from "@/lib/wizard/wizard";
import { useGenreGate, toGenreDirective } from "@/lib/workbench/useGenreGate";
import { useAiConversation } from "@/lib/workbench/useAiConversation";
import { useChapterListCollapse } from "@/lib/workbench/useChapterListCollapse";
interface WorkbenchProps {
project: ProjectResponse;
@@ -104,6 +105,8 @@ export function Workbench({
// AC-3 AI 对话抽屉Workbench 级单实例append 即使抽屉关着也能触发flag 灰度。
const [conversationOpen, setConversationOpen] = useState(false);
const conversation = useAiConversation(project.id, chapterNo);
// P2-2桌面「目录」列可收起记忆状态收起时把宽让给正文窄轨留一个展开入口。
const chapterList = useChapterListCollapse();
const conversationTriggerRef = useRef<HTMLButtonElement>(null);
const toast = useToast();
const autosave = useAutosave(project.id, chapterNo, initialText);
@@ -292,11 +295,19 @@ export function Workbench({
projectId={project.id}
activeNav="write"
>
<div className="grid min-h-[calc(100vh-var(--chrome,4rem))] grid-cols-1 xl:h-[calc(100vh-var(--chrome,4rem))] xl:grid-cols-[12rem_1fr_18rem]">
<div
className={`grid min-h-[calc(100vh-var(--chrome,4rem))] grid-cols-1 xl:h-[calc(100vh-var(--chrome,4rem))] ${
chapterList.collapsed
? "xl:grid-cols-[2.75rem_1fr_18rem]"
: "xl:grid-cols-[12rem_1fr_18rem]"
}`}
>
<ChapterList
projectId={project.id}
chapters={chapters}
currentChapterNo={chapterNo}
collapsed={chapterList.collapsed}
onToggle={chapterList.toggle}
/>
<section className="flex min-w-0 flex-col bg-bg">