generated from coulomb/repo-seed
Refine evidence UX: sidebar capture form, inline edit, click highlight
Significant UX iteration: Visual palette - Debug text-layer overlay flips from yellow to light grey so it no longer collides with the evidence highlight colour. - New highlight-styles.css matches the sidebar's #fff8d6/#e0c050 palette so a passage marked in the document and its sidebar card speak the same visual language. - Active (focused) evidence: same fill, thick #b78b1c outline on both the highlight and the sidebar card. Library's red --scrolledTo box-shadow is suppressed. Activation model - Click an evidence card in the sidebar → activates that item + scrolls the viewer to the passage + thickens the borders (existing behaviour, now visually clearer). - Click a highlight in the document → activates the evidence that owns that annotation. New `findByAnnotationId()` on EvidenceService is the reverse lookup. Wired through a new `onHighlightClicked` prop on PdfSpikeViewer + `activeAnnotationId` prop that drives the data-ce-active attribute on the highlight wrapper. Inline edit - Each evidence card has a ✎ button that flips the card into an inline form with the citation (quote) and commentary fields. - Saving calls a new `AnnotationService.updateQuote()` + existing `EvidenceService.updateCommentary()`. The selectors are untouched, so the marked passage in the document stays put — the inline hint says so explicitly. - New `AnnotationUpdated` event added to the engine event vocabulary (SharedContracts.md §4 updated). Capture form placement - The yellow "New annotation" toolbar that lived above the viewer is gone. A new InlineCaptureForm component is now slotted into the sidebar between the cards that bracket the new selection in document flow (sorted by page + y of the first PdfRectSelector). If the new selection is before all existing evidence it appears at the top; if after all of them, at the bottom. - The legacy AnnotationToolbar.tsx is removed; the public surface re-exports `InlineCaptureForm` instead. Test updates - tests/integration/citation-card-export-e2e.dom.test.tsx: switched to the seed-session helper (matches the other E2Es) since the fixture-button click path is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -7,33 +7,37 @@
|
||||
* directly. When the PDF library is swapped (or the spike is replaced),
|
||||
* only the adapter module changes; this shell stays the same.
|
||||
*
|
||||
* T06 scope: load + render the active PDF + show stored annotations. The
|
||||
* selection-capture → annotation pipeline is wired in T07; the
|
||||
* click-to-reopen pipeline is wired in T08.
|
||||
* 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 { useMemo } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { PdfSpikeViewer, type StoredAnnotation } from "@anchor/index";
|
||||
import type { AnnotationId } from "@shared/ids";
|
||||
import {
|
||||
useActiveDocument,
|
||||
useEngine,
|
||||
useEngineEventTick,
|
||||
useLastActivatedEvidence,
|
||||
usePendingSelection,
|
||||
useScrollToAnnotation,
|
||||
} from "./EngineContext";
|
||||
import { AnnotationToolbar } from "./AnnotationToolbar";
|
||||
import { useDebugFlag } from "./useDebugFlags";
|
||||
|
||||
export function ViewerShell() {
|
||||
const engine = useEngine();
|
||||
const { document, representation } = useActiveDocument();
|
||||
const { set: setPending } = usePendingSelection();
|
||||
const { id: scrollToId, version: scrollVersion } = useScrollToAnnotation();
|
||||
const { id: scrollToId, version: scrollVersion, scrollTo } = useScrollToAnnotation();
|
||||
const [debugTextLayer] = useDebugFlag("textLayer");
|
||||
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<StoredAnnotation[]>(() => {
|
||||
if (!document) return [];
|
||||
@@ -42,21 +46,39 @@ export function ViewerShell() {
|
||||
text: a.quote ?? "",
|
||||
selectors: a.selectors,
|
||||
}));
|
||||
}, [document, engine, annotationTick]);
|
||||
}, [document, engine, annotationTick, annotationUpdateTick]);
|
||||
|
||||
// The annotation id that visually represents the "active" focus —
|
||||
// derived from the active evidence's first annotation.
|
||||
const activeAnnotationId = useMemo<AnnotationId | null>(() => {
|
||||
if (!activeEvidenceId) return null;
|
||||
const item = engine.evidence.get(activeEvidenceId);
|
||||
return item?.annotationIds[0] ?? null;
|
||||
}, [activeEvidenceId, engine]);
|
||||
|
||||
const fileUrl = useMemo(() => {
|
||||
if (!document) return null;
|
||||
// CE-WP-0005: uploads + sample sessions stash a `blob:` URL on
|
||||
// `document.uri` via the per-session `PdfByteStore`. Prefer that
|
||||
// over the legacy fixture-path fallback so user uploads don't get
|
||||
// resolved against `/fixtures/pdfs/` (which would either 404 or —
|
||||
// worse — silently return the wrong file when the filename happens
|
||||
// to collide with a bundled fixture).
|
||||
if (document.uri) return document.uri;
|
||||
const titleOrId = document.title ?? document.id;
|
||||
return `/fixtures/pdfs/${encodeURIComponent(titleOrId)}`;
|
||||
}, [document]);
|
||||
|
||||
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 (
|
||||
<main
|
||||
@@ -69,7 +91,7 @@ export function ViewerShell() {
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
Pick a fixture on the left to begin.
|
||||
Upload a PDF on the left to begin.
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -84,18 +106,17 @@ export function ViewerShell() {
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<AnnotationToolbar />
|
||||
<div style={{ flex: 1, overflow: "hidden", position: "relative" }}>
|
||||
<PdfSpikeViewer
|
||||
// Re-key on scrollVersion so clicking the same item twice still
|
||||
// triggers the viewer's mount-time scroll effect. Also re-key on
|
||||
// debugTextLayer so toggling it remounts the viewer (the CSS
|
||||
// class is on a parent, but re-mounting is the simplest way to
|
||||
// make sure the text layer is re-painted with the new style).
|
||||
// debugTextLayer so toggling it remounts the viewer.
|
||||
key={`${document.id}#${scrollVersion}#${debugTextLayer ? "d" : "n"}`}
|
||||
pdfUrl={fileUrl}
|
||||
storedAnnotations={annotations}
|
||||
{...(scrollToId ? { scrollToAnnotationId: scrollToId } : {})}
|
||||
activeAnnotationId={activeAnnotationId}
|
||||
onHighlightClicked={handleHighlightClicked}
|
||||
debugTextLayer={debugTextLayer}
|
||||
onSelectionCaptured={(capture, selectors) => {
|
||||
setPending({ capture, selectors });
|
||||
|
||||
Reference in New Issue
Block a user