Contract 2.0.0 · package sha256
a857eca95b35921109faf997094bb4192f12527d9d0fcd4be7e97f06425bad6e· generated from spec section 5.6 andaccessibilityPagebyservices/onsite-scripter/scripts/build-contract-docs.mjs; edit the source, not this file.
What the runtime does: its badges are <button>s named by their label, in the Tab order in array order, drawn while focused even when hidden, and fired by Enter or Space (Overlays); their default edge keeps 3:1 contrast over any part, with system colours in forced-colors mode. Its canvas is aria-hidden="true". What it doesn't do, and the page adds: a way to turn the view without a drag, a spoken line for each pick, and the view's name.
2.0 has no keyboard camera: the orbit is a pointer drag and the zoom a wheel or a pinch. Put your own buttons in a sibling of el (the runtime owns el's shadow root and renders none of its children) that turn and tilt the view from camera.state with camera.set, and reset it with camera.fit or a named view (camera.views.go). A move clamps to the page's limits, as a drag does. The example is spec section 5.6's, compiled against the declaration (examples/5.06-camera-2.md):
import type { Session } from '@holostep-configurators/runtime-client/contract';
declare const session: Session;
declare const signal: AbortSignal;
declare function addButton(label: string, run: () => void): void; // your <button> beside the view, never inside `el`
declare function setStatus(text: string): void; // your role="status" region's text
declare function describeChoice(group: string, material: string): string; // your catalogue's words
const turn = (byAzimuthDeg: number, byPolarDeg: number) => (): void => {
const { target, azimuthDeg, polarDeg, distance } = session.camera.state;
void session.camera.set(
{ target, azimuthDeg: azimuthDeg + byAzimuthDeg, polarDeg: polarDeg + byPolarDeg, distance },
{ durationMs: 300 },
);
};
addButton('Turn left', turn(-22.5, 0));
addButton('Turn right', turn(22.5, 0));
addButton('View from higher', turn(0, -15)); // polarDeg 0 looks straight down
addButton('View from lower', turn(0, 15));
addButton('Reset view', () => void session.camera.fit(undefined, { durationMs: 300 }));
// one sentence per pick, once it is shown: never per frame or per write
session.on('material:selected', ({ group, material }) => setStatus(describeChoice(group, material)), { signal });
Announce what changed in your own role="status" region (an aria-live="polite" one), one sentence per pick once it is shown: from the pick's event ('material:selected' above, or 'committed' for a change your own write made), never per write, per frame or per camera move. While the first model loads, say "Loading" there and set aria-busy="true" on your wrapper; clear both when idle() resolves.
Name the view on el or a wrapper with role="group" and aria-label, or point aria-describedby at your own text summary of the configured product (BootOptions.el). Never put role="img" on el or an ancestor while badges are set: an image's children are presentational, so the badges leave the accessibility tree. label items are pixels only, so put what they say in the DOM too.
Every action a drag does has another way in: a module you drag (input.drag) also has buttons or a list that place it, and a view you turn has the controls above. A full-width view on a phone page leaves a vertical swipe and the plain wheel to the page with camera.limits({ gestures: 'cooperative' }), so a shopper scrolling to the options never turns the product by accident. The browser takes the intersection of touch-action over the view and its ancestors, so el and every ancestor up to the page's scroller must allow pan-y: remove touch-action: none (Tailwind's touch-none) from them, or a vertical swipe still turns the product. The runtime records 'unsupported-spec' at '/camera/limits/gestures', naming the element, when one blocks it.
Give each badge a label that names the action, the side and the module, never two alike on one module. A ::part(badge) style of your own keeps a 3:1 edge against the parts behind it, as the default does, and a visible focus outline.