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:
@@ -23,12 +23,11 @@ import { act, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { Document, DocumentRepresentation } from "@shared/document";
|
||||
import type { DocumentId, RepresentationId } from "@shared/ids";
|
||||
import type { Selector } from "@shared/selector";
|
||||
|
||||
import type { PdfSelectionCapture } from "@anchor/index";
|
||||
import manifest from "../../fixtures/pdfs/manifest.json" with { type: "json" };
|
||||
import { seedSessionWithDoc } from "./helpers/seed-session";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks
|
||||
@@ -64,38 +63,6 @@ vi.mock("@anchor/index", async (importOriginal) => {
|
||||
const FIXTURE = manifest.fixtures.find((f) => f.id === "fristsetzung-bezifferung")!;
|
||||
const SYNTHETIC_CANONICAL = ["Pre.", FIXTURE.known_good_quote, "Post."].join(" ");
|
||||
|
||||
vi.mock("@source/index", async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import("@source/index")>();
|
||||
return {
|
||||
...original,
|
||||
ingestPdf: vi.fn(async (_input: unknown, options?: { filename?: string }) => {
|
||||
const documentId = ("doc_test_" + Math.random().toString(36).slice(2, 10)) as DocumentId;
|
||||
const representationId = ("rep_test_" + Math.random().toString(36).slice(2, 10)) as RepresentationId;
|
||||
const document: Document = {
|
||||
id: documentId,
|
||||
mediaType: "application/pdf",
|
||||
...(options?.filename ? { title: options.filename } : {}),
|
||||
fingerprint: "synthetic-fingerprint-for-test",
|
||||
createdAt: "2026-05-25T00:00:00.000Z",
|
||||
updatedAt: "2026-05-25T00:00:00.000Z",
|
||||
};
|
||||
const representation: DocumentRepresentation = {
|
||||
id: representationId,
|
||||
documentId,
|
||||
representationType: "pdf-text",
|
||||
contentHash: "synthetic-fingerprint-for-test",
|
||||
canonicalText: SYNTHETIC_CANONICAL,
|
||||
pageMap: [{ page: 1, width: 595, height: 842 }],
|
||||
offsetMap: [
|
||||
{ page: 1, globalStart: 0, globalEnd: SYNTHETIC_CANONICAL.length, pageLength: SYNTHETIC_CANONICAL.length },
|
||||
],
|
||||
generatedAt: "2026-05-25T00:00:00.000Z",
|
||||
};
|
||||
return { document, representation };
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -139,15 +106,14 @@ describe("CE-WP-0004-T05 — PRD scenario steps 10-11 end-to-end", () => {
|
||||
viewerSnapshot.pdfUrl = null;
|
||||
viewerSnapshot.onSelectionCaptured = null;
|
||||
globalThis.localStorage?.clear();
|
||||
globalThis.fetch = vi.fn(async () =>
|
||||
new Response(new Uint8Array([0x25, 0x50, 0x44, 0x46]).buffer, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/pdf" },
|
||||
}),
|
||||
);
|
||||
if (typeof window !== "undefined") {
|
||||
history.replaceState(null, "", window.location.pathname);
|
||||
}
|
||||
seedSessionWithDoc({
|
||||
sessionName: "T05",
|
||||
documentTitle: FIXTURE.filename,
|
||||
canonicalText: SYNTHETIC_CANONICAL,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -163,8 +129,7 @@ describe("CE-WP-0004-T05 — PRD scenario steps 10-11 end-to-end", () => {
|
||||
installClipboardSpy();
|
||||
|
||||
// --- Steps 1-5 (recap from CE-WP-0002-T09) -------------------------
|
||||
const fixtureBtn = screen.getByRole("button", { name: new RegExp(FIXTURE.id) });
|
||||
await user.click(fixtureBtn);
|
||||
// CE-WP-0005: doc pre-seeded into session — skip fixture-button click.
|
||||
await waitFor(() => expect(viewerSnapshot.onSelectionCaptured).not.toBeNull());
|
||||
await act(async () => {
|
||||
viewerSnapshot.onSelectionCaptured!(
|
||||
|
||||
Reference in New Issue
Block a user