Add registratie wizard, BFF dashboard-view, contracts/value-objects, and architecture docs

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>
This commit is contained in:
2026-06-26 17:23:52 +02:00
parent 8a8a2f0f29
commit 64385999eb
58 changed files with 7271 additions and 556 deletions

View File

@@ -0,0 +1,39 @@
import { Component, input } from '@angular/core';
import { RouterLink } from '@angular/router';
export interface BreadcrumbItem {
label: string;
link?: string; // omit on the current (last) page
}
/** Chrome: breadcrumb navigation. Renders the RHC/Utrecht breadcrumb pattern; the
last item is the current page (no link, aria-current). Domain-free — the caller
supplies the trail. */
@Component({
selector: 'app-breadcrumb',
imports: [RouterLink],
styles: [`
:host{display:block}
.list{display:flex;flex-wrap:wrap;gap:var(--rhc-space-max-md);align-items:center;list-style:none;margin:0;padding:0}
.sep{color:var(--rhc-color-foreground-subtle)}
`],
template: `
<nav class="rhc-breadcrumb-nav utrecht-breadcrumb-nav" aria-label="Kruimelpad">
<ol class="utrecht-breadcrumb-nav__list list">
@for (item of items(); track item.label; let last = $last) {
<li class="utrecht-breadcrumb-nav__item">
@if (item.link && !last) {
<a class="utrecht-breadcrumb-nav__link rhc-link nl-link" [routerLink]="item.link">{{ item.label }}</a>
<span class="sep" aria-hidden="true">/</span>
} @else {
<span class="utrecht-breadcrumb-nav__link rhc-breadcrumb-nav__link--current" aria-current="page">{{ item.label }}</span>
}
</li>
}
</ol>
</nav>
`,
})
export class BreadcrumbComponent {
items = input.required<BreadcrumbItem[]>();
}

View File

@@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { applicationConfig } from '@storybook/angular';
import { provideRouter } from '@angular/router';
import { BreadcrumbComponent } from './breadcrumb.component';
const meta: Meta<BreadcrumbComponent> = {
title: 'Layout/Breadcrumb',
component: BreadcrumbComponent,
decorators: [applicationConfig({ providers: [provideRouter([])] })],
render: (args) => ({
props: args,
template: `<app-breadcrumb [items]="items" />`,
}),
};
export default meta;
type Story = StoryObj<BreadcrumbComponent>;
export const TweeNiveaus: Story = {
args: { items: [{ label: 'Mijn omgeving', link: '/dashboard' }, { label: 'Inschrijven in het BIG-register' }] },
};
export const DrieNiveaus: Story = {
args: { items: [{ label: 'Mijn omgeving', link: '/dashboard' }, { label: 'Registratie', link: '/registratie' }, { label: 'Inschrijven' }] },
};

View File

@@ -1,22 +1,31 @@
import { Component, input } from '@angular/core';
import { HeadingComponent } from '@shared/ui/heading/heading.component';
import { LinkComponent } from '@shared/ui/link/link.component';
import { BreadcrumbComponent, BreadcrumbItem } from '@shared/layout/breadcrumb/breadcrumb.component';
/** Template: standard page body — optional back-link, a heading, optional intro,
and projected content. Rendered inside the persistent ShellComponent via the
router outlet, so it owns only the content (not the header/footer chrome). */
/** Template: standard page body — optional breadcrumb, optional back-link, a
heading, optional intro, and projected content. Rendered inside the persistent
ShellComponent via the router outlet, so it owns only the content (not chrome). */
@Component({
selector: 'app-page-shell',
imports: [HeadingComponent, LinkComponent],
styles: [':host{display:block}'],
imports: [HeadingComponent, LinkComponent, BreadcrumbComponent],
styles: [`
:host{display:block}
.body--narrow{max-inline-size:var(--app-form-narrow)}
.crumb{margin-block-end:var(--rhc-space-max-xl)}
.intro{margin-block-end:var(--rhc-space-max-2xl)}
`],
template: `
<div [style.max-width]="width() === 'narrow' ? '32rem' : null">
<div [class.body--narrow]="width() === 'narrow'">
@if (breadcrumb()) {
<app-breadcrumb class="crumb" [items]="breadcrumb()!" />
}
@if (backLink()) {
<p><app-link [to]="backLink()!">← {{ backLabel() }}</app-link></p>
}
<app-heading [level]="1">{{ heading() }}</app-heading>
@if (intro()) {
<p class="rhc-paragraph" style="margin-bottom:1.5rem">{{ intro() }}</p>
<p class="rhc-paragraph intro">{{ intro() }}</p>
}
<ng-content />
</div>
@@ -28,4 +37,5 @@ export class PageShellComponent {
backLink = input<string>();
backLabel = input('Terug naar overzicht');
width = input<'default' | 'narrow'>('default');
breadcrumb = input<BreadcrumbItem[]>();
}

View File

@@ -8,13 +8,19 @@ import { SiteFooterComponent } from '@shared/layout/site-footer/site-footer.comp
@Component({
selector: 'app-shell',
imports: [RouterOutlet, SiteHeaderComponent, SiteFooterComponent],
styles: [':host{display:block}'],
styles: [`
:host{display:block}
.skip{position:absolute;left:var(--app-skip-link-offset)}
.layout{min-block-size:100vh;align-items:stretch}
.main{flex:1;inline-size:100%;box-sizing:border-box}
.content{max-inline-size:var(--app-content-max);margin-inline:auto;padding:var(--rhc-space-max-3xl) var(--rhc-space-max-2xl);box-sizing:border-box}
`],
template: `
<a href="#main" class="rhc-skip-link" style="position:absolute;left:-999px">Naar de inhoud</a>
<div class="utrecht-page-layout utrecht-page-layout--stretch" style="--app-content-max:64rem;min-height:100vh;align-items:stretch">
<a href="#main" class="rhc-skip-link skip">Naar de inhoud</a>
<div class="utrecht-page-layout utrecht-page-layout--stretch layout">
<app-site-header />
<main id="main" class="utrecht-page-content" style="flex:1;width:100%;box-sizing:border-box">
<div style="max-width:var(--app-content-max);margin:0 auto;padding:2rem 1.5rem;box-sizing:border-box">
<main id="main" class="utrecht-page-content main">
<div class="content">
<router-outlet />
</div>
</main>

View File

@@ -3,13 +3,18 @@ import { Component } from '@angular/core';
/** Organism: site footer. */
@Component({
selector: 'app-site-footer',
styles: [':host{display:block}'],
styles: [`
:host{display:block}
.bar{background:var(--rhc-color-donkerblauw-700);color:var(--rhc-color-wit);margin-block-start:var(--rhc-space-max-5xl);inline-size:100%}
.inner{max-inline-size:var(--app-content-max);margin-inline:auto;padding:var(--rhc-space-max-2xl);display:flex;gap:var(--rhc-space-max-3xl);flex-wrap:wrap;box-sizing:border-box}
.end{margin-inline-start:auto}
`],
template: `
<footer class="utrecht-page-footer" style="background:var(--rhc-color-lintblauw-900,#01689b);color:#fff;margin-top:3rem;width:100%">
<div style="max-width:var(--app-content-max);margin:0 auto;padding:1.5rem;display:flex;gap:2rem;flex-wrap:wrap;box-sizing:border-box">
<footer class="utrecht-page-footer bar">
<div class="inner">
<span>BIG-register</span>
<span>CIBG — Ministerie van Volksgezondheid, Welzijn en Sport</span>
<span style="margin-left:auto;opacity:0.85">Demo / POC — geen echte gegevens</span>
<span class="end">Demo / POC — geen echte gegevens</span>
</div>
</footer>
`,

View File

@@ -8,17 +8,26 @@ import { RouterLink } from '@angular/router';
imports: [RouterLink],
// :host display:block so the full-bleed bar fills the flex column (custom
// elements default to display:inline, which collapsed the bar to its content).
styles: [':host{display:block}'],
styles: [`
:host{display:block}
.bar{background:var(--rhc-color-lintblauw-700);color:var(--rhc-color-wit);inline-size:100%}
.inner{display:flex;align-items:center;gap:var(--rhc-space-max-xl);max-inline-size:var(--app-content-max);margin-inline:auto;padding:var(--rhc-space-max-xl) var(--rhc-space-max-2xl);box-sizing:border-box}
.brand{display:flex;align-items:center;gap:var(--rhc-space-max-lg);color:inherit;text-decoration:none}
.mark{display:inline-block;inline-size:var(--rhc-space-max-md);block-size:var(--rhc-space-max-4xl);background:var(--rhc-color-wit)}
.name{font-weight:var(--rhc-text-font-weight-bold);line-height:var(--rhc-text-line-height-sm)}
.sub{font-weight:var(--rhc-text-font-weight-regular);font-size:var(--rhc-text-font-size-sm)}
.portal{margin-inline-start:auto;font-weight:var(--rhc-text-font-weight-semi-bold)}
`],
template: `
<header class="utrecht-page-header" style="background:var(--rhc-color-lintblauw-700,#154273);color:#fff;width:100%">
<div style="display:flex;align-items:center;gap:1rem;max-width:var(--app-content-max);margin:0 auto;padding:1rem 1.5rem;box-sizing:border-box">
<a routerLink="/dashboard" style="display:flex;align-items:center;gap:0.75rem;color:inherit;text-decoration:none">
<span aria-hidden="true" style="display:inline-block;width:0.5rem;height:2.25rem;background:#fff"></span>
<span style="font-weight:700;line-height:1.1">
Rijksoverheid<br><span style="font-weight:400;font-size:0.9rem">BIG-register</span>
<header class="utrecht-page-header bar">
<div class="inner">
<a routerLink="/dashboard" class="brand">
<span aria-hidden="true" class="mark"></span>
<span class="name">
Rijksoverheid<br><span class="sub">BIG-register</span>
</span>
</a>
<span style="margin-left:auto;font-weight:500">{{ subtitle() }}</span>
<span class="portal">{{ subtitle() }}</span>
</div>
</header>
`,