feat(design): adopt CIBG component patterns (header, forms, wizards, dashboard)

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>
This commit is contained in:
2026-07-02 14:33:05 +02:00
parent 7887355ca3
commit 6257d7ede3
44 changed files with 769 additions and 591 deletions

View File

@@ -1,9 +1,6 @@
import { Component, computed, input, output } from '@angular/core';
import { DatePipe } from '@angular/common';
import { HeadingComponent } from '@shared/ui/heading/heading.component';
import { StatusBadgeComponent } from '@shared/ui/status-badge/status-badge.component';
import { ButtonComponent } from '@shared/ui/button/button.component';
import { CardComponent } from '@shared/ui/card/card.component';
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
import { Aanvraag, AanvraagType } from '@registratie/domain/aanvraag';
import { blockActions } from '@registratie/domain/block-actions';
@@ -13,53 +10,30 @@ const TYPE_LABELS: Record<AanvraagType, string> = {
intake: $localize`:@@aanvraagBlock.type.intake:Herregistratie-intake`,
};
/** Organism: one application on the dashboard's"Mijn aanvragen" list. Which badge
+ actions it shows is driven by the pure `blockActions(status)` and the status
tag; the block only renders. Composes card + status-badge + button. */
/** Organism: one application on the dashboard's "Mijn aanvragen" list, rendered as
a CIBG Huisstijl "aanvragen" row (application-link). Which text/actions it shows
is driven by the pure `blockActions(status)` and the status tag; the block only
renders. Only a resumable Concept is clickable — a resolved status has nothing
to navigate to, so it renders as a plain (non-anchor) row. */
@Component({
selector: 'app-aanvraag-block',
imports: [DatePipe, HeadingComponent, StatusBadgeComponent, ButtonComponent, CardComponent],
styles: [`
.head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--rhc-space-max-md); flex-wrap: wrap; }
.actions { display: flex; gap: var(--rhc-space-max-md); margin-block-start: var(--rhc-space-max-md); }
`],
imports: [ButtonComponent, ApplicationLinkComponent],
// display:contents — see application-link.component.ts (keeps <li> a direct <ul> child).
styles: [`:host{display:contents}`],
template: `
<app-card>
<div class="head">
<app-heading [level]="3">{{ typeLabel() }}</app-heading>
<app-status-badge [label]="badgeLabel()" [color]="badgeColor()" />
</div>
@switch (aanvraag().status.tag) {
@case ('Concept') {
<p i18n="@@aanvraagBlock.stap">Stap {{ stap().index }} van {{ stap().count }}</p>
}
@case ('InBehandeling') {
<p i18n="@@aanvraagBlock.inBehandeling">Referentie {{ ref() }} · ingediend op {{ aanvraag().submittedAt | date: 'longDate' }}</p>
@if (manual()) {
<p class="app-text-subtle" i18n="@@aanvraagBlock.manual">Uw aanvraag wordt handmatig beoordeeld in de backoffice.</p>
}
}
@case ('Goedgekeurd') {
<p i18n="@@aanvraagBlock.goedgekeurd">Referentie {{ ref() }}</p>
}
@case ('Afgewezen') {
<p i18n="@@aanvraagBlock.afgewezen">Referentie {{ ref() }}</p>
<p class="app-text-subtle">{{ reden() }}</p>
}
}
@if (actions().length) {
<div class="actions">
@if (actions().includes('resume')) {
<app-button (click)="resume.emit()" i18n="@@aanvraagBlock.verderGaan">Verder gaan</app-button>
}
@if (actions().includes('cancel')) {
<app-button variant="subtle" (click)="cancel.emit()" i18n="@@aanvraagBlock.annuleren">Annuleren</app-button>
}
<app-application-link
[heading]="typeLabel()"
[status]="statusText()"
[subtitle]="subtitle()"
[cta]="actions().includes('resume') ? verderGaan : ''"
[clickable]="actions().includes('resume')"
(activate)="resume.emit()">
@if (actions().includes('cancel')) {
<div applicationActions>
<app-button variant="subtle" (click)="cancel.emit()" i18n="@@aanvraagBlock.annuleren">Annuleren</app-button>
</div>
}
</app-card>
</app-application-link>
`,
})
export class AanvraagBlockComponent {
@@ -68,42 +42,30 @@ export class AanvraagBlockComponent {
resume = output<void>();
cancel = output<void>();
protected readonly verderGaan = $localize`:@@aanvraagBlock.verderGaan:Verder gaan`;
protected typeLabel = computed(() => TYPE_LABELS[this.aanvraag().type]);
protected actions = computed(() => blockActions(this.aanvraag().status));
// Status-tag → badge presentation (the UI's mapping, not a domain rule).
protected badgeLabel = computed(() => {
switch (this.aanvraag().status.tag) {
case 'Concept': return $localize`:@@aanvraagBlock.badge.concept:Concept`;
case 'InBehandeling': return $localize`:@@aanvraagBlock.badge.inBehandeling:In behandeling`;
case 'Goedgekeurd': return $localize`:@@aanvraagBlock.badge.goedgekeurd:Goedgekeurd`;
case 'Afgewezen': return $localize`:@@aanvraagBlock.badge.afgewezen:Afgewezen`;
// Status-tag → row text (the UI's mapping, not a domain rule).
protected statusText = computed(() => {
const s = this.aanvraag().status;
switch (s.tag) {
case 'Concept': return $localize`:@@aanvraagBlock.stap:Stap ${s.stepIndex + 1}:index: van ${s.stepCount}:count:`;
case 'InBehandeling': return $localize`:@@aanvraagBlock.inBehandeling:Referentie ${s.referentie}:ref: · ingediend op ${formatNL(this.aanvraag().submittedAt)}:datum:`;
case 'Goedgekeurd': return $localize`:@@aanvraagBlock.goedgekeurd:Referentie ${s.referentie}:ref:`;
case 'Afgewezen': return $localize`:@@aanvraagBlock.afgewezen:Referentie ${s.referentie}:ref:`;
}
});
protected badgeColor = computed(() => {
switch (this.aanvraag().status.tag) {
case 'Concept': return 'var(--rhc-color-cool-grey-300)';
case 'InBehandeling': return 'var(--rhc-color-oranje-500)';
case 'Goedgekeurd': return 'var(--rhc-color-groen-500)';
case 'Afgewezen': return 'var(--rhc-color-rood-500)';
}
});
// Field accessors per tag (the template @switch guarantees the right shape).
protected stap = computed(() => {
// Secondary note under the status line, when there's one to show.
protected subtitle = computed(() => {
const s = this.aanvraag().status;
return s.tag === 'Concept' ? { index: s.stepIndex + 1, count: s.stepCount } : { index: 0, count: 0 };
});
protected ref = computed(() => {
const s = this.aanvraag().status;
return s.tag !== 'Concept' ? s.referentie : '';
});
protected manual = computed(() => {
const s = this.aanvraag().status;
return s.tag === 'InBehandeling' && s.manual;
});
protected reden = computed(() => {
const s = this.aanvraag().status;
return s.tag === 'Afgewezen' ? s.reden : '';
if (s.tag === 'InBehandeling' && s.manual) return $localize`:@@aanvraagBlock.manual:Uw aanvraag wordt handmatig beoordeeld in de backoffice.`;
if (s.tag === 'Afgewezen') return s.reden;
return '';
});
}
function formatNL(iso?: string): string {
return iso ? new Date(iso).toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' }) : '';
}

View File

@@ -1,4 +1,6 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { applicationConfig } from '@storybook/angular';
import { provideRouter } from '@angular/router';
import { AanvraagBlockComponent } from './aanvraag-block.component';
import { Aanvraag } from '@registratie/domain/aanvraag';
@@ -14,6 +16,12 @@ const base = {
const meta: Meta<AanvraagBlockComponent> = {
title: 'Organisms/Aanvraag Block',
component: AanvraagBlockComponent,
decorators: [applicationConfig({ providers: [provideRouter([])] })],
render: (args) => ({
props: args,
// A row is an <li> — the "applications" list styling needs the real list context.
template: `<ul class="list-unstyled"><app-aanvraag-block [aanvraag]="aanvraag" /></ul>`,
}),
};
export default meta;
type Story = StoryObj<AanvraagBlockComponent>;