generated from coulomb/repo-seed
Unify capture/edit form, thicker active document border, layer-hide toggles
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>
This commit is contained in:
@@ -33,3 +33,27 @@
|
||||
.ce-debug-textlayer canvas {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/*
|
||||
* Layer-visibility toggles. Each `.ce-hide-<layer>` class is applied
|
||||
* to the same viewer-wrapper element so a single parent can hide any
|
||||
* combination of layers. Useful for diagnosing layer stacking issues
|
||||
* (e.g. "is the textLayer covering the canvas?") by elimination.
|
||||
*/
|
||||
|
||||
.ce-hide-canvas canvas {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ce-hide-text-layer .textLayer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ce-hide-annotation-layer .annotationLayer,
|
||||
.ce-hide-annotation-layer .annotationEditorLayer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ce-hide-xfa-layer .xfaLayer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -199,6 +199,15 @@ export interface PdfSpikeViewerProps {
|
||||
* image-only. Also logs every onSelection event to the console.
|
||||
*/
|
||||
readonly debugTextLayer?: boolean;
|
||||
/**
|
||||
* Hide specific PDF.js layers so you can see what sits underneath.
|
||||
* Helps diagnose layer-stacking issues (e.g. "is the text layer
|
||||
* covering the canvas content?").
|
||||
*/
|
||||
readonly hideCanvas?: boolean;
|
||||
readonly hideTextLayer?: boolean;
|
||||
readonly hideAnnotationLayer?: boolean;
|
||||
readonly hideXfaLayer?: boolean;
|
||||
}
|
||||
|
||||
export interface StoredAnnotation {
|
||||
@@ -222,11 +231,24 @@ export function PdfSpikeViewer(props: PdfSpikeViewerProps) {
|
||||
activeAnnotationId,
|
||||
onHighlightClicked,
|
||||
debugTextLayer,
|
||||
hideCanvas,
|
||||
hideTextLayer,
|
||||
hideAnnotationLayer,
|
||||
hideXfaLayer,
|
||||
} = props;
|
||||
const HighlightContainer = useMemo(
|
||||
() => makeSpikeHighlightContainer({ activeAnnotationId, onHighlightClicked }),
|
||||
[activeAnnotationId, onHighlightClicked],
|
||||
);
|
||||
const wrapperClasses = [
|
||||
debugTextLayer ? "ce-debug-textlayer" : null,
|
||||
hideCanvas ? "ce-hide-canvas" : null,
|
||||
hideTextLayer ? "ce-hide-text-layer" : null,
|
||||
hideAnnotationLayer ? "ce-hide-annotation-layer" : null,
|
||||
hideXfaLayer ? "ce-hide-xfa-layer" : null,
|
||||
]
|
||||
.filter((c): c is string => c !== null)
|
||||
.join(" ");
|
||||
const utilsRef = useRef<PdfHighlighterUtils | null>(null);
|
||||
const [didScroll, setDidScroll] = useState<string | null>(null);
|
||||
|
||||
@@ -273,7 +295,7 @@ export function PdfSpikeViewer(props: PdfSpikeViewerProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={debugTextLayer ? "ce-debug-textlayer" : undefined}
|
||||
className={wrapperClasses.length > 0 ? wrapperClasses : undefined}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<PdfLoader document={pdfUrl}>
|
||||
|
||||
@@ -36,6 +36,10 @@ export function SessionMenu({ onExportZip, onImportZip, onOpenSamples }: Session
|
||||
const tick = useSessionListTick();
|
||||
const active = useActiveSession();
|
||||
const [debugTextLayer, setDebugTextLayer] = useDebugFlag("textLayer");
|
||||
const [hideCanvas, setHideCanvas] = useDebugFlag("hideCanvas");
|
||||
const [hideTextLayer, setHideTextLayer] = useDebugFlag("hideTextLayer");
|
||||
const [hideAnnotationLayer, setHideAnnotationLayer] = useDebugFlag("hideAnnotationLayer");
|
||||
const [hideXfaLayer, setHideXfaLayer] = useDebugFlag("hideXfaLayer");
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [newName, setNewName] = useState("");
|
||||
@@ -407,25 +411,52 @@ export function SessionMenu({ onExportZip, onImportZip, onOpenSamples }: Session
|
||||
)}
|
||||
|
||||
<hr style={dividerStyle} />
|
||||
<label
|
||||
data-testid="session-menu-debug-textlayer"
|
||||
<div
|
||||
style={{
|
||||
...menuItemStyle,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
cursor: "pointer",
|
||||
padding: "4px 8px",
|
||||
color: "#666",
|
||||
fontSize: 11,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.5,
|
||||
}}
|
||||
title="Paint the PDF text-layer spans in yellow so you can see what's selectable. Also logs every selection event to the browser console."
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={debugTextLayer}
|
||||
onChange={(e) => setDebugTextLayer(e.target.checked)}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
Debug text layer
|
||||
</label>
|
||||
PDF diagnostics
|
||||
</div>
|
||||
<DebugCheckbox
|
||||
label="Highlight text layer"
|
||||
testid="session-menu-debug-textlayer"
|
||||
title="Paint the PDF text-layer spans in light grey so you can see what's selectable. Logs every selection event to the browser console."
|
||||
checked={debugTextLayer}
|
||||
onChange={setDebugTextLayer}
|
||||
/>
|
||||
<DebugCheckbox
|
||||
label="Hide canvas layer"
|
||||
testid="session-menu-hide-canvas"
|
||||
title="Hide the rendered glyphs so only the text/annotation overlay layers remain. Use to see if the textLayer covers regions where the canvas has no content."
|
||||
checked={hideCanvas}
|
||||
onChange={setHideCanvas}
|
||||
/>
|
||||
<DebugCheckbox
|
||||
label="Hide text layer"
|
||||
testid="session-menu-hide-textlayer"
|
||||
title="Hide the invisible selection text layer entirely. Use to see if it's covering the canvas content underneath."
|
||||
checked={hideTextLayer}
|
||||
onChange={setHideTextLayer}
|
||||
/>
|
||||
<DebugCheckbox
|
||||
label="Hide annotation layer"
|
||||
testid="session-menu-hide-annotation"
|
||||
title="Hide PDF annotations (stamps, form widgets, links). Use to see if a stamp is obscuring content or capturing your clicks."
|
||||
checked={hideAnnotationLayer}
|
||||
onChange={setHideAnnotationLayer}
|
||||
/>
|
||||
<DebugCheckbox
|
||||
label="Hide XFA layer"
|
||||
testid="session-menu-hide-xfa"
|
||||
title="Hide the XFA form layer (rare; only present on Adobe XFA forms)."
|
||||
checked={hideXfaLayer}
|
||||
onChange={setHideXfaLayer}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
@@ -459,6 +490,38 @@ export function SessionMenu({ onExportZip, onImportZip, onOpenSamples }: Session
|
||||
);
|
||||
}
|
||||
|
||||
interface DebugCheckboxProps {
|
||||
readonly label: string;
|
||||
readonly testid: string;
|
||||
readonly title: string;
|
||||
readonly checked: boolean;
|
||||
onChange(next: boolean): void;
|
||||
}
|
||||
|
||||
function DebugCheckbox(p: DebugCheckboxProps) {
|
||||
return (
|
||||
<label
|
||||
data-testid={p.testid}
|
||||
title={p.title}
|
||||
style={{
|
||||
...menuItemStyle,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={p.checked}
|
||||
onChange={(e) => p.onChange(e.target.checked)}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
{p.label}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
const menuItemStyle: CSSProperties = {
|
||||
display: "block",
|
||||
width: "100%",
|
||||
|
||||
@@ -106,13 +106,14 @@ export function CollectionList({ upload, title }: CollectionListProps) {
|
||||
<li key={doc.id} style={{ marginBottom: 6 }}>
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #ccc",
|
||||
border: isActive ? "3px solid #0050b3" : "1px solid #ccc",
|
||||
background: isActive ? "#e8f0ff" : "white",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
fontSize: 12,
|
||||
}}
|
||||
data-testid={`collection-item-${doc.id}`}
|
||||
data-active={isActive ? "true" : "false"}
|
||||
>
|
||||
<button
|
||||
onClick={() => setId(doc.id)}
|
||||
|
||||
104
src/work/EvidenceFormBody.tsx
Normal file
104
src/work/EvidenceFormBody.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* EvidenceFormBody — the shared "citation + commentary" editor.
|
||||
*
|
||||
* One form drives both:
|
||||
* - the *capture* flow (creating evidence from a fresh selection
|
||||
* inside `InlineCaptureForm`), and
|
||||
* - the *edit* flow (modifying an existing evidence card inside
|
||||
* `EvidenceSidebar`).
|
||||
*
|
||||
* Keeping a single component means changes to the field layout, hint
|
||||
* placement, or save-button copy land in one file and apply to both
|
||||
* surfaces. Callers control the labels and any badge/helper text.
|
||||
*/
|
||||
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
|
||||
export interface EvidenceFormBodyProps {
|
||||
readonly quote: string;
|
||||
readonly commentary: string;
|
||||
onChangeQuote(next: string): void;
|
||||
onChangeCommentary(next: string): void;
|
||||
onSave(): void;
|
||||
onCancel(): void;
|
||||
/** Save-button label, defaults to "Save". */
|
||||
readonly saveLabel?: string;
|
||||
/** Cancel-button label, defaults to "Cancel". */
|
||||
readonly cancelLabel?: string;
|
||||
/** Short caption rendered at the top of the form, e.g. a selector
|
||||
* count badge or "Editing". */
|
||||
readonly badge?: ReactNode;
|
||||
/** Inline note shown under the buttons (e.g. "won't move the
|
||||
* marked passage" when editing an existing item). */
|
||||
readonly helper?: ReactNode;
|
||||
/** data-testid prefix for the input + button hooks. */
|
||||
readonly testidPrefix: string;
|
||||
}
|
||||
|
||||
export function EvidenceFormBody(p: EvidenceFormBodyProps) {
|
||||
const saveLabel = p.saveLabel ?? "Save";
|
||||
const cancelLabel = p.cancelLabel ?? "Cancel";
|
||||
return (
|
||||
<div style={{ padding: 8, fontSize: 12 }}>
|
||||
{p.badge && (
|
||||
<div style={{ marginBottom: 6, fontWeight: 600 }}>{p.badge}</div>
|
||||
)}
|
||||
<label style={labelStyle}>Citation text</label>
|
||||
<textarea
|
||||
value={p.quote}
|
||||
onChange={(e) => p.onChangeQuote(e.target.value)}
|
||||
data-testid={`${p.testidPrefix}-quote`}
|
||||
rows={3}
|
||||
style={{
|
||||
...textareaStyle,
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
/>
|
||||
<label style={labelStyle}>Commentary</label>
|
||||
<textarea
|
||||
value={p.commentary}
|
||||
onChange={(e) => p.onChangeCommentary(e.target.value)}
|
||||
data-testid={`${p.testidPrefix}-commentary`}
|
||||
rows={2}
|
||||
placeholder="(optional)"
|
||||
style={textareaStyle}
|
||||
/>
|
||||
<div style={{ display: "flex", gap: 6, alignItems: "center" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={p.onSave}
|
||||
data-testid={`${p.testidPrefix}-save`}
|
||||
style={{ fontSize: 12, padding: "4px 10px" }}
|
||||
>
|
||||
{saveLabel}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={p.onCancel}
|
||||
data-testid={`${p.testidPrefix}-cancel`}
|
||||
style={{ fontSize: 12, padding: "4px 10px" }}
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
{p.helper && (
|
||||
<span style={{ fontSize: 11, color: "#888" }}>{p.helper}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const labelStyle: CSSProperties = {
|
||||
display: "block",
|
||||
color: "#666",
|
||||
fontSize: 11,
|
||||
marginBottom: 2,
|
||||
};
|
||||
|
||||
const textareaStyle: CSSProperties = {
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
fontSize: 12,
|
||||
padding: 4,
|
||||
marginBottom: 6,
|
||||
};
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
type ExportFormat,
|
||||
type ExportResult,
|
||||
} from "./useExportEvidence";
|
||||
import { EvidenceFormBody } from "./EvidenceFormBody";
|
||||
import { InlineCaptureForm } from "./InlineCaptureForm";
|
||||
|
||||
const TOAST_TIMEOUT_MS = 2000;
|
||||
@@ -375,79 +376,18 @@ function EvidenceCard(p: EvidenceCardProps) {
|
||||
}}
|
||||
>
|
||||
{p.isEditing ? (
|
||||
<div style={{ padding: 8, fontSize: 12 }}>
|
||||
<label
|
||||
style={{
|
||||
display: "block",
|
||||
color: "#666",
|
||||
fontSize: 11,
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
Citation text
|
||||
</label>
|
||||
<textarea
|
||||
value={p.editQuote}
|
||||
onChange={(e) => p.onChangeQuote(e.target.value)}
|
||||
data-testid={`evidence-edit-quote-${p.item.id}`}
|
||||
rows={3}
|
||||
style={{
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
fontSize: 12,
|
||||
padding: 4,
|
||||
marginBottom: 6,
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
style={{
|
||||
display: "block",
|
||||
color: "#666",
|
||||
fontSize: 11,
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
Commentary
|
||||
</label>
|
||||
<textarea
|
||||
value={p.editCommentary}
|
||||
onChange={(e) => p.onChangeCommentary(e.target.value)}
|
||||
data-testid={`evidence-edit-commentary-${p.item.id}`}
|
||||
rows={2}
|
||||
placeholder="(empty)"
|
||||
style={{
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
fontSize: 12,
|
||||
padding: 4,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={p.onSaveEdit}
|
||||
data-testid={`evidence-edit-save-${p.item.id}`}
|
||||
style={{ fontSize: 12, padding: "4px 10px" }}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={p.onCancelEdit}
|
||||
data-testid={`evidence-edit-cancel-${p.item.id}`}
|
||||
style={{ fontSize: 12, padding: "4px 10px" }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<span
|
||||
style={{ fontSize: 11, color: "#888", alignSelf: "center" }}
|
||||
>
|
||||
The marked passage in the document stays the same.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<EvidenceFormBody
|
||||
quote={p.editQuote}
|
||||
commentary={p.editCommentary}
|
||||
onChangeQuote={p.onChangeQuote}
|
||||
onChangeCommentary={p.onChangeCommentary}
|
||||
onSave={p.onSaveEdit}
|
||||
onCancel={p.onCancelEdit}
|
||||
saveLabel="Save"
|
||||
cancelLabel="Cancel"
|
||||
helper="The marked passage in the document stays the same."
|
||||
testidPrefix={`evidence-edit-${p.item.id}`}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
/**
|
||||
* `InlineCaptureForm` — the "I just selected text, let me save it as
|
||||
* evidence" form. Renders only when a pendingSelection is set; the
|
||||
* EvidenceSidebar slots it into the right position in document order
|
||||
* so the form appears between the cards that bracket the new
|
||||
* selection.
|
||||
* evidence" form. Renders only when a `pendingSelection` is set;
|
||||
* `EvidenceSidebar` slots it into the right position in document
|
||||
* order so the new capture appears between the cards that bracket
|
||||
* it.
|
||||
*
|
||||
* Behaviour matches the pre-iteration AnnotationToolbar (which used to
|
||||
* live above the viewer):
|
||||
* Uses the shared `EvidenceFormBody` so the field layout matches the
|
||||
* edit form on an existing card. The user can refine the
|
||||
* auto-captured citation text before saving (handy when the
|
||||
* underlying text layer captured fragments — they can paste in the
|
||||
* correct quote without re-selecting).
|
||||
*
|
||||
* Save pipeline:
|
||||
* 1. `createSelectors(capture, representation)` — anchor builds the
|
||||
* maximal selector set against the active representation.
|
||||
* 2. `engine.annotations.create(...)` — engine mints an Annotation +
|
||||
@@ -25,15 +29,18 @@ import {
|
||||
useEngine,
|
||||
usePendingSelection,
|
||||
} from "./EngineContext";
|
||||
import { EvidenceFormBody } from "./EvidenceFormBody";
|
||||
|
||||
export function InlineCaptureForm() {
|
||||
const engine = useEngine();
|
||||
const { document, representation } = useActiveDocument();
|
||||
const { pending, set } = usePendingSelection();
|
||||
const [quote, setQuote] = useState("");
|
||||
const [commentary, setCommentary] = useState("");
|
||||
|
||||
// Reset the commentary box whenever a fresh selection arrives.
|
||||
// Re-seed the form whenever a fresh selection arrives.
|
||||
useEffect(() => {
|
||||
setQuote(pending?.capture.text ?? "");
|
||||
setCommentary("");
|
||||
}, [pending]);
|
||||
|
||||
@@ -45,7 +52,7 @@ export function InlineCaptureForm() {
|
||||
documentId: document.id,
|
||||
representationId: representation.id,
|
||||
selectors,
|
||||
quote: pending.capture.text,
|
||||
quote: quote.trim().length > 0 ? quote : pending.capture.text,
|
||||
});
|
||||
engine.evidence.create({
|
||||
annotationIds: [annotation.id],
|
||||
@@ -56,8 +63,7 @@ export function InlineCaptureForm() {
|
||||
|
||||
const handleDiscard = () => set(null);
|
||||
|
||||
const quote = pending.capture.text;
|
||||
const shortQuote = quote.length > 200 ? `${quote.slice(0, 200)}…` : quote;
|
||||
const selectorCount = pending.selectors.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -65,49 +71,28 @@ export function InlineCaptureForm() {
|
||||
style={{
|
||||
border: "1px dashed #b78b1c",
|
||||
background: "#fff8d6",
|
||||
padding: 8,
|
||||
marginBottom: 8,
|
||||
borderRadius: 2,
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 6, fontWeight: 600 }}>
|
||||
New annotation ({pending.selectors.length} selector
|
||||
{pending.selectors.length === 1 ? "" : "s"})
|
||||
</div>
|
||||
<div style={{ marginBottom: 6, fontStyle: "italic", color: "#444" }}>
|
||||
“{shortQuote}”
|
||||
</div>
|
||||
<textarea
|
||||
value={commentary}
|
||||
onChange={(e) => setCommentary(e.target.value)}
|
||||
placeholder="Add a one-line comment (optional)…"
|
||||
rows={2}
|
||||
style={{
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
fontSize: 12,
|
||||
padding: 4,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
<EvidenceFormBody
|
||||
quote={quote}
|
||||
commentary={commentary}
|
||||
onChangeQuote={setQuote}
|
||||
onChangeCommentary={setCommentary}
|
||||
onSave={handleSave}
|
||||
onCancel={handleDiscard}
|
||||
saveLabel="Save evidence"
|
||||
cancelLabel="Discard"
|
||||
badge={
|
||||
<>
|
||||
New evidence (
|
||||
{selectorCount} selector{selectorCount === 1 ? "" : "s"}
|
||||
) — refine the citation if needed
|
||||
</>
|
||||
}
|
||||
testidPrefix="inline-capture"
|
||||
/>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
data-testid="inline-capture-save"
|
||||
style={{ fontSize: 12, padding: "4px 10px" }}
|
||||
>
|
||||
Save evidence
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDiscard}
|
||||
style={{ fontSize: 12, padding: "4px 10px" }}
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ export function ViewerShell() {
|
||||
const { set: setPending } = usePendingSelection();
|
||||
const { id: scrollToId, version: scrollVersion, scrollTo } = useScrollToAnnotation();
|
||||
const [debugTextLayer] = useDebugFlag("textLayer");
|
||||
const [hideCanvas] = useDebugFlag("hideCanvas");
|
||||
const [hideTextLayer] = useDebugFlag("hideTextLayer");
|
||||
const [hideAnnotationLayer] = useDebugFlag("hideAnnotationLayer");
|
||||
const [hideXfaLayer] = useDebugFlag("hideXfaLayer");
|
||||
const activeEvidenceId = useLastActivatedEvidence();
|
||||
|
||||
// The viewer needs to re-fetch its highlight list whenever annotations
|
||||
@@ -108,16 +112,29 @@ export function ViewerShell() {
|
||||
>
|
||||
<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.
|
||||
key={`${document.id}#${scrollVersion}#${debugTextLayer ? "d" : "n"}`}
|
||||
// Re-key on scrollVersion + debug flags so toggling any of
|
||||
// them remounts the viewer (the CSS classes are on a parent,
|
||||
// but a re-render is the simplest way to make sure the layers
|
||||
// get re-laid-out).
|
||||
key={[
|
||||
document.id,
|
||||
scrollVersion,
|
||||
debugTextLayer ? "d" : "n",
|
||||
hideCanvas ? "hc" : "",
|
||||
hideTextLayer ? "ht" : "",
|
||||
hideAnnotationLayer ? "ha" : "",
|
||||
hideXfaLayer ? "hx" : "",
|
||||
].join("#")}
|
||||
pdfUrl={fileUrl}
|
||||
storedAnnotations={annotations}
|
||||
{...(scrollToId ? { scrollToAnnotationId: scrollToId } : {})}
|
||||
activeAnnotationId={activeAnnotationId}
|
||||
onHighlightClicked={handleHighlightClicked}
|
||||
debugTextLayer={debugTextLayer}
|
||||
hideCanvas={hideCanvas}
|
||||
hideTextLayer={hideTextLayer}
|
||||
hideAnnotationLayer={hideAnnotationLayer}
|
||||
hideXfaLayer={hideXfaLayer}
|
||||
onSelectionCaptured={(capture, selectors) => {
|
||||
setPending({ capture, selectors });
|
||||
}}
|
||||
|
||||
@@ -15,7 +15,12 @@ import { useCallback, useEffect, useState } from "react";
|
||||
const KEY_PREFIX = "citation-evidence:debug:";
|
||||
const STORAGE_EVENT = "ce-debug-flag-change";
|
||||
|
||||
export type DebugFlag = "textLayer";
|
||||
export type DebugFlag =
|
||||
| "textLayer"
|
||||
| "hideCanvas"
|
||||
| "hideTextLayer"
|
||||
| "hideAnnotationLayer"
|
||||
| "hideXfaLayer";
|
||||
|
||||
function storageKey(flag: DebugFlag): string {
|
||||
return `${KEY_PREFIX}${flag}`;
|
||||
|
||||
@@ -138,10 +138,10 @@ describe("CE-WP-0004-T05 — PRD scenario steps 10-11 end-to-end", () => {
|
||||
);
|
||||
});
|
||||
await user.type(
|
||||
screen.getByPlaceholderText(/Add a one-line comment/),
|
||||
screen.getByTestId("inline-capture-commentary"),
|
||||
"Export E2E commentary",
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: /Save evidence/ }));
|
||||
await user.click(screen.getByTestId("inline-capture-save"));
|
||||
await screen.findByText(/Export E2E commentary/);
|
||||
|
||||
// --- Step 10: click Export → Copy as Markdown ----------------------
|
||||
|
||||
@@ -251,10 +251,10 @@ describe("CE-WP-0005-T08 — full create → annotate → export → reimport E2
|
||||
);
|
||||
});
|
||||
await user.type(
|
||||
screen.getByPlaceholderText(/Add a one-line comment/),
|
||||
screen.getByTestId("inline-capture-commentary"),
|
||||
"E2E session commentary",
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: /Save evidence/ }));
|
||||
await user.click(screen.getByTestId("inline-capture-save"));
|
||||
await screen.findByText(/E2E session commentary/);
|
||||
|
||||
// Step 5 (sanity): export the evidence as Markdown via the sidebar.
|
||||
|
||||
Reference in New Issue
Block a user