Repository navigation
feat: colour, tone and motion options, with the playground controls to drive them - #7
Merged
Merged
Conversation
Captured against unmodified main, before the colour pipeline changes, so the premultiplied-alpha rewrite has something to be measured against.
test:look now runs pnpm build first, so a bundle stale relative to src/ can never produce a silent false pass against the golden images — the exact failure mode the baseline exists to catch. Also ignore the *.actual.png scratch files the harness writes on a mismatch, so a careless `git add` of test/golden can't commit them.
Both parseHexColor and parseColorRGBA duplicated 3-digit shorthand expansion and bit-shift RGB extraction. Extract expandBody and rgbFrom so the two overlapping hex grammars can't drift apart.
…TIONS The mock in packages/react/src/index.test.tsx hand-spells DEFAULT_OPTIONS to keep the React tests hermetic and independent of the built core package. It fell out of sync when Task 3 added five options to the real DEFAULT_OPTIONS, breaking the root typecheck. Bring the mirror current.
The tone curve is applied before the shared pointer pass, so a flattened field still takes the cursor at full strength. Asserted in golden-rule, because nothing else would catch the order being swapped.
Golden-image comparison against the pre-change baseline passes, so the default look is unchanged.
The golden cases only exercise defaults, so they prove the four uniforms are uploaded but not that a non-default background/ink/contrast/brightness reaches its uniform and moves the output the way main.frag predicts. Adds five property assertions (direction + rough margin, not exact pixels) to test:look, reusing its harness rather than inventing a new one.
Pointer reactivity stays live — WCAG 2.2.2 is about autoplaying motion, and a response to the user's own cursor is not that.
…check A golden image for a brand-new option only ever enshrines whatever the current code produces, and comparing two independently-launched pages by wall-clock screenshot timing made the check ~50% flaky (worst diffs of 126/255 against a 2/255 tolerance, from coincidental phase alignment). Instead, capture two frames from one running instance's own rAF clock, far apart, and require bit-exact equality: `u_time * u_speed` collapses to 0 regardless of elapsed time once `u_speed` is truly 0, so nothing but the freeze can explain identical pixels 117 frames apart.
Adds diagnoseContrast, a pure luminance-ratio check on the resolved background/ink pair. Calibrated at 1.5:1 — far below WCAG's 4.5:1 — because this is a decorative field, not body text, and the shipped default already sits at ~4.7:1 on the bright end of its ramp while going much lower by design at the dim end. Skipped for a non-opaque background, since what's behind a transparent canvas isn't ours to judge. Wired into motes.ts's existing once-per-instance diagnostics gate, alongside the DOM checks.
Three stacked pickers would have outgrown the panel, and would have implied the three colours are unrelated. They are one ramp.
Selecting a chip doesn't choose a value the way a radio does — it rebinds the one shared editor below to a different channel. That's tab semantics, not radio: tablist/tab/aria-selected, roving tabIndex, arrow-key navigation with wrapping plus Home/End, and a tabpanel wired to the active tab via aria-labelledby/aria-controls.
packages/core/README.md and packages/react/README.md ship to npm verbatim and still listed only the original nine options; apps/playground's React docs page carried the same stale prose list. Extend all three with background, ink, contrast, brightness and respectMotionPreference.
- look.browser.mjs: add the missing converse of the reduced-motion freeze assertion — under no-preference the field must still animate, which nothing in the suite previously proved (a regression that froze it unconditionally would have passed unchanged). - Presets.tsx: sync the roving tabIndex on click, not just onFocus, so desktop Safari (which doesn't focus buttons on click) can't leave the toolbar's focus stop on a stale preset. - Palette.tsx: reference DEFAULT_OPTIONS.background instead of a hardcoded '#050403' when un-checking "transparent".
set() reassigned `options` before parsing accent/ink/background, so a bad hex left getOptions() reporting a value the renderer and parsed colour locals never received, with no path back for a retry with the same good value, and silently dropped any other field in the same patch. Colours that changed are now parsed into locals before anything is committed, so a throw leaves the instance untouched and the call is a clean no-op. Also names the option in parseHexColor's error message (it was hardcoded to "accent" even when parsing ink) and adds a browser property assertion in test:look proving the atomicity fix, demonstrated to fail on the old ordering.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Gives the field a colour surface —
background,inkandaccentas real options — plus the tone and motion controls that go with them, the playground UI to drive them, and the docs. 26 commits, 38 files.Core
background,ink,contrast,brightnessandmotionjoin the option surface.backgroundaccepts#rgb,#rrggbb,#rrggbbaaandtransparent; the pipeline is premultiplied throughout, so a transparent background is justvec4(0)in the shader and the opaque case is arithmetically unchanged.backgroundtoinkand the pointer driving it toaccent.prefers-reduced-motionfreezes the ambient animation. The golden that covered this was replaced with a self-relative freeze check — a fixed image can't tell "frozen" from "rendering the same frame twice".inkandbackgroundare indistinguishable, which is the one way to configure an invisible field.set()is atomic across colour parsing — a bad colour in a patch no longer leaves half the patch applied.test/look.browser.mjs), three baselines plus nine property assertions.The golden rule is untouched:
golden-rule.test.tsstill pins cursor math topointer.glsl, and it passes.React
Forwards the new props and mirrors them in the test mock's
DEFAULT_OPTIONS.Playground
One palette control for
background/ink/accent— three chips that select, one picker that edits whichever is selected, with tab semantics rather than radio.Preset looks and the tone sliders, with a roving toolbar for the presets.
On-field tone (the last three commits). The effect page draws its title directly on the field, held legible by a wash. That wash was hardcoded near-black — invisible by construction back when the field always was, and a dark rectangle in the corner of the artwork once
backgroundshipped. NowfieldTone()derives the wash from the field's own colour, so it reads as the field thickening rather than a panel laid over it, and a light/dark verdict flips three--on-field*tokens that every piece of type on the field consumes. Every constant is measured against the actual tokens: the light ramp needed its own third step because--ink-3measures 4.4545:1 against Paper and misses AA at micro size.A second, independent fault fixed in the same rule: the wash's 148deg gradient was clipped by its own box on the right and bottom edges, terminating at alpha 0.1–0.4. That was a hard line at every preset, only invisible on near-black. Pulling the last colour stop in can't fix it — on that box the bottom edge sits at 47.2% of the gradient's axis, so any stop low enough would end the wash above the description. The edges are now zeroed by two
mask-imagelayers combined withmask-composite: intersect, which makes "alpha reaches zero inside the box on every edge" structural rather than a property of particular stop values.Palette chip labels lifted to their x-height so they centre optically in the chip.
Docs
The colour, tone and motion options documented across the published READMEs and the React docs page.
Verification
pnpm build3/3,pnpm typecheck5/5,pnpm test5/5 — core 71, react 15, playground 31. Core's browser diagnostics 8/8 and the look harness clean (3 baselines, 9 property assertions).Not in this PR
No version bump and no tag.
packages/coreandpackages/reactstay at 0.1.3 here; releasing is a separate step once this lands.