feat(udg): anchor + extent — a pull can occupy part of the element #154

Merged
peterswimm merged 1 commit from feat/udg-press-placement into main 2026-09-16 21:00:30 +00:00
Owner

Now that #153 has merged, this targets main directly.

Every pull covered its whole element, so plate.ui.line-horizontal — a hand-drawn rule at 33.6:1, obviously drawn for a boundary — shipped as an orphan in #148: registered, emitting a handle, referenced by no press. I dropped it from the demo because the model could not hold it. That is the gap stating itself, and docs/UDG_PRESS_AUTHORING.md already named the two missing concepts.

anchor and extent

Scope

anchor: fill (default, = today) | top | bottom | left | right
extent: a number (px, like registration) or "auto"

extent: auto derives the band from the plate's own viewBox ratio, so the drawing's proportions hold at any width: 10.69px at 360 wide, 19.02 at 640, 30.42 at 1024 — measured, not predicted. The thickness nobody has to guess.

Verified before building, in two engines

The whole design rests on CSS transferring an aspect ratio to an absolutely-positioned box with one auto inset (CSS Position 3 §4.1). Firefox is the engine to distrust here — it is also the one with no mask-border — so I probed Chrome and Firefox before writing a line of generator code. Both report the same heights to two decimals. A percentage-padding contingency was probed alongside and also works, if an engine ever regresses.

Three corrections to my own first design, caught before shipping

  • The aspect is baked into skin.css, not referenced as --plate-<id>-aspect. artifact.py inlines plate handles with a regex matching only url() declarations, so a non-url plate var would be undefined there — aspect-ratio would fall back to auto and every band would be 0px tall in the design board alone. Silently plausible, and in only one of three consumers.
  • inset/w/h/aspect are emitted for every pull, fill included, and every recipe reads them as var(--pN-inset, 0). An undefined custom property is invalid-at-computed-value-time, which takes the property's initial value — auto for inset, collapsing the box. An inset: 0 in the shared rule does not save it: IACVT applies after the cascade has already picked a winner. That inset: 0 is now gone from all three recipes; it read as a fallback and was not one.
  • extent is a bare number, not a CSS length string. "20px" invites "20 px", which computes invalid and silently collapses the pull.

Seven guards, each for a case that currently renders something plausible

unknown plate (floods with raw pigment today, reachable by one typo) · unknown anchor · extent on a fill pull · extent as a string · extent: 0 · extent: auto on a side anchor with a landscape plate (the band would be 33.6× the panel's height) · fit: slice on an anchored pull (mask-box-image slices both axes, so a thin band is consumed by its two end slices and the middle never draws).

No fourth field

registration already places the gap. ruled-panel-fixed lifts its rule 12px off the edge with registration: [0, 12] alone — that is the proof, and it ships.

Testing

just check green, 4 presses · 255 tests · self-check reports 20 boxes, zero failures.

  • The design was probe-gated: Chrome and Firefox agreed before any generator code was written.
  • Rendered at 360 / 640 / 1024 and measured — the numbers above are from getComputedStyle, not arithmetic.
  • The doc's §4 recipe was rendered verbatim against an anchored press (640 × 19.02px, mask ok), so it is still copy-paste.
  • Fill pulls emit inset: 0 / auto / auto / auto — the two shipped presses are unchanged.

The self-check earned its keep twice more, once flagging the single-pull presses' unused ::after as a dropped mask (it now reads --pulls), and before that the substrate-only panel. Both times the check was wrong, not the code — but a check that cries wolf gets ignored, so both are now distinctions it knows.

Impact Statement

Additive. anchor defaults to fill, which is exactly today's behaviour; the two existing presses are unchanged apart from gaining five vars per pull. Two new presses ship, so dist/spelwork and the review artifact churn. Versions bumped in meta.yml and sync-manifest.yml.

One thing this deliberately does not settle. extent: auto preserves the drawing's proportions and lets the band's weight track the container; an explicit extent preserves the weight and lets the proportions go. In a 1600px hero, auto is a 47px slab. Both are defensible — it is the anisotropy problem fit: slice solves for frames, wearing a different coat — so the review artifact shows them side by side and the call is left to design rather than baked into a default nobody chose.

Untested engine: Safari. WebKit shipped abspos aspect-ratio transfer in the 271xxx changesets (→ Safari 15) and the spec is explicit, but I can only drive Chrome and Firefox headlessly here. Worth one human look before anyone relies on extent: auto on an Apple surface.

Reversibility: high. Remove anchor from a pull and it is a fill pull again.

Now that #153 has merged, this targets `main` directly. Every pull covered its whole element, so `plate.ui.line-horizontal` — a hand-drawn rule at **33.6:1**, obviously drawn for a boundary — shipped as an **orphan** in #148: registered, emitting a handle, referenced by no press. I dropped it from the demo because the model could not hold it. That is the gap stating itself, and `docs/UDG_PRESS_AUTHORING.md` already named the two missing concepts. ![anchor and extent](https://git.toilville.dev/Toilville/toilville-design/raw/commit/f001759/scratchpad/press-review/05-anchor.png) ## Scope ``` anchor: fill (default, = today) | top | bottom | left | right extent: a number (px, like registration) or "auto" ``` **`extent: auto` derives the band from the plate's own viewBox ratio**, so the drawing's proportions hold at any width: **10.69px at 360 wide, 19.02 at 640, 30.42 at 1024** — measured, not predicted. The thickness nobody has to guess. ### Verified before building, in two engines The whole design rests on CSS transferring an aspect ratio to an absolutely-positioned box with one `auto` inset ([CSS Position 3 §4.1](https://drafts.csswg.org/css-position-3/#abspos-auto-size)). **Firefox is the engine to distrust here** — it is also the one with no `mask-border` — so I probed Chrome *and* Firefox before writing a line of generator code. Both report the same heights to two decimals. A percentage-padding contingency was probed alongside and also works, if an engine ever regresses. ### Three corrections to my own first design, caught before shipping - **The aspect is baked into `skin.css`, not referenced as `--plate-<id>-aspect`.** `artifact.py` inlines plate handles with a regex matching only `url()` declarations, so a non-url plate var would be undefined *there* — `aspect-ratio` would fall back to `auto` and every band would be **0px tall in the design board alone**. Silently plausible, and in only one of three consumers. - **`inset`/`w`/`h`/`aspect` are emitted for *every* pull, fill included**, and every recipe reads them as `var(--pN-inset, 0)`. An undefined custom property is invalid-at-computed-value-time, which takes the property's **initial** value — `auto` for `inset`, collapsing the box. An `inset: 0` in the shared rule does **not** save it: IACVT applies after the cascade has already picked a winner. That `inset: 0` is now gone from all three recipes; it read as a fallback and was not one. - **`extent` is a bare number**, not a CSS length string. `"20px"` invites `"20 px"`, which computes invalid and silently collapses the pull. ### Seven guards, each for a case that currently renders something plausible unknown plate (floods with raw pigment **today**, reachable by one typo) · unknown anchor · extent on a fill pull · extent as a string · `extent: 0` · `extent: auto` on a side anchor with a landscape plate (the band would be 33.6× the panel's height) · `fit: slice` on an anchored pull (`mask-box-image` slices **both** axes, so a thin band is consumed by its two end slices and the middle never draws). ### No fourth field `registration` already places the gap. `ruled-panel-fixed` lifts its rule 12px off the edge with `registration: [0, 12]` alone — that is the proof, and it ships. ## Testing `just check` green, 4 presses · **255 tests** · self-check reports **20 boxes, zero failures**. - The design was **probe-gated**: Chrome and Firefox agreed before any generator code was written. - Rendered at 360 / 640 / 1024 and measured — the numbers above are from `getComputedStyle`, not arithmetic. - The doc's §4 recipe was rendered **verbatim against an anchored press** (640 × 19.02px, mask ok), so it is still copy-paste. - Fill pulls emit `inset: 0 / auto / auto / auto` — the two shipped presses are unchanged. **The self-check earned its keep twice more**, once flagging the single-pull presses' unused `::after` as a dropped mask (it now reads `--pulls`), and before that the substrate-only panel. Both times the *check* was wrong, not the code — but a check that cries wolf gets ignored, so both are now distinctions it knows. ## Impact Statement **Additive.** `anchor` defaults to `fill`, which is exactly today's behaviour; the two existing presses are unchanged apart from gaining five vars per pull. Two new presses ship, so `dist/spelwork` and the review artifact churn. Versions bumped in `meta.yml` and `sync-manifest.yml`. **One thing this deliberately does not settle.** `extent: auto` preserves the *drawing's proportions* and lets the band's weight track the container; an explicit `extent` preserves the *weight* and lets the proportions go. In a 1600px hero, `auto` is a 47px slab. Both are defensible — it is the anisotropy problem `fit: slice` solves for frames, wearing a different coat — so the review artifact shows them **side by side** and the call is left to design rather than baked into a default nobody chose. **Untested engine:** Safari. WebKit shipped abspos aspect-ratio transfer in the 271xxx changesets (→ Safari 15) and the spec is explicit, but I can only drive Chrome and Firefox headlessly here. Worth one human look before anyone relies on `extent: auto` on an Apple surface. **Reversibility:** high. Remove `anchor` from a pull and it is a fill pull again.
feat(udg): anchor + extent — a pull can occupy part of the element
All checks were successful
PR policy check / policy-gate (pull_request) Successful in 0s
Security Scans / Trivy filesystem scan (pull_request) Successful in 32s
Security Scans / Gitleaks secret scan (pull_request) Successful in 26s
Test Generator / lint-workflows (pull_request) Successful in 18s
Test Generator / lint-prose (pull_request) Successful in 21s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 18s
Block Validation / validate (pull_request) Successful in 1m0s
Copy coverage / Design copy reaching rendered HTML (pull_request) Successful in 28s
Test Generator / test (pull_request) Successful in 3m16s
f001759533
Every pull covered its whole element, so plate.ui.line-horizontal — a hand-drawn
rule at 33.6:1, obviously drawn for a boundary — shipped as an ORPHAN in #148:
registered, emitting a handle, referenced by no press. I dropped it from the demo
because the model could not hold it. That is the gap stating itself, and
docs/UDG_PRESS_AUTHORING.md already named the two missing concepts.

  anchor: fill (default, = today) | top | bottom | left | right
  extent: a number (px, like registration) or "auto"

extent: auto derives the band from the plate's own viewBox ratio, so the drawing's
proportions hold at any width: 10.69px at 360 wide, 19.02 at 640, 30.42 at 1024 —
measured, not predicted. The thickness nobody has to guess.

VERIFIED BEFORE BUILDING, in Chrome AND Firefox. The whole design rests on CSS
transferring an aspect ratio to an abspos box with one auto inset (CSS Position 3
§4.1), and Firefox is the engine to distrust here — it is also the one with no
mask-border. Both report the same heights to two decimals. A padding-percentage
contingency was probed alongside and also works, if an engine ever regresses.

Three corrections to my own first design, each caught before it shipped:

  - The aspect is BAKED into skin.css, not referenced as --plate-<id>-aspect.
    artifact.py inlines plate handles with a regex matching only url()
    declarations, so a non-url plate var would be undefined THERE — aspect-ratio
    would fall back to auto and every band would be 0px tall in the design board
    alone. Silently plausible, and only in one of three consumers.
  - inset/w/h/aspect are emitted for EVERY pull, fill included, and every recipe
    reads them as var(--pN-inset, 0). An undefined custom property is
    invalid-at-computed-value-time, which takes the property's INITIAL value —
    `auto` for inset, collapsing the box. An `inset:0` in the shared rule does
    NOT save it: IACVT applies after the cascade has already picked a winner. So
    that inset:0 is gone from all three recipes; it read as a fallback and was not.
  - extent is a bare number, not a CSS length string. "20px" invites "20 px",
    which computes invalid and silently collapses the pull.

Seven guards, each for a case that currently renders something plausible: unknown
plate (floods with raw pigment today, reachable by one typo), unknown anchor,
extent on a fill pull, extent as a string, extent: 0, extent: auto on a side
anchor with a landscape plate (the band would be 33.6x the panel's height), and
fit: slice on an anchored pull (mask-box-image slices BOTH axes, so a thin band is
consumed by its two end slices and the middle never draws).

No fourth field: registration already places the gap. ruled-panel-fixed lifts its
rule 12px off the edge with registration: [0, 12] alone, which is the proof.

Two presses ship, deliberately one of each: extent: auto preserves the drawing's
proportions and lets the band's weight track the container; an explicit extent
preserves the weight and lets the proportions go. Both are defensible — it is the
anisotropy problem fit: slice solves for frames, wearing a different coat — so the
review artifact shows them side by side and leaves the call to design.

The review self-check earned its keep twice more: once flagging the single-pull
presses' unused ::after as a dropped mask (it now reads --pulls), and before that
the substrate-only panel. Both were the check being wrong, not the code — but a
check that cries wolf gets ignored, so both are now distinctions it knows.

Verified: just check green, 4 presses; 255 tests; self-check reports 20 boxes and
zero failures; the doc's §4 recipe rendered VERBATIM against an anchored press.
peterswimm deleted branch feat/udg-press-placement 2026-09-16 21:00:30 +00:00
Member

Decision: extent is a fixed number, not auto. One thickness for the whole system.

auto keeps the drawing's proportions and lets the weight go. At 1024 that is a 30px band and at 1600 a 47px slab — past a certain width it has stopped being a rule and become a panel. A boundary should read as a boundary at every size, so the weight is the property worth holding.

The cost is real and accepted: stretched horizontally, the drawn line flattens as the element widens. That is the right thing to lose. The hand-drawn character is a texture on a rule; it is not the rule.

One value, not a choice per use

The point of fixing it is that nobody picks a number at the call site. If each press names its own thickness, drawn rules end up at four different weights across the site and the system has bought inconsistency in exchange for control.

Use the value that already exists: border.width-emphasis, 3px. It is the same role — the app's borderThick carries the same 3.0 with the comment "thick accent bar — quote stripe, visual dividers". A drawn rule and a plain one should sit at the same weight; that they are drawn is a texture difference, not a weight difference.

The plate itself needs a second pass at this weight

Separate from the model: the drawn detail reads aggressive right now. plate.ui.line-horizontal was drawn at 33.6:1 for a band that scales, and a locked 3px band asks it to carry the same vertical variation in a fraction of the height. The wobble that looks like a hand at 30px looks like noise at 3.

That is a change to the drawing, not to anchor/extent. The model is right; the plate was drawn for the other answer.

What this settles

extent: auto stays in the model and stays guarded, but nothing ships on it. The untested-Safari note therefore stops being on the critical path — no shipping surface depends on abspos aspect-ratio transfer.

The rest of the PR reads well. Probing two engines before writing generator code, and catching three of your own design errors on the grounds that each would have rendered something plausible, is the part worth keeping as a habit.

**Decision: `extent` is a fixed number, not `auto`. One thickness for the whole system.** `auto` keeps the drawing's proportions and lets the weight go. At 1024 that is a 30px band and at 1600 a 47px slab — past a certain width it has stopped being a rule and become a panel. A boundary should read as a boundary at every size, so the weight is the property worth holding. The cost is real and accepted: stretched horizontally, the drawn line flattens as the element widens. That is the right thing to lose. The hand-drawn character is a texture on a rule; it is not the rule. ## One value, not a choice per use The point of fixing it is that nobody picks a number at the call site. If each press names its own thickness, drawn rules end up at four different weights across the site and the system has bought inconsistency in exchange for control. Use the value that already exists: **`border.width-emphasis`, 3px.** It is the same role — the app's `borderThick` carries the same 3.0 with the comment "thick accent bar — quote stripe, visual dividers". A drawn rule and a plain one should sit at the same weight; that they are drawn is a texture difference, not a weight difference. ## The plate itself needs a second pass at this weight Separate from the model: the drawn detail reads aggressive right now. `plate.ui.line-horizontal` was drawn at 33.6:1 for a band that scales, and a locked 3px band asks it to carry the same vertical variation in a fraction of the height. The wobble that looks like a hand at 30px looks like noise at 3. That is a change to the drawing, not to `anchor`/`extent`. The model is right; the plate was drawn for the other answer. ## What this settles `extent: auto` stays in the model and stays guarded, but nothing ships on it. The untested-Safari note therefore stops being on the critical path — no shipping surface depends on abspos aspect-ratio transfer. The rest of the PR reads well. Probing two engines before writing generator code, and catching three of your own design errors on the grounds that each would have rendered something plausible, is the part worth keeping as a habit.
Author
Owner

@peterswimm fillies up

@peterswimm fillies up
Sign in to join this conversation.
No description provided.