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:
@@ -28,6 +28,7 @@ import {
|
||||
} from "react-pdf-highlighter-plus";
|
||||
import "react-pdf-highlighter-plus/style/style.css";
|
||||
import "react-pdf-highlighter-plus/style/pdf_viewer.css";
|
||||
import "./highlight-styles.css";
|
||||
import "./debug-textlayer.css";
|
||||
|
||||
import type { NormalizedRect, Selector } from "@shared/selector";
|
||||
@@ -112,20 +113,34 @@ function captureFromPdfSelection(sel: PdfSelection): PdfSelectionCapture {
|
||||
* For the spike, no editing tooling — just visual proof of "did the saved
|
||||
* coordinates land on the right passage on the right page after reload?"
|
||||
*/
|
||||
function SpikeHighlightContainer(): ReactNode {
|
||||
const { highlight, isScrolledTo } = useHighlightContainerContext();
|
||||
// Wrap the highlight in a data-tagged container so the visual-guide
|
||||
// overlay's HighlightRectBridge can locate it via DOM query. The
|
||||
// wrapper uses display: contents so it doesn't affect layout — the
|
||||
// bounding rect is gathered from the live TextHighlight children at
|
||||
// query time.
|
||||
return (
|
||||
<div data-highlight-id={highlight.id} style={{ display: "contents" }}>
|
||||
<MonitoredHighlightContainer>
|
||||
<TextHighlight highlight={highlight} isScrolledTo={isScrolledTo} />
|
||||
</MonitoredHighlightContainer>
|
||||
</div>
|
||||
);
|
||||
interface HighlightContainerProps {
|
||||
readonly activeAnnotationId: string | null | undefined;
|
||||
readonly onHighlightClicked: ((annotationId: string) => void) | undefined;
|
||||
}
|
||||
|
||||
function makeSpikeHighlightContainer(props: HighlightContainerProps) {
|
||||
return function SpikeHighlightContainer(): ReactNode {
|
||||
const { highlight, isScrolledTo } = useHighlightContainerContext();
|
||||
const isActive = props.activeAnnotationId === highlight.id;
|
||||
return (
|
||||
<div
|
||||
data-highlight-id={highlight.id}
|
||||
data-ce-active={isActive ? "true" : "false"}
|
||||
style={{ display: "contents" }}
|
||||
onClickCapture={(e) => {
|
||||
// Click-capture so the click registers before the library's
|
||||
// built-in selection-clearing logic eats it. Stop propagation
|
||||
// so the highlight click doesn't also start a new selection.
|
||||
e.stopPropagation();
|
||||
props.onHighlightClicked?.(highlight.id);
|
||||
}}
|
||||
>
|
||||
<MonitoredHighlightContainer>
|
||||
<TextHighlight highlight={highlight} isScrolledTo={isScrolledTo} />
|
||||
</MonitoredHighlightContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,7 +183,18 @@ export interface PdfSpikeViewerProps {
|
||||
/** Annotation id to scroll to and highlight on mount, if any. */
|
||||
readonly scrollToAnnotationId?: string;
|
||||
/**
|
||||
* When true, paint the PDF text-layer spans in yellow so it's
|
||||
* Annotation id currently focused. The matching highlight gets a
|
||||
* thicker border (see highlight-styles.css). `null`/undefined means
|
||||
* "no active highlight".
|
||||
*/
|
||||
readonly activeAnnotationId?: string | null;
|
||||
/**
|
||||
* Called when the user clicks an existing highlight in the page.
|
||||
* The receiver typically activates the matching evidence item.
|
||||
*/
|
||||
onHighlightClicked?(annotationId: string): void;
|
||||
/**
|
||||
* When true, paint the PDF text-layer spans in light grey so it's
|
||||
* obvious which glyphs have a selectable text overlay and which are
|
||||
* image-only. Also logs every onSelection event to the console.
|
||||
*/
|
||||
@@ -188,7 +214,19 @@ export interface StoredAnnotation {
|
||||
* - scrolls to `scrollToAnnotationId` if its highlight can be reconstructed
|
||||
*/
|
||||
export function PdfSpikeViewer(props: PdfSpikeViewerProps) {
|
||||
const { pdfUrl, storedAnnotations, onSelectionCaptured, scrollToAnnotationId, debugTextLayer } = props;
|
||||
const {
|
||||
pdfUrl,
|
||||
storedAnnotations,
|
||||
onSelectionCaptured,
|
||||
scrollToAnnotationId,
|
||||
activeAnnotationId,
|
||||
onHighlightClicked,
|
||||
debugTextLayer,
|
||||
} = props;
|
||||
const HighlightContainer = useMemo(
|
||||
() => makeSpikeHighlightContainer({ activeAnnotationId, onHighlightClicked }),
|
||||
[activeAnnotationId, onHighlightClicked],
|
||||
);
|
||||
const utilsRef = useRef<PdfHighlighterUtils | null>(null);
|
||||
const [didScroll, setDidScroll] = useState<string | null>(null);
|
||||
|
||||
@@ -261,7 +299,7 @@ export function PdfSpikeViewer(props: PdfSpikeViewerProps) {
|
||||
onSelectionCaptured(capture, selectors);
|
||||
}}
|
||||
>
|
||||
<SpikeHighlightContainer />
|
||||
<HighlightContainer />
|
||||
</PdfHighlighter>
|
||||
)}
|
||||
</PdfLoader>
|
||||
|
||||
Reference in New Issue
Block a user