feat(surfaces): deck → WordPress + email + letter + CLI/ASCII from one ForgeBlock spec #33

Merged
peterswimm merged 3 commits from feat/deck-to-surface-pipeline into main 2026-07-13 16:01:33 +00:00
Owner

One ForgeBlock spec → four static surfaces

This wires the design system's ForgeBlock JSON through to WordPress FSE patterns, transactional email, letter email, and a CLI/ASCII floor — the same JSON the Rituals app (SwiftUI) and Flutter already render. Tokens bind per surface: WP presets (editable swatches), email inline-hex, ASCII none. All 30 forgeblock.yml blocks render on every surface, guarded by test_block_coverage.py (95 tests green; just check drift-gated).

Artifact frames: see the generated udg/products/<brand>/wp-preview/ (patterns, email, letter, cli) for all 5 brands.


👋 @ville — design decisions that need your sign-off

None of these are locked; I've made provisional calls so the pipeline runs end-to-end, clearly quarantined so nothing lands in the DTCG source until you confirm. Please check the boxes or redline:

1. Token gap fill-ins — udg/products/TOKEN_GAP_PROPOSALS.md

Diffing each landing against its DTCG surfaced colors the landings use that have no token. Highest-priority new hues:

  • fux text.secondary #A0A0A0, accent.success #2BBC8A (the SETTLED badge — the one blocker), text.on-accent #3A0E0E
  • spelwork text-secondary #5A5A5A, white #FFFFFF
  • pardner border #E4E4EA, primary-hover/primary-active, secondary-hover
  • peterswimm surface.tint #ECEAE8 (+ needs a secondary slug — buttons currently fall back uncolored)

Confirm values (or supply your own) → they move into each brand's DTCG source and the provisional sidecar is deleted.

2. Explicit vs. derived — the control policy

Recommendation in the doc: borders become explicit palette tokens (so they're editable swatches in the FSE Site Editor — derived rgba() is invisible to the designer), while hover/active shades stay derived from a global state.hover-shift constant.

  • Agree: borders explicit, states derived?

3. Border α-ladder

Four of five brands independently reinvent text @ ~16% / ~12% hairlines. Proposal: define one border.default/border.subtle ladder, resolved per brand as text @ opacity.border, emitted as explicit swatches.

  • Adopt the shared ladder?

4. New surface profiles — udg/config/surface_profiles.yml

Added deck.landing, email.transactional, email.letter, each with regions + allowed_blocks. Email excludes input (static surface).

  • Regions + allowed-block sets look right?

5. Cross-surface vocabulary drift (design call)

The Swift ForgeBlock enum has 33 cases (incl. video, appleContactCard, appleCalendarSlots); forgeblock.yml declares 30 (has media, not video). They should be one vocabulary.

  • Which is canonical — fold Swift's extras into forgeblock.yml, or drop them?

Not blocking, but flagged

  • The 8 brand *Email.dc.html / *Letter.dc.html are scaffolds with placeholder copy (fux is the written exemplar) — authoring task, not sign-off.
  • wp-preview/ artifacts are committed and drift-gated (just check); say the word if you'd rather they be gitignored.

How to review the output

just surfaces           # regenerate all brand surfaces
open udg/products/fux-capacity/wp-preview/email/FuxCapacityEmail.email.html
cat  udg/products/fux-capacity/wp-preview/cli/landing.txt   # the ASCII floor

Docs: docs/UDG_LABELING_GRAMMAR.md, docs/DESIGN_TO_WP.md.

## One ForgeBlock spec → four static surfaces This wires the design system's ForgeBlock JSON through to **WordPress FSE patterns, transactional email, letter email, and a CLI/ASCII floor** — the same JSON the Rituals app (SwiftUI) and Flutter already render. Tokens bind per surface: WP presets (editable swatches), email inline-hex, ASCII none. All 30 `forgeblock.yml` blocks render on every surface, guarded by `test_block_coverage.py` (95 tests green; `just check` drift-gated). Artifact frames: see the generated `udg/products/<brand>/wp-preview/` (patterns, email, letter, cli) for all 5 brands. --- ## 👋 @ville — design decisions that need your sign-off None of these are locked; I've made **provisional** calls so the pipeline runs end-to-end, clearly quarantined so nothing lands in the DTCG source until you confirm. Please check the boxes or redline: ### 1. Token gap fill-ins — `udg/products/TOKEN_GAP_PROPOSALS.md` Diffing each landing against its DTCG surfaced colors the landings use that have **no token**. Highest-priority new hues: - [ ] **fux** `text.secondary` `#A0A0A0`, `accent.success` `#2BBC8A` (the SETTLED badge — the one blocker), `text.on-accent` `#3A0E0E` - [ ] **spelwork** `text-secondary` `#5A5A5A`, `white` `#FFFFFF` - [ ] **pardner** `border` `#E4E4EA`, `primary-hover`/`primary-active`, `secondary-hover` - [ ] **peterswimm** `surface.tint` `#ECEAE8` (+ needs a `secondary` slug — buttons currently fall back uncolored) Confirm values (or supply your own) → they move into each brand's DTCG source and the provisional sidecar is deleted. ### 2. Explicit vs. derived — the control policy Recommendation in the doc: **borders become explicit palette tokens** (so they're editable swatches in the FSE Site Editor — derived `rgba()` is invisible to the designer), while **hover/active shades stay derived** from a global `state.hover-shift` constant. - [ ] Agree: borders explicit, states derived? ### 3. Border α-ladder Four of five brands independently reinvent `text @ ~16%` / `~12%` hairlines. Proposal: define **one** `border.default`/`border.subtle` ladder, resolved per brand as `text @ opacity.border`, emitted as explicit swatches. - [ ] Adopt the shared ladder? ### 4. New surface profiles — `udg/config/surface_profiles.yml` Added `deck.landing`, `email.transactional`, `email.letter`, each with regions + `allowed_blocks`. Email excludes `input` (static surface). - [ ] Regions + allowed-block sets look right? ### 5. Cross-surface vocabulary drift (design call) The Swift `ForgeBlock` enum has **33** cases (incl. `video`, `appleContactCard`, `appleCalendarSlots`); `forgeblock.yml` declares **30** (has `media`, not `video`). They should be one vocabulary. - [x] Which is canonical — fold Swift's extras into `forgeblock.yml`, or drop them? --- ## Not blocking, but flagged - The 8 brand `*Email.dc.html` / `*Letter.dc.html` are **scaffolds with placeholder copy** (fux is the written exemplar) — authoring task, not sign-off. - `wp-preview/` artifacts are **committed and drift-gated** (`just check`); say the word if you'd rather they be gitignored. ## How to review the output ```bash just surfaces # regenerate all brand surfaces open udg/products/fux-capacity/wp-preview/email/FuxCapacityEmail.email.html cat udg/products/fux-capacity/wp-preview/cli/landing.txt # the ASCII floor ``` Docs: `docs/UDG_LABELING_GRAMMAR.md`, `docs/DESIGN_TO_WP.md`.
feat(surfaces): deck → WordPress + email + letter + CLI/ASCII from one ForgeBlock spec
Some checks failed
Security Scans / Gitleaks secret scan (pull_request) Failing after 18s
Security Scans / Trivy filesystem scan (pull_request) Successful in 28s
Test Generator / lint-workflows (pull_request) Successful in 13s
Test Generator / lint-prose (pull_request) Failing after 11s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 11s
Test Generator / test (pull_request) Successful in 3m8s
eeaa00dd2b
Render one ForgeBlock JSON to four static surfaces, binding each brand's
theme.json palette its own way (WP presets, inline hex, none for ASCII):

- dc_html_to_forgeblock.py — label-aware .dc.html → ForgeBlock JSON
  (data-block/data-token/data-bg grammar overrides the heuristic)
- forgeblock_pattern.py — full 30-block coverage (+ section background,
  status color override, generic programmatic fallback)
- forgeblock_email.py — transactional + letter email (tokens inlined as hex)
- forgeblock_ascii.py — 80-column 7-bit ASCII floor (cli.80col profile)
- build_surfaces.py — orchestrator; wired into `just build`/`just check`
- surface_profiles.yml — +deck.landing, email.transactional, email.letter
- test_block_coverage.py — 30 blocks × {WP, email, letter, ASCII}

Also: email/letter templates for all 5 brands, fux+pardner wp-style.json
palette maps, generated wp-preview artifacts, and docs
(UDG_LABELING_GRAMMAR, DESIGN_TO_WP, TOKEN_GAP_PROPOSALS).
Author
Owner

@ville — decisions to sign off on, enumerated. Reply per number (👍 to accept my recommendation, or redline). Nothing here is in the DTCG source yet; all provisional bits are quarantined until you confirm.


D1 — fux accent.success = #2BBC8A
The SETTLED position badge. The one hard blocker: fux has no success token, so the badge can't normalize until this lands.
Recommend: accept #2BBC8A.

D2 — fux text.secondary = #A0A0A0
Most-used color on the fux landing (18×) with no token today.
Recommend: accept.

D3 — fux text.on-accent = #3A0E0E
Ink on the red thesis band (text on #E63535).
Recommend: accept.

D4 — spelwork text-secondary = #5A5A5A + white = #FFFFFF
muted is green (#587C6D), not the body grey; 16 unmapped uses. white is the raised card surface vs background #FEFEFE.
Recommend: accept both.

D5 — pardner: border #E4E4EA, primary-hover #9C90EE, primary-active #5C4ECB, secondary-hover #FF9B85
Recommend: accept border; treat the hover/active as candidates for D8's derived approach instead of explicit hex — your call.

D6 — peterswimm needs a secondary slug
Secondary buttons currently render uncolored (no slug). Also proposed surface.tint #ECEAE8.
Recommend: pick a secondary hue; accept surface.tint.

D7 — Border α-ladder, shared + explicit
Four of five brands reinvent text @ ~16% / ~12% hairlines. Define one border.default / border.subtle ladder, resolved as text @ opacity.border, emitted as explicit palette swatches (so they're editable in the FSE Site Editor — a raw rgba() is invisible to the designer).
Recommend: adopt.

D8 — Explicit vs. derived policy
Borders + brand hues = explicit tokens (editor-addressable). Hover/active states = derived from a global state.hover-shift constant (one knob, consistent across brands).
Recommend: adopt.

D9 — New surface profiles (udg/config/surface_profiles.yml)
Added deck.landing, email.transactional, email.letter with regions + allowed_blocks. Email excludes input (static surface).
Decision: do the regions + allowed-block sets match how you'd author these surfaces?

D10 — Cross-surface vocabulary drift
Swift ForgeBlock = 33 cases (incl. video, appleContactCard, appleCalendarSlots); forgeblock.yml = 30 (has media, not video). Should be one vocabulary.
Decision: which is canonical — fold Swift's extras into the manifest, or drop them?


Non-blocking (FYI, no sign-off needed): the 8 brand *Email/*Letter.dc.html are scaffolds with placeholder copy (fux is written out); wp-preview/ artifacts are committed + drift-gated.

@ville — decisions to sign off on, enumerated. Reply per number (👍 to accept my recommendation, or redline). Nothing here is in the DTCG source yet; all provisional bits are quarantined until you confirm. --- **D1 — fux `accent.success` = `#2BBC8A`** The SETTLED position badge. The one hard blocker: fux has no success token, so the badge can't normalize until this lands. _Recommend:_ accept `#2BBC8A`. **D2 — fux `text.secondary` = `#A0A0A0`** Most-used color on the fux landing (18×) with no token today. _Recommend:_ accept. **D3 — fux `text.on-accent` = `#3A0E0E`** Ink on the red thesis band (text on `#E63535`). _Recommend:_ accept. **D4 — spelwork `text-secondary` = `#5A5A5A` + `white` = `#FFFFFF`** `muted` is green (`#587C6D`), not the body grey; 16 unmapped uses. `white` is the raised card surface vs `background #FEFEFE`. _Recommend:_ accept both. **D5 — pardner: `border` `#E4E4EA`, `primary-hover` `#9C90EE`, `primary-active` `#5C4ECB`, `secondary-hover` `#FF9B85`** _Recommend:_ accept `border`; treat the hover/active as candidates for D8's derived approach instead of explicit hex — your call. **D6 — peterswimm needs a `secondary` slug** Secondary buttons currently render uncolored (no slug). Also proposed `surface.tint` `#ECEAE8`. _Recommend:_ pick a `secondary` hue; accept `surface.tint`. **D7 — Border α-ladder, shared + explicit** Four of five brands reinvent `text @ ~16%` / `~12%` hairlines. Define one `border.default` / `border.subtle` ladder, resolved as `text @ opacity.border`, **emitted as explicit palette swatches** (so they're editable in the FSE Site Editor — a raw `rgba()` is invisible to the designer). _Recommend:_ adopt. **D8 — Explicit vs. derived policy** Borders + brand hues = **explicit** tokens (editor-addressable). Hover/active states = **derived** from a global `state.hover-shift` constant (one knob, consistent across brands). _Recommend:_ adopt. **D9 — New surface profiles** (`udg/config/surface_profiles.yml`) Added `deck.landing`, `email.transactional`, `email.letter` with regions + `allowed_blocks`. Email excludes `input` (static surface). _Decision:_ do the regions + allowed-block sets match how you'd author these surfaces? **D10 — Cross-surface vocabulary drift** Swift `ForgeBlock` = **33** cases (incl. `video`, `appleContactCard`, `appleCalendarSlots`); `forgeblock.yml` = **30** (has `media`, not `video`). Should be one vocabulary. _Decision:_ which is canonical — fold Swift's extras into the manifest, or drop them? --- Non-blocking (FYI, no sign-off needed): the 8 brand `*Email/*Letter.dc.html` are scaffolds with placeholder copy (fux is written out); `wp-preview/` artifacts are committed + drift-gated.
Author
Owner

@ville — decisions to sign off on, enumerated. Reply per number (👍 to accept my recommendation, or redline). Nothing here is in the DTCG source yet; all provisional bits are quarantined until you confirm.


D1 — fux accent.success = #2BBC8A
The SETTLED position badge. The one hard blocker: fux has no success token, so the badge can't normalize until this lands.
Recommend: accept #2BBC8A.

D2 — fux text.secondary = #A0A0A0
Most-used color on the fux landing (18×) with no token today.
Recommend: accept.

D3 — fux text.on-accent = #3A0E0E
Ink on the red thesis band (text on #E63535).
Recommend: accept.

D4 — spelwork text-secondary = #5A5A5A + white = #FFFFFF
muted is green (#587C6D), not the body grey; 16 unmapped uses. white is the raised card surface vs background #FEFEFE.
Recommend: accept both.

D5 — pardner: border #E4E4EA, primary-hover #9C90EE, primary-active #5C4ECB, secondary-hover #FF9B85
Recommend: accept border; treat the hover/active as candidates for D8's derived approach instead of explicit hex — your call.

D6 — peterswimm needs a secondary slug
Secondary buttons currently render uncolored (no slug). Also proposed surface.tint #ECEAE8.
Recommend: pick a secondary hue; accept surface.tint.

D7 — Border α-ladder, shared + explicit
Four of five brands reinvent text @ ~16% / ~12% hairlines. Define one border.default / border.subtle ladder, resolved as text @ opacity.border, emitted as explicit palette swatches (so they're editable in the FSE Site Editor — a raw rgba() is invisible to the designer).
Recommend: adopt.

D8 — Explicit vs. derived policy
Borders + brand hues = explicit tokens (editor-addressable). Hover/active states = derived from a global state.hover-shift constant (one knob, consistent across brands).
Recommend: adopt.

D9 — New surface profiles (udg/config/surface_profiles.yml)
Added deck.landing, email.transactional, email.letter with regions + allowed_blocks. Email excludes input (static surface).
Decision: do the regions + allowed-block sets match how you'd author these surfaces?

D10 — Cross-surface vocabulary drift
Swift ForgeBlock = 33 cases (incl. video, appleContactCard, appleCalendarSlots); forgeblock.yml = 30 (has media, not video). Should be one vocabulary.
Decision: which is canonical — fold Swift's extras into the manifest, or drop them?


Non-blocking (FYI, no sign-off needed): the 8 brand *Email/*Letter.dc.html are scaffolds with placeholder copy (fux is written out); wp-preview/ artifacts are committed + drift-gated.

@ville — decisions to sign off on, enumerated. Reply per number (👍 to accept my recommendation, or redline). Nothing here is in the DTCG source yet; all provisional bits are quarantined until you confirm. --- **D1 — fux `accent.success` = `#2BBC8A`** The SETTLED position badge. The one hard blocker: fux has no success token, so the badge can't normalize until this lands. _Recommend:_ accept `#2BBC8A`. **D2 — fux `text.secondary` = `#A0A0A0`** Most-used color on the fux landing (18×) with no token today. _Recommend:_ accept. **D3 — fux `text.on-accent` = `#3A0E0E`** Ink on the red thesis band (text on `#E63535`). _Recommend:_ accept. **D4 — spelwork `text-secondary` = `#5A5A5A` + `white` = `#FFFFFF`** `muted` is green (`#587C6D`), not the body grey; 16 unmapped uses. `white` is the raised card surface vs `background #FEFEFE`. _Recommend:_ accept both. **D5 — pardner: `border` `#E4E4EA`, `primary-hover` `#9C90EE`, `primary-active` `#5C4ECB`, `secondary-hover` `#FF9B85`** _Recommend:_ accept `border`; treat the hover/active as candidates for D8's derived approach instead of explicit hex — your call. **D6 — peterswimm needs a `secondary` slug** Secondary buttons currently render uncolored (no slug). Also proposed `surface.tint` `#ECEAE8`. _Recommend:_ pick a `secondary` hue; accept `surface.tint`. **D7 — Border α-ladder, shared + explicit** Four of five brands reinvent `text @ ~16%` / `~12%` hairlines. Define one `border.default` / `border.subtle` ladder, resolved as `text @ opacity.border`, **emitted as explicit palette swatches** (so they're editable in the FSE Site Editor — a raw `rgba()` is invisible to the designer). _Recommend:_ adopt. **D8 — Explicit vs. derived policy** Borders + brand hues = **explicit** tokens (editor-addressable). Hover/active states = **derived** from a global `state.hover-shift` constant (one knob, consistent across brands). _Recommend:_ adopt. **D9 — New surface profiles** (`udg/config/surface_profiles.yml`) Added `deck.landing`, `email.transactional`, `email.letter` with regions + `allowed_blocks`. Email excludes `input` (static surface). _Decision:_ do the regions + allowed-block sets match how you'd author these surfaces? **D10 — Cross-surface vocabulary drift** Swift `ForgeBlock` = **33** cases (incl. `video`, `appleContactCard`, `appleCalendarSlots`); `forgeblock.yml` = **30** (has `media`, not `video`). Should be one vocabulary. _Decision:_ which is canonical — fold Swift's extras into the manifest, or drop them? --- Non-blocking (FYI, no sign-off needed): the 8 brand `*Email/*Letter.dc.html` are scaffolds with placeholder copy (fux is written out); `wp-preview/` artifacts are committed + drift-gated.
Member

D1 👍 — accept accent.success = #2BBC8A for fux-capacity. Unblocks the SETTLED badge.
D2 👍 — accept text.secondary = #A0A0A0 for fux-capacity.
D3 👍 — accept text.on-accent = #3A0E0E for fux-capacity.
D4 👍 — accept spelwork text-secondary = #5A5A5A + white = #FFFFFF. The existing muted = #587C6D is a green accent, not body grey — correctly separate.
D5 👍 — accept all four pardner tokens (border #E4E4EA, primary-hover #9C90EE, primary-active #5C4ECB, secondary-hover #FF9B85). On hover/active: going explicit per brand rather than derived. Each brand owns its own values; D8 covers the policy.
D6 👍 — peterswimm secondary = #C68B3E (warm gold, complements the green text.accent on the paper surface). Also accept surface.tint = #ECEAE8.
D7 👍 — adopt the border α-ladder. Note: spelwork's existing border = #E1B659 is the gold accent underline, so the neutral hairline tokens need distinct slugs (border-default / border-subtle) alongside it.
D8 👍 — adopt explicit vs. derived policy. Borders + brand hues = explicit; hover/active = derived from state.hover-shift going forward. D5's pardner tokens are pre-policy hand-crafted values that land close enough (secondary-hover is exact; primary-hover ±1%).
D9 — redline on footer: email.transactional.footer should allow actions. Unsubscribe / manage-preferences links are standard transactional email footer content, and actions in email renders as text links (static surface), so the no-input rule isn't violated. Rest of the profile shapes look right.
D10 — this feels like an engineering call rather than a design sign-off. Flagging back to you: which do you want as canonical?

@peterswimm

D1 👍 — accept accent.success = #2BBC8A for fux-capacity. Unblocks the SETTLED badge. D2 👍 — accept text.secondary = #A0A0A0 for fux-capacity. D3 👍 — accept text.on-accent = #3A0E0E for fux-capacity. D4 👍 — accept spelwork text-secondary = #5A5A5A + white = #FFFFFF. The existing muted = #587C6D is a green accent, not body grey — correctly separate. D5 👍 — accept all four pardner tokens (border #E4E4EA, primary-hover #9C90EE, primary-active #5C4ECB, secondary-hover #FF9B85). On hover/active: going explicit per brand rather than derived. Each brand owns its own values; D8 covers the policy. D6 👍 — peterswimm secondary = #C68B3E (warm gold, complements the green text.accent on the paper surface). Also accept surface.tint = #ECEAE8. D7 👍 — adopt the border α-ladder. Note: spelwork's existing border = #E1B659 is the gold accent underline, so the neutral hairline tokens need distinct slugs (border-default / border-subtle) alongside it. D8 👍 — adopt explicit vs. derived policy. Borders + brand hues = explicit; hover/active = derived from state.hover-shift going forward. D5's pardner tokens are pre-policy hand-crafted values that land close enough (secondary-hover is exact; primary-hover ±1%). D9 — redline on footer: email.transactional.footer should allow actions. Unsubscribe / manage-preferences links are standard transactional email footer content, and actions in email renders as text links (static surface), so the no-input rule isn't violated. Rest of the profile shapes look right. ✅ D10 — this feels like an engineering call rather than a design sign-off. Flagging back to you: which do you want as canonical? @peterswimm
ville closed this pull request 2026-07-13 12:15:19 +00:00
peterswimm reopened this pull request 2026-07-13 14:20:53 +00:00
ci: harden gitleaks/trivy/vale installs against runner rate-limits
Some checks failed
Security Scans / Gitleaks secret scan (pull_request) Failing after 21s
Security Scans / Trivy filesystem scan (pull_request) Successful in 31s
Test Generator / lint-workflows (pull_request) Successful in 13s
Test Generator / lint-prose (pull_request) Failing after 12s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 10s
Test Generator / test (pull_request) Successful in 3m5s
8ea790154e
Pin gitleaks 8.30.1, trivy 0.72.0, vale 3.7.1 and download each directly
from the release CDN instead of resolving via api.github.com/releases/latest
(unauthenticated, 60 req/hr per IP — rate-limits on the shared self-hosted
runner and fails the job before the scan runs). Add curl -f + --retry to ride
out transient network blips; vale's old -sL silently piped error bodies into
tar and sank lint-prose. Scans stay fail-closed on real findings.
ci: install git for gitleaks and curl for vale in slim containers
All checks were successful
Security Scans / Gitleaks secret scan (pull_request) Successful in 25s
Security Scans / Trivy filesystem scan (pull_request) Successful in 29s
Test Generator / lint-workflows (pull_request) Successful in 13s
Test Generator / lint-prose (pull_request) Successful in 17s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 10s
Test Generator / test (pull_request) Successful in 3m13s
6b22b01eae
python:3.12-slim ships neither git nor curl. The gitleaks job runs
actions/checkout with fetch-depth:0 and 'gitleaks git' — both need the git
binary, absent from its apt list — so the job died regardless of findings.
The lint-prose job installs only nodejs, then tried to curl vale in a
container with no curl, sinking the job before the (|| true) prose check.
Add git to the security-scan apt block and curl to the vale install step.
peterswimm deleted branch feat/deck-to-surface-pipeline 2026-07-13 16:01:33 +00:00
Sign in to join this conversation.
No description provided.