export type ChangeDecision = "pending" | "accepted" | "rejected"; /** * THE REVIEW BOOKKEEPING, WITHOUT A SURFACE. * * Keeping which proposals are in and which are out is genuinely fiddly — the * counter, keep-all, undo, the commit gate — and genuinely the same every time. * What is NOT the same every time is where any of it renders. So this is a hook: * the host gets the state machine and draws whatever its screen needs, instead * of adopting a container to get the arithmetic. * * Decisions are stored as OVERRIDES against a default, never seeded from `ids` * into state. Seeding would need an effect to keep in step, and `ids` is * typically a `useMemo` over query rows — a new array every render, so the * effect fires every render and sets state every time. (That exact loop has bit * this codebase before: `Maximum update depth exceeded` in a settle drawer that * seeded "everything ticked" from its rows.) Storing the exceptions makes the * selection derived: nothing to seed, nothing to re-sync, and ids that come and * go simply pick up the default. */ export interface ChangeSet { status: (id: Id) => ChangeDecision; accept: (id: Id) => void; reject: (id: Id) => void; /** Back to the default — the Undo on a decided row. */ undo: (id: Id) => void; acceptAll: () => void; rejectAll: () => void; /** Every id back to the default. */ reset: () => void; accepted: readonly Id[]; rejected: readonly Id[]; pending: readonly Id[]; keptCount: number; total: number; /** Nothing is left undecided — the usual gate on a commit button. */ settled: boolean; } export interface UseChangeSetOptions { /** * What an untouched proposal counts as. Default `accepted`: the operator * drops the exceptions rather than approving each of eight identical lines, * which is the difference between a review and a second round of data entry. * Use `pending` when each change genuinely deserves its own verdict — and * gate the commit on `settled`. * * **A MAP when one set mixes kinds with different safe defaults** — filling a * blank arrives `accepted` while overwriting a value a human already set * arrives `rejected`, so the destructive half is opt-in. An id the map does * not name arrives `accepted`. Deciding those rows by calling `reject()` from * the result handler instead writes overrides the operator never made, and * `undo` on such a row then returns it to `accepted` rather than to the safe * default it was supposed to arrive at. * * A VALUE and not a predicate, so the default is a dependency like any other: * `status` and the group arrays are derived from it together and change * identity together when it changes. The caller closes over its own rows to * build it (`useMemo` over the same rows it already has), which is what keeps * this hook ignorant of what a proposal IS. A predicate written inline is a * new function every render, so it can only be honoured by hiding it from the * dependency lists — and a `status` that never changes identity is a `status` * a memoizing screen reads once and then never again, which is the row * rendering as kept while the commit bar counts it as dropped. */ initial?: ChangeDecision | ReadonlyMap; } export declare function useChangeSet(ids: readonly Id[], options?: UseChangeSetOptions): ChangeSet;