feat(udg): a press tier — assemble UX the way screenprinting does #148
No reviewers
Labels
No labels
build-dist
priority
P1
priority
P2
priority
P3
priority
P4
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Toilville/toilville-design!148
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/udg-press-tier"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Assemble UX the way screenprinting does. PR #147 shipped three monochrome plates as EPS — one path, one fill, no colour operators, true print separations drawn to be inked by whoever prints them. The UDG had nowhere to put them: all nine tiers describe flat properties, so nothing could say "this panel is a hatched frame in terracotta, misregistered 2px, over cocoa paper, with the copy on top."
Adds a tenth tier. A press is a substrate plus an ordered list of pulls; each pull is a plate inked in a pigment at a registration offset, with
opacity,blend,fit. The off-register fringe is the aesthetic, not a defect.What it looks like
Rendered from the real
dist/spelwork/skin.css+dist/assets/plates.css— no mockups.Interactive one-pager:
scratchpad/press-review.html(self-contained, opens from acheckout with no build). Rebuild it all with
python3 scratchpad/build_press_review.py.The two presses. Two pulls fit
::before/::afterwith no extra DOM; a third needsone wrapper — which makes ~3 the honest budget, and a three-colour run the classic job.
Re-inking. The same press token with only its two pigment vars re-pointed — mechanically
exactly what a
[data-theme]block does. Pigments are DTCG aliases, so a press carries noper-theme variant of its own. (spelwork ships light only, so this shows the mechanism rather
than pretending a theme exists.)
Degradation.
flattenedis identical toliveby design — same appearance, baked tostatic hex. Only
substrate onlyvisibly drops the press, and that is the contract foremail and CLI, stated rather than hoped for.
The known gap, measured. A frame plate stretched far from its native ratio distorts —
its thickness is filled geometry, so a non-uniform scale scales bar weight anisotropically.
Both panels are 860×190, magnified 3× on the top-left corner.
Scope
Tier —
Brand/UDG/products/spelwork/dtcg/press.json;pressadded toDTCG_TIERS; a DTCG{alias}resolver and a structured→CSS var-family emit inextract.py.Plates —
platecategory inassets.py; the three PR #147 EPS registered inassets.yml; opt-injust plates(ghostscript + poppler) converting to a committed, provenance-stamped derivative inBrand/source/derived/;dist/assets/plates.cssmask handles.Block binding —
press_eligibleonsectionandcolumn_set; apressfield validated inforgeblock_validate.pyagainst the authored source;docs/examples/pressed-panel.forgeblock.json.Native — structured
udgPressMapin Dart and Swift plus the JSON manifest. Nosync-manifest.ymlchange needed: the existingdart-colors/json-tokens-macosconsumers already carry every tier, so a press flows the moment a product declares one.Preview — a Press frame on the design board: each press composed, its separations sheet, and its degradation strip.
Docs —
docs/UDG_PRESS_AUTHORING.md, cross-linked fromBrand/UDG/README.md,UDG_LABELING_GRAMMAR.mdandCLAUDE.md.Three decisions the repo made, not me
wcag.audit_channels()requires every block to degrade to text · BBS · SMS · RCS · ABC, and the catalog is derived fromforgeblock.ymlrather than curated. Decoration has no plain-text reduction, so aprinted_panelblock would have to invent one to pass the gate. A field on blocks that already paint a band means unknowing renderers paint the substrate — degradation by construction, not by policy.$value, not a baked CSS string. A string would have ridden the existing verbatim emit with zero generator change.TOKEN_GAP_PROPOSALS.mdrules otherwise: a token is editable by the designer only if it is explicit. A composite string hides every pigment and offset from the FSE editor, the artifact, and Rituals.bg.surface/text.primary, and a baseline press hard-failed their build.Two words changed from the approved sketch
ink→pigment:--inkalready means text colour in the wireframe kit, 31 uses insideartifact.py's own stylesheet.stack→pulls:shell_kind: stackis taken insurface_profiles.yml.Testing
just check— green. Block count unchanged at 33; no block was added, so the channel-reduction gate is untouched.python -m pytest generator/tests/— 209 pass (25 new). Covers the var-family expansion, alias-to-var resolution, the native emit carrying token paths rather than baked hex, and the ForgeBlock binding. Negative cases are inline, not fixture files —_test_ignore: trueis honoured by both the validator and pytest discovery, so a marked-ignore bad fixture would be exercised by nothing.yamllint --strict .forgejo/workflows/— clean.dist/is reproducible: a secondjust buildfrom the committed state produces no diff.fitsilently letterboxing, and plate handles failing to resolve. The §4 CSS recipe in the authoring doc was rendered verbatim before publishing.Impact Statement
Blast radius is small and bounded.
dist/churn is confined to spelwork (the only product declaring a press) plus the shared asset index — every other product is byte-identical, because_press_framereturns""and the design board's renumbering skips empty frames. No existing token, block or surface changed meaning.Nothing downstream moves yet. Rituals declares no press, so
udg_tokens.{dart,swift,json}gain no press symbols and the sibling repo is untouched. Whether Rituals ships one — and with which art and pigments — is a brand decision, deliberately left to @ville rather than invented here.What ville is being asked to sign off: the vocabulary (
press·pulls·plate·pigment·registration·substrate), which becomes a public contract across three repos, and the two spelwork presses rendered in the Press frame ofdist/spelwork/artifact.html. Renaming after the native emit lands downstream would be a cross-repo break, so the naming decision wants to happen now.Risks accepted, stated plainly:
just platesneeds ghostscript + poppler. Only when a plate's EPS changes — verification is stdlib-only, so CI and a normaljust buildnever need either.plates.cssis 277 KB of self-contained data-URIs, in line with the existing 210 KB icon sprite. Relative paths were tried first and are unusable: a relativeurl()in a custom property resolves against the using document.-webkit-mask-box-image-*longhands (the shorthand silently yields an empty mask) plus a second intrinsically-sized plate export, and Firefox degrades cleanly to the stretch mask. Corner + edge plates remain the fully portable alternative. Which one is a design call, now backed by the comparison above rather than by my prose. Not implemented here — it is its own PR.Reversibility: high. The tier is additive and product-scoped; deleting
press.jsonreturns every product to byte-identical output.The tier shipped without a procedure. Adds docs/UDG_PRESS_AUTHORING.md — land the plate, author the press, bind it, consume it — and cross-links it from the three places someone would start: the model in Brand/UDG/README.md, the block binding in UDG_LABELING_GRAMMAR.md, and CLAUDE.md's key commands. Procedure only; it points at the authorities rather than restating their values. Carries the four rules that cost an afternoon during the build, because each one fails silently-plausible rather than loudly: - load plates.css at :root, or the mask drops and raw pigment floods the panel - never put a relative url() in a custom property - `fit` needs preserveAspectRatio="none" or stretch silently letterboxes - normalise poppler's percentage-rgb, or a literal colour bakes into a token-driven asset Also records the two open gaps honestly: a frame plate smears when stretched far from its native ratio (fix is corner + edge plates, not a 9-slice hack), and a pull has no anchor or extent, so a rule that should sit at one edge cannot yet be expressed. The §4 CSS recipe was rendered verbatim from the doc against the real skin.css + plates.css before publishing — it is copy-paste, not illustrative. Verified: just check green, 209 tests pass, and every `just` target the guide names exists.I wrote that the frame-stretch fix was corner + edge plates, "not an SVG 9-slice hack (mask-border is effectively Safari-only)". Testing it disproves that, and the claim was sitting in a PR under review, so it is corrected in both places it appeared. What is actually true, measured rather than read off a support table: - Being vector is not the lever. The plate already IS an SVG; the frame's thickness is FILLED geometry, so a non-uniform scale scales bar weight anisotropically. At 860x190 the vertical bars come out ~4x the weight of the horizontal, wobble stretched into coarse teeth. - 9-slice fixes it, on Blink as well as WebKit. Equal bar weight, wobble intact on every edge. - It needs the -webkit-mask-box-image-* LONGHANDS: the shorthand silently yields an empty mask, which floods the panel with pigment — the same failure mode as the :root handle bug. - It needs a second, intrinsically-sized plate export. Slice percentages resolve against intrinsic size, and the plates emitted for `fit` are viewBox-only with preserveAspectRatio="none", which `fit` requires. - Firefox ignores mask-border but degrades cleanly to the stretch mask when both are set. Verified in Firefox, not assumed. Also: `fit: cover` fully fixes TEXTURE plates (crop, not distort) but is unreliable for a frame — at some aspect ratios it crops the border band away. The review artifact gains a fourth section carrying the comparison at 3x magnification, so the choice between 9-slice and corner + edge plates is a design call made against evidence rather than against my prose. Corner + edge plates remain the fully portable answer and need no engine support. Not implementing `fit: slice` here — it is a second plate export plus a new enum value, and belongs in its own PR rather than growing this one.