- Datablock entries had no separators: CIBG ships the border on dt/dd with a
:last-of-type reset, but the one-row-per-<div> grouping (for axe) made every
dt/dd a last-of-type, stripping them all. Carry the separator on the row
(:host:not(:last-of-type)) in the CIBG line colour (#f1f5f9 = cool-grey-200).
- Upload "Bestand toevoegen" button read as a pale outline: btn-outline-primary
forced a transparent background over .btn-upload's accent fill. Use
btn-primary btn-upload (solid #01689b, white text, folder glyph) and drop the
redundant centred background-image folder.
GREEN: lint, tokens, 183 tests, build, 137 axe stories. Verified via Storybook
screenshots (datablock separators, filled blue upload button).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Aanvragen rows now show what the aanvraag is for (purpose subtitle) and an
explicit status label (In behandeling / Goedgekeurd / Afgewezen) alongside the
reference + submit date, via an expanded aanvraag-view (purposeLabel,
statusLabel, referentie, detailRows) + spec.
- Rows link to a new /aanvraag/:id case-detail page, so the CIBG chevron shows
and each aanvraag opens as a (stub) case — it lists soort/waarvoor/status/
referentie/ingediend in a Datablock, with a note that full handling is future.
GREEN: lint, tokens, 183 tests, build, 137 axe stories. Verified visually
(dashboard aanvragen rows, upload drop-zone, datablock) via Storybook screenshots.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The upload suite now wraps the vendored CIBG file-upload classes instead of
hand-rolling from tokens:
- file-input becomes a .file-picker-drop-area with drag-and-drop + a real
.btn-upload button and a visually-hidden, focusable <input>; renders an
always-visible instruction (allowed types + max size) linked to the input via
aria-describedby (pattern requirement).
- The file list is a semantic ul.file-list; single-upload is an
li[app-single-upload] .file-container (native <li> child) with .actions
(retry/.icon-remove) + progress; document-chip renders the .file block
(status glyph, .file-name link, .file-meta size/status).
- Category validation moves ABOVE the block as .upload-validation > .feedback.
- Instruction text uses the default (not subtle) foreground for WCAG AA contrast
on the grey drop-area.
DocumentUpload's public inputs/outputs are unchanged — both wizards keep working.
GREEN: lint, tokens, 181 tests, build, 137 axe stories.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- application-link switches to a `li[app-application-link]` attribute selector
(native <li> child of the <ul> — axe-clean list) and drops the invented,
dead `.application` / `.application-title` classes for the real vendored
`.dashboard-block.applications li a` chain (h3.h3 / .subtitle / .status / .cta).
Content stacks in a flex column; a non-navigating row mirrors the card surface
from tokens. Re-enables a11y on the application-link/list stories.
- Dashboard "Mijn aanvragen" now renders through app-application-list +
<li app-application-link> rows (was a keuzelijst), mapped by a new pure
submittedRow() view helper (+ spec). Concepts stay the resumable melding.
- aanvraag-block is now concept-only (submitted mapping moved to aanvraag-view).
WP-11 grep gate clean. GREEN: lint, tokens, 181 tests, build, 136 axe stories.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adopt the vendored CIBG Datablock (.data-block / .block-wrapper) as the way to
show application data:
- New app-data-block molecule (grey surface + white panel + projected rows,
optional heading, stacked variant, aria-label) + stories.
- data-row switches to a `div[app-data-row]` attribute selector so the <dl>'s
direct child is a native <div> (HTML5.1 dl > div > dt+dd). This makes the
definition list axe-clean — a bare custom element between <dl> and its dt/dd
trips axe's definition-list rule regardless of display:contents, a defect the
dashboard shipped live. Re-enables a11y on the data-row / review-section /
registration-summary stories (previously disabled pending this rework).
- review-section folds onto app-data-block (drops its hand-carried classes).
- registration-summary + dashboard "Persoonsgegevens (BRP)" drop app-card and
render as datablocks; both wizards' review rows + the beroep row convert to
the div selector.
GREEN: lint, check:tokens, 178 tests, build, build-storybook, 136 axe stories.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Move the guard to scripts/check-tokens.sh; regex now catches hex +
rgb()/hsl() (was hex-only) across ALL src/app components (was three
ui/layout dirs). `token-ok` marker suppresses justified false positives;
px stays out of scope (documented in the script).
- Zero exclusions: debug-state's dark code-editor palette moves to
--app-devpanel-* tokens in styles.scss (the one exempt file), dropping its
--exclude hole.
- Tokenize remaining hits: site-footer border via color-mix; three brief
border widths via --rhc-border-width-* (new --rhc-border-width-lg: 3px).
Verified: planted violation fails the guard; GREEN + test-storybook:ci.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Turn the interactive Storybook a11y addon into a build gate:
- @storybook/test-runner + axe-playwright over the static build
(.storybook/test-runner.ts reads the a11y tags from story context)
- test-storybook / test-storybook:ci scripts; storybook-a11y CI job
- triage: escape-hatch a11y.disable on stories whose display:contents
wrapper splits <ul>/<li> or <dl>/<dt>/<dd> (structural, deferred to
WP-11/WP-12, each with justification + cross-ref)
- fix trivial violations: footer/wizard-shell contrast, text-input label,
wizard stories missing provideApiClient
Verified: broken story fails the gate; 133 stories pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Concept status now renders as a CIBG "melding" (warning) with its own
verwijderen/openen actions, matching the real CIBG pattern for an in-progress
application, instead of sharing the keuzelijst card shape used by resolved
statuses. Alert atom switches from a hand-rolled surface to the vendored
`.feedback` classes with a visually-hidden icon label per CIBG's a11y
requirement.
"Annuleren" rendered as a separate line below the grey card — visually disconnected
from the choice it belongs to. Fixing this properly means the card can't just be one
big <a> anymore: a <button> can't nest inside an anchor (invalid HTML, broken a11y),
and choice-link's [choiceActions] slot needs Annuleren to sit inside the same box.
choice-link.component.ts now makes the card a <div> always, with the title wrapped in
a vendored Bootstrap `.stretched-link` (its ::after overlay keeps the whole card
clickable, same as before) instead of the whole box being the anchor. The projected
action gets its own `position:relative;z-index:2` (in aanvraag-block.component.ts,
which owns that markup) to stay clickable above the stretched-link overlay. Added
`:focus-within` on the card to restore the focus-accent CIBG's `:focus` rule would
have given the card itself, since focus now lands on the inner title link.
Verified: lint/check:tokens/test/build green; drove it end-to-end — clicking anywhere
on a card body still resumes the wizard, clicking Annuleren cancels without navigating.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both sections offer a set of choices the user picks between to proceed, matching
designsystem.cibg.nl/componenten/keuzelijst rather than the "aanvragen" row pattern
("Wat wilt u doen?" keeps that look — it's a static nav list, not a choice list).
- New shared/ui molecules: choice-list (heading + keuzelijst__list, wired via
aria-labelledby per CIBG's a11y guidance) and choice-link (one keuzelijst__link
choice; routerLink, imperative-clickable, or a plain non-interactive block).
- choice-link's non-interactive block needed a `--static` modifier: CIBG's
`.keuzelijst__link:after`/`:hover`/`:focus` key off the bare class (keuzelijst
assumes every item is a link), unlike `.applications li a::after` which is scoped
to the anchor — without it, a non-actionable aanvraag row inherited a chevron and
hover accent it shouldn't have.
- task-list.component.ts now composes choice-list/choice-link internally; public
API unchanged except a new required `listHeading` input (the heading moves inside
the list for the aria-labelledby link, so dashboard.page.ts stops rendering it
separately — same fix applied to "Mijn aanvragen").
- aanvraag-block.component.ts moves from application-link to choice-link, combining
its separate status/subtitle text into one instructions paragraph (keuzelijst has
no cta field — the row itself is the action). Only a resumable Concept renders as
a real choice; InBehandeling/Goedgekeurd/Afgewezen stay non-interactive, unchanged
from before.
Verified: lint/check:tokens/build green, 178 tests pass, build-storybook succeeds,
and manually driven end-to-end (dashboard renders both sections as keuzelijst cards,
confirmed via screenshot that non-actionable rows have no chevron after the fix).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-skins the app's layout on top of the CIBG Huisstijl theme (previous commit) so it
matches designsystem.cibg.nl, not just its colour tokens — magenta ("robijn") header,
horizontal nav, and the CIBG component markup for forms/wizards/dashboard.
- Header: logo block + robijn titlebar (breadcrumb + user menu) + grey horizontal nav
(4 links) replacing the dashboard side-nav; breadcrumb restyled for the titlebar
(no background of its own — CIBG's global `header nav` rule otherwise bleeds a grey
fill into it, fixed by scoping an override inside BreadcrumbComponent).
- Forms: form-field/radio-group/checkbox rebuilt on CIBG's horizontal `form-group row`
/ `form-check.styled` markup (label col-md-4, control col-md-8); same input() APIs.
- Wizards: stepper rebuilt as the CIBG "stappenindicator" (numbered circles, visited
steps clickable for back-nav, title merged in); wizard-shell adopts the CIBG
procesnavigatie button row. Back-navigation wired into all three wizard machines
(registratie-wizard already had it; added `GaNaarStap` to intake/herregistratie
machines, pure + spec'd).
- New shared/ui molecules: confirmation (animated bevestiging checkmark, replaces
plain alerts on submit), review-section (controlestap sections with "Wijzigen"),
application-list/application-link (CIBG "aanvragen" rows, replace the dashboard's
card grid and aanvraag-block).
- Cleanup: delete side-nav and now-unused styles.scss utilities (.app-overview,
.app-form-panel, .app-card-grid); correct design-tokens.mdx (it referenced tokens
that no longer exist) and document the CIBG-value token bridge.
Verified: build/lint/check:tokens green, 178 tests pass (4 new GaNaarStap cases), and
manually driven end-to-end (dashboard, a full herregistratie submission through to the
confirmation screen, mobile width, keyboard focus).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The portal now adheres to the CIBG design system (designsystem.cibg.nl) — a customized
Bootstrap 5.2 build — replacing the Rijkshuisstijl-Community / Utrecht theme. See ADR-0003.
- Vendor @cibg/huisstijl@3.22.0 under public/cibg-huisstijl/, loaded via a <link> in
index.html (Storybook serves it via staticDirs). Drop the two @rijkshuisstijl-community deps.
- Token bridge in styles.scss: redefine the app's ~54 --rhc-* tokens onto CIBG/--bs-* values,
so components keep referencing tokens (no 300+ site rewrite). System-font stack; licensed
RO/Rijks text fonts intentionally not shipped.
- Re-skin every shared atom to Bootstrap/CIBG classes (btn, form-control, form-check-*,
table, breadcrumb, …) keeping their input() APIs. alert is hand-rolled (CIBG drops .alert);
local .card/.badge renamed to avoid Bootstrap collisions.
- Domain pages: drop stray rhc-*/utrecht-* classes; registration-table → table table-striped.
Verified: build green, check:tokens OK, lint clean, 174 tests pass, build-storybook OK,
and the served build loads the vendored CSS (200, .btn-primary present).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- brief.machine: reducer refuses edits to locked (predefined) sections as
defense-in-depth; LetterSection gains a `locked` flag
- rich-text: paragraphs gain optional `list` kind; editor gets bullet/numbered
list buttons, keyboard shortcuts, and backspace-deletes-adjacent-chip
- placeholder chips distinguish auto-resolvable (grey) vs manual (yellow), in
both the editor and the read-only preview
- fix: preview chip now renders matching {…} braces (was a one-sided ⌗ glyph),
aligned with the editor's chip styling
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New `brief` context — a letter-composition feature with a drafter/approver
approval workflow, built as a teaching vertical slice on the repo's existing
FP + Elm + atomic-design patterns (see plan in ~/.claude/plans).
Domain (pure):
- Rich text as a serialisable value tree (placeholders are first-class nodes),
moved to @shared/kernel/rich-text.ts so the shared editor can use it.
- lintPlaceholders: a pure, total content -> Diagnostic[] linter, derived never stored.
- brief.machine.ts: status sum-type with guarded transitions; frozen-snapshot =
deep value copy; derived diagnostics/editability. Full specs.
Backend (.NET stub):
- BriefStore + seed, GET/PUT /brief and submit/approve/reject/send endpoints,
role via X-Role header (mirrors X-Admin), transition + approver!=drafter guards,
audit logging. Regenerated typed client via gen:api. +6 backend tests.
Seam:
- brief.adapter.ts maps flat wire unions <-> domain discriminated unions at the
parse boundary (+ spec).
UI (atomic):
- shared atoms: checkbox, placeholder-chip; molecule: rich-text-editor (no-dep
contenteditable, DOM<->RichTextBlock round-trip tested).
- brief/ui: letter-block, passage-picker, diagnostics-panel, rejection-comments,
letter-section, letter-composer, letter-preview, brief.page + /brief route.
- Dev-only ?role=drafter|approver toggle + roleInterceptor; dashboard nav link.
Enforcement: @brief/* alias + eslint layer boundary (brief depends only on shared).
Also included (same session):
- Value-object specs (postcode/uren/big-nummer) — closes the "domain must have a spec" gap.
- src/docs/ Storybook MDX foundation pages (atomic design, tokens, FP-in-UI).
- .storybook/tsconfig.json: add @angular/localize to types (Storybook was fully
broken — $localize unresolved — dev + build).
Verified: 168 FE tests, 68 backend tests, lint/build/check:tokens green,
Storybook boots, end-to-end HTTP smoke (self-approve 403, approver 200, full flow).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Categories stay server-owned (ADR-0001); the FE sends its answers to
/uploads/categories and re-fetches reactively when they change:
- Diplomabewijs required only for a handmatig diploma (DUO is verified digitally;
nothing required before a diploma is chosen).
- Bewijs Nederlandse taalvaardigheid required only when the applicant answers "ja"
to the nl-taalvaardigheid (B2) policy question.
CategoriesFor(wizardId, diplomaHerkomst, taalvaardigheid) decides; Find uses the
maximal set so uploads still validate. CategoriesLoaded drops orphaned uploads
when a category disappears. Also: show the foreground-only upload banner only when
there is at least one category.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- animate.enter/leave on aanvraag cards and upload rows (native Angular, no @angular/animations)
- reduced-motion: skip animation → instant removal
- api-client: null-body statuses (204/205/304) must pass null to Response()
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-opening a Concept wizard now lets the user preview/download what they already
uploaded (PRD 0001 goal 2).
- document-chip: optional previewUrl input → a "Voorbeeld / Download" link (opens
the stored bytes; server serves inline for pdf/image, attachment otherwise).
- previewUrlFor callback threaded document-upload → document-category → single-upload
(which builds the URL from a completed upload's documentId). Keeps URL-building out
of the presentational atoms.
- registratie + herregistratie wizards supply previewUrlFor via UploadAdapter.contentUrl,
returning undefined for dev-simulation `demo-*` ids (no stored bytes → no link).
- Story: DocumentChip/WithPreview.
Gates green: vitest 128, lint, ng build, check:tokens; backend dotnet 56.
(build-storybook has a pre-existing Compodoc/$localize issue in untouched files.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- upload-slow/upload-fail scenarios simulated in the adapter (XHR POST bypasses the
HTTP interceptor); categories/status/delete already honour the global slow/error
- file-input gets a per-category accessible name (aria-label + visible button text)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap every user-facing Dutch string in Angular's first-party i18n — `i18n`/
`i18n-<attr>` in templates, `$localize` in TS (value-objects, machines, commands,
label constants, shared-component defaults). Source locale stays nl; a second
locale is now a translation file, not a code change.
- M3: ~145 strings localized with stable @@ ids across registratie,
herregistratie, auth, shared/ui, shared/layout. Skipped: showcase, debug-state,
scenario interceptor, generated client, specs/stories, raw status enum tags,
internal parse* diagnostics.
- M4: single shared JA_NEE (localized labels) in radio-group; both wizard copies
removed.
Gate green: lint, check:tokens, build, test 77/77.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Domain reference data already lives in the backend (per ADR-0001); the only
residual "move" was ~90 inlined Dutch UI strings with no i18n layer.
- Wire @angular/localize ($localize) — Angular first-party, no third-party lib
- Pin the pattern on shared/ui/async: Dutch fallbacks → language-agnostic
input()s with localizable $localize defaults (English-shared-UI rule)
- CLAUDE.md: drop i18n (now in scope) + stale "real backend"/"OpenAPI codegen"
(both already shipped); document the $localize convention
Bulk string sweep deferred to Step 2 to avoid touching ~36 files twice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chrome: two-tier Rijksoverheid header (white brand bar + lint-blue
breadcrumb bar, route-driven), dark multi-column footer, white page
surface. Session shown via a shared SESSION_PORT token (keeps shared/
free of the auth context).
Overview ("Mijn overzicht") rebuilt to the NL Design System #392 pattern:
side-nav + "Wat moet ik regelen" task list (derived) + "Mijn registratie"
cards. New shared components: card, task-list, side-nav; pure
tasksFromProfile (+spec).
Wizards: grey form panel, connected numbered stepper, form-field
"(verplicht)" markers + styled description/error, full-width inputs.
Propagated to login, detail, change-request, address-fields.
Bug fixes:
- wizard-shell: add FormsModule so NgForm intercepts submit (wizards now
advance; no native GET leaking choices into the URL).
- wizard-shell: error-summary links focus the field instead of navigating
(a fragment href resolved against <base href="/"> reloaded to "/" and
bounced to login).
- wizard-shell: error-summary focus only on the rising edge, so typing
while errors are shown no longer scrolls the page up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A floating, read-only panel that renders the current root-store state
(SessionStore + BigProfileStore) live via the json pipe. Whole thing is
gated by isDevMode() so it never renders or ships in production.
- Observer only — no new store/library/state pattern (PRD prime directive).
- BigProfileStore resolved lazily on first open; its httpResources fetch
eagerly on construction, so we avoid a personal-data fetch on pages that
don't use it.
- bsn masked before render; no persistence/logging/network in the feature.
- maskBsn has a unit spec; UI exercised via a Devtools Storybook story,
per repo convention (no TestBed component tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Checkpoint of in-progress work: the registration wizard (address prefill,
DUO diploma lookup, policy questions), decision-DTO contracts, parse-don't-
validate value objects, infrastructure adapters, plus CLAUDE.md and the
architecture/ADR docs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- radio-group: the Utrecht radio paints its dot only with the
`utrecht-radio-button--checked` class (not the native :checked); we set
[checked] but not the class, so a selected radio looked empty. Bind the class
too. The intake step-1 choice now visibly selects.
- data-row: drop the border-block-end on the last row (:host:last-child) so a
summary ends cleanly instead of showing a trailing empty row (visible in the
concepts discriminated-unions card).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A second wizard demonstrating a BRANCHING flow: the visible steps are derived
from the answers by a pure `visibleSteps` function rather than stored, so
answering "buiten Nederland gewerkt? -> ja" or reporting few hours adds steps
and the progress denominator changes live. Same Elm-style store + RemoteData
patterns as the fixed wizard; answers persist to localStorage.
- intake.machine.ts: IntakeState union + Answers + visibleSteps + pure reduce (+spec)
- intake-wizard organism, intake.page, submit-intake command
- new radio-group atom (ControlValueAccessor) in shared/ui
- /intake route + dashboard link + concepts showcase section
- tighten Aantekening.type to a 'Specialisme' | 'Aantekening' union
- README + ARCHITECTURE updated
Verified live end-to-end (branches add steps 4->5->6, review, submit) with no
console errors; build, unit tests, and Storybook all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reorganise from atomic-design-only folders into bounded contexts
(auth / registratie / herregistratie) over a shared kernel, each split into
domain / application / infrastructure / ui layers. Dependencies point inward;
the domain layer is framework-free. Path aliases (@shared/@auth/@registratie/
@herregistratie) make import direction explicit.
State management (Elm-style, native TS, no new deps):
- shared/application/store.ts — createStore(init, update): pure reducer + signal
- shared/application/remote-data.ts — add map/map2/map3/andThen combinators so
several services fold into one RemoteData; <app-async> gains an [rd] input
- registratie/application/big-profile.store.ts — root singleton combining the
BIG-register and BRP services via map2 into one state; holds the optimistic
herregistratie flag shared with the dashboard
- herregistratie: machine gains a WizardMsg union + pure reduce; submit is a
command that calls infra and dispatches the result, with optimistic update +
rollback against the shared store
- auth: SessionStore + DigiD adapter + functional route guard; login establishes
the session, protected routes use canActivate
Rich domain: registration.policy.ts (statusColor/label, herregistratie
eligibility, invariants); BigNummer/Postcode/Uren value objects with smart
constructors. status-badge is now domain-free (colour/label inputs).
Specs for the reducer, RemoteData combinators, and eligibility policy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>