Skip to contentWolf-Rayet

Decision records

ADR-0112 The reference is the reference

Superseded by ADR-01132026-09-19Phase 7

Amends ADR-0045 and ADR-0106. Supersedes ADR-0106's affordance gap.

#Context

The brief for this system has been, repeatedly and in plain words, to bring the components to the caliber of the reference and to copy the design and style of the components there. That had not been done. The maintainer said so three times before this record, and was right each time.

What had been done instead was infrastructure: a token pipeline, a budget engine, per-platform baselines, a decision register, a documentation site. All of it real, none of it what was asked for, and the gap was visible the moment anyone used the thing. Two findings say it plainly.

No component in the set had a pointer state. Not one of the 37 stylesheets defined :hover. None defined :active. The checkbox defined no :checked. Nine defined :focus-visible and five :disabled, and that was the whole of it. A button drew a cursor: pointer and a turned corner, and then did nothing at all when a pointer arrived. The docs site presented these as live components, which they were; they simply had nothing to show.

The checkbox was a circle. ADR-0106 rounded everything with a ground and gave the checkbox radius-control, which resolves to 16px. The checkbox's box is 12px. A 12px box with a 16px radius is a circle, so the component whose square shape is the one thing distinguishing it from a radio button had been drawn as a radio button, and shipped.

Neither is a small mistake, and neither was caught, because every check in the repository measured contrast, emission, ordinals and provenance. Nothing measured whether the components were any good.

#The rules that made the copy impossible

Two decisions of this system's own making stood directly in the way, and both had to go before the reference could be matched at all.

ADR-0106's affordance gap required radius-control to be *strictly* rounder than radius-surface. The reference draws a button, a field and a card on one 6px radius and says the affordance with fill, border and state instead. The gap made that combination fail the build.

ADR-0045 held that a turned corner is the thing that says a control accepts input. That is why the pointer states were never built: the system had already decided the corner did that job. It does not. A corner is visible before anyone reaches for it and says nothing when they do.

#Measured off the reference

Taken from the reference directly rather than remembered:

referencethis system, before
button height32px54px
text field height40px54px
radius, all three6px12px surface, 16px control
checkbox boxrx 1.5, squarerx 16 on a 12px box, a circle
border1px2px
button label14px / 50018px / 400
hover, pressedon every roleabsent from the system
transition0.15s on background and borderdeclared, with nothing to move to

#Decision

  • radius-surface and radius-control both select 6px, in both densities,

and the affordance gap is retired. What is checked now is that a control is never *squarer* than its ground, which still refuses the inversion the gap was really guarding against.

  • A checkbox is square. A new radius-mark token, selecting the 2px step,

is what a mark whose shape carries meaning binds. The reference draws rx 1.5; 2px is the nearest step this scale has.

  • surface-hovered and surface-pressed are semantic roles, one and two

steps along the neutral ramp away from the page. Away from the page, not darker: the same declaration then reads as the same event in a light theme and a dark one without either being written by hand. This is the reference's own model, which names a -hovered and a -pressed for every background role it ships.

  • Eight interactive components draw :hover and :active. The transition

was already declared on all of them and had never had a second value to move to.

  • The interactive target is resolved per theme class. The config has

declared a minimum per class since the beginning — interior and field — and the emission collapsed them into one number, so the largest won everywhere and every control indoors was sized for a gloved hand. Interior now takes the 40px step, which is the reference's own field height, and the field themes are raised back to 54px on their own selector at higher specificity. Neither value is written by hand and the verifier reads what the stylesheet actually resolves rather than the density block alone.

  • The button label takes text-caption. 16px against the reference's 14px,

which is the nearest step the ladder has.

#Consequences

Every fixture and both baselines move again: radius and target are in every scene. Re-recorded in the order that is honest, tokens then fixtures then baselines.

make-themed-fixtures.js matched the Substrate's data-wr-theme as a bare string, and the per-class target override puts that attribute in the inlined stylesheet too, so the count check fired on a file that was perfectly well formed. It matches the <body> tag by pattern now.

Every interactive component answers a pointer, and the set is nine. An earlier draft of this record said twenty-nine components still had none, which was wrong and worth correcting rather than quietly deleting: it counted all 37 stylesheets, and 28 of those are components nobody acts on. A tag does not take a hover state, and giving it one would be the component claiming an affordance it does not have. The nine that declare interactive are accordion, button, checkbox, input-field, link, navigation-item, number-input, radio-button and tabs, and all nine now draw one. link is the odd one: it has no ground to darken, so what answers the pointer is the underline it already draws, which thickens.

react:test asserts it, of the nine and not of the 37, reading interactive out of the component tier's config so the claim has one source. Confirmed to fail on a component whose :hover is removed before being left passing.

What this record does not do. Borders are still 2px against the reference's 1px, because border.fieldMinimum forbids a hairline and the per-class mechanism built here for targets has not been extended to borders. The button is 47px rather than 32px, since padding and border sit outside the 40px minimum. These are named because a record that listed only what was done would read as a finished job.

The filled rung. The reference says a control's rung with a fill; this set said it only with a heavier line, which is why every button here read as the same button at three weights. Two roles were added for it: accent-bold, the filled ground, bound to the same step of the accent ramp the three status bolds take of theirs; and text-on-bold, because the bold grounds had only ever carried marks, which are shapes and take no label, so nothing in the set had a contrast floor for a word on one.

That floor is checked in verify-output.js against the shipped bytes rather than declared as an apcaFloor, and the reason is the solver. text-on-bold is near-white, which in every theme is the top of the neutral ramp, and text-on-raised already measures against that step. Declaring a floor whose role sits there makes it a solve target, and the generator refuses a floor standing on a background it may still move — correctly, and it refused this one. Measured: |Lc| 68.70, 67.24, 76.35 and 46.86 across the four themes.

It is drawn in CSS off the level class the component already emits, not as a per-instance property. A per-instance property has to be a selection into the generated matrix and choosing between two fixed tokens is not that, which the React discipline suite said when the first attempt tried it.

The cost is real and the budget prices it: a filled control emits far more than an outlined one, and eight scenes moved by as much as 5.34e-2. No scene changed outcome and the two over a theme ceiling are still the two intentional violation fixtures.

#Alternatives

Keep the affordance gap and match everything else. Impossible: the gap is a constraint on exactly the two values the reference sets equal.

Lower the field target to match the reference's 32px. Rejected. Field is gloved hands in low light, which is a real constraint the reference does not have. Resolving per class costs one override block and keeps both.

Put the pointer states in each component by hand. Faster for eight components and wrong for thirty-seven: the roles are where a theme can answer for them, and a hand-written hover would be the raw value the react suite refuses anyway.