Worker portal redesign — Stage 1 design-system extraction

On this page
NOTE

Stage 1 of group epic &51 (#460). Implements #485. Independent of composability work — does not require the Stage-3 runtime to ship. Stage 1.5 (#505 — upgrade four panel-state surfaces to first-class Askama-macro primitives) is the non-blocking follow-up and is out of this plan’s scope.

Status

Step Description Status

1

Macros + smoke fixture + wrapper test (single commit). Add services/canopy-web/templates/_primitives/orchard.html containing 8 Askama macros (panel_frame, overline, gold_rule, big_number, hero_strip, leaf_glyph, status_pill, money_cell). Add services/canopy-web/templates/_primitives/_smoke.html exercising every macro × every meaningful parameter variant. Add services/canopy-web/tests/primitives_test.rs with at least one #[derive(Template)] wrapper consuming _smoke.html. Gate: cargo nextest run -p canopy-web --test primitives_test clean.

Done (2026-05-21)

2

CSS primitives. Add ~220 lines of new classes to services/canopy-web/static/css/canopy-web.css: .panel-frame + BEM variants, .overline + --gold, .gold-rule + 6 width variants, .big-number + 5 size variants, .hero-strip + parts, .leaf-glyph + 3 size variants, .status-pill + 7 kind variants + size variants, .money-cell + --muted, .u-error-block. Status-pill kind backgrounds hardcoded for cross-jurisdiction consistency (matches existing .u-status- convention) — no new --orchard- tokens.

Done (2026-05-21)

3

Expand unit-test coverage. Add one #[derive(Template)] wrapper per macro × per meaningful parameter variant in primitives_test.rs. Assertions check rendered HTML contains the expected class + structure.

Done (2026-05-21)

4

Parent plan + docs + CHANGELOG. Update parent plan worker-portal-redesign.adoc Stage 1 row description, acceptance, files-touched + Status Not startedDone (YYYY-MM-DD) — !XXX. Add "Worker portal patterns" subsection to .claude/docs/coding-conventions.md. CHANGELOG === Added entry under Unreleased.

Done (2026-05-21)

Tracking issue: #485
Epic: &51
Parent plan: worker-portal-redesign.adoc
Branch: feat/wpr-stage1-design-system (single MR)

Context

services/canopy-web/ ships an Askama + htmx + Alpine.js (CSP build) worker portal. Today each template hand-rolls its own panel chrome (.card + .u-label-sm + .u-stat), badges (.badge), empty states (.card u-empty-state), and loading skeletons (.skeleton u-h-10). The composability runtime (Stage 3, ADR-021 / ADR-022) will let jurisdictions compose surfaces from plugins — but each plugin still needs a shared visual vocabulary, or every plugin re-rolls its chrome and the system fragments.

Stage 1 extracts the 8 reusable Askama-macro primitives the rest of the epic depends on, plus one genuinely-new CSS utility. It is independent of Stage 3 — the existing dashboard route, handlers, tests, and consumers continue to work; only template-level chrome changes (and Stage 1 doesn’t even rewrite any consumer template — the smoke fixture is the sole validation surface in this MR).

Design

Decisions locked

  1. Askama {% macro %} definitions (not {% include %} partials). Macros support parameters with defaults + body slots via {{ caller() }}. Verified Askama 0.15.6 supports both (askama_derive 0.15.6 src/generator/node.rs:1206-1318).

  2. Macros live in a single file: services/canopy-web/templates/_primitives/orchard.html. New _primitives/ directory makes intent clear and groups future additions. Imported with {% import "_primitives/orchard.html" as o %}.

  3. Component-style class names: .panel-frame, .overline, .gold-rule, .big-number, .hero-strip, .leaf-glyph, .status-pill, .money-cell. No .cy- prefix. Matches existing convention (.card, .skeleton, .badge, .service-error — all unprefixed component classes).

  4. Reuse existing classes; do not duplicate. .skeleton (canopy-web.css:182), @keyframes pulse (canopy-web.css:187-190), and .u-empty-state (canopy-web.css:398-402, used by 14 templates) all exist. Stage 1 reuses them.

  5. Only one new CSS utility: .u-error-block — panel-scoped error chrome distinct from page-level .service-error (canopy-web.css:193).

  6. gold_rule width is a discrete enum, not parametric pixels. Six variants match every distinct width the design uses: xs (14px), sm (16px), md (18px), lg (20px, default), xl (28px), xxl (40px). Emit as .gold-rule--<size> classes. No inline style= attribute on any primitive — canopy-web ships strict CSP style-src 'self' with no 'unsafe-inline' (services/canopy-web/src/csp.rs:27-35).

  7. Validation target: smoke-fixture template only. services/canopy-web/templates/_primitives/_smoke.html exercises every macro × every meaningful parameter variant; rendered only by unit tests; no route, no consumer template rewrite in this MR. The dashboard.html rewrite is deferred (rewriting to .panel-frame / .big-number would break tests/e2e/specs/dashboard.spec.ts:9-25 which filters on .card + .u-stat).

  8. Unit tests at services/canopy-web/tests/primitives_test.rs, one thin #[derive(Template)] wrapper per macro × parameter variant. The smoke-fixture wrapper lives in this file too.

  9. Step ordering: macros + smoke fixture + ONE wrapper test land together in Step 1 so that Askama actually compiles the smoke fixture. Step 1 gate is cargo nextest run -p canopy-web --test primitives_test — plain cargo build does not compile integration-test (tests/*.rs) Askama templates.

  10. Status-pill kind backgrounds stay hardcoded (matches the existing .u-status- convention at canopy-web.css:412-424; the in-file comment explicitly says "Backgrounds are intentionally hardcoded for a consistent pill recipe across jurisdictions even when they override the primary palette"). New literals add unconfigured (light: #f0eee9 / #8a8170 / #b8ad95; dark: #2a2620 / #b8ad95 / #8a8170) and discrepancy (aliases denied’s error colors). No --orchard-unconfigured- tokens introduced — would fight the existing cross-jurisdiction consistency model.

Macro contracts

Macro Parameters Notes

panel_frame

label="", count="", accent="default", dense=false

Body via {{ caller() }}. accent ∈ {default, error, warning, info}. dense swaps 18px 20px padding → 14px 16px.

overline

gold=false

Body via {{ caller() }}. Small caps section label. gold=true for hero accent.

gold_rule

size="lg"

Size ∈ {xs=14, sm=16, md=18, lg=20 default, xl=28, xxl=40} pixels.

big_number

value, unit="", size="lg"

Size ∈ {sm=24, md=32, lg=44, xl=56, xxl=72} pixels. Tabular numerals.

hero_strip

persona, greeting, stat="", stat_label="", stat_sub=""

Body via {{ caller() }} for optional trailing content. Full-width hero banner.

leaf_glyph

size="md"

Inline SVG (no client JS). Inherits color from currentColor. Size ∈ {sm, md, lg}.

status_pill

kind="neutral", size="sm"

Body via {{ caller() }} for the label. Kind ∈ {approved, pending, denied, info, neutral, unconfigured, discrepancy}. Size ∈ {sm default, lg}.

money_cell

amount, muted=false

amount is a pre-formatted String (server-side Rust format!). Macro applies tabular-numeral typography only.

Files Touched

NEW (added by this MR):

  • services/canopy-web/templates/_primitives/orchard.html — 8 Askama macros (~250 lines)

  • services/canopy-web/templates/_primitives/_smoke.html — fixture for unit-test rendering

  • services/canopy-web/tests/primitives_test.rs#[derive(Template)] wrappers + assertions

MODIFIED:

  • services/canopy-web/static/css/canopy-web.css — adds ~220 lines of new classes. No new --orchard-* tokens (status-pill kind backgrounds hardcoded per existing convention). No deletions.

  • docs/modules/ROOT/pages/plans/worker-portal-redesign.adoc — parent plan updates per the parent plan’s Stage 1 row description, acceptance row, files-touched, and Status cell.

  • .claude/docs/coding-conventions.md — adds "Worker portal patterns" subsection.

  • CHANGELOG.adoc=== Added entry under Unreleased, terse-bullet shape per the #484 convention.

OUT OF SCOPE (deferred):

  • Rewriting dashboard.html to consume the primitives — separate follow-up MR (updates tests/e2e/specs/dashboard.spec.ts selectors at the same time)

  • Migrating other templates (case_detail_summary, the 6 cases/tab_*, applications/, appeals/, notices/, renewals/) — happens per Stage 5-7 child issues

  • Deleting legacy .card / .u-label-sm / .u-stat* classes — separate hygiene MR after migration

  • Stage 1.5 (#505) — upgrading .skeleton + .u-empty-state + .u-error-block to first-class Askama-macro primitives

  • Composability runtime (Stage 3)

Verification

Per-step gates

  • cargo nextest run -p canopy-web --test primitives_test clean at end of Steps 1 and 3

  • cargo nextest run -p canopy-web clean (full canopy-web suite) before push

  • cargo xtask validate full pipeline clean before push (fmt + clippy + nextest + check-docs + Playwright E2E ≥ 135 green; dashboard.spec.ts unaffected since the dashboard template is unchanged)

  • asciidoctor + in-house asciidoctor-lint clean on the updated parent plan + CHANGELOG (binary at /home/bitskrieg/code/cargo-target/debug/asciidoctor-lint)

Stage acceptance

  • All 8 macros exercised in _smoke.html × at least one non-default parameter value per macro

  • All 8 macros have at least one assertion in primitives_test.rs

  • canopy-web.css grows by ~120 lines; zero existing classes removed; zero inline style= attributes added to any primitive (verified via grep on the new macro file)

  • cargo nextest run -p canopy-web --test primitives_test clean — this is the actual Askama compile-time check (the test binary build includes the smoke fixture via its in-tree wrapper; plain cargo build does not compile integration-test templates)

  • Parent plan Stage 1 row description, acceptance, and files-touched all updated — not just Status cell

  • No new TODO/FIXME tokens; no unwrap outside tests; no unsafe; no #[allow]

What this MR does NOT gate

  • Dashboard visual regression — deferred with the dashboard rewrite to a follow-up MR. dashboard.spec.ts continues to pass unchanged because the dashboard template is unchanged.

  • Cross-template primitive adoption — deferred to Stage 5-7 consumer migrations.

Risk + Rollback

  • Risk — Askama macro signature mismatch surfaces in Step 3 (broader tests) rather than Step 1. Mitigation: Step 1’s single wrapper does a basic render assertion against every macro via the smoke fixture so any signature ambiguity surfaces in Step 1’s nextest run before Step 3 expands the matrix.

  • Risk — Light/dark mismatch on .status-pill’s `unconfigured kind. Mitigation: explicit dark-theme overrides under [data-theme="dark"] .status-pill[data-kind="unconfigured"] mirror the existing .u-status-* dark variants (Decision 10).

  • Risk — CSP violation if a future macro variant introduces inline style=. Mitigation: Decision 6 explicitly bans inline style on primitives; manual check before MR open verifies the new macro file contains zero style= attributes.

  • Risk — _smoke.html drift from real consumer usage. Mitigation: smoke fixture’s parameter coverage is the contract; if a future consumer wants a variant that doesn’t exist, the consumer’s MR adds it to the smoke fixture in the same commit.

  • Rollback: revert the MR. _primitives/ directory removed; CSS additions removed; primitives_test.rs removed; parent plan reverts. Existing utility classes, tests, dashboard, etc. unaffected through the round-trip.

Pre-commit Q1-Q8 expectations

  • Q1 — Step 1 + Step 3 add tests for every macro

  • Q2 — no unwrap outside tests; no unsafe; no #[allow]

  • Q3 — no test deletions or weakened assertions

  • Q4 — no plan deviation expected; if material deviations surface, update the plan’s Design section + file separate design-iteration issues

  • Q5 — this MR closes #485; updates parent plan Status table; no new issues filed unless deviations surface

  • Q6 — Stage 1.5 (#505) primitives upgrade stays out of scope; consumer-template migration stays out of scope; legacy class deletion stays out of scope

  • Q7 — CHANGELOG === Added + parent plan updates + .claude/docs/coding-conventions.md subsection

  • Q8 — zero new TODO/FIXME tokens

References

Edit this page · default