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.
This commit is contained in:
@@ -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<E,T>`** (`src/app/shared/application/remote-data.ts`) — `map`/`map2`/
|
||||
`map3`/`andThen`/`fromResource`/`foldRemote`, rendered via the `<app-async>` 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<T>` 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.
|
||||
|
||||
83
docs/backlog/README.md
Normal file
83
docs/backlog/README.md
Normal file
@@ -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
|
||||
`<app-async>` 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 (<commit>)
|
||||
Phase: N — name
|
||||
|
||||
## Why
|
||||
## Read first
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
## Files
|
||||
## Steps
|
||||
## Acceptance criteria
|
||||
## Verification
|
||||
## Out of scope
|
||||
## Risks
|
||||
```
|
||||
80
docs/backlog/WP-01-axe-ci-gate.md
Normal file
80
docs/backlog/WP-01-axe-ci-gate.md
Normal file
@@ -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.
|
||||
67
docs/backlog/WP-02-check-tokens.md
Normal file
67
docs/backlog/WP-02-check-tokens.md
Normal file
@@ -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.
|
||||
76
docs/backlog/WP-03-contracts-purity.md
Normal file
76
docs/backlog/WP-03-contracts-purity.md
Normal file
@@ -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.
|
||||
75
docs/backlog/WP-04-ui-not-infrastructure.md
Normal file
75
docs/backlog/WP-04-ui-not-infrastructure.md
Normal file
@@ -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.
|
||||
66
docs/backlog/WP-05-parse-boundaries.md
Normal file
66
docs/backlog/WP-05-parse-boundaries.md
Normal file
@@ -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<string, …>` 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 <DomainType>` casts in `**/infrastructure/**` (the sanctioned
|
||||
"narrow unknown to `Partial<Dto>` 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.
|
||||
62
docs/backlog/WP-06-typed-async.md
Normal file
62
docs/backlog/WP-06-typed-async.md
Normal file
@@ -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 `<ng-template appAsyncLoaded
|
||||
let-p>` 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<E,T> | Resource<T>` union
|
||||
input): split into two typed inputs (`data` / `resource`) — record the swap here.
|
||||
|
||||
## Files
|
||||
|
||||
- `src/app/shared/ui/async/async.component.ts` — `AsyncComponent<T>`;
|
||||
`AsyncLoadedDirective<T>` with
|
||||
`static ngTemplateContextGuard<T>(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 `<app-async>` adoption (WP-07 — it depends on this WP's typing).
|
||||
|
||||
## Risks
|
||||
|
||||
Angular generic-component inference edge cases — the documented fallback keeps the WP
|
||||
bounded.
|
||||
70
docs/backlog/WP-07-brief-idioms.md
Normal file
70
docs/backlog/WP-07-brief-idioms.md
Normal file
@@ -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 `<app-async>`)
|
||||
|
||||
## Why
|
||||
|
||||
The brief context drifted from the repo's own reflexes: `brief.store.ts` (~lines 22-25)
|
||||
holds `busy: signal<boolean>` + `lastError: signal<string|null>` + 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` + `<app-async>` — 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` + `<app-async>`; 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` + `<app-async>` (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.
|
||||
70
docs/backlog/WP-08-store-idiom.md
Normal file
70
docs/backlog/WP-08-store-idiom.md
Normal file
@@ -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.
|
||||
72
docs/backlog/WP-09-pure-logic.md
Normal file
72
docs/backlog/WP-09-pure-logic.md
Normal file
@@ -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.
|
||||
64
docs/backlog/WP-10-button-fidelity.md
Normal file
64
docs/backlog/WP-10-button-fidelity.md
Normal file
@@ -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.
|
||||
76
docs/backlog/WP-11-markup-fidelity.md
Normal file
76
docs/backlog/WP-11-markup-fidelity.md
Normal file
@@ -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.
|
||||
79
docs/backlog/WP-12-datablock.md
Normal file
79
docs/backlog/WP-12-datablock.md
Normal file
@@ -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.
|
||||
79
docs/backlog/WP-13-cibg-gap-register.md
Normal file
79
docs/backlog/WP-13-cibg-gap-register.md
Normal file
@@ -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: <closest CIBG concept> — <why hand-rolled>` 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 `<app-alert>`
|
||||
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.
|
||||
79
docs/backlog/WP-14-storybook-taxonomy.md
Normal file
79
docs/backlog/WP-14-storybook-taxonomy.md
Normal file
@@ -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/<Context>/` 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 `<Canvas of={…}>`/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.
|
||||
64
docs/backlog/WP-15-missing-stories.md
Normal file
64
docs/backlog/WP-15-missing-stories.md
Normal file
@@ -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/<Name>`.
|
||||
- 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.
|
||||
76
docs/backlog/WP-16-component-a11y.md
Normal file
76
docs/backlog/WP-16-component-a11y.md
Normal file
@@ -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 `<div [id]="fieldId()+'-desc'">` 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.
|
||||
82
docs/backlog/WP-17-app-a11y.md
Normal file
82
docs/backlog/WP-17-app-a11y.md
Normal file
@@ -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.
|
||||
Reference in New Issue
Block a user