"use client"; import Link from "next/link"; import { useEffect, useRef, useState, type RefObject } from "react"; import { ArrowUpRight, BookMarked, X } from "lucide-react"; import { ThinkingIndicator } from "@/components/ThinkingIndicator"; import { Button } from "@/components/ui/Button"; import { StatusNote } from "@/components/ui/StatusNote"; import { handleTabTrap } from "@/lib/a11y/focusTrap"; import { useRestoreFocus } from "@/lib/a11y/useRestoreFocus"; import { useBodyScrollLock } from "@/lib/ui/useBodyScrollLock"; import { badgeClass, cn, overlayScrim } from "@/lib/ui/variants"; import { CONTEXT_TABS, contextTabHref, type CodexGroup, type ContextTabKey, type OutlineRow, } from "@/lib/workbench/contextDrawer"; import { useContextDrawerData, type ContextResource, } from "@/lib/workbench/useContextDrawer"; import { chapterForeshadowTotal } from "@/lib/foreshadow/chapterForeshadow"; import { useChapterForeshadow } from "@/lib/foreshadow/useChapterForeshadow"; import { ChapterForeshadowList } from "./ChapterForeshadowList"; // 速查抽屉总开关:一键关闭即回滚(触发按钮据此隐藏,旧侧栏/全宽页路由不受影响)。 export const CONTEXT_DRAWER_ENABLED = true; interface ContextDrawerProps { projectId: string; // 当前章号:伏笔速查按此分「可回收 / 待回收 / 已逾期」。 chapterNo: number; open: boolean; onClose: () => void; // 关闭时把焦点还给触发按钮(a11y)。 triggerRef?: RefObject; } // 从编辑器右侧滑出的【只读】上下文速查抽屉:不离开写作界面即可查设定库/大纲/伏笔/规则/文风。 // 视口无关(不同于 components/Drawer 的 lg:hidden 移动端抽屉),全只读、绝不写库。 // a11y 复用命令面板/Drawer 范式:role=dialog + aria-modal + Esc 关闭 + focus trap + body scroll lock + 还原焦点。 export function ContextDrawer({ projectId, chapterNo, open, onClose, triggerRef, }: ContextDrawerProps) { const [activeTab, setActiveTab] = useState("codex"); const panelRef = useRef(null); const closeRef = useRef(null); const data = useContextDrawerData(projectId, activeTab, open); useBodyScrollLock(open); useRestoreFocus(open, triggerRef); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent): void => { if (e.key === "Escape") { e.preventDefault(); onClose(); } }; window.addEventListener("keydown", onKey); closeRef.current?.focus(); return () => window.removeEventListener("keydown", onKey); }, [open, onClose]); if (!open) return null; return (
e.stopPropagation()} onKeyDown={(e) => { if (panelRef.current) handleTabTrap(panelRef.current, e); }} className="absolute right-0 top-0 flex h-full w-96 max-w-[90vw] flex-col border-l border-line bg-panel shadow-paper outline-none" >
); } interface TabBarProps { activeTab: ContextTabKey; onSelect: (key: ContextTabKey) => void; } // Tab 选择条(role=tablist):设定库/大纲做完整速查,伏笔/规则/文风为摘要+跳链。 function TabBar({ activeTab, onSelect }: TabBarProps) { return (
{CONTEXT_TABS.map((tab) => { const active = tab.key === activeTab; return ( ); })}
); } interface TabPanelProps { projectId: string; chapterNo: number; activeTab: ContextTabKey; data: ReturnType; } // 按当前 Tab 渲染对应速查面板;每个面板都带一个「去完整页编辑 →」链接。 function TabPanel({ projectId, chapterNo, activeTab, data }: TabPanelProps) { const href = contextTabHref(projectId, activeTab); return (
{activeTab === "codex" ? ( ) : null} {activeTab === "outline" ? ( ) : null} {activeTab === "foreshadow" ? ( ) : null} {activeTab === "rules" ? ( ) : null} {activeTab === "style" ? ( ) : null}
); } // 加载中骨架(统一给设定库/大纲复用)。 function LoadingNote() { return (
); } interface ErrorNoteProps { message: string; onRetry: () => void; } function ErrorNote({ message, onRetry }: ErrorNoteProps) { return (

{message}

); } function EmptyNote({ text }: { text: string }) { return (

{text}

); } interface CodexPanelProps { resource: ContextResource; onRetry: () => void; } // 设定库速查:按类型分组的实体名单(含硬规则条目),完整只读列表。 function CodexPanel({ resource, onRetry }: CodexPanelProps) { if (resource.status === "loading" || resource.status === "idle") { return ; } if (resource.status === "error") { return ; } if (resource.data.length === 0) { return ; } return (
{resource.data.map((group) => (

{group.type} {group.entities.length}

    {group.entities.map((entity) => (
  • {entity.name}

    {entity.rules && entity.rules.length > 0 ? (
      {entity.rules.map((rule, i) => (
    • {rule}
    • ))}
    ) : null}
  • ))}
))}
); } interface OutlinePanelProps { resource: ContextResource; onRetry: () => void; } // 大纲速查:逐章节拍紧凑列表 + 该章伏笔窗口代号徽标,完整只读。 function OutlinePanel({ resource, onRetry }: OutlinePanelProps) { if (resource.status === "loading" || resource.status === "idle") { return ; } if (resource.status === "error") { return ; } if (resource.data.length === 0) { return ; } return (
    {resource.data.map((row) => (
  • 第 {row.no} 章 · 卷 {row.volume} {row.foreshadowCodes.length > 0 ? ( {row.foreshadowCodes.map((code) => ( {code} ))} ) : null}
    {row.beats.length > 0 ? (
      {row.beats.map((beat, i) => (
    • {beat}
    • ))}
    ) : (

    (本章暂无节拍)

    )}
  • ))}
); } interface ForeshadowPanelProps { projectId: string; chapterNo: number; } // 伏笔速查(只读):按当前章号分「可回收 / 待回收 / 已逾期」,与右栏本章伏笔同数据源、同分类函数。 function ForeshadowPanel({ projectId, chapterNo }: ForeshadowPanelProps) { const { groups, status, reload } = useChapterForeshadow(projectId, chapterNo); if (status === "loading") return ; if (status === "error") { return ; } if (chapterForeshadowTotal(groups) === 0) { return ; } return (

本章伏笔 · 按已验收进度

); } // 占位面板(规则/文风):抽屉内一句话摘要,完整编辑走下方跳链。 function PlaceholderPanel({ summary }: { summary: string }) { return (

{summary}

); } // 「去完整页编辑 →」链接:抽屉只读速查 → 跳全宽页编辑。 function FullPageLink({ href }: { href: string }) { return ( 去完整页编辑