Edit the letter's org identity in place on the same canvas the drafter composes
on (editableRegions='template'): letterhead/signature/footer become inline
controls, content a read-only sample. Margins (bounded), logo upload (reuses the
shared upload transport + single-upload), version history + rollback, proefbrief,
and publish-with-impact-confirmation. House form-machine idiom
(org-template.machine.ts) + root store with debounced save. Capability-gated
(orgtemplate:edit) with a deny-by-default alert; route /brief/huisstijl.
Backend + generated client were already in place (WP-23). Also fixes a
pre-existing red check:tokens (WP-24 canvas hex fallbacks) and threads the
published logo through to the drafter's canvas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds LetterHtml.Render, a pure composer mirroring the FE letter canvas'
class vocabulary, behind two ExcludeFromDescription()'d endpoints
(GET /brief/preview, GET /admin/org-template/{subOrgId}/preview).
Auto-resolvable placeholders pull from seed/case data; unresolved
manual ones render as "[NOG IN TE VULLEN: label]". A sent brief
archives its composed HTML (BriefEntity.ArchivedHtml) so a later
org-template republish never changes it. FE gets a hand-written fetch
adapter (text/html, not JSON) and a "Voorbeeld" button that opens the
preview in a new tab.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
One letter surface for every role: LetterCanvasComponent renders the
org template's letterhead/signature/footer around the case-type
sections, with editableRegions content|template|none. public/letter.css
is the FE⇄BE rendering contract (WP-25 inlines it verbatim).
letter-preview deleted — its read-only rendering absorbed into 'none'
mode. brief.machine.ts byte-identical; orgTemplate parses at the
adapter boundary and lives beside the machine in BriefStore.
Also fixes passage-picker multi-select (checkboxes all shared
id="undefined", so labels only toggled the first box) and keeps the
±page-break marks from drawing through canvas content.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second template axis (org identity: letterhead, footer, signature,
margins) server-side: OrgTemplateStore with JSON version history,
publish/rollback, sent-brief version pinning, admin role + capability,
5 admin endpoints, org-logo upload category. FE seam widened only
(Role/Capability unions, interceptor); WP-24/26 consume it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
npm update brought every package to the latest version its existing package.json
range allows (Angular tooling 22.0.2/22.0.4 -> 22.0.5, prettier 3.8.4 -> 3.9.4,
typescript-eslint 8.62.0 -> 8.62.1); package.json itself needed no range changes.
Auditing actual deprecation warnings (not just outdated versions) found nothing
further to fix: @angular/platform-browser-dynamic and @angular-devkit/build-angular
are deprecated by Angular but still required peer dependencies of the latest
published @storybook/angular (10.4.6 — peer range still `>=18.0.0 < 22.0.0`,
already why .npmrc sets legacy-peer-deps); jest-process-manager/expect-playwright
are transitive-only through @storybook/test-runner's latest stable (0.24.4). No
newer version of either Storybook package exists yet that drops them. The
remaining npm audit advisory (@babel/core, low severity) is the same
already-documented, deliberately-left issue in README.md (fixing it downgrades
Angular). Left package.json's overrides untouched.
The prettier bump alone changed formatting opinions on files this session didn't
otherwise touch (a stale markdown italics marker, a few object-literal wrap
points) — reformatted everything so `format:check` (part of CI) doesn't regress.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Correlation id becomes real ASP.NET Core middleware instead of a per-endpoint
read: every request gets one (client-supplied or generated), it's echoed as
an X-Correlation-Id response header, and pushed into the logging scope so
every log line for that request carries it — not just the Submit helper's,
verified against LogBrief which never threads it explicitly.
Idempotency-Key moves from per-HTTP-attempt (defeating its own purpose) to
per-logical-submit: runSubmit mints one key and threads it through a small
bridge (withIdempotencyKey/currentIdempotencyKey) since the NSwag-generated
client has no per-call header hook. Backend gains an IdempotencyStore that
short-circuits a replayed key to the first call's result instead of minting
a second reference — scoped to the Submit-helper endpoints per the WP's own
decision.
GET requests now retry transient failures (rxjs retry({count:2, delay:500}));
writes never auto-retry. Proven with a fake-HttpClient spec
(api-client.provider.spec.ts) rather than a manual network-tab check — the
WP's suggested `?scenario=error` check turned out not to exercise a real
network call at all (the interceptor throws before calling next()), so the
automated test is the actual proof.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
angular.json gains an i18n block (sourceLocale nl, en translation file) and
an `en` build/serve configuration with i18nMissingTranslation: "error" so a
new $localize string without an English unit fails the build, not silently
falls back. CI now runs `ng build --localize` to build both locales every
run. Verified end-to-end, not just "the build succeeded": the nl bundle
ships "Inloggen met DigiD", the en bundle ships "Log in with DigiD".
Incidental: prettier/compodoc regen noise in docs/wcag-checklist.md,
src/docs/a11y.mdx, documentation.json from the same working session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a happy-path spec (login → dashboard → registratie wizard, including
a real identity-document upload → real submit) and a degraded-path spec
(?scenario=error → <app-async> error slot → retry), both driving the real
app against the real .NET backend, plus a CI job that boots both.
Writing the retry spec surfaced a real bug: AsyncComponent's retry() only
reloads a [resource]-fed instance, so every real page (all [data]-fed via
a store's RemoteData) had a silently no-op retry button. Added a
retryClicked output and wired it on the dashboard's two async blocks.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds route-change focus management (new page's h1, afterNextRender) plus
scroll-position restoration wired once in app.config.ts; angular-eslint's
templateAccessibility bundle linting every inline template via
processInlineTemplates (verified firing with a planted violation, one real
hit fixed in rich-text-editor); docs/wcag-checklist.md and Foundations/
Accessibility MDX tying the four a11y layers (axe, lint, play tests,
manual checklist) together. The checklist pass already earned its keep —
it found a real 320px overflow in aanvraag-block's warning alert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires text-input's aria-describedby to the form-field description div
(the BSN hint was rendered but never announced), pins desc-before-error
ordering, and switches alert to role=alert for errors vs role=status
for info/ok/warning. Composition contract enforced by story play tests
(form-field+text-input, alert per variant) run in the WP-01 CI gate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add the 7 stories CLAUDE.md's testing rule ("UI is exercised via Storybook
stories") was missing: shared/layout/shell (Design System/Templates/Shell)
and all six previously-unstoried brief components (passage-picker,
rejection-comments, diagnostics-panel, letter-block, letter-preview,
letter-section — Domein/Brief/*), each with a default state plus the
meaningful variants (locked/editable, findings/clean, show/entry, etc).
Every *.component.ts in the repo now has a co-located story; *.page.ts
files stay unstoried, matching the existing norm.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Retitle all 49 stories into a sidebar that makes the DDD seam visible:
Foundations (curriculum) -> Design System (Atoms/Molecules/Organisms/
Templates/Devtools, everything in shared/ui + shared/layout) -> Domein
(Registratie/Herregistratie/Auth/Brief, everything in a context's ui/).
Pin the order via storySort. Add layers.mdx explaining the split and
linking the enforcing eslint rules; document the story-title convention
in CLAUDE.md. Fix a stale "status banner" reference in atomic-design.mdx
left over from WP-13's upload-status-banner deletion.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mark every hand-rolled shared/ui surface with a `// CIBG-GAP EXTENSION:`
comment + `cibgGap` story parameter (skeleton, spinner, rich-text-editor,
wizard-shell's error summary, application-link's non-navigating row,
debug-state, status-badge, card, placeholder-chip) so deviations from the
CIBG design system are auditable. Add the register MDX
(Foundations/CIBG Gap Register), cross-linked from ADR-0003. Delete the
near-identity upload-status-banner wrapper; its one consumer now uses
<app-alert> directly (a story added to keep the info-banner state covered).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix button atom's dead .btn-outline-primary → .btn-secondary; add 'ghost'
variant (.btn-ghost, CIBG-documented). RTE toolbar drops invented
.btn-outline-secondary/.btn-sm for .btn-ghost. file-input already used the
correct vendored .btn-upload pattern from the earlier CIBG UI fidelity
pass — no change needed there (documented as a deviation).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Consolidate four hand-rolled nl-NL date formatters (tasks.ts, aanvraag-
block, letter-preview, aanvraag-view -- one more than the WP found) into
one shared/kernel/datum.ts::formatDatumNl, spec-pinned and empty-safe.
Add the two missing command specs CLAUDE.md's testing rule calls for:
draft-sync.spec.ts (debounce coalescing + trailing-call + submit Result
shape, via fake timers) and submit-change-request.spec.ts. Remove the
unused RemoteData.map3 (updating the three docs that mentioned it); the
variant input on confirmation.component.ts was already gone. Documents
both stale-WP-text corrections in the backlog file.
This closes out backlog Phase 1 (FP/DDD core, WP-05..09).
Rename change-request.machine.ts's bare State/Msg to ChangeRequestState/
ChangeRequestMsg (the last machine not context-prefixed), document the
createStore-is-the-idiom + naming convention in CLAUDE.md §3, and add the
Foundations/State Machines (TEA) curriculum page. The wizard pages already
wired createStore (confirmed by reading each and by git log) -- the WP's
"hand-wired signal(model)" premise was stale; recorded as a deviation.
Collapse brief.store's busy signal + nullable lastError into one Idle |
Busy | Failed union (saveState gets matching tag-object style), and route
brief.page's load through RemoteData + <app-async> instead of a hand-rolled
@switch, via a BriefStore.remoteData projection of the machine's existing
loading/failed tags -- the machine keeps owning the letter's own status
lifecycle untouched. New brief.store.spec.ts covers the Busy->Idle/Failed
transitions; new Foundations/RemoteData & Async MDX page documents the
pattern and the WP-06 typed-loaded-slot fallback. Deviation from the
original plan recorded in the WP file.
Make AsyncLoadedDirective generic with a static ngTemplateContextGuard for
AsyncComponent's own internal typing. That can't propagate to consumer
`<ng-template appAsyncLoaded let-p>` sites though -- Angular only infers a
structural directive's type parameter from an input bound on that same
node, not from a sibling input on the parent component -- so the ~9
root-cause consumers (dashboard, registration-detail, aanvraag-detail,
registratie-wizard) instead unwrap the RemoteData Success value via a
typed computed() and narrow it locally with `@if (x(); as p)`. The
remaining union-narrowing casts (registration-summary, showcase concepts
page) are replaced with a stable @let binding and a direct resource read,
respectively. Documented as a deviation in WP-06's backlog file.
Close the three remaining unvalidated `as <DomainType>` casts at the wire
boundary (intake-policy, big-register aantekening type, brief passage scope),
each replaced by a Result-returning parser with a rejection-case spec, plus
the Foundations/Parse, don't validate curriculum page.
Replace the FE-computed authorization anti-pattern in BriefStore.editable
(derived from the unverified X-Role header) with server-computed decision
flags, mirroring the existing HerregistratieDecisionsDto pattern:
- Backend: Authz.cs is the single authorization helper — the SAME check
(Authz.CanActOn) both gates BriefStore.Review's mutations and computes
the BriefDecisionsDto flags shipped on every brief response, so emit
and enforce can never drift. New GET /me returns coarse, role-derived
capabilities (PRD-0002 SS6).
- Every brief endpoint (including send, previously ungated on HttpContext)
now returns a fresh BriefViewDto so decisions never go stale after a
mutation.
- FE: brief.store.ts reads canEdit/canApprove/canReject/canSend off the
loaded decisions instead of computing them from currentRole(); the
brief.machine carries decisions through every status transition.
- New shared/domain/capability.ts + shared/application/access.store.ts +
shared/infrastructure/me.adapter.ts: the general capability-spine
infrastructure (AccessStore.can(), capabilityGuard) for future routes.
Deviates from the original WP-18 draft by NOT renaming auth/domain's
Session to a Principal union — ADR-0002 explicitly defers that refactor
until a second actor exists, and the brief workflow's drafter/approver
identity turned out to be a separate axis from the SSP login session
entirely. See docs/backlog/WP-18-abac-capability-spine.md for the full
as-built record.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Replace stale @rijkshuisstijl-community package/theming claims with the
vendored CIBG Huisstijl + token-bridge reality (ADR-0003); system-font
stack instead of Fira Sans; embed the now-existing document-upload story
in atomic-design.mdx.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 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 two wizard lookups behind application-layer facades so ui/ no longer
injects infrastructure adapters directly:
- RegistratieLookupStore (BRP address + DUO diplomas): owns the resources,
runs the trust-boundary parse, exposes adresStatus/prefillAdres/duoLookup.
- IntakePolicyStore (scholing threshold): owns the policy resource, exposes
the derived threshold.
Add the lint rule ui/ + layout/ ↛ **/infrastructure/** (@typescript-eslint
variant so it composes with the base direction rules; stories/specs exempted
as test scaffolding). Add the documented showcase sanction (may read every
context). Fix the docs' inventory: 6 contexts / 5 layers, +brief, +contracts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The inschrijven/herregistratie wizards needed two clicks on "Volgende" to advance
in Firefox (and Zen). Root cause: picking a field (e.g. correspondentie) triggers
draft-sync's debounced create, which router.navigate()s to stamp ?aanvraag=<id>
into the URL. withViewTransitions() animated that same-route navigation, and for
the transition's duration Firefox's ::view-transition overlay swallows pointer
events (confirmed: elementFromPoint over the button returns the overlay, not the
button). Chrome sets pointer-events:none on the overlay so clicks pass through —
hence Firefox-only.
Fix: skip the view transition for same-route navigations (compare leaf routeConfig)
so the silent id-stamp doesn't animate; genuine page-to-page transitions still fade.
Verified in Firefox: page navs still animate (skipped:0), the ?aanvraag stamp is
skipped, and Post → single Next advances step 1 → 2. GREEN + a11y.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lint-enforce two architecture rules that were only documented (ADR-0001),
landing the rules with the fixes so the build stays green:
- contracts/ imports nothing: dashboard-view.dto.ts is now pure wire shapes
(inline string-union enums, no domain imports). The DashboardView FE-view
type moves to the adapter, which maps wire → domain (compiler-enforced seam).
- ApiClient lives only in infrastructure: change-request-form (UI) no longer
injects ApiClient — a new ChangeRequestAdapter owns the client and the submit
becomes a createSubmitChangeRequest() command factory (createDraftSync shape).
draft-sync's wire-DTO import becomes type-only (allowed via allowTypeImports).
- Role type moves to shared/domain/role.ts; the ?role= reader stays in
shared/infrastructure/role.ts.
- eslint: contracts import-ban + @typescript-eslint/no-restricted-imports on
api-client (value-only; type imports permitted; infra + shared/upload exempt).
Also fixes a PRE-EXISTING bug found while verifying the flow: change-request-form
never imported FormsModule, so (ngSubmit) didn't bind and the submit button did a
native form submit (page reload) instead of submitting. Verified end-to-end in the
running app: submit → command → adapter → backend → reference, success alert shown.
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>
`draft().antwoorden[q.id]` types as `string` (Record index, no noUncheckedIndexedAccess),
so the template `?? ''` tripped NG8102's "redundant nullish coalescing" diagnostic — but a
missing key IS `undefined` at runtime, so the guard is real. Moved it into an `antwoord(id)`
component method: same runtime safety, no template-only diagnostic. Build now warning-clean.
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>
draftSync.resume() does async network work and dispatched Seed on completion,
which could land after the user's first action and reset the machine (cursor +
fields) — the "click Volgende twice" symptom. Guard centrally: applyResume()
skips when the user already has progress (snapshot() != null) or there's nothing
to restore. onResume is now only ever called with a real draft on a pristine
machine, so the three wizard callbacks drop the dead `?? initial`.
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>
Two dashboard bugs from the just-built feature.
1. Cancel didn't reflect until a browser refresh. ApplicationsStore now OWNS the
list in a writable RemoteData signal instead of projecting a resource() through
fromResource; cancel removes the row synchronously (guaranteed disappear, no
dependence on CD timing / HTTP cache / the reloading gap), then confirms the
DELETE (rollback on failure, no resync). Adapter gains list(); applicationsResource()
removed. Shared fromResource/remote-data.ts deliberately untouched.
2. Duplicate / inconsistent Concepts per type. createDraftSync.resume() now: a
?aanvraag link wins; else it resumes THIS type's existing Concept (loads its
draft); else fresh. ensureId is gated behind resume so a fast typist can't create
a duplicate before the lookup lands. restart()/reset() deletes the current Concept
(submitted → 409, kept) so there's at most one active Concept per type. A non-Concept
id can't reopen as an editable draft. Backend unchanged.
Gates green: lint, vitest 128, build, check:tokens, backend dotnet 56.
Wiring is not unit-covered — needs live verification (see plan).
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>
The payoff — A–E become visible. The dashboard now shows a "Mijn aanvragen"
section at the top with a block per application.
- ApplicationsStore (registratie/application, root): the list as a RemoteData
signal, parsed at the trust boundary; reload() (dashboard revisit reflects
server-computed auto-approval); optimistic cancel (hide → reload / un-hide on fail).
- aanvraag-block (organism): badge (tag → colour/label) + per-status body
("Stap X van Y" / referentie + ingediend-datum / manual note / reden) + actions
from the pure blockActions. Composes card + status-badge + button. Stories per status.
- dashboard: "Mijn aanvragen" section (hidden when empty), sorted Concept → In
behandeling → resolved; Verder gaan deep-links the wizard (?aanvraag=<id>),
Annuleren cancels via the store.
Deferred to F2: document-chip preview/download affordance.
Gates green: vitest 128, lint, build, check:tokens; backend dotnet 56.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All three wizards now submit through the backend aanvraag lifecycle, so a
submitted Concept actually transitions (dashboard shows it correctly in F).
- blockActions(status) (domain + spec): the pure per-status action decision
(Concept → resume/cancel; In behandeling → viewDocuments; resolved → none).
- createDraftSync.submit(): ensure the Concept exists, then
POST /applications/{id}/submit; folded into a Result like the old commands.
- registratie: submit via draftSync (duo → auto, handmatig → manual pending — the
old 422 path is gone from the wizard).
- intake + herregistratie: adopt createDraftSync (persistence + resume-by-link);
intake retires sessionStorage `intake-v3`; herregistratie gains persistence.
Both submit through the aanvraag too. hasProgress added to each machine (+spec).
- Delete now-dead submit-registratie/submit-intake/submit-herregistratie commands.
Deferred: the old /registrations, /intakes, /herregistraties backend endpoints +
RejectRegistratie are now unused by the FE but still present (+ tested) — retiring
them cascades into backend test rewrites, so it's a focused follow-up cleanup.
Gates green: vitest 128, lint, build; backend unchanged (dotnet 56).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the registratie wizard's sessionStorage draft with a backend-owned
Concept aanvraag (PRD 0001, phase D — the registratie vertical slice).
- createDraftSync (registratie/application): reusable controller (field-initializer
idiom, like createUploadController). Creates the Concept lazily on first progress,
stamps `?aanvraag=<id>` into the URL, debounced-syncs the machine snapshot per
change, and resumes from `?aanvraag` on load. Inert without a Router or when an
explicit seed is present (Storybook/tests) — no network there.
- hasProgress (machine, pure + spec): "worth persisting?" — excludes the automatic
BRP address prefill so a bare page visit creates nothing. Accepted regression:
a step-0-only address edit isn't persisted until the user advances/chooses.
- Wizard: dropped STORAGE_KEY/restore + the sessionStorage effect; restart() detaches
the Concept and drops the link.
Deferred (noted): ApplicationsStore -> phase F (dashboard is its only consumer);
intake-v3 + herregistratie persistence -> phase E (copy this pattern).
Gates green: vitest 125, lint, build; backend unchanged (dotnet 56).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Regenerate the NSwag client against the new backend endpoints (application +
content methods, Aanvraag DTOs) — clears the API-client drift.
- registratie/domain/aanvraag.ts: FE domain view — AanvraagType + AanvraagStatus
discriminated union (illegal states unrepresentable) + Aanvraag/AanvraagDetail.
Lives in registratie: the dashboard consumes it, downstream wizards produce it.
- ApplicationsAdapter (infrastructure, the only new network surface): list resource
+ create/syncDraft/cancel/submit commands, with a hand-written parse* boundary
(parseAanvraagStatus/parseApplicationSummary/parseApplications/parseApplicationDetail)
mapping untrusted DTO -> domain, per ADR-0001. Spec covers each status tag + rejects.
- UploadAdapter.contentUrl(id): direct href for preview/download (browser opens it).
Gates green: dotnet test 56, vitest 122, lint, build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
createUploadController runs an effect() that calls dispatch. store.ts
dispatch was `model.set(update(model(), msg))` — the reactive model()
read made the effect depend on its own write and re-schedule forever,
livelocking the main thread. Angular's NG0103 guard doesn't cover effect
self-rescheduling, so no error was thrown; Firefox just killed the
unresponsive tab. Only /registreren and /herregistratie (which mount the
upload controller) were affected.
dispatch now uses model.update((m) => update(m, msg)) — the current value
is read untracked, so no effect can loop on its own dispatch. Hardens all
wizard stores. Adds a regression spec.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>