/**
* FormsApp — the evidence-backed form mode for CE-WP-0003.
*
* Layout:
*
* ┌────────────┬─────────────────┬─────────────┐
* │ Collection │ FormRenderer │ ViewerShell │
* │ │ (left) │ (right) │
* ├────────────┴─────────────────┴─────────────┤
* │ EvidenceStrip (bottom) │
* └────────────────────────────────────────────┘
*
* Linking interaction (T05):
* 1. User clicks an evidence card in the strip → it becomes "selected
* for linking" (highlighted; banner appears in the form pane).
* 2. User then clicks a form field. The field's `FormFieldActivated`
* event triggers `bindings.linkEvidenceToTarget(selected, field)`.
* 3. The selected-for-linking state clears; the field's link count
* chip increments.
*
* Active-evidence cycling (T06) and the visual guide overlay (T07) build
* on this composition without changing the link interaction.
*/
import { useEffect, useMemo, useState } from "react";
import type { EvidenceItem } from "@shared/evidence";
import type { AnnotationId, EvidenceItemId } from "@shared/ids";
import { Overlay, useActiveState, useBinder } from "@binder/index";
import {
CollectionList,
ViewerShell,
useActiveDocument,
useEngine,
useEngineEventTick,
useScrollToAnnotation,
} from "@work/index";
import { FormRenderer } from "@binder/FormRenderer";
import { ActiveEvidenceChips, type ActiveEvidenceChipsItem } from "./ActiveEvidenceChips";
import { DEMO_SCHEMA } from "./demo-schema";
import { HighlightRectBridge } from "./HighlightRectBridge";
export function FormsApp() {
return (
);
}
/**
* Bridge: the binder's active-state machine doesn't know how to scroll
* the viewer; the work-level `useScrollToAnnotation` does. This subscriber
* reads `activeAnnotationId` from the binder and calls `scrollTo` whenever
* it changes. Lives in app/ because that's where both subsystems meet.
*/
function ScrollBridge() {
const { state } = useActiveState();
const { scrollTo } = useScrollToAnnotation();
useEffect(() => {
if (state.activeAnnotationId) {
scrollTo(state.activeAnnotationId);
}
}, [state.activeAnnotationId, scrollTo]);
return null;
}
function FormPane() {
const { document } = useActiveDocument();
const { bindings } = useBinder();
const engine = useEngine();
const linkTick = useEngineEventTick("EvidenceLinkCreated");
const { state: activeState } = useActiveState();
const [selectedForLinking, setSelectedForLinking] = useState(
null,
);
// Compute per-field link counts. Re-derives on link create.
const linkCounts = useMemo>(() => {
const out: Record = {};
for (const field of DEMO_SCHEMA.fields) {
out[field.id] = bindings.listEvidenceForTarget({
targetType: "form-field",
targetId: field.id,
}).length;
}
void linkTick;
return out;
}, [bindings, linkTick]);
// Compute chip items for the currently-active target.
const activeChipItems = useMemo(() => {
if (!activeState.activeTarget) return [];
const links = bindings.listEvidenceForTarget(activeState.activeTarget);
return links
.map((link): ActiveEvidenceChipsItem | null => {
const item = engine.evidence.get(link.evidenceItemId);
if (!item) return null;
const annotationId: AnnotationId | null = item.annotationIds[0] ?? null;
const annotation = annotationId ? engine.annotations.get(annotationId) : null;
return {
evidenceItemId: link.evidenceItemId,
annotationId,
quote: annotation?.quote ?? "(no quote)",
...(item.commentary ? { commentary: item.commentary } : {}),
};
})
.filter((c): c is ActiveEvidenceChipsItem => c !== null);
// linkTick is included so newly created links populate the chips
// without an explicit refresh.
}, [activeState.activeTarget, bindings, engine, linkTick]);
// Listen for FormFieldActivated and, if an evidence is staged, create
// the link. The state machine reduction (focus-target) happens in
// parallel via ActiveStateProvider's own handler.
useEffect(() => {
return engine.bus.on("FormFieldActivated", (event) => {
if (!selectedForLinking) return;
bindings.linkEvidenceToTarget({
evidenceItemId: selectedForLinking,
target: event.target,
});
setSelectedForLinking(null);
});
}, [engine, bindings, selectedForLinking]);
return (
setSelectedForLinking(null)}
/>
{document ? (
<>
>
) : (
)}
);
}
function EmptyHint() {
return (
Pick a fixture from the collection list to start binding evidence.