Worker portal redesign — Stage 1 design-system extraction
On this page
Status
| Step | Description | Status |
|---|---|---|
1 |
Macros + smoke fixture + wrapper test (single commit). Add |
Done (2026-05-21) |
2 |
CSS primitives. Add ~220 lines of new classes to |
Done (2026-05-21) |
3 |
Expand unit-test coverage. Add one |
Done (2026-05-21) |
4 |
Parent plan + docs + CHANGELOG. Update parent plan worker-portal-redesign.adoc Stage 1 row description, acceptance, files-touched + Status |
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
-
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.6src/generator/node.rs:1206-1318). -
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 %}. -
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). -
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. -
Only one new CSS utility:
.u-error-block— panel-scoped error chrome distinct from page-level.service-error(canopy-web.css:193). -
gold_rulewidth 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 inlinestyle=attribute on any primitive — canopy-web ships strict CSPstyle-src 'self'with no'unsafe-inline'(services/canopy-web/src/csp.rs:27-35). -
Validation target: smoke-fixture template only.
services/canopy-web/templates/_primitives/_smoke.htmlexercises 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-numberwould breaktests/e2e/specs/dashboard.spec.ts:9-25which filters on.card+.u-stat). -
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. -
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— plaincargo builddoes not compile integration-test (tests/*.rs) Askama templates. -
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 addunconfigured(light:#f0eee9/#8a8170/#b8ad95; dark:#2a2620/#b8ad95/#8a8170) anddiscrepancy(aliases denied’s error colors). No--orchard-unconfigured-tokens introduced — would fight the existing cross-jurisdiction consistency model.
Macro contracts
| Macro | Parameters | Notes |
|---|---|---|
|
|
Body via |
|
|
Body via |
|
|
Size ∈ {xs=14, sm=16, md=18, lg=20 default, xl=28, xxl=40} pixels. |
|
|
Size ∈ {sm=24, md=32, lg=44, xl=56, xxl=72} pixels. Tabular numerals. |
|
|
Body via |
|
|
Inline SVG (no client JS). Inherits color from |
|
|
Body via |
|
|
|
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—=== Addedentry under Unreleased, terse-bullet shape per the #484 convention.
OUT OF SCOPE (deferred):
-
Rewriting
dashboard.htmlto consume the primitives — separate follow-up MR (updatestests/e2e/specs/dashboard.spec.tsselectors 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-blockto first-class Askama-macro primitives -
Composability runtime (Stage 3)
Verification
Per-step gates
-
cargo nextest run -p canopy-web --test primitives_testclean at end of Steps 1 and 3 -
cargo nextest run -p canopy-webclean (full canopy-web suite) before push -
cargo xtask validatefull pipeline clean before push (fmt + clippy + nextest + check-docs + Playwright E2E ≥ 135 green;dashboard.spec.tsunaffected since the dashboard template is unchanged) -
asciidoctor+ in-houseasciidoctor-lintclean 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.cssgrows by ~120 lines; zero existing classes removed; zero inlinestyle=attributes added to any primitive (verified via grep on the new macro file) -
cargo nextest run -p canopy-web --test primitives_testclean — this is the actual Askama compile-time check (the test binary build includes the smoke fixture via its in-tree wrapper; plaincargo builddoes 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
unwrapoutside tests; nounsafe; no#[allow]
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
nextestrun before Step 3 expands the matrix. -
Risk — Light/dark mismatch on
.status-pill’s `unconfiguredkind. 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 zerostyle=attributes. -
Risk —
_smoke.htmldrift 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.rsremoved; 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
unwrapoutside tests; nounsafe; 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.mdsubsection -
Q8 — zero new TODO/FIXME tokens
References
-
Issue: #485
-
Parent plan: Worker portal redesign
-
ADR-021: Composability runtime + plugin model (Stage 3 consumes these primitives)
-
Pre-shipped palette work: !295 (orchard tokens the primitives consume)
-
Stage 1.5 follow-up: #505 — first-class panel-state primitives
-
CSP source-of-truth:
services/canopy-web/src/csp.rs:27-35(style-src 'self'— no'unsafe-inline')