style: format frontend, docs and skills with prettier; add .prettierignore
One-time prettier --write so the new format:check CI gate starts green. .prettierignore excludes generated (api-client.ts, documentation.json), vendored (public/cibg-huisstijl), and backend (dotnet format owns it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,13 +20,13 @@ backend team.
|
||||
|
||||
## Options considered
|
||||
|
||||
| Option | Fewer calls? | Unifies policy? | Cost |
|
||||
|---|---|---|---|
|
||||
| 1. Status quo (client calls upstreams, aggregates, owns rules) | No | No | — |
|
||||
| 2. Unified client API layer (one facade in the FE) | No — still N round-trips | No — rules still on client | Low, but misses the goal |
|
||||
| **3. Screen-shaped endpoints on our own backend ("BFF-lite")** | **Yes** — 1 call/screen | **Yes** — server computes decisions | Low–medium |
|
||||
| 4. Separately-deployed BFF service | Yes | Yes | Medium — another deployable |
|
||||
| 5. GraphQL gateway | Yes (client picks fields) | **No, not by itself** — still need resolvers to own rules | Medium–high; new infra |
|
||||
| Option | Fewer calls? | Unifies policy? | Cost |
|
||||
| -------------------------------------------------------------- | ------------------------- | --------------------------------------------------------- | --------------------------- |
|
||||
| 1. Status quo (client calls upstreams, aggregates, owns rules) | No | No | — |
|
||||
| 2. Unified client API layer (one facade in the FE) | No — still N round-trips | No — rules still on client | Low, but misses the goal |
|
||||
| **3. Screen-shaped endpoints on our own backend ("BFF-lite")** | **Yes** — 1 call/screen | **Yes** — server computes decisions | Low–medium |
|
||||
| 4. Separately-deployed BFF service | Yes | Yes | Medium — another deployable |
|
||||
| 5. GraphQL gateway | Yes (client picks fields) | **No, not by itself** — still need resolvers to own rules | Medium–high; new infra |
|
||||
|
||||
GraphQL solves over/under-fetching but does not, on its own, move rules
|
||||
server-side — and our problem is policy unification + drift, not field-selection
|
||||
@@ -40,7 +40,7 @@ them.** Keep it minimal: implement BFF-shaped endpoints on the backend we alread
|
||||
own. Promote to a separately-deployed BFF service only when a second consumer
|
||||
(mobile/partner) or a team boundary demands it — not before.
|
||||
|
||||
### Why DTOs *decouple* rather than couple
|
||||
### Why DTOs _decouple_ rather than couple
|
||||
|
||||
The coupling people fear comes from **not** having DTOs — i.e. serializing internal
|
||||
DB/domain entities straight onto the wire, so every schema change ripples to the
|
||||
@@ -53,7 +53,7 @@ DB entity / domain model → DTO (the wire contract) → FE view model
|
||||
|
||||
Each side keeps its own internal model and refactors freely; only the DTO is a
|
||||
deliberate, versioned change. The one coupling that remains — both sides agreeing
|
||||
on the contract — is the *wanted*, reviewable seam. Manage it with **one source of
|
||||
on the contract — is the _wanted_, reviewable seam. Manage it with **one source of
|
||||
truth** (OpenAPI or TypeSpec) that **generates types for both sides**. That spec is
|
||||
the governance/transparency artifact.
|
||||
|
||||
@@ -76,6 +76,7 @@ This POC has no real backend (static mock JSON + fake submit timers), so the
|
||||
would compute. Two slices were implemented to demonstrate **both** policy shapes:
|
||||
|
||||
**A. Dashboard profile → one aggregated, decision-enriched call (decision-flag).**
|
||||
|
||||
- Contract: `src/app/registratie/contracts/dashboard-view.dto.ts`
|
||||
(`DashboardViewDto` = registration + person + `decisions`).
|
||||
- Endpoint: `public/mock/dashboard-view.json` (one call replaces three).
|
||||
@@ -92,6 +93,7 @@ would compute. Two slices were implemented to demonstrate **both** policy shapes
|
||||
`brp.json`) were deleted — those calls live behind the BFF now.
|
||||
|
||||
**B. Intake scholing threshold → config value.**
|
||||
|
||||
- Contract: `src/app/herregistratie/contracts/intake-policy.dto.ts`.
|
||||
- Endpoint: `public/mock/intake-policy.json` (`{ "scholingThreshold": 1000 }`).
|
||||
- `intake.machine.ts`: the hardcoded `LAGE_UREN_DREMPEL` constant is gone;
|
||||
|
||||
@@ -7,7 +7,7 @@ Status: Proposed · Date: 2026-07-01
|
||||
Today the app knows exactly one actor. `auth/domain/session.ts` is a flat
|
||||
`Session { bsn, naam }`, authentication is a faked DigiD flow, and the backend has no
|
||||
role model at all (only an `X-Admin: true` header seam in `Program.cs` and a stringly-typed
|
||||
`Actor` on audit entries). This whole repo *is* the **Zorgverlener** self-service portal (SSP).
|
||||
`Actor` on audit entries). This whole repo _is_ the **Zorgverlener** self-service portal (SSP).
|
||||
|
||||
We now need a second user group — **Behandelaar** (backoffice: assessing and deciding on
|
||||
applications) — and want room for others later (admin, auditor, institution rep). The question
|
||||
@@ -27,11 +27,11 @@ Confirmed constraints (with the product owner):
|
||||
|
||||
## Options considered
|
||||
|
||||
| Option | Ubiquitous language respected? | Coupling | Verdict |
|
||||
|---|---|---|---|
|
||||
| 1. Split contexts **by role** (`zorgverlener/`, `behandelaar/` folders) | No — role ≠ capability; features smear across personas | High | Reject |
|
||||
| 2. One catch-all **`users`/`identity`** context owning everything about people | No — becomes a god-context; mixes identity, authz, and features | High | Reject |
|
||||
| 3. **Actors are personas; contexts are capabilities; identity is typed** | Yes | Low | **Adopt** |
|
||||
| Option | Ubiquitous language respected? | Coupling | Verdict |
|
||||
| ------------------------------------------------------------------------------ | --------------------------------------------------------------- | -------- | --------- |
|
||||
| 1. Split contexts **by role** (`zorgverlener/`, `behandelaar/` folders) | No — role ≠ capability; features smear across personas | High | Reject |
|
||||
| 2. One catch-all **`users`/`identity`** context owning everything about people | No — becomes a god-context; mixes identity, authz, and features | High | Reject |
|
||||
| 3. **Actors are personas; contexts are capabilities; identity is typed** | Yes | Low | **Adopt** |
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -42,9 +42,9 @@ Confirmed constraints (with the product owner):
|
||||
|
||||
The same real-world thing is described in two different languages:
|
||||
|
||||
- **Zelfbediening (SSP)** — the Zorgverlener: *"ik vraag herregistratie aan"* — eligibility, fill in
|
||||
- **Zelfbediening (SSP)** — the Zorgverlener: _"ik vraag herregistratie aan"_ — eligibility, fill in
|
||||
my data, upload documents, submit. **This repo.**
|
||||
- **Behandeling (backoffice)** — the Behandelaar: *"ik beoordeel de aanvraag"* — werkvoorraad,
|
||||
- **Behandeling (backoffice)** — the Behandelaar: _"ik beoordeel de aanvraag"_ — werkvoorraad,
|
||||
beoordeling, besluit, meer-info-opvragen, SLA, audit. **A sibling application**, not a folder here.
|
||||
|
||||
Diverging verbs over the same noun is the textbook signal for **two bounded contexts**.
|
||||
@@ -52,9 +52,9 @@ Diverging verbs over the same noun is the textbook signal for **two bounded cont
|
||||
### 2. The aggregate is owned by the backend; the contexts integrate through it
|
||||
|
||||
The aanvraag/registration is the **system of record in the backend domain**. Neither frontend owns
|
||||
it. They integrate *through the backend* using the **BFF-lite decision DTOs of ADR-0001** — the same
|
||||
aggregate projected into two screen-shaped views. The **aanvraag status lifecycle** is the *published
|
||||
contract* between the two contexts:
|
||||
it. They integrate _through the backend_ using the **BFF-lite decision DTOs of ADR-0001** — the same
|
||||
aggregate projected into two screen-shaped views. The **aanvraag status lifecycle** is the _published
|
||||
contract_ between the two contexts:
|
||||
|
||||
```
|
||||
Ingediend → In behandeling → (Meer info gevraagd ⇄) → Goedgekeurd / Afgewezen
|
||||
@@ -93,7 +93,7 @@ These are two concerns people habitually conflate; keeping them apart is the cru
|
||||
|
||||
```ts
|
||||
type Principal =
|
||||
| { kind: 'zorgverlener'; bsn: string; naam: string } // DigiD/BSN
|
||||
| { kind: 'zorgverlener'; bsn: string; naam: string } // DigiD/BSN
|
||||
| { kind: 'medewerker'; medewerkerId: string; naam: string; rollen: Rol[] }; // employee SSO
|
||||
```
|
||||
|
||||
@@ -102,14 +102,14 @@ These are two concerns people habitually conflate; keeping them apart is the cru
|
||||
second actor arrives.
|
||||
|
||||
- **Authorization — "what may you do"** → enforced at the **backend / context boundary**, where the
|
||||
backend is the authority (per ADR-0001). It is *not* a permission matrix living in `auth`. The
|
||||
backend is the authority (per ADR-0001). It is _not_ a permission matrix living in `auth`. The
|
||||
frontend receives only the decisions it needs to render (e.g. a `canBeoordelen` flag), exactly like
|
||||
every other server-owned rule.
|
||||
|
||||
### 4. "Other users" slot in without inventing contexts
|
||||
|
||||
Admin, auditor, institution-rep are additional **`Principal` variants** or additional **`rollen` on
|
||||
`medewerker`** — never a new folder-per-role. A genuinely new *bounded context* is warranted only when
|
||||
`medewerker`** — never a new folder-per-role. A genuinely new _bounded context_ is warranted only when
|
||||
an actor brings a new **language and capability** (e.g. an "Toezicht/Handhaving" enforcement context),
|
||||
not merely a new login.
|
||||
|
||||
@@ -121,7 +121,7 @@ not merely a new login.
|
||||
`authGuard`/`SessionStore` seams already localise that (`auth.guard.ts`, `session.store.ts`).
|
||||
- The backend becomes the authority for the **aanvraag status lifecycle** and for **authorization**,
|
||||
publishing both as decision DTOs — a natural extension of ADR-0001, not a new pattern.
|
||||
- `pendingHerregistratie` is understood as a *temporary stand-in* for a real, backend-owned status.
|
||||
- `pendingHerregistratie` is understood as a _temporary stand-in_ for a real, backend-owned status.
|
||||
|
||||
## Out of scope here (next steps, not built)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ layer — not a palette swap.
|
||||
references resolve at runtime. Storybook serves the same via `staticDirs`.
|
||||
2. **Token bridge over token rewrite.** `src/styles.scss` redefines the app's ~54 `--rhc-*` tokens
|
||||
onto CIBG values (`--bs-*` where one exists, CIBG palette hex otherwise). The `--rhc-*` names are
|
||||
now an internal alias set; the *values* are CIBG. This avoided rewriting 300+ token references and
|
||||
now an internal alias set; the _values_ are CIBG. This avoided rewriting 300+ token references and
|
||||
keeps the "components reference tokens" convention intact. (`styles.scss` is exempt from
|
||||
`check:tokens`, so palette hex lives in that one file only.)
|
||||
3. **Re-skin atoms, keep their `input()` APIs.** Each `shared/ui` atom now emits Bootstrap/CIBG classes
|
||||
@@ -42,7 +42,7 @@ layer — not a palette swap.
|
||||
(`public/` already copied), and `.storybook/` — plus the class strings in ~40 `shared/ui` +
|
||||
`shared/layout` + a few domain components. The `@rijkshuisstijl-community/*` deps are dropped.
|
||||
- `check:tokens` still guards raw hex in components; the token bridge + hand-rolled surfaces comply.
|
||||
- Known benign build warning: *"Unable to locate stylesheet: /cibg-huisstijl/css/huisstijl.min.css"* —
|
||||
- Known benign build warning: _"Unable to locate stylesheet: /cibg-huisstijl/css/huisstijl.min.css"_ —
|
||||
Angular's index optimizer doesn't process a `public/` stylesheet at build time. The asset is copied
|
||||
and the link is preserved (verified: served 200, `.btn-primary` present); the build exits green. The
|
||||
alternative (adding the CSS to `angular.json` `styles`) would force-bundle the licensed fonts we
|
||||
|
||||
@@ -38,25 +38,25 @@ only needs re-running if a WP unexpectedly touches `backend/`.
|
||||
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 | done |
|
||||
| [WP-02](WP-02-check-tokens.md) | Harden `check:tokens` + fix what it catches | 0 · gates | done |
|
||||
| [WP-03](WP-03-contracts-purity.md) | Boundaries I: contracts purity + ApiClient confinement | 0 · gates | done |
|
||||
| [WP-04](WP-04-ui-not-infrastructure.md) | Boundaries II: `ui ↛ infrastructure` + showcase sanction | 0 · gates | done |
|
||||
| [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 | done |
|
||||
| [WP-12](WP-12-datablock.md) | CIBG Datablock for application data | 2 · CIBG | done |
|
||||
| [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 |
|
||||
| WP | Title | Phase | Status |
|
||||
| --------------------------------------- | ------------------------------------------------------------ | ------------- | ------ |
|
||||
| [WP-01](WP-01-axe-ci-gate.md) | Axe-on-every-story CI gate | 0 · gates | done |
|
||||
| [WP-02](WP-02-check-tokens.md) | Harden `check:tokens` + fix what it catches | 0 · gates | done |
|
||||
| [WP-03](WP-03-contracts-purity.md) | Boundaries I: contracts purity + ApiClient confinement | 0 · gates | done |
|
||||
| [WP-04](WP-04-ui-not-infrastructure.md) | Boundaries II: `ui ↛ infrastructure` + showcase sanction | 0 · gates | done |
|
||||
| [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 | done |
|
||||
| [WP-12](WP-12-datablock.md) | CIBG Datablock for application data | 2 · CIBG | done |
|
||||
| [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
|
||||
@@ -72,12 +72,20 @@ 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
|
||||
```
|
||||
|
||||
@@ -6,7 +6,7 @@ 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*.
|
||||
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.
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ principle (every response through a hand-written `parse*` returning `Result`).
|
||||
- `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)
|
||||
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`
|
||||
@@ -57,7 +57,7 @@ GREEN + `npm run test-storybook:ci`. Smoke: intake wizard still loads its policy
|
||||
|
||||
## Out of scope
|
||||
|
||||
Runtime validation on *every* endpoint (explicitly out of scope for the POC per
|
||||
Runtime validation on _every_ endpoint (explicitly out of scope for the POC per
|
||||
CLAUDE.md); `digid.adapter.ts` (faked auth, sanctioned).
|
||||
|
||||
## Risks
|
||||
|
||||
@@ -26,7 +26,7 @@ bypassing the shared molecule.
|
||||
(`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
|
||||
_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.
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ the list-family rationale to document.
|
||||
## Files
|
||||
|
||||
Components to mark (closest CIBG concept in parens):
|
||||
|
||||
- `skeleton`, `spinner` (Laadindicatie — no vendored class, verified)
|
||||
- `upload/` suite (Bestand-upload)
|
||||
- `rich-text-editor` (Tekstgebied)
|
||||
@@ -52,7 +53,7 @@ Components to mark (closest CIBG concept in parens):
|
||||
- `debug-state` (devtool, no CIBG concept)
|
||||
- `status-badge` (deliberate custom, documented in code), `card` (`.app-card`),
|
||||
`placeholder-chip`
|
||||
Plus:
|
||||
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`
|
||||
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
|
||||
@@ -6,9 +6,10 @@ 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
|
||||
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.
|
||||
@@ -60,7 +61,7 @@ Three app-level gaps close the WCAG story:
|
||||
|
||||
- [ ] 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.
|
||||
- [ ] 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.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Status: Proposed · Date: 2026-07-01 · Context: SSP / Zorgverlener (see ADR-000
|
||||
|
||||
> Cross-references: **ADR-0001** (BFF-lite endpoints + decision DTOs) and **ADR-0002** (user groups as
|
||||
> actors; the `Concept → In behandeling → Goedgekeurd/Afgewezen` aanvraag lifecycle). This PRD
|
||||
> *materializes* that lifecycle as a backend-owned aggregate — still entirely within the Zorgverlener
|
||||
> _materializes_ that lifecycle as a backend-owned aggregate — still entirely within the Zorgverlener
|
||||
> self-service context; the Behandelaar/backoffice app that advances manual cases stays a separate,
|
||||
> unbuilt context.
|
||||
|
||||
@@ -24,7 +24,7 @@ uploaded. Concretely, today:
|
||||
and there is **no GET-content endpoint**.
|
||||
- A **manual diploma is hard-rejected**: `SubmissionRules.RejectRegistratie("handmatig")` returns a
|
||||
422 (`backend/src/BigRegister.Api/Domain/Submissions/SubmissionRules.cs`). The product wants such a
|
||||
submission to *succeed* and sit in a pending (manual-review) state instead.
|
||||
submission to _succeed_ and sit in a pending (manual-review) state instead.
|
||||
|
||||
## 2. Goals
|
||||
|
||||
@@ -54,25 +54,25 @@ uploaded. Concretely, today:
|
||||
## 4. Personas
|
||||
|
||||
Single actor: the **Zorgverlener** (healthcare professional, DigiD/BSN, self-service). Per ADR-0002,
|
||||
"who may advance a manual application" is the **Behandelaar**, an actor in a *separate* backoffice
|
||||
"who may advance a manual application" is the **Behandelaar**, an actor in a _separate_ backoffice
|
||||
context that is not part of this app.
|
||||
|
||||
## 5. Domain model — the `Aanvraag` aggregate (backend-owned)
|
||||
|
||||
The backend gains an `Aanvraag` (application) aggregate — the system of record the dashboard reads.
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | string (uuid) | client-visible handle; used in the resume deep link |
|
||||
| `type` | `registratie` \| `herregistratie` \| `intake` | which wizard |
|
||||
| `status` | discriminated union (below) | computed on read for auto-approval |
|
||||
| `draft` | opaque JSON | the wizard's persisted machine snapshot (Concept only) |
|
||||
| `stepIndex`, `stepCount` | int | for the "Stap X van Y" progress on the block |
|
||||
| `documentIds` | string[] | documents linked to this aanvraag |
|
||||
| `referentie` | string? | set on submit (e.g. `BIG-2026-456789`) |
|
||||
| `owner` | string | `DemoOwner` |
|
||||
| `autoApprovable` | bool | set at submit: `diplomaHerkomst === 'duo'` (registratie); other types auto |
|
||||
| `createdAt` / `updatedAt` / `submittedAt?` | timestamps | |
|
||||
| Field | Type | Notes |
|
||||
| ------------------------------------------ | --------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `id` | string (uuid) | client-visible handle; used in the resume deep link |
|
||||
| `type` | `registratie` \| `herregistratie` \| `intake` | which wizard |
|
||||
| `status` | discriminated union (below) | computed on read for auto-approval |
|
||||
| `draft` | opaque JSON | the wizard's persisted machine snapshot (Concept only) |
|
||||
| `stepIndex`, `stepCount` | int | for the "Stap X van Y" progress on the block |
|
||||
| `documentIds` | string[] | documents linked to this aanvraag |
|
||||
| `referentie` | string? | set on submit (e.g. `BIG-2026-456789`) |
|
||||
| `owner` | string | `DemoOwner` |
|
||||
| `autoApprovable` | bool | set at submit: `diplomaHerkomst === 'duo'` (registratie); other types auto |
|
||||
| `createdAt` / `updatedAt` / `submittedAt?` | timestamps | |
|
||||
|
||||
### Status lifecycle
|
||||
|
||||
@@ -94,7 +94,7 @@ FE-mirrored discriminated union (illegal states unrepresentable, same reflex as
|
||||
```ts
|
||||
type AanvraagStatus =
|
||||
| { tag: 'Concept'; stepIndex: number; stepCount: number }
|
||||
| { tag: 'InBehandeling'; referentie: string; manual: boolean } // manual=true → "wordt beoordeeld"
|
||||
| { tag: 'InBehandeling'; referentie: string; manual: boolean } // manual=true → "wordt beoordeeld"
|
||||
| { tag: 'Goedgekeurd'; referentie: string }
|
||||
| { tag: 'Afgewezen'; referentie: string; reden: string };
|
||||
```
|
||||
@@ -117,12 +117,12 @@ Concept → In behandeling → resolved. Empty list → the section is hidden.
|
||||
Per-status block (new `aanvraag-block` component; **which** actions/badge a block shows comes from a
|
||||
pure `blockActions(status)`):
|
||||
|
||||
| Status | Badge | Body | Actions |
|
||||
|---|---|---|---|
|
||||
| **Concept** | grey "Concept" | wizard type + "Stap X van Y" | **Verder gaan** (resume), **Annuleren** (cancel) |
|
||||
| **In behandeling** | amber "In behandeling" | referentie + ingediend-datum; manual → "wordt handmatig beoordeeld in de backoffice" | view/preview documents |
|
||||
| **Goedgekeurd** | green "Goedgekeurd" | referentie | — |
|
||||
| **Afgewezen** | red "Afgewezen" | referentie + reden | — |
|
||||
| Status | Badge | Body | Actions |
|
||||
| ------------------ | ---------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------ |
|
||||
| **Concept** | grey "Concept" | wizard type + "Stap X van Y" | **Verder gaan** (resume), **Annuleren** (cancel) |
|
||||
| **In behandeling** | amber "In behandeling" | referentie + ingediend-datum; manual → "wordt handmatig beoordeeld in de backoffice" | view/preview documents |
|
||||
| **Goedgekeurd** | green "Goedgekeurd" | referentie | — |
|
||||
| **Afgewezen** | red "Afgewezen" | referentie + reden | — |
|
||||
|
||||
- **Verder gaan** deep-links to the wizard with `?aanvraag=<id>`; the wizard loads the draft from the
|
||||
backend and seeds its machine.
|
||||
|
||||
@@ -4,7 +4,7 @@ Status: Proposed · Date: 2026-07-02 · Context: SSP / backoffice actors (see AD
|
||||
|
||||
> Cross-references: **ADR-0001** (BFF-lite endpoints + decision DTOs), **ADR-0002** (user groups as
|
||||
> actors; identity vs authorization), and **PRD-0001** (the `Aanvraag` lifecycle those decisions gate).
|
||||
> This PRD *materializes* ADR-0002's authorization half: the AD server authenticates and supplies
|
||||
> This PRD _materializes_ ADR-0002's authorization half: the AD server authenticates and supplies
|
||||
> **coarse roles**; the app layers a **fine-grained, app-owned** access model on top, resolved by the
|
||||
> backend and rendered — never decided — by the UI.
|
||||
|
||||
@@ -19,8 +19,8 @@ administer:
|
||||
|
||||
- **Capability gating** — one role, many buttons: some users in a role may approve letters, reveal a
|
||||
BSN, or advance a manual application; others may not.
|
||||
- **Data-scoping** — the same role sees *different rows*: only their own region / office / caseload.
|
||||
- **Field / PII-level** — restrict *which fields* (notably the BSN and other special-category personal
|
||||
- **Data-scoping** — the same role sees _different rows_: only their own region / office / caseload.
|
||||
- **Field / PII-level** — restrict _which fields_ (notably the BSN and other special-category personal
|
||||
data under GDPR/AVG art. 9) a user may see or edit, independently of their role.
|
||||
- **Segregation-of-duty / step-up** — combinations and conditions: approver ≠ drafter, four-eyes,
|
||||
recent MFA, time-boxed break-glass.
|
||||
@@ -35,13 +35,13 @@ Today the codebase has none of this, and what stands in for a "role" is not a se
|
||||
requests as an `X-Role` header by a dev-only interceptor (`src/app/shared/infrastructure/role.interceptor.ts`,
|
||||
registered only under `isDevMode()` in `src/app/app.config.ts:22`). `X-Admin: true` is the parallel
|
||||
admin stand-in.
|
||||
- One route guard exists — `authGuard` (`src/app/auth/auth.guard.ts:6-10`) — a pure *authentication*
|
||||
- One route guard exists — `authGuard` (`src/app/auth/auth.guard.ts:6-10`) — a pure _authentication_
|
||||
check. There is **no** role/permission guard, and **no** `can` / `hasRole` / `isAuthorized` helper
|
||||
anywhere.
|
||||
- The backend is **fully open**: `backend/src/BigRegister.Api/Program.cs` has no authentication or
|
||||
authorization middleware, no `[Authorize]`, and never reads `HttpContext.User`. Identity is faked via
|
||||
a single `DemoOwner` id (`DocumentStore.cs:26`) plus the client-asserted `X-Role` / `X-Admin`
|
||||
headers. The brief's two-person rule *is* enforced (`BriefStore.Review`, `backend/.../Data/BriefStore.cs:113-123`:
|
||||
headers. The brief's two-person rule _is_ enforced (`BriefStore.Review`, `backend/.../Data/BriefStore.cs:113-123`:
|
||||
`if (actingId == e.DrafterId) return Forbidden`) — but against the **unverified** `X-Role` header, so
|
||||
any caller can assert `X-Role: approver`.
|
||||
|
||||
@@ -49,13 +49,13 @@ The building block we need already exists in one place: the **decision-flag seam
|
||||
computes `(bool, reason)` and embeds it in a screen DTO — `HerregistratieDecisionsDto` inside
|
||||
`DashboardViewDto` (`backend/src/BigRegister.Api/Contracts/Dtos.cs:25-27`), computed by
|
||||
`HerregistratieRule.Evaluate` (`backend/.../Domain/Registrations/HerregistratieRule.cs:16-27`). This
|
||||
PRD extends that same seam from *business* decisions to *authorization* decisions.
|
||||
PRD extends that same seam from _business_ decisions to _authorization_ decisions.
|
||||
|
||||
## 2. Goals
|
||||
|
||||
1. Support all four control types above — **capability gating, data-scoping, field/PII-level, and
|
||||
step-up/SoD** — as one coherent model.
|
||||
2. **Backend is the authority** for every access decision (per ADR-0001). The UI *mirrors* decisions
|
||||
2. **Backend is the authority** for every access decision (per ADR-0001). The UI _mirrors_ decisions
|
||||
for UX; it never computes them.
|
||||
3. **AD roles are the base; the app owns a fine-grained overlay.** The two merge **server-side** into a
|
||||
single `Principal`; capabilities are resolved server-side.
|
||||
@@ -69,7 +69,7 @@ PRD extends that same seam from *business* decisions to *authorization* decision
|
||||
|
||||
## 3. Non-goals / Out of scope (POC)
|
||||
|
||||
- **Real AD / OIDC / SAML integration.** The AD roles remain *simulated*; how claims actually arrive
|
||||
- **Real AD / OIDC / SAML integration.** The AD roles remain _simulated_; how claims actually arrive
|
||||
(token, header, SSO) is a wiring concern for later, isolated to `infrastructure/` + the backend
|
||||
authn middleware.
|
||||
- **A general policy engine (OPA/Cedar/XACML).** We express access as named **capabilities** computed
|
||||
@@ -85,17 +85,17 @@ PRD extends that same seam from *business* decisions to *authorization* decision
|
||||
## 4. Personas & attributes
|
||||
|
||||
Actors (per ADR-0002): the **Zorgverlener** (self-service, DigiD/BSN) and one or more **backoffice**
|
||||
actors (Behandelaar, Beoordelaar). ABAC is what lets these — and finer distinctions *within* a role —
|
||||
actors (Behandelaar, Beoordelaar). ABAC is what lets these — and finer distinctions _within_ a role —
|
||||
diverge without a folder-per-role explosion.
|
||||
|
||||
An access decision is a function of four attribute sets:
|
||||
|
||||
| Attribute set | Source | Examples |
|
||||
|---|---|---|
|
||||
| **Subject** | AD roles **+ app overlay + derived context** | AD: `beoordelaar`, `behandelaar`. Overlay: `mag-bsn-inzien`, `mag-brief-goedkeuren`. Derived: own BIG-registration, own region/office |
|
||||
| **Resource** | the domain entity | owner id, region, sensitivity class (contains BSN / art. 9 data), status |
|
||||
| **Action** | the request | `view`, `edit`, `approve`, `reveal-bsn`, `beoordelen` |
|
||||
| **Environment** | the request context | MFA/assurance level, time-of-day, break-glass flag |
|
||||
| Attribute set | Source | Examples |
|
||||
| --------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Subject** | AD roles **+ app overlay + derived context** | AD: `beoordelaar`, `behandelaar`. Overlay: `mag-bsn-inzien`, `mag-brief-goedkeuren`. Derived: own BIG-registration, own region/office |
|
||||
| **Resource** | the domain entity | owner id, region, sensitivity class (contains BSN / art. 9 data), status |
|
||||
| **Action** | the request | `view`, `edit`, `approve`, `reveal-bsn`, `beoordelen` |
|
||||
| **Environment** | the request context | MFA/assurance level, time-of-day, break-glass flag |
|
||||
|
||||
> AD owns only the first column's first row (coarse roles). Everything else is the app's overlay and
|
||||
> the entity's own attributes — the reason a role alone is too blunt.
|
||||
@@ -124,7 +124,7 @@ from `currentRole()` — this PRD moves that authority to the server flag.)
|
||||
### 5b. Data-scoping (row-level)
|
||||
|
||||
The server **filters rows by the subject's scope attributes at the source** — a Beoordelaar for region
|
||||
*Noord* receives only *Noord* aanvragen. The FE never receives out-of-scope records and so cannot leak
|
||||
_Noord_ receives only _Noord_ aanvragen. The FE never receives out-of-scope records and so cannot leak
|
||||
them (no client-side "fetch all, hide some"). Scope is a subject attribute (overlay/derived), applied
|
||||
in the query, not a UI filter.
|
||||
|
||||
@@ -181,7 +181,7 @@ The FE's job is to **mirror** server decisions cleanly and deny-by-default. It r
|
||||
never read directly by feature code.
|
||||
|
||||
> **Non-negotiable:** none of the above is a security boundary. A user who forges `can()` in the
|
||||
> browser changes only what they *see*; every gated route, action, and field is independently enforced
|
||||
> browser changes only what they _see_; every gated route, action, and field is independently enforced
|
||||
> by the backend (§7).
|
||||
|
||||
## 7. Backend design
|
||||
@@ -193,7 +193,7 @@ Extends ADR-0001's decision-DTO pattern; closes the "fully open" gap.
|
||||
authn middleware later). Merge **AD roles + the app-owned overlay** into one `Principal` here — the
|
||||
FE never sees the merge.
|
||||
- **Resolve + enforce capabilities** in a single shared authorization helper (`Authz.Can(principal,
|
||||
action, resource, env)`), used **on every endpoint** — not merely to *emit* flags but to *gate* the
|
||||
action, resource, env)`), used **on every endpoint** — not merely to _emit_ flags but to _gate_ the
|
||||
operation. Forbidden ⇒ 403 (reuse the existing `Outcome.Forbidden → 403` mapping,
|
||||
`backend/.../Program.cs:330-335`). Emitting a flag and forgetting to enforce it is the classic
|
||||
broken-object-level-authorization bug; the helper makes emit and enforce the same code path.
|
||||
|
||||
@@ -24,82 +24,82 @@ onto: spacing `--rhc-space-max-{sm..5xl}`, type `--rhc-text-font-size-*` /
|
||||
`--rhc-color-{lintblauw,donkerblauw,cool-grey}-*` / `--rhc-color-wit`, radius
|
||||
`--rhc-border-radius-*`.
|
||||
|
||||
| # | Pri | Finding | Resolves to |
|
||||
|---|-----|---------|-------------|
|
||||
| 1.1 | **H** | `var(--rhc-color-grijs-700)` is referenced in 3 wizards but **`--rhc-color-grijs-*` does not exist** in the package → the "Stap X van Y" text falls back to the default color, not grey. | `--rhc-color-foreground-subtle` |
|
||||
| 1.2 | **H** | `site-header`/`site-footer` set bg to `var(--rhc-color-lintblauw-700,#154273)` / `…-900,#01689b` — `lintblauw-900` doesn't exist (only 50–700), so the **hex fallback renders**. | header `--rhc-color-lintblauw-700`; footer `--rhc-color-donkerblauw-700`; text `--rhc-color-wit` |
|
||||
| 1.3 | M | Raw spacing everywhere: `0.25/0.5/0.75/1/1.5/2/3rem` as `gap`/`margin`/`padding` (≈30 occurrences). | `--rhc-space-max-{sm,md,lg,xl,2xl,3xl,5xl}` |
|
||||
| 1.4 | M | Inconsistent form/content widths: `28rem`, `30rem`, `32rem`, `64rem` as raw `max-width`. | app width tokens in `styles.scss` (`--app-form-max`, `--app-content-max`) mapped once |
|
||||
| 1.5 | M | `spinner` & `skeleton` hardcode greys/accent (`#cad0d6`, `#e8ebee`, `#f3f5f6`) and the spinner accent via bogus `--rhc-color-lintblauw-700,#154273` fallback. | `--rhc-color-cool-grey-{200,300}`, `--rhc-color-lintblauw-700` |
|
||||
| 1.6 | L | `site-header` hardcodes `font-weight:700/400`, `font-size:0.9rem`, `opacity:0.85`. | `--rhc-text-font-weight-{bold,regular}`, `--rhc-text-font-size-sm` |
|
||||
| # | Pri | Finding | Resolves to |
|
||||
| --- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| 1.1 | **H** | `var(--rhc-color-grijs-700)` is referenced in 3 wizards but **`--rhc-color-grijs-*` does not exist** in the package → the "Stap X van Y" text falls back to the default color, not grey. | `--rhc-color-foreground-subtle` |
|
||||
| 1.2 | **H** | `site-header`/`site-footer` set bg to `var(--rhc-color-lintblauw-700,#154273)` / `…-900,#01689b` — `lintblauw-900` doesn't exist (only 50–700), so the **hex fallback renders**. | header `--rhc-color-lintblauw-700`; footer `--rhc-color-donkerblauw-700`; text `--rhc-color-wit` |
|
||||
| 1.3 | M | Raw spacing everywhere: `0.25/0.5/0.75/1/1.5/2/3rem` as `gap`/`margin`/`padding` (≈30 occurrences). | `--rhc-space-max-{sm,md,lg,xl,2xl,3xl,5xl}` |
|
||||
| 1.4 | M | Inconsistent form/content widths: `28rem`, `30rem`, `32rem`, `64rem` as raw `max-width`. | app width tokens in `styles.scss` (`--app-form-max`, `--app-content-max`) mapped once |
|
||||
| 1.5 | M | `spinner` & `skeleton` hardcode greys/accent (`#cad0d6`, `#e8ebee`, `#f3f5f6`) and the spinner accent via bogus `--rhc-color-lintblauw-700,#154273` fallback. | `--rhc-color-cool-grey-{200,300}`, `--rhc-color-lintblauw-700` |
|
||||
| 1.6 | L | `site-header` hardcodes `font-weight:700/400`, `font-size:0.9rem`, `opacity:0.85`. | `--rhc-text-font-weight-{bold,regular}`, `--rhc-text-font-size-sm` |
|
||||
|
||||
## 2. Typography & heading hierarchy
|
||||
|
||||
| # | Pri | Finding | Resolves to |
|
||||
|---|-----|---------|-------------|
|
||||
| # | Pri | Finding | Resolves to |
|
||||
| --- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
|
||||
| 2.1 | **H** | Each page has one `<h1>` (page-shell) but the **wizard steps have no `<h2>`** — step title is a plain `<p>`. Screen-reader users get no step heading to navigate to. | per-step `<h2>` via `app-heading [level]="2"` |
|
||||
| 2.2 | M | Type scale not applied to bespoke text (header wordmark uses raw sizes). | `--rhc-text-font-size-*`, `app-heading` |
|
||||
| 2.2 | M | Type scale not applied to bespoke text (header wordmark uses raw sizes). | `--rhc-text-font-size-*`, `app-heading` |
|
||||
|
||||
## 3. Color & contrast
|
||||
|
||||
| # | Pri | Finding | Resolves to |
|
||||
|---|-----|---------|-------------|
|
||||
| 3.1 | **H** | Because of 1.1/1.2 the actual rendered colors are partly accidental (default text color, hex fallbacks) — contrast is unverified. | map to real tokens, then verify AA |
|
||||
| 3.2 | M | Palette discipline: confirm a single primary blue (lintblauw) + neutrals + status-only accents (`alert` types ok/info/warning/error already map to Utrecht alert variants). | keep alert variants; route blues to lintblauw/donkerblauw tokens |
|
||||
| # | Pri | Finding | Resolves to |
|
||||
| --- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
||||
| 3.1 | **H** | Because of 1.1/1.2 the actual rendered colors are partly accidental (default text color, hex fallbacks) — contrast is unverified. | map to real tokens, then verify AA |
|
||||
| 3.2 | M | Palette discipline: confirm a single primary blue (lintblauw) + neutrals + status-only accents (`alert` types ok/info/warning/error already map to Utrecht alert variants). | keep alert variants; route blues to lintblauw/donkerblauw tokens |
|
||||
|
||||
## 4. Spacing & layout
|
||||
|
||||
| # | Pri | Finding | Resolves to |
|
||||
|---|-----|---------|-------------|
|
||||
| 4.1 | M | Repeated inline idioms: `display:flex;gap:0.5rem;margin-top:1rem` (button rows, 6×), `max-width:30rem` (forms, 3×), `margin:1rem 0` (summaries). | utility classes `.app-button-row`, `.app-form`, `.app-stack` in `styles.scss` |
|
||||
| 4.2 | L | `shell` uses a custom `--app-content-max:64rem` defined inline. | promote to the `styles.scss` token layer |
|
||||
| # | Pri | Finding | Resolves to |
|
||||
| --- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
|
||||
| 4.1 | M | Repeated inline idioms: `display:flex;gap:0.5rem;margin-top:1rem` (button rows, 6×), `max-width:30rem` (forms, 3×), `margin:1rem 0` (summaries). | utility classes `.app-button-row`, `.app-form`, `.app-stack` in `styles.scss` |
|
||||
| 4.2 | L | `shell` uses a custom `--app-content-max:64rem` defined inline. | promote to the `styles.scss` token layer |
|
||||
|
||||
## 5. Component reuse
|
||||
|
||||
| # | Pri | Finding | Resolves to |
|
||||
|---|-----|---------|-------------|
|
||||
| 5.1 | L | `intake-wizard` review step uses bespoke `<div><dt><dd>` instead of `app-data-row` (out of scope here; note for later). | `app-data-row` |
|
||||
| 5.2 | M | Button rows / form containers are ad-hoc inline layout rather than a shared idiom. | utility classes (4.1) |
|
||||
| # | Pri | Finding | Resolves to |
|
||||
| --- | --- | ----------------------------------------------------------------------------------------------------------------------- | --------------------- |
|
||||
| 5.1 | L | `intake-wizard` review step uses bespoke `<div><dt><dd>` instead of `app-data-row` (out of scope here; note for later). | `app-data-row` |
|
||||
| 5.2 | M | Button rows / form containers are ad-hoc inline layout rather than a shared idiom. | utility classes (4.1) |
|
||||
|
||||
## 6. Form, validation & status patterns
|
||||
|
||||
| # | Pri | Finding | Resolves to |
|
||||
|---|-----|---------|-------------|
|
||||
| 6.1 | **H** | `form-field` renders the error with `role="alert"` but **no `id`**, and the projected input has **no `aria-describedby`** → error not programmatically linked. | error `id="${fieldId}-error"`; input `aria-describedby` |
|
||||
| 6.2 | **H** | `text-input` never sets `aria-invalid` even when `invalid()` is true. | `[attr.aria-invalid]` |
|
||||
| 6.3 | **H** | `radio-group` has `role="radiogroup"` but **no accessible name** and no invalid state. | `aria-labelledby="${name}-label"`, add `invalid` input → `aria-invalid`/`aria-describedby` |
|
||||
| 6.4 | M | Async states (`app-async`) render but aren't announced (no live region) — SR users miss loading→loaded/empty/error. | wrap in `aria-live="polite"` + `aria-busy` |
|
||||
| 6.5 | L | No error-summary pattern; per-field inline errors only. Acceptable for short steps; revisit if steps grow. | (defer) |
|
||||
| # | Pri | Finding | Resolves to |
|
||||
| --- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| 6.1 | **H** | `form-field` renders the error with `role="alert"` but **no `id`**, and the projected input has **no `aria-describedby`** → error not programmatically linked. | error `id="${fieldId}-error"`; input `aria-describedby` |
|
||||
| 6.2 | **H** | `text-input` never sets `aria-invalid` even when `invalid()` is true. | `[attr.aria-invalid]` |
|
||||
| 6.3 | **H** | `radio-group` has `role="radiogroup"` but **no accessible name** and no invalid state. | `aria-labelledby="${name}-label"`, add `invalid` input → `aria-invalid`/`aria-describedby` |
|
||||
| 6.4 | M | Async states (`app-async`) render but aren't announced (no live region) — SR users miss loading→loaded/empty/error. | wrap in `aria-live="polite"` + `aria-busy` |
|
||||
| 6.5 | L | No error-summary pattern; per-field inline errors only. Acceptable for short steps; revisit if steps grow. | (defer) |
|
||||
|
||||
## 7. Page chrome
|
||||
|
||||
| # | Pri | Finding | Resolves to |
|
||||
|---|-----|---------|-------------|
|
||||
| 7.1 | M | No breadcrumb / location context in the portal chrome. | new `app-breadcrumb` (`.rhc-breadcrumb-nav`), wired via `page-shell` |
|
||||
| 7.2 | M | Step progress is non-semantic text. | new `app-stepper` (`<ol>`, `aria-current="step"`) |
|
||||
| 7.3 | L | Skip-link uses `left:-999px` (works) and landmarks (`header/main/footer`) are correct. | keep; tokenize offset |
|
||||
| # | Pri | Finding | Resolves to |
|
||||
| --- | --- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| 7.1 | M | No breadcrumb / location context in the portal chrome. | new `app-breadcrumb` (`.rhc-breadcrumb-nav`), wired via `page-shell` |
|
||||
| 7.2 | M | Step progress is non-semantic text. | new `app-stepper` (`<ol>`, `aria-current="step"`) |
|
||||
| 7.3 | L | Skip-link uses `left:-999px` (works) and landmarks (`header/main/footer`) are correct. | keep; tokenize offset |
|
||||
|
||||
## 8. Copy & tone
|
||||
|
||||
| # | Pri | Finding | Resolves to |
|
||||
|---|-----|---------|-------------|
|
||||
| 8.1 | M | Microcopy is functional but informal/uneven (labels, helper text, button text, the manual-diploma warning, the controle summary). | formal, plain official Dutch; consistent with domain terms (registratie wizard + chrome only) |
|
||||
| # | Pri | Finding | Resolves to |
|
||||
| --- | --- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| 8.1 | M | Microcopy is functional but informal/uneven (labels, helper text, button text, the manual-diploma warning, the controle summary). | formal, plain official Dutch; consistent with domain terms (registratie wizard + chrome only) |
|
||||
|
||||
## 9. Accessibility (WCAG 2.1 AA) — consolidated
|
||||
|
||||
| # | Pri | Finding | Resolves to |
|
||||
|---|-----|---------|-------------|
|
||||
| 9.1 | **H** | No focus management: after Next/Back focus stays on the button; SR/keyboard users aren't moved to the new step. | move focus to the step `<h2>` (`tabindex="-1"`) on `cursor` change |
|
||||
| 9.2 | **H** | Error/label/invalid association missing (6.1–6.3). | as above |
|
||||
| 9.3 | M | Step changes & async states not announced (6.4). | `aria-live` |
|
||||
| 9.4 | M | `skeleton` placeholders are announced as content. | `aria-hidden="true"` |
|
||||
| 9.5 | L | No automated a11y in CI; only Storybook `addon-a11y` (axe) exists. | add a11y `parameters` in `preview.ts`; keep manual keyboard/SR pass; no new deps |
|
||||
| # | Pri | Finding | Resolves to |
|
||||
| --- | ----- | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| 9.1 | **H** | No focus management: after Next/Back focus stays on the button; SR/keyboard users aren't moved to the new step. | move focus to the step `<h2>` (`tabindex="-1"`) on `cursor` change |
|
||||
| 9.2 | **H** | Error/label/invalid association missing (6.1–6.3). | as above |
|
||||
| 9.3 | M | Step changes & async states not announced (6.4). | `aria-live` |
|
||||
| 9.4 | M | `skeleton` placeholders are announced as content. | `aria-hidden="true"` |
|
||||
| 9.5 | L | No automated a11y in CI; only Storybook `addon-a11y` (axe) exists. | add a11y `parameters` in `preview.ts`; keep manual keyboard/SR pass; no new deps |
|
||||
|
||||
## 10. Responsive
|
||||
|
||||
| # | Pri | Finding | Resolves to |
|
||||
|---|-----|---------|-------------|
|
||||
| 10.1 | M | Fixed `rem` widths and inline flex rows; verify reflow + ≥24px targets at narrow widths. | token widths + `flex-wrap` on button rows; manual check |
|
||||
| # | Pri | Finding | Resolves to |
|
||||
| ---- | --- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| 10.1 | M | Fixed `rem` widths and inline flex rows; verify reflow + ≥24px targets at narrow widths. | token widths + `flex-wrap` on button rows; manual check |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user