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>
The first cut of "Debug text layer" only painted direct `<span>`
children of `.textLayer`. PDF.js 4.x wraps marked content in nested
spans/divs, so the entire selectable area wasn't visible — making it
hard to tell whether a region is "no text layer at all" vs. "text
layer present but small/dense".
Changes:
- CSS now targets every descendant of `.textLayer`, dims the canvas
underneath, and outlines the `.textLayer` container itself so its
full extent is obvious.
- TextHighlight rectangles flip to green in debug mode so saved
highlights don't get washed out by the debug yellow.
- The viewer now logs:
[ce] viewer highlights — which annotations rendered, which
were skipped, with rects + page
[ce] scrollToAnnotation — whether the target was found in
the highlights array when an
activation arrived
This is the diagnostic loop for the "viewport scrolls but the
highlight doesn't appear" report — if highlight count is > 0 in the
first log but the green rectangle is off-screen, the saved rects
inherited the same text-layer misalignment that caused the partial
selection captures in the first place.
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>
T01 EvidenceLink/EvidenceSet types
- src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target
- src/shared/evidence-set.ts: ordered group + activeEvidenceItemId
- enum-conformance test parses SharedContracts.md and asserts the
runtime lists match exactly
T02 Binding service + in-memory link repo + active-state machine
- src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository
- src/binder/services/bindings.ts: link/unlink/list/update/setActive
emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated /
EvidenceItemActivated
- src/binder/state/active.ts: (target, evidence, annotation) reducer
+ ActiveStateProvider + useActiveState hook
- extended engine/events/types.ts with EvidenceLinkCreated,
EvidenceLinkUpdated, FormFieldActivated payloads
T03 Rect registry (SharedContracts §7 — contract FROZEN)
- src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe
+ invalidate + getVersion for useSyncExternalStore
- events.ts: scroll/resize/focus pumps via window + ResizeObserver +
IntersectionObserver, rAF-throttled
- react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref),
useRectRegistryVersion
T04 Form schema + renderer
- src/app/forms/demo-schema.ts: text/textarea/date minimal schema
- src/binder/FormRenderer.tsx: renders schema, each field registers
as rect kind="field"; active field gets aria-current="true"
- placed in binder/ (not work/) because work cannot import binder per
DependencyMap.md §2 and the renderer needs the rect-registry hook;
workplan T04 was amended in-place to document this
T05 Side-by-side Forms layout + click-to-link
- src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with
hash route #/forms/demo
- BinderProvider mounted at app root so links survive tab switching
- stage-evidence-then-click-field linking interaction with banner
+ per-field link-count chip
T06 Active-evidence cycling
- src/app/forms/ActiveEvidenceChips.tsx: chips per active target,
Tab cycles natively, first chip auto-activates on field focus,
each chip registers as rect kind="evidence-card"
- ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll
- EvidenceSidebar + EvidenceStrip highlight the active item via the
new useLastActivatedEvidence hook in work/EngineContext
T07 SVG visual-guide overlay
- src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG,
draws field→card and card→highlight bezier curves for the active
triple, rAF-throttled via the registry
- src/anchor exposes getHighlightClientRects(annotationId); the
spike viewer wraps highlights in [data-highlight-id] so the helper
can locate them
- src/app/forms/HighlightRectBridge.tsx: registers the active
annotation's rect via that helper
T08 End-to-end test (PRD scenario steps 5-9)
- tests/integration/forms-overlay-e2e.dom.test.tsx: full path from
Review-mode capture through Forms-mode link to active triple +
aria-current assertions + 2 SVG paths in the overlay
- additional integration coverage: forms-link-flow + forms-active-cycling
Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
T01: shared engine types (Document, Selector union, Annotation, EvidenceItem,
branded IDs with newId factory) per wiki/SharedContracts.md §1-§3.
T02: react-pdf-highlighter-plus v1.1.4 spike behind the §5
DocumentViewerAdapter contract in src/anchor/. Pure round-trip math
extracted to pdf-selector-math.ts with 11 unit tests proving lossless
capture → selectors → JSON → restored-rects. ADR-0004 accepted; full
user-flow Playwright verification deferred to T09.
Adds Vite app shell (index.html, src/app/SpikeApp.tsx) so the spike is
exercisable via pnpm dev. tsconfig --noEmit prevents tsc -b from
littering src/ with stray .js outputs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>