From f769242f767965ddf82382f1147a4e07b7a8a41c Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Thu, 2 Jul 2026 17:07:59 +0200 Subject: [PATCH] docs: replace SHOWCASE-ROADMAP.md with docs/backlog/ (17 WPs) Turns the prior roadmap sketch into ordered, gated work packages (enforcement gates, FP/DDD consistency, CIBG fidelity, Storybook curriculum, a11y) from the 2026-07-02 showcase-hardening audit. --- docs/SHOWCASE-ROADMAP.md | 100 ++------------------ docs/backlog/README.md | 83 ++++++++++++++++ docs/backlog/WP-01-axe-ci-gate.md | 80 ++++++++++++++++ docs/backlog/WP-02-check-tokens.md | 67 +++++++++++++ docs/backlog/WP-03-contracts-purity.md | 76 +++++++++++++++ docs/backlog/WP-04-ui-not-infrastructure.md | 75 +++++++++++++++ docs/backlog/WP-05-parse-boundaries.md | 66 +++++++++++++ docs/backlog/WP-06-typed-async.md | 62 ++++++++++++ docs/backlog/WP-07-brief-idioms.md | 70 ++++++++++++++ docs/backlog/WP-08-store-idiom.md | 70 ++++++++++++++ docs/backlog/WP-09-pure-logic.md | 72 ++++++++++++++ docs/backlog/WP-10-button-fidelity.md | 64 +++++++++++++ docs/backlog/WP-11-markup-fidelity.md | 76 +++++++++++++++ docs/backlog/WP-12-datablock.md | 79 ++++++++++++++++ docs/backlog/WP-13-cibg-gap-register.md | 79 ++++++++++++++++ docs/backlog/WP-14-storybook-taxonomy.md | 79 ++++++++++++++++ docs/backlog/WP-15-missing-stories.md | 64 +++++++++++++ docs/backlog/WP-16-component-a11y.md | 76 +++++++++++++++ docs/backlog/WP-17-app-a11y.md | 82 ++++++++++++++++ 19 files changed, 1326 insertions(+), 94 deletions(-) create mode 100644 docs/backlog/README.md create mode 100644 docs/backlog/WP-01-axe-ci-gate.md create mode 100644 docs/backlog/WP-02-check-tokens.md create mode 100644 docs/backlog/WP-03-contracts-purity.md create mode 100644 docs/backlog/WP-04-ui-not-infrastructure.md create mode 100644 docs/backlog/WP-05-parse-boundaries.md create mode 100644 docs/backlog/WP-06-typed-async.md create mode 100644 docs/backlog/WP-07-brief-idioms.md create mode 100644 docs/backlog/WP-08-store-idiom.md create mode 100644 docs/backlog/WP-09-pure-logic.md create mode 100644 docs/backlog/WP-10-button-fidelity.md create mode 100644 docs/backlog/WP-11-markup-fidelity.md create mode 100644 docs/backlog/WP-12-datablock.md create mode 100644 docs/backlog/WP-13-cibg-gap-register.md create mode 100644 docs/backlog/WP-14-storybook-taxonomy.md create mode 100644 docs/backlog/WP-15-missing-stories.md create mode 100644 docs/backlog/WP-16-component-a11y.md create mode 100644 docs/backlog/WP-17-app-a11y.md diff --git a/docs/SHOWCASE-ROADMAP.md b/docs/SHOWCASE-ROADMAP.md index 76a43ec..6f1d0fe 100644 --- a/docs/SHOWCASE-ROADMAP.md +++ b/docs/SHOWCASE-ROADMAP.md @@ -1,96 +1,8 @@ -# Showcase roadmap +# Showcase roadmap — superseded -Prioritised backlog for making this POC a stronger teaching showcase for **atomic -design + FP in the UI** — optimising for maintainability, speed, and low cognitive load -for junior *and* senior developers. +**This roadmap is superseded by [`docs/backlog/`](backlog/README.md)** (2026-07-02). -## How to read this - -Each track below is a table of concrete items: **Item · Where · Why · Effort · Priority**. -Nothing here is built yet — pick items off the top. Priorities: **P1** = highest -teaching-value-per-effort, do first; **P2** = strengthens the showcase; **P3** = backlog. -Effort is a rough solo estimate. - -This roadmap is deliberately scoped to three tracks (Storybook-as-curriculum, FP -primitives, Enforcement depth). A fourth track — onboarding docs (GETTING-STARTED, -GLOSSARY, layer-dependency diagram, ADR index) — was identified but de-scoped for now. - -### Current strengths (what NOT to touch) - -The foundations are already exemplary and should be preserved as-is: - -- **Pure reducers / state machines** — `*.machine.ts` model illegal states away with - tagged unions; effects live in commands, never in `reduce`. -- **`RemoteData`** (`src/app/shared/application/remote-data.ts`) — `map`/`map2`/ - `map3`/`andThen`/`fromResource`/`foldRemote`, rendered via the `` molecule. -- **Parse-don't-validate value objects** (`src/app/registratie/domain/value-objects/`) — - branded types built only through a `Result`-returning parser. -- **Anti-corruption parsers** at every DTO seam (`infrastructure/*.adapter.ts`). -- **ESLint-enforced layer boundaries** (`eslint.config.mjs`): no `any`, `domain/` imports - no Angular, cross-context direction locked. -- **Storybook** — 96% story coverage, a11y addon + docs addon on, titled `Layer/Name`. -- **Token linting** (`npm run check:tokens`) blocks hardcoded colours. - -The gaps below are about *teaching reach*, not correctness. - ---- - -## Track A — Storybook as a curriculum - -Storybook is the showcase vehicle, but today it renders isolated components with no -narrative. `.storybook/main.ts` already globs `../src/**/*.mdx` and loads -`@storybook/addon-docs`, so MDX pages drop in with zero config. - -| Item | Where | Why (teaching value / cognitive load) | Effort | Prio | -|------|-------|----------------------------------------|--------|------| -| **Atomic-design primer (MDX)** | `src/docs/atomic-design.mdx` | The pyramid + the "each layer imports only below" rule, with a *live* composition chain of real components: `button` (atom) → `form-field` (molecule) → `document-upload` (organism) → `page-shell` (template). Links to the ESLint rules that enforce it. Gives newcomers the mental model before they read code. | 2–3h | P1 | -| **Design-token gallery (MDX)** | `src/docs/design-tokens.mdx` | RHC colours, the `--rhc-space-max-*` spacing scale (with resolved px), typography; plus when to use a raw `--rhc-*` token vs. an `--app-*` wrapper from `src/styles.scss`. Answers the senior's "what's the actual value of `--rhc-space-max-md`?" and ties to `check:tokens`. | 2h | P1 | -| **FP-in-the-UI primer (MDX)** | `src/docs/fp-in-ui.mdx` | A Storybook front door to the concepts already in `src/app/showcase/concepts.page.ts` and `docs/fp-tea-atomic-design.md` (RemoteData, machine, parse-don't-validate) so the component library and the FP guide cross-link instead of living apart. | 1.5h | P1 | -| **`argTypes` / controls** | high-traffic atom stories: `button`, `text-input`, `radio-group`, `status-badge`, `alert` (`src/app/shared/ui/*/*.stories.ts`) | Lets devs explore every variant from the Controls panel instead of editing story code. Fastest "play with it" path for juniors. | 1.5h | P2 | -| **Anatomy story for `async`** | `src/app/shared/ui/async/async.stories.ts` | Show all four states (Loading / Empty / Failure / Success) in one view — it teaches the exhaustive fold but doesn't currently *display* it exhaustively. | 45m | P2 | -| **`play` / interaction tests** | 2–3 stories (button disabled-during-submit; `form-field` error announced via `role="alert"`; `async` retry calls `reload()`) | Demonstrates behaviour, not just appearance, and doubles as regression cover. Feeds Track C's CI gate. | 2h | P2 | -| **Shell story** | `src/app/shared/layout/shell/shell.stories.ts` | The only component with no story; a canvas showing the persistent header/footer completes the library. | 30m | P3 | - ---- - -## Track B — FP primitives - -The pure core is excellent but under-demonstrates a few composition tools; learners -currently hand-branch and reinvent them. - -| Item | Where | Why (teaching value / cognitive load) | Effort | Prio | -|------|-------|----------------------------------------|--------|------| -| **`Result` combinators** | `src/app/shared/kernel/fp.ts` (+ new `fp.spec.ts`) | Add `map`, `mapErr`, `andThen`, `fold`, `getOrElse`. Today only direct `r.ok ? … : …` branching exists — fine for one step, painful across a pipeline. **Biggest single FP gap.** Then refactor **one** real call site to show `parse ▸ mapErr(localize) ▸ map(toDomain)`. | 1.5h | P1 | -| **Value-object specs** | `postcode.spec.ts`, `uren.spec.ts`, `big-nummer.spec.ts` next to `src/app/registratie/domain/value-objects/*.ts` | The repo's own rule is "domain must have a spec," yet only `email` does. Cover normalisation (`"1234AB"`→`"1234 AB"`), boundaries, and invalid inputs. Closing this makes the rule credible and teaches value-object testing. | 1h | P1 | -| **Property-based test example** | one spec (e.g. Postcode normalisation idempotence, or the `RemoteData.map` functor-composition law); adds `fast-check` dev-dep | Every current test is example-based. One law-based test shows how to catch edge cases you didn't enumerate. **Flag:** introduces a new dev dependency — justified because property testing is an explicit teaching goal. | 1h | P2 | -| **`NonEmptyArray` helper** | `src/app/shared/kernel/fp.ts`; use for `vraagIds` / required upload categories | Makes "at least one" a *type* rather than a runtime check — a crisp "make illegal states unrepresentable" lesson applied to collections. | 1h | P3 | -| **Effect/command testability (note)** | `src/app/registratie/application/{draft-sync,submit-*}.ts` | Direction only, not built here: inject the HTTP effect so commands are unit-testable without a full component. Documents how to close the one untested layer. | — | P3 | - ---- - -## Track C — Enforcement depth - -ESLint checks imports; several documented rules are otherwise only hoped-for. Turning -them into machine-checked guarantees is itself a teachable "fitness function" pattern. - -| Item | Where | Why (teaching value / cognitive load) | Effort | Prio | -|------|-------|----------------------------------------|--------|------| -| **Architecture-fitness test** | `src/architecture.spec.ts` + `npm run check:architecture`, wired into `.github/workflows/ci.yml` | A vitest that scans `domain/` for Angular/HTTP/RxJS imports and asserts cross-context direction. Belt-and-suspenders over ESLint, and a worked example of an executable architecture rule. | 2h | P1 | -| **a11y as a CI gate** | Storybook test-runner + axe (or vitest + axe on rendered atoms), added to `ci.yml` | The a11y addon's checks are visible in the panel but not enforced. Gating them turns "we care about a11y" into a guarantee — high value for a design-system showcase. | 2–3h | P1 | -| **Interaction tests in CI** | run Track-A `play` tests via the Storybook test-runner in `ci.yml` | Makes behavioural stories part of the pipeline, not just docs. | 1h | P2 | -| **Domain coverage gate** | vitest coverage threshold on `domain/**` | Guarantees the "domain must have a spec" rule holds as the code grows. | 45m | P3 | -| **Dependency-graph visual** | `madge` script producing an SVG/JSON of the layer graph | A generated picture makes the architecture graspable at a glance and catches accidental edges. | 45m | P3 | - ---- - -## Suggested sequencing - -Do P1 across tracks, interleaved so each shows value early: - -1. **[B]** `Result` combinators + value-object specs — small, self-contained, immediate. -2. **[A]** the three MDX pages — the visible "showcase" upgrade. -3. **[C]** architecture-fitness test + a11y-in-CI — locks the guarantees in. - -Then P2 (controls, anatomy/interaction stories, property test, interaction tests in CI). -P3 items are backlog. None of this changes runtime behaviour or the public component -API; it is additive teaching + enforcement. +The backlog absorbs and corrects this document: its Storybook-as-curriculum track became +WP-14/15 (+ per-invariant MDX pages in WP-05/07/08/13/17), its enforcement track became +WP-01…04, and several of its claims had gone stale (value-object specs exist; more than +one component lacked a story). Two backlogs would drift — this one is closed. diff --git a/docs/backlog/README.md b/docs/backlog/README.md new file mode 100644 index 0000000..a73e75a --- /dev/null +++ b/docs/backlog/README.md @@ -0,0 +1,83 @@ +# Backlog — showcase hardening + +Ordered work packages that take this POC from "good" to **reference showcase**: CIBG +design-system fidelity, DDD/FP consistency, Storybook as curriculum, and WCAG compliance +with automated gates. Source: the architecture/CIBG/a11y audit of 2026-07-02 (plan: +"Showcase hardening"). + +This backlog **supersedes `docs/SHOWCASE-ROADMAP.md`**. + +## Session protocol + +- **Switch to Opus first** (`/model opus`) before tackling any WP. +- **One WP per session.** Read `CLAUDE.md`, this README, the WP file, and the WP's + "Read first" list — then execute. Do not start the next WP in the same session. +- The **Decisions** block in each WP is pre-made — don't relitigate it. +- A WP ends **GREEN** (below) with its acceptance criteria checked off and its Status + updated to `done` (+ commit hash). +- No WP leaves a lint rule/check disabled without an inline justification comment **and** + a cross-reference to the WP that will remove it. + +## GREEN (global definition of done) + +```bash +npm run lint && npm run check:tokens && npm test && npm run build && npm run build-storybook +``` + +From WP-01 onward, additionally: + +```bash +npm run test-storybook:ci +``` + +Backend stays untouched throughout (frontend-only backlog); `cd backend && dotnet test` +only needs re-running if a WP unexpectedly touches `backend/`. + +## Order + +Gates land before the work they cover; each lint rule lands in the same WP as the fixes +for its existing violations, so every WP ends green. + +| WP | Title | Phase | Status | +|----|-------|-------|--------| +| [WP-01](WP-01-axe-ci-gate.md) | Axe-on-every-story CI gate | 0 · gates | todo | +| [WP-02](WP-02-check-tokens.md) | Harden `check:tokens` + fix what it catches | 0 · gates | todo | +| [WP-03](WP-03-contracts-purity.md) | Boundaries I: contracts purity + ApiClient confinement | 0 · gates | todo | +| [WP-04](WP-04-ui-not-infrastructure.md) | Boundaries II: `ui ↛ infrastructure` + showcase sanction | 0 · gates | todo | +| [WP-05](WP-05-parse-boundaries.md) | Parse-don't-validate closure + MDX | 1 · FP/DDD | todo | +| [WP-06](WP-06-typed-async.md) | Generic async template contexts — kill `$any()` | 1 · FP/DDD | todo | +| [WP-07](WP-07-brief-idioms.md) | Brief on the shared idioms + RemoteData MDX | 1 · FP/DDD | todo | +| [WP-08](WP-08-store-idiom.md) | One store idiom + machine naming + TEA MDX | 1 · FP/DDD | todo | +| [WP-09](WP-09-pure-logic.md) | Pure-logic closure: dates + missing command specs | 1 · FP/DDD | todo | +| [WP-10](WP-10-button-fidelity.md) | CIBG button fidelity | 2 · CIBG | todo | +| [WP-11](WP-11-markup-fidelity.md) | CIBG markup fidelity: application-link + absent-class triage | 2 · CIBG | todo | +| [WP-12](WP-12-datablock.md) | CIBG Datablock for application data | 2 · CIBG | todo | +| [WP-13](WP-13-cibg-gap-register.md) | CIBG-gap register + hygiene + MDX | 2 · CIBG | todo | +| [WP-14](WP-14-storybook-taxonomy.md) | Storybook taxonomy reorg + Layers MDX | 3 · Storybook | todo | +| [WP-15](WP-15-missing-stories.md) | Missing stories: shell + brief components | 3 · Storybook | todo | +| [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | todo | +| [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | todo | + +Sequencing dependencies (stated in the WPs too): 01 before 10–15 (axe covers story churn); +03/04 before 05–09 (boundaries stop new violations during refactors); 06 before 07 (typed +`` before brief adopts it); 13 defines the gap-marker format that 11/12 reference +— if 11/12 run first, they define it and 13 adopts it. + +## WP template + +```markdown +# WP-NN — Title + +Status: todo | in-progress | done () +Phase: N — name + +## Why +## Read first +## Decisions (pre-made, don't relitigate) +## Files +## Steps +## Acceptance criteria +## Verification +## Out of scope +## Risks +``` diff --git a/docs/backlog/WP-01-axe-ci-gate.md b/docs/backlog/WP-01-axe-ci-gate.md new file mode 100644 index 0000000..f88a852 --- /dev/null +++ b/docs/backlog/WP-01-axe-ci-gate.md @@ -0,0 +1,80 @@ +# WP-01 — Axe-on-every-story CI gate + +Status: todo +Phase: 0 — enforcement & gates + +## Why + +The Storybook a11y addon (`@storybook/addon-a11y`, configured in `.storybook/preview.ts` +for `wcag2a, wcag2aa, wcag21a, wcag21aa`) only surfaces violations *interactively*. +Nothing gates CI. This WP turns "a panel you can look at" into "a check that fails the +build", so every story added or changed by later WPs is automatically covered. + +## Read first + +- `.storybook/main.ts`, `.storybook/preview.ts` (existing a11y config — reuse its tags) +- `.github/workflows/ci.yml` (job layout to extend) +- `package.json` (scripts; note `.npmrc` has `legacy-peer-deps=true`) + +## Decisions (pre-made, don't relitigate) + +- Mechanism: **`@storybook/test-runner` + `axe-playwright`** against the static build. + The repo uses `@storybook/angular` on the **webpack** builder, which rules out + `@storybook/addon-vitest` (Vite-only). Fallback if test-runner doesn't support + Storybook 10 yet: a hand-rolled Playwright + `axe-core` loop over `storybook-static` + (same gate semantics — document the swap in this file if taken). +- Escape hatch: `parameters: { a11y: { disable: true } }` per story, allowed **only** with + an inline justification comment and a cross-reference to the WP that will fix it + (WP-16/WP-17). + +## Files + +- `package.json` (devDependencies + scripts) +- `.storybook/test-runner.ts` (new) +- `.github/workflows/ci.yml` (new job) +- Possibly a handful of story/component files for triaged violations + +## Steps + +1. Check version fit first: `npm view @storybook/test-runner peerDependencies` and pick + the release matching the installed Storybook 10 (`legacy-peer-deps` absorbs minor lag). +2. `npm i -D @storybook/test-runner axe-playwright http-server wait-on concurrently` + (+ `npx playwright install chromium` locally). +3. `.storybook/test-runner.ts`: + - `preVisit`: `injectAxe(page)`. + - `postVisit`: `getStoryContext(page, context)`; skip when + `parameters.a11y?.disable === true`; else `checkA11y(page, '#storybook-root', …)`, + honoring `parameters.a11y.config`/`options.runOnly` from `preview.ts` (don't + duplicate the tag list — read it from the story context). +4. Scripts: + - `"test-storybook": "test-storybook"` + - `"test-storybook:ci": "concurrently -k -s first -n sb,axe \"http-server storybook-static -p 6006 --silent\" \"wait-on tcp:127.0.0.1:6006 && test-storybook --url http://127.0.0.1:6006\""` +5. CI job `storybook-a11y` in `ci.yml`: checkout → setup-node (same version as the + `frontend` job) + npm cache → `npm ci` → `npx playwright install --with-deps chromium` + → `npm run build-storybook` → `npm run test-storybook:ci`. +6. Run locally against a fresh `build-storybook`; triage violations: fix trivial ones + (labels, roles, contrast via `--rhc-*` tokens); anything structural gets the escape + hatch + comment + WP cross-ref. +7. Update `docs/backlog/README.md`: GREEN now includes `npm run test-storybook:ci`. + +## Acceptance criteria + +- [ ] `npm run test-storybook:ci` passes locally against a fresh `storybook-static/`. +- [ ] CI has a `storybook-a11y` job; an axe violation makes it fail (verify once with a + deliberately broken story, then revert). +- [ ] Every `a11y.disable` skip carries a justification comment + WP cross-ref. +- [ ] Backlog README GREEN updated. + +## Verification + +GREEN + `npm run test-storybook:ci`. + +## Out of scope + +Fixing structural a11y issues in components (WP-16/WP-17); Playwright e2e page sweeps +(explicitly not chosen). + +## Risks + +- test-runner ↔ Storybook 10 peer mismatch → use the documented fallback. +- Unknown violation count on first run → the escape hatch keeps the WP bounded. diff --git a/docs/backlog/WP-02-check-tokens.md b/docs/backlog/WP-02-check-tokens.md new file mode 100644 index 0000000..6da29ed --- /dev/null +++ b/docs/backlog/WP-02-check-tokens.md @@ -0,0 +1,67 @@ +# WP-02 — Harden `check:tokens` + fix what it then catches + +Status: todo +Phase: 0 — enforcement & gates + +## Why + +The token guard (`check:tokens` in `package.json`) only greps for hex colors and only +scans `registratie/ui`, `shared/ui`, `shared/layout`. It misses `rgb()`/`hsl()` values +and skips `brief/`, `auth/`, `herregistratie/`, `showcase/` entirely. The guard must +cover the whole app before the CIBG work (WP-10…13) leans on it. + +## Read first + +- `package.json` (`check:tokens` script) +- `src/styles.scss` (the token bridge — the vocabulary fixes must come from) +- `CLAUDE.md` theming section + +## Decisions (pre-made, don't relitigate) + +- Raw `px` values are **not** grepped (too many false positives); border-width fixes are + done manually in this WP and the script documents why px is out of scope. +- A `/* token-ok */` inline marker suppresses a justified false positive (e.g. `rgb` in a + comment or data-URI) — each use needs a reason in the comment. + +## Files + +- `package.json` → move logic to `scripts/check-tokens.sh` (new), keep the npm script name +- `src/app/shared/layout/site-footer/site-footer.component.ts` — `rgb(255 255 255 / 0.25)` +- `src/app/shared/ui/debug-state/debug-state.component.ts` — raw rgba/hex; tokenize and + drop its exclusion (it's dev-only chrome but the rule should have no holes) +- `src/app/brief/ui/letter-block/letter-block.component.ts` — raw `3px` border +- `src/app/brief/ui/letter-preview/letter-preview.component.ts` — raw `1px` border +- `src/app/brief/ui/passage-picker/passage-picker.component.ts` — raw `1px` border + +## Steps + +1. Create `scripts/check-tokens.sh`; `package.json` `check:tokens` calls it. +2. Extend the regex to `#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(` (skip lines containing + `token-ok`). +3. Extend the scanned set to **all** `src/app/**/*.component.ts`. +4. Run it; fix every hit using tokens from the `src/styles.scss` bridge (add a bridge + token only if no existing one fits — keep the vocabulary small). +5. Fix the raw border widths in the three brief components (`--rhc-border-width-*`), + even though px isn't grepped. +6. Plant a violation, confirm the script fails, remove it. + +## Acceptance criteria + +- [ ] Script scans all contexts and matches hex + rgb()/hsl(). +- [ ] Zero exclusions; any `token-ok` marker has a reason. +- [ ] site-footer, debug-state, and the three brief components are tokenized. +- [ ] A planted violation provably fails `npm run check:tokens`. + +## Verification + +GREEN + `npm run test-storybook:ci`. + +## Out of scope + +Grepping px/rem values; scss files (`styles.scss` is the one place palette values are +allowed — it IS the bridge). + +## Risks + +Regex false positives in strings/URLs — that's what the `token-ok` marker is for; keep +its bar high. diff --git a/docs/backlog/WP-03-contracts-purity.md b/docs/backlog/WP-03-contracts-purity.md new file mode 100644 index 0000000..12b9e76 --- /dev/null +++ b/docs/backlog/WP-03-contracts-purity.md @@ -0,0 +1,76 @@ +# WP-03 — Boundaries I: contracts purity + ApiClient confinement + +Status: todo +Phase: 0 — enforcement & gates + +## Why + +`contracts/` is the FE⇄BE wire seam and must import **nothing** (CLAUDE.md §1, +ADR-0001) — but `registratie/contracts/dashboard-view.dto.ts` imports domain types, and +`change-request-form.component.ts` injects `ApiClient` straight into UI. Neither rule is +lint-enforced. Rule + fixes land together so this WP ends green. + +## Read first + +- `CLAUDE.md` §1 + §4 (layers, BFF-lite) +- `eslint.config.mjs` (existing zone style to extend) +- `src/app/registratie/contracts/dashboard-view.dto.ts` +- `src/app/registratie/infrastructure/dashboard-view.adapter.ts` +- `src/app/registratie/ui/change-request-form/change-request-form.component.ts` (~line 53) +- `src/app/registratie/application/submit-change-request.ts` +- `src/app/shared/infrastructure/role.ts` and `src/app/brief/ui/letter-composer/letter-composer.component.ts` (line 2) + +## Decisions (pre-made, don't relitigate) + +- Wire DTOs express enums as plain string-literal unions **inlined in the DTO file** — + they describe the wire, not the domain. The adapter's `parse*` maps them to domain types. +- The `Role` **type** moves out of infrastructure (to `shared/domain` or + `shared/application` — pick where its current consumers point most naturally); the + role-reading mechanism (`role.interceptor.ts` etc.) stays infrastructure. + +## Files + +- `eslint.config.mjs` — two new restrictions +- `src/app/registratie/contracts/dashboard-view.dto.ts` — remove domain imports +- `src/app/registratie/infrastructure/dashboard-view.adapter.ts` — absorb the mapping +- `src/app/registratie/ui/change-request-form/change-request-form.component.ts` — stop + injecting `ApiClient`; the component calls the application command, which owns the + client dependency (move the `inject` into `submit-change-request.ts` or a thin + registratie adapter, matching how other commands get the client) +- `src/app/shared/infrastructure/role.ts` + new home for the `Role` type + the + `letter-composer.component.ts` / `brief.store.ts` import sites + +## Steps + +1. Rewrite `dashboard-view.dto.ts` as pure wire shapes (no imports at all); update the + adapter's `parse*`/`toDomain` to map wire → domain; run the adapter spec. +2. Move the `Role` type; update import sites (mechanical; lint will find them). +3. Refactor change-request-form: UI keeps dispatching to the machine; the submit command + receives/owns the `ApiClient` (pattern: other `submit-*.ts` commands). +4. Add eslint zones: + - `src/app/**/contracts/**` may import nothing from `@angular/*`, any `@*` context + alias, or relative app code. + - `@shared/infrastructure/api-client` importable only from `**/infrastructure/**` + (+ the app config/provider and the generated file itself). +5. Lint the repo; there must be zero new disables. + +## Acceptance criteria + +- [ ] `dashboard-view.dto.ts` has no import statements. +- [ ] `grep -rn "api-client" src/app --include="*.ts" | grep -v infrastructure` returns + only the generated client itself (and its provider wiring, if outside infra). +- [ ] Both eslint rules active; a planted violation fails lint. +- [ ] Change-request flow works (existing machine/command specs pass). + +## Verification + +GREEN + `npm run test-storybook:ci`. Manual smoke: `npm start` → Gegevens wijzigen → +submit a change request. + +## Out of scope + +The `ui ↛ infrastructure` rule for adapter injections in wizards — that's WP-04. + +## Risks + +Role-type move ripples through brief imports — mechanical; run tests before/after. diff --git a/docs/backlog/WP-04-ui-not-infrastructure.md b/docs/backlog/WP-04-ui-not-infrastructure.md new file mode 100644 index 0000000..05c5400 --- /dev/null +++ b/docs/backlog/WP-04-ui-not-infrastructure.md @@ -0,0 +1,75 @@ +# WP-04 — Boundaries II: `ui ↛ infrastructure` + showcase sanction + +Status: todo +Phase: 0 — enforcement & gates + +## Why + +The documented rule `ui → application → domain` (CLAUDE.md §1) is not lint-enforced, and +three components inject infrastructure adapters directly. This is the biggest refactor of +Phase 0 — a pure move of wiring, no behavior change. + +## Read first + +- `CLAUDE.md` §1, `docs/ARCHITECTURE.md` +- `eslint.config.mjs` +- `src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts` (lines + ~18-19: `BrpAdapter`/`parseBrpAddress`, `DuoAdapter`/`parseDuoLookup`) +- `src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts` (~line 28: + `IntakePolicyAdapter`) +- `src/app/registratie/application/big-profile.store.ts` (the facade pattern to copy) +- `src/app/showcase/ui/concepts.page.ts` + +## Decisions (pre-made, don't relitigate) + +- **Showcase is sanctioned, not fixed**: `showcase/` is a teaching page whose point is + showing multiple contexts side by side. Add an explicit eslint override allowing + `@registratie/*` / `@herregistratie/*` from `src/app/showcase/**`, with a message + comment — same precedent as the existing `debug-state` exemption. Document in + ARCHITECTURE.md: "showcase may read every context; nothing imports showcase." +- Move wiring only — do not redesign adapter signatures. Existing machine specs are the + safety net. + +## Files + +- `eslint.config.mjs` — forbid `**/ui/**` and `**/layout/**` → `**/infrastructure/**`; + add the showcase sanction +- `src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts` — BRP/DUO + lookups move behind a registratie `application/` facade (command or store methods) +- `src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts` — policy resource + moves behind **new** `src/app/herregistratie/application/` (folder doesn't exist yet) +- `docs/ARCHITECTURE.md` — fix "three contexts, four layers" → six contexts + (shared, auth, registratie, herregistratie, brief, showcase), five layers (+ contracts); + add the showcase sanction +- `CLAUDE.md` §1 — add `brief` to the context list; note the showcase sanction + +## Steps + +1. Create the application-layer facades (pattern: `big-profile.store.ts` / + `submit-*.ts` commands); components inject those instead of adapters. +2. Add the eslint restriction; add the showcase override with rationale message. +3. Fix any further hits lint finds (`letter-composer`'s infra import is handled in + WP-03; if it slipped, fix here). +4. Update both docs' context/layer inventory. + +## Acceptance criteria + +- [ ] Rule active; lint green; no disables beyond the documented showcase + debug-state + exemptions. +- [ ] No `**/ui/**` file imports from `**/infrastructure/**`. +- [ ] Both wizards behave unchanged (specs pass; manual smoke). +- [ ] CLAUDE.md and ARCHITECTURE.md list 6 contexts / 5 layers. + +## Verification + +GREEN + `npm run test-storybook:ci`. Manual smoke: `npm start` → run the registratie +wizard (BRP prefill + DUO lookup steps) and the intake wizard end-to-end. + +## Out of scope + +Redesigning the adapters or resource loading; parse boundaries (WP-05). + +## Risks + +Resource wiring moving layers can subtly change injection timing — keep facades +`providedIn: 'root'` like existing stores and hand the resource through unchanged. diff --git a/docs/backlog/WP-05-parse-boundaries.md b/docs/backlog/WP-05-parse-boundaries.md new file mode 100644 index 0000000..35a7fee --- /dev/null +++ b/docs/backlog/WP-05-parse-boundaries.md @@ -0,0 +1,66 @@ +# WP-05 — Parse-don't-validate closure + MDX + +Status: todo +Phase: 1 — FP/DDD core + +## Why + +Three adapters trust the wire, contradicting the repo's flagship "parse, don't validate" +principle (every response through a hand-written `parse*` returning `Result`). + +## Read first + +- `CLAUDE.md` §3 + §4; `docs/architecture/0001-bff-lite-decision-dtos.md` +- `src/app/registratie/infrastructure/applications.adapter.ts` (+ its spec — the pattern + to copy) +- The three offenders below + +## Decisions (pre-made, don't relitigate) + +- Unknown enum-ish values from the wire map to an explicit `Failure` (never a silent + fallback); specs pin that behavior. +- The MDX curriculum page ships in this WP (the WP that establishes an invariant teaches + it). + +## Files + +- `src/app/herregistratie/infrastructure/intake-policy.adapter.ts` — returns the raw DTO + resource with no parse; add `parseIntakePolicy(raw: unknown): Result` and a + domain-side type +- `src/app/registratie/infrastructure/big-register.adapter.ts` (~line 25) — + `n.type as AantekeningType` → validated parse +- `src/app/brief/infrastructure/brief.adapter.ts` (~line 189) — `dto.scope as + PassageScope` → validated parse (the file is otherwise parse-heavy; this one field skips) +- New co-located specs: `intake-policy.adapter.spec.ts`, extend + `big-register.adapter.spec.ts` / `brief.adapter.spec.ts` (create if missing) +- New `src/docs/parse-dont-validate.mdx` — title `Foundations/Parse, don't validate` + +## Steps + +1. For each hole: write the small parser (pattern: `parseAanvraagStatus` in + `applications.adapter.ts`), wire it in, delete the cast. +2. Spec each parser: valid shape, missing field, unknown tag/enum value → `err`. +3. MDX page: the principle, value objects (`registratie/domain/value-objects/`), boundary + parsers, linking the three real call sites just fixed (before/after snippet). + +## Acceptance criteria + +- [ ] No unvalidated `as ` casts in `**/infrastructure/**` (the sanctioned + "narrow unknown to `Partial` then parse" entry-cast is fine). +- [ ] Each new parser has a spec including a rejection case. +- [ ] MDX renders under Foundations in Storybook. + +## Verification + +GREEN + `npm run test-storybook:ci`. Smoke: intake wizard still loads its policy +(`npm start` → /intake). + +## Out of scope + +Runtime validation on *every* endpoint (explicitly out of scope for the POC per +CLAUDE.md); `digid.adapter.ts` (faked auth, sanctioned). + +## Risks + +Seeded backend data containing values the new parsers reject → surfaces as `Failure` in +the UI; check the backend seed while writing the parsers and align the accepted set. diff --git a/docs/backlog/WP-06-typed-async.md b/docs/backlog/WP-06-typed-async.md new file mode 100644 index 0000000..97ef204 --- /dev/null +++ b/docs/backlog/WP-06-typed-async.md @@ -0,0 +1,62 @@ +# WP-06 — Generic async template contexts: kill `$any()` (18×) + +Status: todo +Phase: 1 — FP/DDD core + +## Why + +18 `$any()` casts in templates defeat strict template checking. Root cause for ~9 of +them: `AsyncLoadedDirective` types its template context as `{ $implicit: unknown }` +(`src/app/shared/ui/async/async.component.ts`), so every `` consumer must cast. The rest are template union-narrowing workarounds. + +## Read first + +- `src/app/shared/ui/async/async.component.ts` (component + directives) +- Consumers with `$any`: `src/app/registratie/ui/dashboard.page.ts`, + `registration-detail.page.ts`, `registration-summary/registration-summary.component.ts` + (×5, union peeking), `registratie-wizard.component.ts` (×4, step data), + `src/app/showcase/ui/concepts.page.ts` (×2) + +## Decisions (pre-made, don't relitigate) + +- Fix the root cause with generics + `static ngTemplateContextGuard`, not per-consumer + casts. +- Fallback (only if Angular's inference fights the `RemoteData | Resource` union + input): split into two typed inputs (`data` / `resource`) — record the swap here. + +## Files + +- `src/app/shared/ui/async/async.component.ts` — `AsyncComponent`; + `AsyncLoadedDirective` with + `static ngTemplateContextGuard(dir, ctx): ctx is { $implicit: T }` (same for the + failure directive's error type if applicable) +- Every `$any()` call site (grep `-rn '\$any(' src/app`) + +## Steps + +1. Make the async component/directives generic; keep the public API otherwise identical. +2. Remove the now-unneeded `$any()`s in async consumers. +3. Remaining union narrowing: replace with `@switch` on the status tag + (registration-summary) or small typed `computed()` getters (wizard step data, + showcase fake resource). +4. `npm run build` (strict templates) is the real check here. + +## Acceptance criteria + +- [ ] `grep -rn '\$any(' src/app` → zero hits. +- [ ] No `as` casts added to compensate in component classes (typed getters are fine). +- [ ] Build green with strict template checking. + +## Verification + +GREEN + `npm run test-storybook:ci`. Smoke: dashboard + registration detail render. + +## Out of scope + +Brief page's `` adoption (WP-07 — it depends on this WP's typing). + +## Risks + +Angular generic-component inference edge cases — the documented fallback keeps the WP +bounded. diff --git a/docs/backlog/WP-07-brief-idioms.md b/docs/backlog/WP-07-brief-idioms.md new file mode 100644 index 0000000..88d1c68 --- /dev/null +++ b/docs/backlog/WP-07-brief-idioms.md @@ -0,0 +1,70 @@ +# WP-07 — Brief on the shared idioms + RemoteData MDX + +Status: todo +Phase: 1 — FP/DDD core +Depends on: WP-06 (typed ``) + +## Why + +The brief context drifted from the repo's own reflexes: `brief.store.ts` (~lines 22-25) +holds `busy: signal` + `lastError: signal` + a separate `saveState` +union side by side — representable illegal combos, the exact "second boolean" smell +CLAUDE.md §3 bans. And `brief.page.ts` hand-renders its load lifecycle with `@switch` + +spinner/alert instead of `RemoteData` + `` — the only async flow in the app +bypassing the shared molecule. + +## Read first + +- `CLAUDE.md` §3; `src/app/shared/application/remote-data.ts` +- `src/app/brief/application/brief.store.ts`, `src/app/brief/ui/brief.page.ts`, + `src/app/brief/domain/brief.machine.ts` (+ spec) +- `src/app/registratie/application/applications.store.ts` (a store doing it right) + +## Decisions (pre-made, don't relitigate) + +- Transient submit/save state becomes **one** tagged union + (`Idle | Busy | Failed{error}`), replacing `busy`+`lastError`. `saveState` keeps its + union shape (align tag style). +- Load lifecycle → `RemoteData` + ``; the machine keeps owning the letter's + *domain* lifecycle (loading tags move out of the machine only if they purely mirror + the fetch — keep the seam: RemoteData = fetch, machine = letter). +- Keep the debounced-save sequencing identical; only re-type the state. + +## Files + +- `src/app/brief/application/brief.store.ts` +- `src/app/brief/ui/brief.page.ts` +- `src/app/brief/domain/brief.machine.ts` + `brief.machine.spec.ts` +- New `src/docs/remote-data.mdx` — title `Foundations/RemoteData & Async` + +## Steps + +1. Replace the signal trio with one union signal; update consumers (letter-composer bar, + autosave status line). +2. Route the page's load through `RemoteData` + `` (typed via WP-06); wire the + existing loading/empty/failure templates. +3. Update machine/store specs for the union transitions. +4. MDX page: the four states, `map2`/`andThen`, the delay-gated spinner, and the + `?scenario=` dev toggle — linking `brief.page.ts` and `dashboard.page.ts` as live + examples. + +## Acceptance criteria + +- [ ] No boolean-plus-error signal pairs in `brief/`. +- [ ] `/brief` renders all four async states (check with `?scenario=slow|empty|error`). +- [ ] Specs cover the transition union (Busy→Failed, Busy→Idle). +- [ ] MDX renders under Foundations. + +## Verification + +GREEN + `npm run test-storybook:ci`. Manual: `npm start` → `/brief` with +`?scenario=slow`, `?scenario=error`; exercise autosave + submit + rejection flow. + +## Out of scope + +Brief component stories (WP-15); machine renaming conventions (WP-08). + +## Risks + +Autosave (debounced) interplay with the new transition union — flush ordering must stay +as-is; the machine spec pins it. diff --git a/docs/backlog/WP-08-store-idiom.md b/docs/backlog/WP-08-store-idiom.md new file mode 100644 index 0000000..afe058e --- /dev/null +++ b/docs/backlog/WP-08-store-idiom.md @@ -0,0 +1,70 @@ +# WP-08 — One store idiom + machine naming + TEA MDX + +Status: todo +Phase: 1 — FP/DDD core + +## Why + +Docs (`docs/fp-tea-atomic-design.md`, ARCHITECTURE §2c) teach `createStore` as THE +wiring, but the wizard pages hand-wire `signal(model)` + local `dispatch()` — juniors see +two idioms and copy the wrong one. Machine naming also drifts: +`change-request.machine.ts` exports bare `State`/`Msg`; `upload.machine.ts` exports +`reduceUpload`/`initialUpload`. + +## Read first + +- `src/app/shared/application/store.ts` (`createStore`) + its spec +- `docs/fp-tea-atomic-design.md` Part on TEA-in-Angular +- All `*.machine.ts` files (6) and their UI wiring sites (wizard components/pages) + +## Decisions (pre-made, don't relitigate) + +- **`createStore` is THE idiom** — migrate the hand-wired pages to it. One shape + everywhere is the teachability win; the migration is ~5 lines per page. +- Naming convention (goes into CLAUDE.md §3): + - State/Msg types are context-prefixed: `ChangeRequestState`, `ChangeRequestMsg` (rename + the bare ones; other machines already comply). + - A top-level machine exports `initial` + `reduce`. + - A **composable sub-machine** (embedded in a parent model) keeps prefixed value + exports: `initialUpload`/`reduceUpload` **stay** — prefixing avoids alias noise at + the composition site. + +## Files + +- `src/app/registratie/domain/change-request.machine.ts` (+ spec + consumers) — rename +- Wizard UI wiring: `registratie-wizard.component.ts`, `intake-wizard.component.ts`, + `herregistratie-wizard.component.ts` (whichever hand-wire `signal`+dispatch) → + `createStore` +- `CLAUDE.md` §3 — the naming + wiring convention +- New `src/docs/machines.mdx` — title `Foundations/State Machines (TEA)` + +## Steps + +1. Rename `State`/`Msg` in change-request (mechanical; lint/build finds consumers). +2. Swap hand-wired model signals for `createStore(initial, reduce)` in the wizard + components; templates keep dispatching messages — no template changes expected. +3. Write the convention into CLAUDE.md §3 (including the sub-machine rule). +4. MDX page: Model/Msg/reduce, commands-outside-reducers, `createStore` wiring, the + naming rules — linking `herregistratie.machine.ts` as the worked example. + +## Acceptance criteria + +- [ ] `grep -rn "export type State\b\|export type Msg\b" src/app` → empty. +- [ ] Every machine consumer wires via `createStore`; no local `signal(model)` + + hand-rolled dispatch remains. +- [ ] Convention documented in CLAUDE.md; MDX renders. +- [ ] All machine specs pass unchanged (reducers untouched). + +## Verification + +GREEN + `npm run test-storybook:ci`. Smoke: all three wizards step forward/back and +submit. + +## Out of scope + +Changing reducer logic or messages; store specs for root stores (they're covered where +logic exists). + +## Risks + +Rename ripple — purely mechanical; reducer specs are the net. diff --git a/docs/backlog/WP-09-pure-logic.md b/docs/backlog/WP-09-pure-logic.md new file mode 100644 index 0000000..a419b55 --- /dev/null +++ b/docs/backlog/WP-09-pure-logic.md @@ -0,0 +1,72 @@ +# WP-09 — Pure-logic closure: dates + missing command specs + +Status: todo +Phase: 1 — FP/DDD core + +## Why + +Three hand-rolled copies of the same `nl-NL {day: numeric, month: long, year: numeric}` +formatter exist with divergent signatures, while other components use the Angular `date` +pipe — two mechanisms plus drift risk. And two application commands with real logic have +no spec despite "domain and pure logic must have a spec" (CLAUDE.md §5). + +## Read first + +- `src/app/registratie/domain/tasks.ts` (~line 15, `formatNL(d: Date)`) +- `src/app/registratie/ui/aanvraag-block/aanvraag-block.component.ts` (~line 96, + `formatNL(iso?: string)`) +- `src/app/brief/ui/letter-preview/letter-preview.component.ts` (~line 96, inline copy) +- `src/app/registratie/application/draft-sync.ts` (debounce logic) +- `src/app/registratie/application/submit-change-request.ts` +- `src/app/app.config.ts` (LOCALE_ID `nl` is already set) + +## Decisions (pre-made, don't relitigate) + +- **Templates use `DatePipe`; pure TS uses one `formatDatumNl`** in + `src/app/shared/kernel/datum.ts` (Intl.DateTimeFormat, accepts ISO string | Date, + empty-safe). Rule goes into CLAUDE.md conventions. +- Kernel naming is Dutch (`datum.ts`) — it formats for the Dutch UI; if that clashes with + the "shared = English" rule during implementation, `format-date-nl.ts` is the + alternative — pick one and note it here. + +## Files + +- New `src/app/shared/kernel/datum.ts` + `datum.spec.ts` +- The three formatter copies (delete, redirect to pipe or kernel fn) +- New `src/app/registratie/application/draft-sync.spec.ts` +- New `src/app/registratie/application/submit-change-request.spec.ts` +- `CLAUDE.md` conventions (one line: DatePipe in templates, `formatDatumNl` in pure TS) + +## Steps + +1. Write `formatDatumNl` + spec (pin exact expected strings, e.g. `2 juli 2026`; + undefined/empty → `''`). +2. Replace the three copies; templates that can use the pipe use the pipe. +3. `draft-sync.spec.ts` with vitest fake timers: coalescing (n rapid changes → 1 sync), + trailing call, flush-on-submit if applicable. +4. `submit-change-request.spec.ts`: ok path, error path (stubbed client), Result shape. +5. Also sweep the tiny dead exports flagged in the audit: remove unused `map3` from + `shared/kernel/fp.ts` (verify unused first) and the never-set `variant` input on + `confirmation.component.ts`. + +## Acceptance criteria + +- [ ] Exactly one hand-written date formatter in the repo; `grep -rn "toLocaleDateString" src/app` + hits only `datum.ts`. +- [ ] Both command specs exist; debounce coalescing + error path covered. +- [ ] `map3` / unused `variant` input removed (or a note here why kept). +- [ ] CLAUDE.md rule added. + +## Verification + +GREEN + `npm run test-storybook:ci`. + +## Out of scope + +`Result` combinators (map/andThen/fold) — nice-to-have from the old roadmap, not in this +backlog. + +## Risks + +Node ICU vs browser locale output differences — the spec pins strings; if CI's Node lacks +full ICU (it shouldn't on Node 24), pin via `Intl.DateTimeFormat('nl-NL', …)` explicitly. diff --git a/docs/backlog/WP-10-button-fidelity.md b/docs/backlog/WP-10-button-fidelity.md new file mode 100644 index 0000000..5af7c2b --- /dev/null +++ b/docs/backlog/WP-10-button-fidelity.md @@ -0,0 +1,64 @@ +# WP-10 — CIBG button fidelity + +Status: todo +Phase: 2 — CIBG fidelity + +## Why + +The vendored CIBG build ships `.btn-primary / .btn-secondary / .btn-danger / .btn-ghost / +.btn-link / .btn-link--underline / .btn-inline` — and **no** `.btn-outline-primary`, +`.btn-outline-secondary`, or `.btn-sm` (verified absent from +`public/cibg-huisstijl/css/huisstijl.min.css`). Three components emit those dead classes +and render as unstyled Bootstrap defaults instead of CIBG buttons. + +## Read first + +- https://designsystem.cibg.nl/componenten/knoppen/ (or grep the vendored CSS) +- `src/app/shared/ui/button/button.component.ts` (~line 14) +- `src/app/shared/ui/upload/file-input/file-input.component.ts` (~line 38) +- `src/app/shared/ui/rich-text-editor/rich-text-editor.component.ts` (toolbar, ~lines 55-60) + +## Decisions (pre-made, don't relitigate) + +- `variant='secondary'` → `.btn-secondary` (CIBG's documented secondary). +- RTE toolbar buttons → `.btn-ghost`; drop `.btn-sm` — if the toolbar needs a smaller + size, that's token-bridge CSS carrying the CIBG-gap marker (format per WP-13; define it + inline if WP-13 hasn't run). +- Add `'ghost'` to the atom's variant union (CIBG documents it; the RTE needs it). +- Icon affordances (chevron/pijl): implement **only if** icon classes exist in the + vendored CSS — inspect first; otherwise record as a gap in the WP-13 register and skip. + +## Files + +- `src/app/shared/ui/button/button.component.ts` + `button.stories.ts` +- `src/app/shared/ui/upload/file-input/file-input.component.ts` +- `src/app/shared/ui/rich-text-editor/rich-text-editor.component.ts` +- Any story/snapshot referencing the old classes + +## Steps + +1. Fix the variant map in the button atom; add `ghost`. +2. file-input label-button → `btn btn-secondary`. +3. RTE toolbar → `btn btn-ghost` (+ marked size CSS if needed). +4. Grep-check `btn-outline`/`btn-sm` gone repo-wide. +5. Storybook visual review of Button (all variants), FileInput, RichTextEditor against + designsystem.cibg.nl/componenten/knoppen. + +## Acceptance criteria + +- [ ] `grep -rn "btn-outline\|btn-sm" src/app` → empty. +- [ ] Button story shows all CIBG variants incl. ghost; visuals match the design system. +- [ ] Axe still green (contrast can change with real button styles). + +## Verification + +GREEN + `npm run test-storybook:ci`. Manual Storybook review of the three components. + +## Out of scope + +Other invented classes (WP-11); datablock (WP-12). + +## Risks + +Visual regressions where outline-secondary "worked by accident" via Bootstrap base rules +— the story review step is the catch. diff --git a/docs/backlog/WP-11-markup-fidelity.md b/docs/backlog/WP-11-markup-fidelity.md new file mode 100644 index 0000000..36c7e2d --- /dev/null +++ b/docs/backlog/WP-11-markup-fidelity.md @@ -0,0 +1,76 @@ +# WP-11 — CIBG markup fidelity: application-link + absent-class triage + +Status: todo +Phase: 2 — CIBG fidelity + +## Why + +`application-link.component.ts` invents `.application` / `.application-title` — absent +from the vendored CSS. The real "aanvragen" pattern styles +`.dashboard-block.applications li a` with `h3`/`.h3`, and `.subtitle/.status/.cta` +scoped **inside** `.applications`. The invented classes mean the rows only look right by +accident of parent scoping, and `.application-title` is dead. A few other components use +classes that don't exist in the vendored build. + +## Read first + +- https://designsystem.cibg.nl/componenten/aanvragen/ +- `public/cibg-huisstijl/css/huisstijl.min.css` (grep `.applications`, `.breadcrumb`, + error-summary candidates) +- `src/app/shared/ui/application-link/application-link.component.ts` + + `application-list/application-list.component.ts` +- `src/app/shared/layout/page-shell/page-shell.component.ts` (`.back`), + `wizard-shell/wizard-shell.component.ts` (`.es-list`/`.es-title`), + `src/app/shared/ui/upload/delivery-channel-toggle/delivery-channel-toggle.component.ts` + (`.radio-option`), `shared/layout/breadcrumb/breadcrumb.component.ts` + +## Decisions (pre-made, don't relitigate) + +- application-list/link: rework markup to the documented `.dashboard-block.applications` + scoping chain; keep the component API (`heading/subtitle/cta/to`) unchanged. +- For each remaining absent class: **adopt a vendored pattern if one exists, else mark as + CIBG-gap extension** (marker format per WP-13; define inline if WP-13 hasn't run). + This fallback is explicit so the WP can't stall on a missing upstream pattern. +- Wizard error summary: check whether CIBG's Foutmelding/Veldvalidatie pattern classes + are vendored; adopt if so, else mark as gap (the current behavior/a11y stays — + `role="alert"`, focus management). +- Breadcrumb: `.breadcrumb` IS vendored — verify usage against CIBG Kruimelpad and + annotate only; no rework expected. + +## Files + +- `application-link.component.ts`, `application-list.component.ts` (+ stories) +- `page-shell.component.ts`, `wizard-shell.component.ts`, + `delivery-channel-toggle.component.ts`, `breadcrumb.component.ts` (annotations/markers + or adopted patterns) +- `src/app/registratie/ui/dashboard.page.ts` (visual consumer — no code change expected) + +## Steps + +1. Rework application-list/link markup; delete `.application`/`.application-title`. +2. Grep the vendored CSS for Foutmelding/Veldvalidatie-ish classes; adopt or mark. +3. Triage `.back` and `.radio-option`: adopt or mark. +4. Verify breadcrumb; add the "wraps vendored `.breadcrumb` (CIBG Kruimelpad)" comment. +5. Eyeball the dashboard — a visual diff on "Wat wilt u doen?" is intended. + +## Acceptance criteria + +- [ ] `grep -rn '"application"\|application-title' src/app` → empty (component selector + aside). +- [ ] Every invented class either replaced by a vendored pattern or carries the gap + marker. +- [ ] Dashboard + wizard stories reviewed; axe green. + +## Verification + +GREEN + `npm run test-storybook:ci`. Manual: `npm start` → dashboard ("Wat wilt u +doen?" list) + a wizard validation error (error summary). + +## Out of scope + +Datablock adoption (WP-12); the full gap register (WP-13). + +## Risks + +The intended dashboard visual diff needs human eyes — flag it in the session summary for +the user. diff --git a/docs/backlog/WP-12-datablock.md b/docs/backlog/WP-12-datablock.md new file mode 100644 index 0000000..b389a57 --- /dev/null +++ b/docs/backlog/WP-12-datablock.md @@ -0,0 +1,79 @@ +# WP-12 — CIBG Datablock for application data + +Status: todo +Phase: 2 — CIBG fidelity + +## Why + +CIBG documents **Datablock** (designsystem.cibg.nl/componenten/datablock/) as THE way to +display user/application data: a grey `dl/dt/dd` container, `.data-block` + +`.block-wrapper`, Bootstrap grid for label/value widths, a `--stacked` variant, and +optional expandable help text (`.dt-item`, `.dt-help-btn-open/-close`, `.dt-help-panel`). +All classes are **verified present** in `public/cibg-huisstijl/css/huisstijl.min.css`. +The app currently renders application data with bare `data-row`s inside a custom card — +adopting Datablock makes the data views design-system-native. + +## Read first + +- https://designsystem.cibg.nl/componenten/datablock/ +- `public/cibg-huisstijl/css/huisstijl.min.css` (grep `.data-block`) +- `src/app/shared/ui/data-row/data-row.component.ts` +- `src/app/shared/ui/review-section/review-section.component.ts` (already uses + `.data-block`/`.block-wrapper` ad hoc) +- Consumers: `src/app/registratie/ui/dashboard.page.ts` ("Persoonsgegevens (BRP)" card), + `registration-summary/registration-summary.component.ts`, + `registration-detail.page.ts` + +## Decisions (pre-made, don't relitigate) + +- New **`data-block` molecule** in `shared/ui` wrapping the vendored classes: inputs for + optional heading and `stacked` variant; content = projected `data-row`s (or a typed + rows input — match how `review-section` projects today; prefer projection for + composability). +- `data-row` stays as the row primitive **inside** the datablock (document that role in + its header comment) — don't fork a second row component. +- `review-section` aligns onto the new molecule instead of hand-carrying the classes. +- Expandable help text (`.dt-item` + help button/panel) only if a real field needs it — + if built, follow CIBG's focus management (focus into panel on open, back to trigger on + close) and the documented `aria-*` state on the trigger. + +## Files + +- New `src/app/shared/ui/data-block/data-block.component.ts` + `data-block.stories.ts` +- `src/app/shared/ui/review-section/review-section.component.ts` +- `src/app/registratie/ui/dashboard.page.ts` (Persoonsgegevens card → datablock) +- `registration-summary.component.ts`, `registration-detail.page.ts` +- `src/app/shared/ui/data-row/data-row.component.ts` (header comment; possibly minor + class alignment) + +## Steps + +1. Build the molecule (standard + stacked variants), stories for both (axe-gated). +2. Migrate the three data views; keep copy/i18n ids untouched. +3. Fold review-section onto the molecule; delete its duplicated class carrying. +4. Optional `aria-label` on the `dl` where the block has no visible heading. +5. Visual review dashboard + registration pages. + +## Acceptance criteria + +- [ ] Application data (BRP card, registration summary/detail, wizard review) renders in + CIBG datablocks. +- [ ] `data-row` documented as the datablock row primitive; no duplicate dl styling + anywhere. +- [ ] Stories for standard + stacked; axe green. + +## Verification + +GREEN + `npm run test-storybook:ci`. Manual: `npm start` → dashboard + /registratie +detail; wizard review step still renders. + +## Out of scope + +The help-panel variant unless a real field needs it; table views (registration-table +stays a CIBG Tabel). + +## Risks + +The grey datablock surface may double up visually inside the custom `.app-card` — decide +per view whether the card wrapper drops (datablock replaces it) and note the choice in +the component comment. diff --git a/docs/backlog/WP-13-cibg-gap-register.md b/docs/backlog/WP-13-cibg-gap-register.md new file mode 100644 index 0000000..a532924 --- /dev/null +++ b/docs/backlog/WP-13-cibg-gap-register.md @@ -0,0 +1,79 @@ +# WP-13 — CIBG-gap register + hygiene + MDX + +Status: todo +Phase: 2 — CIBG fidelity + +## Why + +User decision: hand-rolled token-bridge components are allowed **only if explicitly +marked** as CIBG-gap extensions in code + Storybook, so every deviation from the design +system is auditable. Plus two hygiene items from the audit: a dead wrapper to delete and +the list-family rationale to document. + +## Read first + +- `docs/architecture/0003-*.md` (ADR-0003 — the token-bridge rationale this extends) +- The audit's gap list (below) + +## Decisions (pre-made, don't relitigate) + +- Marker format (header comment in the component): + `// CIBG-GAP EXTENSION: ` plus + `parameters: { cibgGap: true }` and a "CIBG-gap extension" line in the story + description. +- **Keep all three list families** — each maps a distinct CIBG pattern: `task-list` → + Actieblok, `application-list` → Aanvragen, `choice-list` → Keuzelijst. Header comment + in each names its pattern. Consolidation would blur exactly the design-system mapping + this showcase demonstrates. +- **Delete `upload-status-banner`** (`shared/ui/upload/upload-status-banner/`): a + 23-line near-identity wrapper over `alert` with one consumer — inline `` + there. + +## Files + +Components to mark (closest CIBG concept in parens): +- `skeleton`, `spinner` (Laadindicatie — no vendored class, verified) +- `upload/` suite (Bestand-upload) +- `rich-text-editor` (Tekstgebied) +- `wizard-shell` (Wizard; error summary → Foutmelding, if WP-11 marked it) +- `confirmation` (Bevestiging — verify: it wraps vendored classes; if so it needs no + marker, only the pattern comment) +- `debug-state` (devtool, no CIBG concept) +- `status-badge` (deliberate custom, documented in code), `card` (`.app-card`), + `placeholder-chip` +Plus: +- Delete `upload-status-banner` + its story; inline alert at its consumer +- Header comments on `task-list`/`application-list`/`choice-list` +- New `src/docs/cibg-gaps.mdx` — title `Foundations/CIBG Gap Register` +- ADR-0003 gets a cross-link to the register + +## Steps + +1. Add the marker to each gap component + `cibgGap` story parameter. +2. Delete the banner; migrate its one consumer; run its former story's states through + the consumer's story if coverage would drop. +3. Write the MDX register: a table (component → closest CIBG concept → why) + the rule + "grep the vendored CSS before hand-rolling anything". +4. Optional but cheap: a `check:cibg-gaps` script that greps markers and diffs against + the MDX table, so the register can't silently rot — add to `check:tokens`' CI step if + trivial, else note as skipped. + +## Acceptance criteria + +- [ ] Every component with hand-rolled surface CSS either wraps vendored classes or + carries the marker (spot-check with a grep for `styles: [` vs markers). +- [ ] Register MDX complete, linked from ADR-0003. +- [ ] `upload-status-banner` gone; consumer green; no story coverage lost. +- [ ] List trio documented. + +## Verification + +GREEN + `npm run test-storybook:ci`. + +## Out of scope + +Rebuilding any gap component against the design system (they're sanctioned by decision). + +## Risks + +Low — mostly annotation; the deletion is the only behavior-touching change. diff --git a/docs/backlog/WP-14-storybook-taxonomy.md b/docs/backlog/WP-14-storybook-taxonomy.md new file mode 100644 index 0000000..6a62d84 --- /dev/null +++ b/docs/backlog/WP-14-storybook-taxonomy.md @@ -0,0 +1,79 @@ +# WP-14 — Storybook taxonomy reorg + Layers MDX + +Status: todo +Phase: 3 — Storybook as curriculum + +## Why + +49 stories sit in a flat `Atoms/Molecules/Organisms/Templates/Layout` scheme with domain +outliers (`Registratie/…` wizards, one stray `Shared UI/Status Badge`). The sidebar +should tell the repo's actual story: a reusable design system on one side, domain +contexts on the other — the DDD seam made visible. The sidebar becomes the curriculum's +table of contents. + +## Read first + +- `.storybook/preview.ts`, `.storybook/main.ts` +- `src/docs/*.mdx` (three existing Foundations pages) +- `CLAUDE.md` naming rule (shared = English, domain = Dutch) + +## Decisions (pre-made, don't relitigate) + +Sidebar scheme (respects the English/Dutch seam): + +``` +Foundations/ ← MDX curriculum (all concept pages) +Design System/ + Atoms/… Molecules/… Organisms/… Templates/… ← everything in shared/ui + shared/layout +Domein/ + Registratie/… Herregistratie/… Brief/… Auth/… ← everything in a context's ui/ +``` + +- Rule: **a component in a context folder gets a `Domein//` title, full stop** + (context organisms like `aanvraag-block` move from `Organisms/` to + `Domein/Registratie/`). Component display names stay as authored (the folder tells the + context). +- `Devtools/` (debug-state) files under `Design System/Devtools/`. +- Order pinned via `parameters.options.storySort`: + `['Foundations', 'Design System', ['Atoms','Molecules','Organisms','Templates','Devtools'], 'Domein']`. + +## Files + +- All `*.stories.ts` `title:` fields (~49, mechanical) +- `.storybook/preview.ts` (`storySort`) +- Existing `src/docs/*.mdx` (fix any story references broken by retitling) +- New `src/docs/layers.mdx` — title `Foundations/Layers & Contexts` +- `CLAUDE.md` — the story-title convention (one paragraph in §2 or Conventions) + +## Steps + +1. Retitle all stories per the scheme (fix `Shared UI/Status Badge`). +2. Add `storySort`; build Storybook and check the sidebar. +3. `layers.mdx`: 6 contexts, 5 layers, the inward dependency rule, "this is enforced" (link + the eslint rules from WP-03/04), the English/Dutch seam — i.e. the sidebar's own + rationale, with links into real stories on both sides. +4. Check every MDX ``/story import still resolves (build-storybook + catches broken refs — do an explicit pass anyway). +5. Convention into CLAUDE.md. + +## Acceptance criteria + +- [ ] Sidebar shows exactly Foundations → Design System → Domein with the sub-order + pinned. +- [ ] Zero story titles outside the scheme (grep `title:` and eyeball). +- [ ] `layers.mdx` renders; existing MDX pages unbroken. +- [ ] Convention in CLAUDE.md. + +## Verification + +GREEN + `npm run test-storybook:ci` (the axe run also re-visits every retitled story). + +## Out of scope + +New component stories (WP-15); the remaining curriculum pages (they ship with their +invariant-WPs: 05, 07, 08, 13, 17). + +## Risks + +MDX story references breaking on retitle — `build-storybook` is the catch; fix-forward +in the same session. diff --git a/docs/backlog/WP-15-missing-stories.md b/docs/backlog/WP-15-missing-stories.md new file mode 100644 index 0000000..6c928f3 --- /dev/null +++ b/docs/backlog/WP-15-missing-stories.md @@ -0,0 +1,64 @@ +# WP-15 — Missing stories: shell + brief components + +Status: todo +Phase: 3 — Storybook as curriculum +Depends on: WP-14 (titles), WP-01 (axe gate covers the new stories automatically) + +## Why + +"UI is exercised via Storybook stories" (CLAUDE.md §5) — but 7 components have none: +`shared/layout/shell` and six of seven brief components. Unstoried components are also +invisible to the axe gate. + +## Read first + +- `src/app/brief/ui/letter-composer/letter-composer.stories.ts` (the one brief story — + fixture + decorator pattern to reuse) +- The seven components below + +## Decisions (pre-made, don't relitigate) + +- Titles per WP-14 scheme: `Design System/Templates/Shell`, `Domein/Brief/`. +- If fixture duplication across brief stories bites, extract a shared + `src/app/brief/ui/brief.fixtures.ts` — otherwise keep fixtures co-located. + +## Files (new) + +- `src/app/shared/layout/shell/shell.stories.ts` +- `src/app/brief/ui/rejection-comments/rejection-comments.stories.ts` +- `src/app/brief/ui/diagnostics-panel/diagnostics-panel.stories.ts` +- `src/app/brief/ui/letter-block/letter-block.stories.ts` +- `src/app/brief/ui/letter-preview/letter-preview.stories.ts` +- `src/app/brief/ui/letter-section/letter-section.stories.ts` +- `src/app/brief/ui/passage-picker/passage-picker.stories.ts` + +## Steps + +1. One story file per component: default state + at least one meaningful variant + (e.g. letter-block locked/editable, diagnostics with findings/empty, rejection-comments + filled, preview with sample data toggle). +2. Provide router/HTTP/store context via `applicationConfig`/`moduleMetadata` decorators + like the wizard stories do. +3. Run the axe gate; fix trivial violations in the components (label/role level); anything + structural → escape hatch + cross-ref WP-16. + +## Acceptance criteria + +- [ ] Every component in `src/app` has ≥1 story (verify: list components without a + co-located `*.stories.ts`; expect zero, pages excepted if that's the existing + norm — note the norm in this file when checked). +- [ ] All new stories pass the axe gate (or carry a justified skip). +- [ ] Titles follow WP-14. + +## Verification + +GREEN + `npm run test-storybook:ci`. + +## Out of scope + +Play-test assertions (WP-16 adds those where they enforce a contract). + +## Risks + +Brief components may lean on `BriefStore` state — stub via the store's public API in a +decorator rather than reaching into internals. diff --git a/docs/backlog/WP-16-component-a11y.md b/docs/backlog/WP-16-component-a11y.md new file mode 100644 index 0000000..a96726d --- /dev/null +++ b/docs/backlog/WP-16-component-a11y.md @@ -0,0 +1,76 @@ +# WP-16 — Component a11y: description wiring + alert role + +Status: todo +Phase: 4 — a11y + +## Why + +Audit findings axe can't (fully) catch: +- `form-field` renders a description `
` that **no control + ever references** — `text-input` sets `aria-describedby` only to `…-error` and only + when invalid. Screen readers never announce field descriptions (e.g. the BSN hint on + the login form). +- The field↔control id pairing is manual (`fieldId` must equal the input's `name`/`id`) + with nothing enforcing it. +- The `alert` atom is always `role="status"` — error alerts are announced politely while + other errors in the app use `role="alert"`; urgency is inconsistent. + +## Read first + +- `src/app/shared/ui/form-field/form-field.component.ts` (~line 15) +- `src/app/shared/ui/text-input/text-input.component.ts` (~lines 17-18) +- `src/app/shared/ui/radio-group/radio-group.component.ts`, `checkbox/checkbox.component.ts` +- `src/app/shared/ui/alert/alert.component.ts` +- `src/app/auth/ui/login-form/login-form.component.ts` (a live desc that's never wired) + +## Decisions (pre-made, don't relitigate) + +- `aria-describedby` = space-joined ids: `-desc` **always when a description exists** + + `-error` **when invalid**; order pinned (desc first, error second). +- Pairing contract: form-field exposes its `fieldId`; the composition contract + (`fieldId === control id/name`) is documented in both components and **enforced by a + story play test** on the canonical form-field+text-input composition + (`expect(input).toHaveAttribute('aria-describedby', 'x-desc')`, flip validity, assert + `'x-desc x-error'`). Play tests run in the WP-01 test-runner for free. +- DI-based auto-wiring (form-field providing the id via injection) is **out of scope** — + more clever than this POC needs; the play test catches drift. Revisit only if the + manual contract actually breaks in practice. +- Alert: `type === 'error'` → `role="alert"`; others keep `role="status"`. Rationale + comment in the atom. + +## Files + +- `form-field.component.ts`, `text-input.component.ts`, `radio-group.component.ts`, + `checkbox.component.ts` (describedby joins; only where the component takes a hint) +- `alert.component.ts` (+ story asserting the role per variant) +- `form-field.stories.ts` (or a composition story) with the play test +- Call sites that pass descriptions (verify login-form BSN hint is now announced) + +## Steps + +1. Implement the describedby join in the input atoms; form-field renders `-desc` only + when a description input is set (it already does — verify). +2. Write the play tests (form-field composition + alert roles). +3. Alert role switch + rationale comment. +4. Manual screen-reader spot check (optional but recommended — note result here). + +## Acceptance criteria + +- [ ] Description text is programmatically associated in the canonical composition; + login-form BSN hint announced. +- [ ] `-error` id appended exactly when invalid; order stable. +- [ ] Error alerts are `role="alert"`; play tests assert both behaviors and run in the + CI gate. + +## Verification + +GREEN + `npm run test-storybook:ci` (includes the new play tests). + +## Out of scope + +Route-change focus and template lint (WP-17); rewriting form-field's layout. + +## Risks + +`aria-describedby` churn on validity flips re-announcing content — pinned order + play +test coverage keeps it deterministic. diff --git a/docs/backlog/WP-17-app-a11y.md b/docs/backlog/WP-17-app-a11y.md new file mode 100644 index 0000000..35941b3 --- /dev/null +++ b/docs/backlog/WP-17-app-a11y.md @@ -0,0 +1,82 @@ +# WP-17 — App-level a11y: route focus/scroll, template lint, WCAG checklist + MDX + +Status: todo +Phase: 4 — a11y + +## Why + +Three app-level gaps close the WCAG story: +- **No route-change focus/scroll management** — `app.config.ts` has only + `provideRouter(routes, withViewTransitions())`; after navigation, focus stays wherever + it was and scroll position is unmanaged. (The wizard manages focus *within* steps; the + skip link is the only cross-page mechanism.) +- **No template a11y linting** — `@angular-eslint` is entirely absent. +- User decision: a **manual WCAG checklist** documents what automation can't test. + +## Read first + +- `src/app/app.config.ts` +- `eslint.config.mjs`, `package.json` +- `src/app/shared/layout/shell/shell.component.ts` (`#main` target), + `page-shell/page-shell.component.ts` (candidate focus target) + +## Decisions (pre-made, don't relitigate) + +- Scroll: `withInMemoryScrolling({ scrollPositionRestoration: 'enabled' })`. +- Focus: on `NavigationEnd`, move focus to the new page's `h1` (or `#main` fallback, + `tabindex="-1"`), **deferred via `afterNextRender`** so it coexists with view + transitions; skip the initial load. Implement as a small root service/effect wired in + `app.config.ts` — not per page. +- Lint: `angular-eslint` at the version matching Angular 22 (~v21+ — check + `npm view angular-eslint peerDependencies`), with `processInlineTemplates` (this repo + has **no** `.html` templates). Rules: `alt-text`, `label-has-associated-control`, + `interactive-supports-focus`, `click-events-have-key-events`, `valid-aria`, + `elements-content`. **Verify the processor fires** by planting a known-bad pattern + before trusting green. +- The checklist is a living doc with per-page status columns — not prose. + +## Files + +- `src/app/app.config.ts` + a new small `shared/` focus-on-navigation service +- `eslint.config.mjs`, `package.json` (angular-eslint) +- Any template the new lint rules flag +- New `docs/wcag-checklist.md` +- New `src/docs/a11y.mdx` — title `Foundations/Accessibility` + +## Steps + +1. Router scroll + focus service; manual keyboard smoke (tab through, navigate, confirm + focus lands on the new page's heading). +2. Add angular-eslint; enable the template rules on inline templates; plant a bad + pattern, see it fail, remove it; fix real hits. +3. `docs/wcag-checklist.md`: manual checks per page (dashboard, wizards, brief, login) — + keyboard walk & focus order, no traps, 200% zoom/reflow, NVDA or VoiceOver pass, + visible focus, error announcement; status columns (page × check). +4. `src/docs/a11y.mdx`: the layered approach — axe gate (WP-01) + template lint + play + tests (WP-16) + manual checklist; the alert-role rationale; where the + `a11y.disable` skip register lives and its rules. + +## Acceptance criteria + +- [ ] Navigating between routes moves focus to the new page's heading; scroll resets; + view transitions still play. +- [ ] Template a11y rules active and *proven* to fire; lint green. +- [ ] Checklist checked in with an initial pass filled in for the dashboard at minimum. +- [ ] `a11y.mdx` renders; links to checklist and WP-01 skip rules. + +## Verification + +GREEN + `npm run test-storybook:ci`. Manual: keyboard-only walk of +login → dashboard → wizard → brief. + +## Out of scope + +Full manual audit of every page (the checklist enables it; filling it beyond the +dashboard is ongoing work, not this WP). + +## Risks + +- angular-eslint's inline-template processor silently not running → the planted-violation + step is mandatory. +- Focus-move vs `withViewTransitions` timing → `afterNextRender` + test in a real browser + (`npm start`), not just unit-level.