/** * ViewerShell — the centre pane. * * Hosts the viewer adapter (currently the T02 PDF spike) and shows whatever * is active. `work/` consumes only the adapter's public surface * (`PdfSpikeViewer`) — it never touches PDF.js or react-pdf-highlighter-plus * directly. When the PDF library is swapped (or the spike is replaced), * only the adapter module changes; this shell stays the same. * * The annotation toolbar lived here in earlier iterations; CE-WP-0005-iter4 * moved it into the evidence sidebar so the capture form appears in the * sidebar's document-flow position. The viewer now only renders the PDF * and surfaces the activate/click events. */ import { useCallback, useMemo } from "react"; import { PdfSpikeViewer, type StoredAnnotation } from "@anchor/index"; import { resolvePdfViewerUrl } from "@source/pdf/viewer-url"; import type { AnnotationId } from "@shared/ids"; import { useActiveDocument, useEngine, useEngineEventTick, useLastActivatedEvidence, usePendingSelection, usePdfByteStore, useScrollToAnnotation, } from "./EngineContext"; import { useDebugFlag } from "./useDebugFlags"; export function ViewerShell() { const engine = useEngine(); const byteStore = usePdfByteStore(); const { document, representation } = useActiveDocument(); const { set: setPending } = usePendingSelection(); const { id: scrollToId, version: scrollVersion, scrollTo } = useScrollToAnnotation(); const [debugTextLayer] = useDebugFlag("textLayer"); const [hideCanvas] = useDebugFlag("hideCanvas"); const [hideTextLayer] = useDebugFlag("hideTextLayer"); const [hideAnnotationLayer] = useDebugFlag("hideAnnotationLayer"); const [hideXfaLayer] = useDebugFlag("hideXfaLayer"); const activeEvidenceId = useLastActivatedEvidence(); // The viewer needs to re-fetch its highlight list whenever annotations // change. The tick is included in the memo deps so the list re-resolves. const annotationTick = useEngineEventTick("AnnotationCreated"); const annotationUpdateTick = useEngineEventTick("AnnotationUpdated"); const annotations = useMemo(() => { if (!document) return []; return engine.annotations.listByDocument(document.id).map((a) => ({ id: a.id, text: a.quote ?? "", selectors: a.selectors, })); }, [document, engine, annotationTick, annotationUpdateTick]); // The annotation id that visually represents the "active" focus — // derived from the active evidence's first annotation. const activeAnnotationId = useMemo(() => { if (!activeEvidenceId) return null; const item = engine.evidence.get(activeEvidenceId); return item?.annotationIds[0] ?? null; }, [activeEvidenceId, engine]); const fileUrl = useMemo(() => { if (!document) return null; return resolvePdfViewerUrl(document, byteStore); }, [document, byteStore]); const scrollRequestKey = scrollToId !== null ? `${scrollToId}:${scrollVersion}` : null; const handleHighlightClicked = useCallback( (annotationId: string) => { if (!document) return; const item = engine.evidence.findByAnnotationId( document.id, annotationId as AnnotationId, ); if (!item) return; engine.evidence.activate(item.id, "citation-card"); // Re-trigger scroll so a click on the highlight also keeps it // centred in the viewport. scrollTo(annotationId as AnnotationId); }, [document, engine, scrollTo], ); if (!document || !representation || !fileUrl) { return (
Upload a PDF on the left to begin.
); } return (
{ setPending({ capture, selectors }); }} />
); }