feat(udg): type the press vars — @property carries the contract to the client #158

Merged
peterswimm merged 1 commit from feat/udg-typed-properties into main 2026-09-16 23:11:00 +00:00
Owner

The UDG is well-typed right up to the CSS boundary, and then stops. A custom property is untyped: an unset or wrong-typed one is invalid-at-computed-value-time and silently takes the property's initial value — auto for inset, which collapses the box and the press vanishes.

It doesn't load unhappily. It loads plausibly, which is worse — and it's where every bug in this tier has lived.

skin.css now registers all 116 press vars with @property before :root, so each carries a real type and a declared fallback.

Scope

Verified in Chrome and Firefox, identically, before any of this was written:

result
unregistered + unset 0×0 box — the bug as it ships today
registered + unset the declared initial-value
registered + wrong type the declared initial-value (not IACVT)
registered + var() indirection resolves — so the token defaults survive
syntax: "*" + unset the initial-value — no type check, but the fallback, which is the half that matters

Demonstrated end to end: a client feeding banana into dx, not-a-number into opacity and wobble into blend now renders identically to one that adheres — 12.47px band, mask intact, in both engines.

The generator already knew every type (dx is a <length>, opacity a <number>, blend a keyword set), so this is emitted, not authored. Zero new burden on anyone writing a press.

inherits: true, and that is not a detail

Press vars are declared at :root and consumed anywhere in the tree. With inherits: false a registered property is not visible below its declaring element, so every alias in every recipe read the initial-value instead — the anchored band filled its whole panel rather than sitting at the edge.

The typing feature shipped the exact class of silent failure it exists to prevent, for about ten minutes. just check was green throughout; it was caught by rendering it and reading the computed values. Its test was verified to fail when inherits: false is reintroduced.

Testing

just check green · 276 tests · both engines agree · skin.css 260 → 346 lines, all generated.

New tests pin: every consumed var is registered or explicitly untyped; every registration inherits; every registration declares a fallback; the geometry vars specifically; registrations land before :root; and the emitted skin registers every press var it declares.

Impact Statement

Additive and inert for an adhering client. Every value resolves exactly as before — the two shipped presses render byte-identically. What changes is the behaviour of a client that gets something wrong.

dist/ churn is skin.css for the one product with presses, plus the review artifact. Versions bumped.

What this does NOT fix, stated plainly: a dropped mask. An unset plate types to none, which is a valid value meaning "no mask" — so the element still paints raw pigment. @property makes that failure predictable, not loud. Making absence loud is the review artifact's self-check, not the type system, and that remains the open half.

Only the press tier is typed here. It is the one tier whose vars a consumer wires into layout, where a miss collapses a box rather than just losing a colour. Extending registration to colour/space/radius is a reasonable follow-up but a much larger diff for a much smaller failure mode.

Browser support: @property is Chrome 85+, Safari 16.4+, Firefox 128+. An engine without it ignores the at-rule and behaves exactly as today — so this degrades to the status quo rather than breaking.

The UDG is well-typed right up to the CSS boundary, and then stops. A custom property is untyped: an unset or wrong-typed one is invalid-at-computed-value-time and silently takes the **property's** initial value — `auto` for `inset`, which collapses the box and the press vanishes. It doesn't load unhappily. It loads *plausibly*, which is worse — and it's where every bug in this tier has lived. `skin.css` now registers all 116 press vars with `@property` before `:root`, so each carries a real type and a declared fallback. ## Scope **Verified in Chrome _and_ Firefox, identically, before any of this was written:** | | result | |---|---| | unregistered + unset | **`0×0` box** — the bug as it ships today | | registered + unset | the declared `initial-value` | | registered + **wrong type** | the declared `initial-value` (not IACVT) | | registered + `var()` indirection | resolves — so the token defaults survive | | `syntax: "*"` + unset | the `initial-value` — no type *check*, but the fallback, which is the half that matters | **Demonstrated end to end:** a client feeding `banana` into `dx`, `not-a-number` into `opacity` and `wobble` into `blend` now renders **identically** to one that adheres — 12.47px band, mask intact, in both engines. The generator already knew every type (`dx` is a `<length>`, `opacity` a `<number>`, `blend` a keyword set), so this is **emitted, not authored**. Zero new burden on anyone writing a press. ### `inherits: true`, and that is not a detail Press vars are declared at `:root` and consumed anywhere in the tree. With `inherits: false` a registered property is **not visible below its declaring element**, so every alias in every recipe read the `initial-value` instead — the anchored band filled its whole panel rather than sitting at the edge. **The typing feature shipped the exact class of silent failure it exists to prevent**, for about ten minutes. `just check` was green throughout; it was caught by rendering it and reading the computed values. Its test was verified to fail when `inherits: false` is reintroduced. ## Testing `just check` green · **276 tests** · both engines agree · `skin.css` 260 → 346 lines, all generated. New tests pin: every consumed var is registered or explicitly untyped; every registration inherits; every registration declares a fallback; the geometry vars specifically; registrations land before `:root`; and the emitted skin registers every press var it declares. ## Impact Statement **Additive and inert for an adhering client.** Every value resolves exactly as before — the two shipped presses render byte-identically. What changes is the behaviour of a client that gets something *wrong*. **`dist/` churn is `skin.css` for the one product with presses**, plus the review artifact. Versions bumped. **What this does NOT fix, stated plainly:** a dropped mask. An unset `plate` types to `none`, which is a *valid* value meaning "no mask" — so the element still paints raw pigment. `@property` makes that failure **predictable, not loud**. Making absence loud is the review artifact's self-check, not the type system, and that remains the open half. **Only the press tier is typed here.** It is the one tier whose vars a consumer wires into *layout*, where a miss collapses a box rather than just losing a colour. Extending registration to colour/space/radius is a reasonable follow-up but a much larger diff for a much smaller failure mode. **Browser support:** `@property` is Chrome 85+, Safari 16.4+, Firefox 128+. An engine without it ignores the at-rule and behaves exactly as today — so this degrades to the status quo rather than breaking.
feat(udg): type the press vars — @property carries the contract to the client
All checks were successful
PR policy check / policy-gate (pull_request) Successful in 0s
Security Scans / Trivy filesystem scan (pull_request) Successful in 38s
Block Validation / validate (pull_request) Successful in 1m0s
Security Scans / Gitleaks secret scan (pull_request) Successful in 26s
Test Generator / lint-workflows (pull_request) Successful in 17s
Test Generator / lint-prose (pull_request) Successful in 18s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 13s
Copy coverage / Design copy reaching rendered HTML (pull_request) Successful in 28s
Test Generator / test (pull_request) Successful in 3m23s
70b115b2c5
The UDG is well-typed right up to the CSS boundary, and then stops. A custom
property is untyped: an unset or wrong-typed one is invalid-at-computed-value-time
and silently takes the PROPERTY's initial value — `auto` for inset, which
collapses the box and the press vanishes. It does not load unhappily. It loads
plausibly, which is worse, and it is where every bug in this tier has lived.

skin.css now registers all 116 press vars with @property before :root, so each
carries a real type and a declared fallback. A client that adheres gets its
value; one that does not gets the fallback instead of a collapse.

Verified in Chrome AND Firefox, identically, BEFORE any of this was written:

  unregistered + unset            -> 0x0 box        (the bug as it ships today)
  registered   + unset            -> initial-value
  registered   + WRONG type       -> initial-value  (not IACVT)
  registered   + var() indirection -> resolves      (token defaults survive)
  syntax "*"   + unset            -> initial-value  (no type check, but the
                                                     fallback, which is the half
                                                     that matters)

Demonstrated end to end: a client feeding `banana` into dx, `not-a-number` into
opacity and `wobble` into blend now renders IDENTICALLY to one that adheres.

The generator already knew every type — dx is a <length>, opacity a <number>,
blend a keyword set — so this is emitted, not authored. Zero new burden.

`inherits: true`, and that is not a detail. Press vars are declared at :root and
consumed anywhere in the tree; with inherits:false a registered property is not
visible below its declaring element, so every alias in every recipe read the
initial-value instead and the anchored band filled its whole panel. The typing
feature shipped the exact class of silent failure it exists to prevent, for about
ten minutes, and was caught by rendering it rather than by the gate. Its test was
verified to fail when inherits:false is reintroduced.

What this does NOT fix, stated plainly: a dropped mask. An unset plate types to
`none`, which is a VALID value meaning "no mask" — so the element still paints
raw pigment. @property makes that failure predictable, not loud. Making absence
loud is the review artifact's self-check, not the type system.

Verified: just check green; 276 tests; both engines agree; skin.css 260 -> 346
lines, all generated.
peterswimm deleted branch feat/udg-typed-properties 2026-09-16 23:11:00 +00:00
Sign in to join this conversation.
No description provided.