feat(design): re-theme to CIBG Huisstijl (Bootstrap 5.2), replacing RHC/Utrecht

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>
This commit is contained in:
2026-07-02 12:32:50 +02:00
parent 180c08d4ee
commit b443f1fc04
74 changed files with 4286 additions and 1116 deletions

View File

@@ -13,7 +13,7 @@ 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
/** 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. */
@Component({
@@ -32,20 +32,20 @@ const TYPE_LABELS: Record<AanvraagType, string> = {
@switch (aanvraag().status.tag) {
@case ('Concept') {
<p class="rhc-paragraph" i18n="@@aanvraagBlock.stap">Stap {{ stap().index }} van {{ stap().count }}</p>
<p i18n="@@aanvraagBlock.stap">Stap {{ stap().index }} van {{ stap().count }}</p>
}
@case ('InBehandeling') {
<p class="rhc-paragraph" i18n="@@aanvraagBlock.inBehandeling">Referentie {{ ref() }} · ingediend op {{ aanvraag().submittedAt | date: 'longDate' }}</p>
<p i18n="@@aanvraagBlock.inBehandeling">Referentie {{ ref() }} · ingediend op {{ aanvraag().submittedAt | date: 'longDate' }}</p>
@if (manual()) {
<p class="rhc-paragraph app-text-subtle" i18n="@@aanvraagBlock.manual">Uw aanvraag wordt handmatig beoordeeld in de backoffice.</p>
<p class="app-text-subtle" i18n="@@aanvraagBlock.manual">Uw aanvraag wordt handmatig beoordeeld in de backoffice.</p>
}
}
@case ('Goedgekeurd') {
<p class="rhc-paragraph" i18n="@@aanvraagBlock.goedgekeurd">Referentie {{ ref() }}</p>
<p i18n="@@aanvraagBlock.goedgekeurd">Referentie {{ ref() }}</p>
}
@case ('Afgewezen') {
<p class="rhc-paragraph" i18n="@@aanvraagBlock.afgewezen">Referentie {{ ref() }}</p>
<p class="rhc-paragraph app-text-subtle">{{ reden() }}</p>
<p i18n="@@aanvraagBlock.afgewezen">Referentie {{ ref() }}</p>
<p class="app-text-subtle">{{ reden() }}</p>
}
}

View File

@@ -25,8 +25,8 @@ export type AdresErrors = Partial<Record<keyof AdresValue, string>>;
app-form-field + app-form-field{display:block;margin-block-start:var(--rhc-space-max-lg)}
`],
template: `
<fieldset class="utrecht-form-fieldset utrecht-form-fieldset--html-fieldset">
<legend class="utrecht-form-fieldset__legend utrecht-form-fieldset__legend--html-legend">{{ legend() }}</legend>
<fieldset>
<legend>{{ legend() }}</legend>
<app-form-field i18n-label="@@address.straat" label="Straat en huisnummer" [fieldId]="idPrefix() + '-straat'" required [error]="errors().straat">
<app-text-input [inputId]="idPrefix() + '-straat'" [invalid]="!!errors().straat"
[ngModel]="value().straat" (ngModelChange)="fieldChange.emit({ key: 'straat', value: $event })"

View File

@@ -19,8 +19,8 @@ import { Registration } from '@registratie/domain/registration';
import { Aanvraag, AanvraagType } from '@registratie/domain/aanvraag';
import { tasksFromProfile } from '@registratie/domain/tasks';
/** Page: "Mijn overzicht" — the portal home, following the NL Design System
"Mijn omgeving" pattern (side nav + "Wat moet ik regelen" + "Mijn zaken"). */
/** Page:"Mijn overzicht" — the portal home, following the NL Design System
"Mijn omgeving" pattern (side nav +"Wat moet ik regelen" +"Mijn zaken"). */
@Component({
selector: 'app-dashboard-page',
imports: [
@@ -58,7 +58,7 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
@if (tasks.length) {
<app-task-list class="app-section" [tasks]="tasks" />
} @else {
<p class="rhc-paragraph app-text-subtle" i18n="@@dashboard.nietsOpenstaan">U heeft op dit moment niets openstaan.</p>
<p class="app-text-subtle" i18n="@@dashboard.nietsOpenstaan">U heeft op dit moment niets openstaan.</p>
}
</section>
@@ -68,7 +68,7 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
<app-registration-summary [reg]="$any(p).registration" />
</div>
<app-card class="app-section" i18n-heading="@@dashboard.persoonsgegevens" heading="Persoonsgegevens (BRP)">
<dl class="rhc-data-summary rhc-data-summary--row">
<dl>
<app-data-row i18n-key="@@dashboard.straat" key="Straat" [value]="$any(p).person.adres.straat" />
<app-data-row i18n-key="@@dashboard.postcode" key="Postcode" [value]="$any(p).person.adres.postcode" />
<app-data-row i18n-key="@@dashboard.woonplaats" key="Woonplaats" [value]="$any(p).person.adres.woonplaats" />
@@ -92,7 +92,7 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
<app-skeleton height="2.5rem" [count]="3" />
</ng-template>
<ng-template appAsyncEmpty>
<p class="rhc-paragraph app-text-subtle" i18n="@@dashboard.geenSpecialismen">U heeft nog geen specialismen of aantekeningen.</p>
<p class="app-text-subtle" i18n="@@dashboard.geenSpecialismen">U heeft nog geen specialismen of aantekeningen.</p>
</ng-template>
</app-async>
</div>
@@ -104,7 +104,7 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
@for (a of acties; track a.to) {
<li>
<app-card [heading]="a.titel">
<p class="rhc-paragraph">{{ a.tekst }}</p>
<p>{{ a.tekst }}</p>
<app-link [to]="a.to">{{ a.actie }} →</app-link>
</app-card>
</li>
@@ -128,7 +128,7 @@ export class DashboardPage {
}
/** The user's applications, sorted Concept → In behandeling → resolved. Empty →
the "Mijn aanvragen" section is hidden (see template). */
the"Mijn aanvragen" section is hidden (see template). */
protected aanvragen = computed<Aanvraag[]>(() => {
const rd = this.apps.applications();
if (rd.tag !== 'Success') return [];

View File

@@ -38,8 +38,8 @@ const KANALEN = [
{ value: 'email', label: $localize`:@@registratie.kanaalEmail:E-mail` },
{ value: 'post', label: $localize`:@@registratie.kanaalPost:Post` },
];
const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
/** The server-owned geldigheidsvraag whose "ja" answer requires a Dutch-taalvaardigheid
const HANDMATIG = '__handmatig__'; // sentinel option:"my diploma isn't listed"
/** The server-owned geldigheidsvraag whose"ja" answer requires a Dutch-taalvaardigheid
upload (proof of the confirmed B2 level). Stable id shared with the backend. */
const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
@@ -47,7 +47,7 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
the pure `reduce` (registratie-wizard.machine.ts). The BRP address prefills the
draft via an effect; the DUO diploma list renders through <app-async>; choosing
a diploma reveals its server-derived beroep. The draft is persisted to the
backend as a Concept aanvraag (createDraftSync) so a reload — or a "Verder gaan"
backend as a Concept aanvraag (createDraftSync) so a reload — or a"Verder gaan"
from the dashboard via `?aanvraag=<id>` — resumes progress. Built from existing
atoms/molecules. */
@Component({
@@ -119,7 +119,7 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
[ngModel]="draft().beroep ?? ''" (ngModelChange)="dispatch({ tag: 'DeclareerBeroep', beroep: $event })" />
</app-form-field>
} @else if (draft().beroep) {
<dl class="rhc-data-summary rhc-data-summary--row app-section">
<dl class="app-section">
<app-data-row i18n-key="@@regWizard.beroepAfgeleid" key="Beroep (afgeleid uit diploma)" [value]="draft().beroep ?? ''" />
</dl>
}
@@ -156,7 +156,7 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
}
@case ('controle') {
<app-alert type="info" i18n="@@regWizard.controleer">Controleer uw gegevens en dien de registratie in.</app-alert>
<dl class="rhc-data-summary rhc-data-summary--row app-section">
<dl class="app-section">
<app-data-row i18n-key="@@regWizard.summary.adres" key="Adres" [value]="adresSamenvatting()" />
<app-data-row i18n-key="@@regWizard.summary.herkomstAdres" key="Herkomst adres" [value]="adresHerkomstLabel()" />
<app-data-row i18n-key="@@regWizard.summary.correspondentie" key="Correspondentie" [value]="correspondentieLabel()" />
@@ -274,7 +274,7 @@ export class RegistratieWizardComponent {
protected correspondentieLabel = computed(() => ({ email: $localize`:@@regWizard.corr.email:Per e-mail`, post: $localize`:@@regWizard.corr.post:Per post` }[this.draft().correspondentie ?? 'post']));
protected diplomaHerkomstLabel = computed(() => ({ duo: $localize`:@@regWizard.herkomst.diplomaDuo:Geverifieerd via DUO`, handmatig: $localize`:@@regWizard.herkomst.diplomaHandmatig:Handmatig ingevoerd (wordt beoordeeld)` }[this.draft().diplomaHerkomst ?? 'handmatig']));
/** BRP lookup outcome. A failure or "geen adres" never blocks the wizard — both
/** BRP lookup outcome. A failure or"geen adres" never blocks the wizard — both
fall back to manual entry (PRD §7). 'geen' covers both no-address-found and a
malformed response; 'fout' is an unreachable BRP. */
protected adresStatus = computed<'laden' | 'gevonden' | 'geen' | 'fout'>(() => {
@@ -311,7 +311,7 @@ export class RegistratieWizardComponent {
/** True while the user is entering a diploma manually (not in the DUO list). */
protected handmatigActief = computed(() => this.draft().diplomaHerkomst === 'handmatig');
/** The radio selection: a diploma id, or the "not listed" sentinel in manual mode. */
/** The radio selection: a diploma id, or the"not listed" sentinel in manual mode. */
protected diplomaKeuze = computed(() => (this.handmatigActief() ? HANDMATIG : this.draft().diplomaId ?? ''));
protected diplomaOptions = (data: DuoLookupDto) => [
@@ -384,7 +384,7 @@ export class RegistratieWizardComponent {
}
/** Reset the wizard to a fresh start. Reload the BRP lookup so the address
re-prefills, keeping the form and the "vooraf ingevuld" note consistent. */
re-prefills, keeping the form and the"vooraf ingevuld" note consistent. */
restart() {
this.draftSync.reset(); // discard the current Concept; a fresh one starts on next progress
this.dispatch({ tag: 'Seed', state: initial });

View File

@@ -12,7 +12,7 @@ import { CardComponent } from '@shared/ui/card/card.component';
imports: [DatePipe, DataRowComponent, StatusBadgeComponent, CardComponent],
template: `
<app-card>
<dl class="rhc-data-summary rhc-data-summary--row">
<dl>
<app-data-row i18n-key="@@summary.bigNummer" key="BIG-nummer" [value]="reg().bigNummer" />
<app-data-row i18n-key="@@summary.naam" key="Naam" [value]="reg().naam" />
<app-data-row i18n-key="@@summary.beroep" key="Beroep" [value]="reg().beroep" />

View File

@@ -7,21 +7,21 @@ import { Aantekening } from '@registratie/domain/registration';
selector: 'app-registration-table',
imports: [DatePipe],
template: `
<div class="utrecht-table-container utrecht-table-container--overflow-inline">
<table class="utrecht-table utrecht-table--html-table utrecht-table--alternate-row-color">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th class="utrecht-table__header-cell" i18n="@@table.type">Type</th>
<th class="utrecht-table__header-cell" i18n="@@table.omschrijving">Omschrijving</th>
<th class="utrecht-table__header-cell" i18n="@@table.datum">Datum</th>
<th scope="col" i18n="@@table.type">Type</th>
<th scope="col" i18n="@@table.omschrijving">Omschrijving</th>
<th scope="col" i18n="@@table.datum">Datum</th>
</tr>
</thead>
<tbody>
@for (row of rows(); track row.omschrijving) {
<tr>
<td class="utrecht-table__cell">{{ row.type }}</td>
<td class="utrecht-table__cell">{{ row.omschrijving }}</td>
<td class="utrecht-table__cell">{{ row.datum | date:'mediumDate' }}</td>
<td>{{ row.type }}</td>
<td>{{ row.omschrijving }}</td>
<td>{{ row.datum | date:'mediumDate' }}</td>
</tr>
}
</tbody>