Files
writer-work-flow/apps/web/components/workbench/ChapterAssistant.tsx
Yaojia Wang 9b893a6c61 feat(ux): M1/M2 移动端可用 — 导航汉堡抽屉 + 写作工具条不溢出
M1 左导航响应式抽屉:抽出 lib/nav/items.ts(条目数据+激活判定,纯逻辑+vitest)
共用于桌面静态侧栏(hidden lg:block)与移动汉堡抽屉。新增通用 Drawer(遮罩/
Esc 关闭/打开聚焦,复用 CommandPalette a11y 模式)+ NavDrawer + NavItems;
LeftNav 改 <lg 隐藏,AppShell 顶栏加汉堡(aria-label 打开导航)。

M2 写作工具条不溢出 + 移动端可达章节/助手:Toolbar 改 flex-wrap,390px 不再
横切「尚未保存」;ChapterList/ChapterAssistant 抽出 *Content,Workbench 经
左/右 Drawer 在 <lg 触达目录/助手。

实景 390px 复验(browse):汉堡显示/静态导航隐藏/抽屉开合聚焦+Esc、工具条
两行不溢出、助手抽屉真注入面板。门禁:lint/typecheck 干净 · vitest 177 · build OK。
2026-06-20 11:26:14 +02:00

107 lines
3.4 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 Link from "next/link";
import {
kindLabel,
reasonLabel,
type InjectionEntity,
} from "@/lib/workbench/injection";
import { useInjection } from "@/lib/workbench/useInjection";
interface ChapterAssistantProps {
projectId: string;
chapterNo: number;
}
// 右栏「本章助手」UX §6.3)。
// 「本章注入(透明)」读 B0 端点,列出确定性选中的设定/角色 + 入选理由徽标——
// 这是「看到的=写章用的」的信任牌(不变量 #6。「四审」指向审稿页入口。
// 桌面 aside 包裹;移动端经 Workbench 抽屉复用 AssistantContent。
export function ChapterAssistant({
projectId,
chapterNo,
}: ChapterAssistantProps) {
return (
<aside className="hidden border-l border-line bg-panel py-4 lg:block">
<AssistantContent projectId={projectId} chapterNo={chapterNo} />
</aside>
);
}
export function AssistantContent({
projectId,
chapterNo,
}: ChapterAssistantProps) {
const injection = useInjection(projectId, chapterNo);
const selected = injection.data?.selected ?? [];
return (
<div className="px-4">
<h2 className="text-sm font-semibold text-ink"></h2>
<section className="mt-4">
<h3 className="text-xs font-semibold uppercase tracking-wide text-ink-soft">
</h3>
{injection.loading ? (
<p className="mt-2 text-xs text-ink-soft"></p>
) : injection.error ? (
<p className="mt-2 rounded border border-dashed border-line bg-bg p-3 text-xs text-ink-soft">
{injection.error}
</p>
) : selected.length === 0 ? (
<p className="mt-2 rounded border border-dashed border-line bg-bg p-3 text-xs text-ink-soft">
/
</p>
) : (
<ul className="mt-2 space-y-2">
{selected.map((entity) => (
<EntityRow key={`${entity.kind}:${entity.name}`} entity={entity} />
))}
</ul>
)}
</section>
<section className="mt-6">
<h3 className="text-xs font-semibold uppercase tracking-wide text-ink-soft">
</h3>
<p className="mt-2 text-xs text-ink-soft">
/ / / 稿
</p>
<Link
href={`/projects/${projectId}/review?chapter=${chapterNo}`}
className="mt-2 inline-block rounded border border-cinnabar px-3 py-1.5 text-xs text-cinnabar hover:bg-[var(--color-cinnabar-wash)]"
>
稿
</Link>
</section>
</div>
);
}
function EntityRow({ entity }: { entity: InjectionEntity }) {
return (
<li className="rounded border border-line bg-bg p-2">
<div className="flex items-center gap-2">
<span className="rounded bg-panel px-1.5 py-0.5 text-[10px] text-ink-soft">
{kindLabel(entity.kind)}
</span>
<span className="text-sm text-ink">{entity.name}</span>
</div>
<div className="mt-1.5 flex flex-wrap gap-1">
{(entity.reasons ?? []).map((reason) => (
<span
key={reason}
className="rounded bg-[var(--color-cinnabar-wash)] px-1.5 py-0.5 text-[10px] text-cinnabar"
>
{reasonLabel(reason)}
</span>
))}
</div>
</li>
);
}