AUDIO
Source snapshot · 12 October 2026

Local split-pane layout

RavetControls.SplitPane lays out two consumer-owned panes with a labelled, keyboard-focusable separator. It uses CSS grid in one connected live original HTML document. The root must have exactly three direct element children in the order first, separator, second; both panes need distinct existing IDs whose lookup resolves to those exact elements. The separator is an empty, non-native- interactive HTML element. The separator must remain element-child-free and non-interactive/non-editable (including inherited contenteditable). Plain separator text is allowed; adding an element child, native-interactive href or editable state retires before input admission. Arbitrary text and elements inside panes remain consumer-owned. Root direct text must consist only of CSS-collapsible ASCII space/tab/LF/CR/form-feed; NBSP and other text would create anonymous grid items and are rejected. Root direct child nodes are bounded to 64; comments and collapsible whitespace are allowed. Later illegal direct content retires the wrapper. Duplicate live wrappers on a root or separator reject before DOM changes.

const split = new RavetControls.SplitPane(root, first, separator, second, {
  label: 'Editor and preview sizes',
  axis: 'horizontal',
  minimumPixels: 120,
  secondMinimumPixels: 160,
  onResize: ({value, source}) => savePaneFraction(value),
});

The explicit label contains 1–256 UTF-16 code units and at least one non-whitespace character. axis is horizontal or vertical; rtl is an explicit boolean and reverses horizontal placement and arrow/pointer direction. value is a finite fraction in 0–1, initially 0.5. Integer minimumPixels and secondMinimumPixels are each 0–4096; integer separator thickness is 1–64 CSS pixels, initially 8. The finite positive keyboard step is at most 1, initially 0.01. An optional onResize is a function or null. Invalid profiles reject atomically.

setValue(fraction, {notify: false}) validates and clamps to measured legal pixel minima before committing. With notify: true, a changed accepted value sends one frozen {value, source: 'programmatic'}. Axis arrows use step, Shift uses one tenth of it, and Home/End choose measured feasible bounds. Accepted keyboard and pointer changes send the corresponding keyboard or pointer source. Unrelated keys and Ctrl/Meta/Alt remain consumer-owned. An effective same-value update succeeds quietly. Programmatic updates reject while a pointer drag is held; an accepted keyboard resize cancels the old pointer anchor before committing. currentValue() returns the committed fraction.

refresh() measures the available root content extent, updates layout/ARIA, and silently clamps the current value if constraints changed. ResizeObserver and original-document mutation observation call the same bounded control operation; there is no audio callback or timer-polled focus trap. If the extent minus the separator cannot fit both minima, refresh() returns false and lastError() is TooSmall. Fractional tracks still fit the viewport and clip consumer content; the requested pixel minima cannot be honored. The separator becomes disabled and leaves sequential focus. A later feasible refresh restores its Tab stop without moving global focus. Consumer content should create its own scroll containers if clipping is undesirable.

A drag owns one pointer ID in the original window, attempts native pointer capture, and stops on exact release/cancel/lost capture, original-window blur, hidden document, removal, unavailable members, or changed available extent. Changed extent cancels the delta origin rather than guessing a new coordinate. In-flow generated root ::before/::after content is rejected at construction and later suspends with Layout. Pseudo content none/normal, display:none, or out-of-flow absolute/fixed decorators is allowed; the component supplies no decorator pointer/occlusion policy. Every CSS transform, individual translate/rotate/scale, and non-default zoom on a member or any of up to 64 root ancestors is unsupported, even an identity transform. Construction rejects such layouts. Later transforms suspend input with Transform; removing them allows refresh() to recover. No transformed coordinate, ancestor beyond that bound, shadow DOM, or cross-origin policy is implied.

Removal/adoption, child order, pane ID, duplicate ID ownership, or fixed association changes retire the wrapper. Ownership records that retirement. Healthy unavailable/hidden/inert/display-none layouts suspend with Unavailable. lastError() also reports Callback when notification throws; committed layout cannot be rolled back after the consumer received it. Callback and focus-time reentry cannot start another change or refresh. Disposal is legal in callbacks, and post-callback/focus checks reject stale success. Consumer callbacks that hide/transform or change association suspend/retire synchronously, including throwing callbacks. No global focus restoration is attempted.

The component owns enumerated root grid templates/areas/auto-placement/spacing/horizontal writing mode/direction/overflow styles, member static position/float/grid placement/order/alignment, pane width/height/minimum/maximum and clipping styles, member box sizing/edge spacing/border widths, separator touch action, and separator role/name/orientation/controls/value/disabled/tabindex attributes. Consumers must not edit those owned properties during a live wrapper; theme or content styles outside that set remain consumer-owned. Original inline style text is bounded to 16384 code units per element; each owned original longhand/attribute is bounded to 4096. dispose() is idempotent, removes listeners/observers, ends the drag, and restores exact original owned values and priorities. If no unrelated inline style edits remain, it restores raw style-attribute serialization and presence too; otherwise unrelated edits survive while owned longhands restore. Cleanup works after association changes. Queued observer work cannot mutate a disposed wrapper.

The focused fixtures exercise 37 groups in Chromium and hidden WKWebView: actual horizontal/vertical/RTL geometry, measured minima, TooSmall recovery and oversized content clipping, single-pointer ownership, reentrant callbacks, transforms on all members, fixed associations, exact cleanup, queued observer work, and child-document routing. Trusted Chromium Tab/arrow/Home/End delivery also changed the separator value as expected. Pointer/focus reentry fixtures use explicit synthetic input; the hidden Chromium document reports document.hasFocus() === false outside key delivery. These are browser DOM and keyboard contracts, with no native pointer routing, native focus, native accessibility-tree, screen-reader, window minimum, or deadline certification.

Build and run the native hidden fixture from an ordinary local checkout:

cmake -S crates/ravet-surface/native -B target/native-editor -DRAVET_WEB_EDITOR_TESTS=ON
cmake --build target/native-editor --target ravet-web-split-pane -j 2
ctest --test-dir target/native-editor -R '^ravet-web-split-pane$' --output-on-failure

The CMake fixture embeds the same trusted shared script inline within its local read scope. It opens no host/device/window. See decision 0204.