/** * The a11y projection's DOM **ordering** engine: what order the projected * mirrors sit in, and what that reordering must not break. * * Extraction 2 of the `Scene.ts` decomposition * (`forge/decisions/file-decomposition-2026-08.md` §2), reduced in scope by * carryctx `DEC-0020`. `Scene` keeps `enforceA11yDomOrder` and * `a11yNeedsReorder` under their original names; the ordering half of the former * and the whole of the latter live here. * * ## What this owns * * The per-pass scratch collections (all reused rather than reallocated, which is * why they are fields and not locals), the reorder flag, the visual reading-order * sort, and the cursor-based `insertBefore` pass with its focus and `Selection` * preservation. * * ## What it deliberately does not own * * `Scene.enforceA11yDomOrder` still runs the collect-and-prune walk. Collecting * needs `shouldProjectA11y` — which reaches the pointer position (extraction 5) * and the content-projection tier (extraction 3) — and pruning needs * `focusedA11yElement`, `caretBlinkTimer` and `preserveFocusOnRemoval`. So * `Scene` walks and prunes, feeding each element in through {@link collect}, and * this class orders what it was given. * * Three members the original extraction plan assigned here stayed on `Scene`, * measured rather than assumed (`DEC-0020`): * * - `syncA11y` is not an a11y method. It is the shared depth-first walk driver * for a11y **and** content projection: it calls `syncContentProjection` at its * own recursion point and initialises four per-sync fields whose readers live * in three other domains. It moves once extraction 3 has taken its co-driver * out, not before — taking it now would need a back-edge into `Scene`, which * `DEC-0019` rule 1 forbids. * - `removeA11yRecursively` deletes content projections and releases DOM portals, * which are extraction 3 and the ninth portal domain respectively. * - `syncOverlayGeometry` is overlay-**layer** geometry: every dependency except * `a11yRoot` (`canvas`, `glCanvas`, `gpuCanvas`, `width`, `height`) belongs to * extraction 5. It sits under an a11y banner, which is a fourth instance of * `DEC-0016`'s finding that the banners expose wrong cuts. * * ## Reading direction is passed in * * The inline sort is direction-sensitive and `_readingDirection` belongs to the * text/layout side of `Scene`, so {@link reorder} takes it as an argument rather * than reaching for it — `DEC-0019` rule 5, the same shape as * `WasmBackendFacade.report(webgpuActive)`. */ import type { Entity } from '../Entity'; export declare class A11yProjectionManager { /** * Set by anything that changes which elements exist or how they nest, and * cleared by the reorder pass. Public so `Scene` can expose it under its * original name: `Entity` assigns to `scene.a11yNeedsReorder` as a public * cross-class contract, so the flag needs a setter and not only * {@link markNeedsReorder}. */ needsReorder: boolean; /** Overlay mirrors, kept in insertion order — they cover everything, so the * author's declared order is the right one. */ private readonly fullViewportElements; /** Everything else, sorted into visual reading order by {@link reorder}. */ private readonly normalElements; /** Ids collected this pass, read by `Scene`'s prune pass through * {@link isActive}. */ private readonly activeIdSet; /** Per-parent insertion cursor, reused by {@link reorder}. */ private readonly orderCursors; /** Membership set for the elements being ordered, reused per reorder pass. */ private readonly orderMembers; /** * Elements that are an *ancestor* of another ordered element, reused per pass. * * A composite widget's container (a `grid` around its rows, a `tree` around its * items) spans every descendant row, so it must not extend a visual row band — * see {@link sortNormalElementsVisually}. */ private readonly orderContainers; /** * Nearest region-establishing ancestor per ordered element — its *region*. * Written by `Scene.enforceA11yDomOrder`'s collect walk through * {@link collect}, which already has the entity in hand, so a region costs one * comparison per node rather than an ancestor walk per element. * * An ancestor establishes a region by setting `a11yRegion` (grouping declared * directly) or `clipChildren` (a clipper is usually the column boundary you * want anyway). Absent means the element sits under neither and belongs to the * implicit root region. See {@link sortNormalElementsVisually}. */ private readonly orderRegions; /** Mark the projected DOM as needing a reorder on the next pass. */ markNeedsReorder(): void; /** Reset the per-pass collections. Zero-GC: length/clear, never reallocate. */ beginCollect(): void; /** * Add one projected element to this pass. * * `region` is the nearest ancestor that sets `a11yRegion` or `clipChildren`, * or `null` for the implicit root region. */ collect(el: HTMLElement, fullViewport: boolean, region: Entity | null): void; /** Record that `id` still has a live a11y mirror this pass. */ markActive(id: string): void; /** Whether `id` was collected this pass. Drives `Scene`'s prune pass. */ isActive(id: string): boolean; /** * Put the collected elements into visual reading order in the DOM. * * No-op unless {@link needsReorder} is set; clears it on the way out. */ reorder(rtl: boolean): void; /** * Reorder `normalElements` (in place) into visual reading order using the * positions `syncA11y` already wrote to each element's inline style * (`top`/`left`/`height`). Elements are grouped into rows top-to-bottom (an * element belongs to the current row while its top is above the row's * running bottom edge), then sorted within a row by `left` — ascending for * `'ltr'`, descending for `'rtl'`. The sort is stable, so entities at the * same position keep their scene-graph (collection) order as a tiebreak. * * Those inline values are world coordinates for a top-level mirror but * PARENT-RELATIVE for a nested one, so this list mixes coordinate spaces. * That is sound because the result is only ever applied per DOM parent * ({@link reorder} advances a cursor per parent), and all of one parent's * children share one space: a `grid`'s rows are all grid-relative, a `row`'s * cells all row-relative. Comparisons ACROSS spaces do happen while banding, * but they only affect the relative order of elements in different parents, * which no `insertBefore` ever acts on. Normalizing everything back to world * coordinates here would cost a transform per element per frame to change * nothing observable. * * Banding runs **per region** — per nearest ancestor setting `a11yRegion` or * `clipChildren`, recorded by `Scene.enforceA11yDomOrder`'s collect walk — * rather than once over the whole scene. Purely visual banding is right for a * screen reader but wrong for selection: a DOM `Selection` covers everything * between anchor and focus in DOM order, so under one global banding a * vertical drag through a transcript also swallowed a sidebar whose headings * happened to fall in the same rows. Regions are laid out side by side, so * ordering region-major keeps each one a contiguous DOM run and a drag stays * inside it, while reading order *within* a region is unchanged. Regions are * emitted in the order their establishing ancestor is first reached by the * depth-first walk, so a screen reader still meets them in the author's * declared order. */ private sortNormalElementsVisually; }