evidence-binder/src/visual-guide/react-hooks.ts
tegwick a10f080f12
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Extract binder package from citation-evidence (EBIND-WP-0001)
Extracts citation-evidence/src/binder/ into this repo as the standalone
@citation-evidence/evidence-binder package: headless binding service +
in-memory link repo, active-state machine, the SharedContracts §7
rect-registry contract (registry, change pumps, hooks, SVG overlay), and
the target-neutral reference FormRenderer.

- toolchain mirrors sibling extracted repos (pnpm/tsc/vitest/eslint);
  imports rewritten from @shared/@engine aliases to the engine's
  @citation-evidence/engine package specifiers
- dependency boundary (engine + anchor only; no source/work/umbrella)
  enforced via eslint no-restricted-imports
- docs: extraction inventory + contract deltas, ADR-0001 (reference UI
  kept as supported exports), refreshed README/SCOPE/INTENT, populated
  capabilities index
- typecheck + lint green, 37 tests passing

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 01:36:33 +02:00

98 lines
2.9 KiB
TypeScript

/**
* React hooks for the rect registry.
*
* Components mount, get a ref to a DOM node, and ask the registry to
* track it via `useRegisterRect(kind, id, ref)`. Unmount/ref-change
* unregisters automatically.
*
* The registry itself lives behind a React context so multiple subtrees
* can share one registry (the overlay sees what every renderer publishes).
*/
import {
createContext,
useCallback,
useContext,
useEffect,
useSyncExternalStore,
type RefObject,
} from "react";
import {
createRectRegistry,
type RectKind,
type RectRegistry,
} from "./rect-registry";
import { attachRectChangePumps, type RectChangeObserverHandle } from "./events";
export interface RectRegistryContextValue {
readonly registry: RectRegistry;
readonly observer: RectChangeObserverHandle;
}
const RectRegistryContext = createContext<RectRegistryContextValue | null>(null);
/**
* Create an isolated registry + change pump pair for tests or app
* composition roots that wire their own provider.
*/
export function createRectRegistryContextValue(): RectRegistryContextValue {
const registry = createRectRegistry();
const observer = attachRectChangePumps(registry);
return { registry, observer };
}
export function useRectRegistryContext(): RectRegistryContextValue {
const ctx = useContext(RectRegistryContext);
if (!ctx) {
throw new Error(
"useRectRegistryContext must be used inside <RectRegistryProvider />",
);
}
return ctx;
}
export const RectRegistryProvider = RectRegistryContext.Provider;
/**
* Register a DOM ref's bounding rect with the registry.
*
* Re-runs when `kind`/`id`/`ref.current` change. The observer also starts
* watching the element for scroll/resize so the overlay can re-query
* without polling.
*/
export function useRegisterRect(
kind: RectKind,
id: string,
ref: RefObject<Element | null>,
): void {
const { registry, observer } = useRectRegistryContext();
useEffect(() => {
const el = ref.current;
if (!el) return;
const unregister = registry.register(kind, id, () => el.getBoundingClientRect());
const unobserve = observer.observe(el);
return () => {
unobserve();
unregister();
};
}, [kind, id, ref, registry, observer]);
}
/**
* Subscribe to registry change events from inside React. Returns a
* monotonically-increasing version number that bumps on every event, so
* `useMemo`/`useEffect` deps can include it to re-derive cached values.
*
* Implementation: leans on `registry.getVersion()` for the snapshot so
* `useSyncExternalStore` doesn't accumulate per-render subscribers.
*/
export function useRectRegistryVersion(): number {
const { registry } = useRectRegistryContext();
const subscribe = useCallback(
(callback: () => void) => registry.subscribe(callback),
[registry],
);
const getSnapshot = useCallback(() => registry.getVersion(), [registry]);
return useSyncExternalStore(subscribe, getSnapshot, () => 0);
}