Three UX iterations rolled into one:
1. Unified evidence form
- New EvidenceFormBody is the single source for "citation +
commentary" editing. Both InlineCaptureForm (creating fresh
evidence from a selection) and the EvidenceCard edit mode render
this body with their own save/cancel labels + badge/helper text.
- The capture form now exposes the citation as an editable
textarea — pre-filled with the selection text — so the user can
refine a partial capture before saving without re-selecting.
- Old testid prefixes are unchanged for the inline-capture flow
(`inline-capture-quote/commentary/save/cancel`); edit-mode
testids are now `evidence-edit-<id>-{quote,commentary,save,cancel}`.
2. Active document card
- The blue background alone was the only "this is open" cue. Added
a 3px #0050b3 border (matching the evidence-card thick-border
pattern, but in the documents-are-blue palette) plus a
`data-active` attribute.
3. PDF layer-hide diagnostics
- New debug flags `hideCanvas`, `hideTextLayer`, `hideAnnotationLayer`,
`hideXfaLayer` — applied as `.ce-hide-<layer>` classes on the viewer
wrapper, each `display: none`-ing the matching PDF.js layer.
- SessionMenu groups the toggles under a "PDF diagnostics" header
with a new shared DebugCheckbox helper. The existing "Debug text
layer" highlight toggle now lives in the same group.
- Lets the user isolate stacking issues by elimination — e.g.
"hide text layer, can I now see the canvas content underneath?".
Tests
- citation-card-export-e2e + session-export-reimport switched from
placeholder/role-name lookups to the inline-capture testids so
they survive form-copy changes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
PDF text selection misbehaviour (some glyphs unselectable, selections
jumping to other positions) is almost always caused by misalignment
between the visible canvas-rendered glyphs and the invisible text
layer that PDF.js overlays for selection. There's no way to see this
without devtools — which makes it hard for end users to tell whether
a specific PDF is OCR-noisy, has bad font fallbacks, or has no text
layer at all.
This adds a developer-facing toggle in the SessionMenu ("Debug text
layer") that:
- paints every text-layer span yellow with a blue outline so it's
obvious where text is selectable and where it isn't, and
- logs every onSelection event to the browser console with the
captured text, page, normalized rects, and the selectors the
pipeline derived from it.
Preference persists in localStorage under
`citation-evidence:debug:textLayer`. Surfaced via a new
`useDebugFlag()` hook in @work so the SessionMenu (app layer) and the
ViewerShell (work layer) can both subscribe without breaching the
boundary plugin.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
UX gaps that surfaced while running the demo:
- ViewerShell hardcoded `/fixtures/pdfs/<title>` for the PDF URL,
ignoring the `document.uri` blob URL that uploaded PDFs carry. The
viewer either 404'd or — worse — silently served a fixture whose
filename happened to collide. Prefer document.uri when present.
- SessionMenu only let you delete the *active* session. Added a small
per-row "✕" button next to every session in the Switch-to list so a
user can drop a session's data without first switching to it. Same
click-to-confirm pattern as the existing Delete action.
- Added a "Reset all data…" affordance in both the SessionMenu and the
empty-state landing. Calls a new `clearAllSessionData()` helper that
wipes every `citation-evidence:*` key from localStorage, then forces
a reload so all in-memory caches start fresh.
- `attachSessionPersister.writeOnDelete` was leaking the per-session
`active-document-id:v1` key on every session delete. Now removed
alongside the engine snapshot key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Completes the PDF review slice end-to-end. After this commit a user can
open a fixture, select text, save an evidence item with commentary, see
it in the sidebar, reload the page, click the item, and the viewer
scrolls to the passage.
- T03 src/source/pdf/{fingerprint,extract,ingest}.ts + 39 fixture tests
- SHA-256 fingerprint over a fresh ArrayBuffer (TS BufferSource-safe)
- PDF.js text extract; per-page normalize then join with "\n\n"
- PageMap + OffsetMap (gap-free coverage); pageLength = end - start
- Updated manifest's Betriebskosten quote to one PDF.js extracts cleanly
- T04 src/anchor/selectors/{create,resolve}.ts + 25 unit + 7 fixture tests
- createSelectors emits the maximal redundant set (TextQuote +
TextPosition + PdfRect + PdfPageText when available)
- resolveSelectors implements the SharedContracts §7 ladder; confidence
1.0 (pos+quote) → 0.7 (rect-only) → 0 (unresolved)
- Cross-module integration test moved to tests/integration/ to honor
the anchor↛source boundary lint rule
- T05 engine: sync event bus over the closed §4 vocabulary, Map-backed
repos, services, createEngine() composition root, 12 tests
- T06 work + app: three-pane shell (CollectionList | ViewerShell |
EvidenceSidebar) wired through EngineProvider; EngineContext lives in
src/work/ to respect the work↛app boundary; SpikeApp deleted
- T07 AnnotationToolbar: pendingSelection in context; Save runs
createSelectors → engine.annotations.create → engine.evidence.create
- T08 click-to-reopen + localStorage persistence
- scrollToAnnotation state in context with a version counter so a
second click on the same item re-fires the viewer scroll
- captureSnapshot/restoreSnapshot/attachPersister/restoreFromStorage;
restore bypasses services to avoid event-loops
- active-document id persisted alongside the snapshot so reload lands
on the same fixture; ADR-0005 written
- 9 persistence tests
- T09 tests/integration/app-prd-scenario.dom.test.tsx
- end-to-end happy-dom test of PRD scenario steps 1-8 through the real
React tree; viewer + ingest mocked per ADR-0004's headless-Chromium
limitation. Fixed memo-deps bug in EvidenceSidebar/ViewerShell where
useEngineEventTick values were not included in the useMemo deps,
leaving stale memoization across event-driven re-renders
- vitest.config.ts: happy-dom for *.dom.test.{ts,tsx} files
- noEmit added to tsconfig so tsc -b doesn't litter src/ with .js outputs
Gates: typecheck ✓ lint ✓ test 109/109 across 11 files ✓ build ✓
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>