diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 161f386..f794cbd 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -4,6 +4,10 @@ import { setCompodocJson } from '@storybook/addon-docs/angular'; import docJson from '../documentation.json'; setCompodocJson(docJson); +// Activate CIBG's official palette in the story iframe. The override is `body.brand--cibg`, +// so it must sit on (a wrapper
won't match) — mirrors index.html. +if (typeof document !== 'undefined') document.body.classList.add('brand--cibg'); + const preview: Preview = { // CIBG/Bootstrap styles bare elements — no theme wrapper class needed; just pad. // The token bridge lives in styles.scss (loaded via the app build target). diff --git a/documentation.json b/documentation.json index 959b7aa..90a442f 100644 --- a/documentation.json +++ b/documentation.json @@ -19135,7 +19135,7 @@ }, { "name": "SiteFooterComponent", - "id": "component-SiteFooterComponent-f2ac4822d1c0c5075e0552c4aad07325fc213f385c9022177b0d98f69d53ccee5c3d3047ecc3310cee0fc731007c614de1bdc4e995df993bc15ebecb86653323", + "id": "component-SiteFooterComponent-b55e94270c51daed95df229eeda44deeb81363b642b02f091f839880142571cee393d522730381bfc3fee17187c7283c8c85a22fd3a8af42c8dffc1ff32904a7", "file": "src/app/shared/layout/site-footer/site-footer.component.ts", "encapsulation": [], "entryComponents": [], @@ -19145,7 +19145,7 @@ "selector": "app-site-footer", "styleUrls": [], "styles": [ - "\n :host{display:block}\n .bar{background:var(--rhc-color-donkerblauw-700);color:var(--rhc-color-foreground-on-primary);margin-block-start:var(--rhc-space-max-5xl);inline-size:100%}\n .inner{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;display:flex;gap:var(--rhc-space-max-3xl);flex-wrap:wrap;justify-content:space-between}\n .tagline{font-style:italic;font-weight:var(--rhc-text-font-weight-semi-bold);font-size:var(--rhc-text-font-size-lg);max-inline-size:18rem}\n .ministry{margin-block-start:var(--rhc-space-max-md);font-size:var(--rhc-text-font-size-sm);color:var(--rhc-color-foreground-on-primary)}\n .col h2{margin:0 0 var(--rhc-space-max-md);font-size:var(--rhc-text-font-size-sm);font-weight:var(--rhc-text-font-weight-bold);text-transform:uppercase;letter-spacing:.04em}\n .links{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--rhc-space-max-sm);font-size:var(--rhc-text-font-size-sm)}\n .links a{color:var(--rhc-color-foreground-on-primary);text-decoration:none}\n .links a:hover{text-decoration:underline}\n .meta{inline-size:100%;border-block-start:var(--rhc-border-width-sm) solid var(--rhc-color-lintblauw-600);margin-block-start:var(--rhc-space-max-xl);padding-block-start:var(--rhc-space-max-lg);font-size:var(--rhc-text-font-size-sm);opacity:.85}\n " + "\n :host{display:block}\n .bar{background:var(--rhc-color-layout);color:var(--rhc-color-foreground-on-primary);margin-block-start:var(--rhc-space-max-5xl);inline-size:100%}\n .inner{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;display:flex;gap:var(--rhc-space-max-3xl);flex-wrap:wrap;justify-content:space-between}\n .tagline{font-style:italic;font-weight:var(--rhc-text-font-weight-semi-bold);font-size:var(--rhc-text-font-size-lg);max-inline-size:18rem}\n .ministry{margin-block-start:var(--rhc-space-max-md);font-size:var(--rhc-text-font-size-sm);color:var(--rhc-color-foreground-on-primary)}\n .col h2{margin:0 0 var(--rhc-space-max-md);font-size:var(--rhc-text-font-size-sm);font-weight:var(--rhc-text-font-weight-bold);text-transform:uppercase;letter-spacing:.04em}\n .links{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--rhc-space-max-sm);font-size:var(--rhc-text-font-size-sm)}\n .links a{color:var(--rhc-color-foreground-on-primary);text-decoration:none}\n .links a:hover{text-decoration:underline}\n .meta{inline-size:100%;border-block-start:var(--rhc-border-width-sm) solid rgb(255 255 255 / 0.25);margin-block-start:var(--rhc-space-max-xl);padding-block-start:var(--rhc-space-max-lg);font-size:var(--rhc-text-font-size-sm);opacity:.85}\n " ], "template": "\n", "templateUrl": [], @@ -19164,15 +19164,15 @@ "description": "

Organism: Rijksoverheid-style site footer — dark-blue, with the\n"De Rijksoverheid. Voor Nederland." tagline, responsible-ministry attribution,\nand a small "Over deze site" link column. ponytail: links point at the real\nrijksoverheid.nl pages, not a fabricated dead-link forest.

\n", "rawdescription": "\nOrganism: Rijksoverheid-style site footer — dark-blue, with the\n\"De Rijksoverheid. Voor Nederland.\" tagline, responsible-ministry attribution,\nand a small \"Over deze site\" link column. ponytail: links point at the real\nrijksoverheid.nl pages, not a fabricated dead-link forest.", "type": "component", - "sourceCode": "import { Component } from '@angular/core';\n\n/** Organism: Rijksoverheid-style site footer — dark-blue, with the\n \"De Rijksoverheid. Voor Nederland.\" tagline, responsible-ministry attribution,\n and a small \"Over deze site\" link column. ponytail: links point at the real\n rijksoverheid.nl pages, not a fabricated dead-link forest. */\n@Component({\n selector: 'app-site-footer',\n styles: [`\n :host{display:block}\n .bar{background:var(--rhc-color-donkerblauw-700);color:var(--rhc-color-foreground-on-primary);margin-block-start:var(--rhc-space-max-5xl);inline-size:100%}\n .inner{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;display:flex;gap:var(--rhc-space-max-3xl);flex-wrap:wrap;justify-content:space-between}\n .tagline{font-style:italic;font-weight:var(--rhc-text-font-weight-semi-bold);font-size:var(--rhc-text-font-size-lg);max-inline-size:18rem}\n .ministry{margin-block-start:var(--rhc-space-max-md);font-size:var(--rhc-text-font-size-sm);color:var(--rhc-color-foreground-on-primary)}\n .col h2{margin:0 0 var(--rhc-space-max-md);font-size:var(--rhc-text-font-size-sm);font-weight:var(--rhc-text-font-weight-bold);text-transform:uppercase;letter-spacing:.04em}\n .links{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--rhc-space-max-sm);font-size:var(--rhc-text-font-size-sm)}\n .links a{color:var(--rhc-color-foreground-on-primary);text-decoration:none}\n .links a:hover{text-decoration:underline}\n .meta{inline-size:100%;border-block-start:var(--rhc-border-width-sm) solid var(--rhc-color-lintblauw-600);margin-block-start:var(--rhc-space-max-xl);padding-block-start:var(--rhc-space-max-lg);font-size:var(--rhc-text-font-size-sm);opacity:.85}\n `],\n template: `\n \n `,\n})\nexport class SiteFooterComponent {}\n", + "sourceCode": "import { Component } from '@angular/core';\n\n/** Organism: Rijksoverheid-style site footer — dark-blue, with the\n \"De Rijksoverheid. Voor Nederland.\" tagline, responsible-ministry attribution,\n and a small \"Over deze site\" link column. ponytail: links point at the real\n rijksoverheid.nl pages, not a fabricated dead-link forest. */\n@Component({\n selector: 'app-site-footer',\n styles: [`\n :host{display:block}\n .bar{background:var(--rhc-color-layout);color:var(--rhc-color-foreground-on-primary);margin-block-start:var(--rhc-space-max-5xl);inline-size:100%}\n .inner{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;display:flex;gap:var(--rhc-space-max-3xl);flex-wrap:wrap;justify-content:space-between}\n .tagline{font-style:italic;font-weight:var(--rhc-text-font-weight-semi-bold);font-size:var(--rhc-text-font-size-lg);max-inline-size:18rem}\n .ministry{margin-block-start:var(--rhc-space-max-md);font-size:var(--rhc-text-font-size-sm);color:var(--rhc-color-foreground-on-primary)}\n .col h2{margin:0 0 var(--rhc-space-max-md);font-size:var(--rhc-text-font-size-sm);font-weight:var(--rhc-text-font-weight-bold);text-transform:uppercase;letter-spacing:.04em}\n .links{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--rhc-space-max-sm);font-size:var(--rhc-text-font-size-sm)}\n .links a{color:var(--rhc-color-foreground-on-primary);text-decoration:none}\n .links a:hover{text-decoration:underline}\n .meta{inline-size:100%;border-block-start:var(--rhc-border-width-sm) solid rgb(255 255 255 / 0.25);margin-block-start:var(--rhc-space-max-xl);padding-block-start:var(--rhc-space-max-lg);font-size:var(--rhc-text-font-size-sm);opacity:.85}\n `],\n template: `\n \n `,\n})\nexport class SiteFooterComponent {}\n", "assetsDirs": [], "styleUrlsData": "", - "stylesData": "\n :host{display:block}\n .bar{background:var(--rhc-color-donkerblauw-700);color:var(--rhc-color-foreground-on-primary);margin-block-start:var(--rhc-space-max-5xl);inline-size:100%}\n .inner{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;display:flex;gap:var(--rhc-space-max-3xl);flex-wrap:wrap;justify-content:space-between}\n .tagline{font-style:italic;font-weight:var(--rhc-text-font-weight-semi-bold);font-size:var(--rhc-text-font-size-lg);max-inline-size:18rem}\n .ministry{margin-block-start:var(--rhc-space-max-md);font-size:var(--rhc-text-font-size-sm);color:var(--rhc-color-foreground-on-primary)}\n .col h2{margin:0 0 var(--rhc-space-max-md);font-size:var(--rhc-text-font-size-sm);font-weight:var(--rhc-text-font-weight-bold);text-transform:uppercase;letter-spacing:.04em}\n .links{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--rhc-space-max-sm);font-size:var(--rhc-text-font-size-sm)}\n .links a{color:var(--rhc-color-foreground-on-primary);text-decoration:none}\n .links a:hover{text-decoration:underline}\n .meta{inline-size:100%;border-block-start:var(--rhc-border-width-sm) solid var(--rhc-color-lintblauw-600);margin-block-start:var(--rhc-space-max-xl);padding-block-start:var(--rhc-space-max-lg);font-size:var(--rhc-text-font-size-sm);opacity:.85}\n \n", + "stylesData": "\n :host{display:block}\n .bar{background:var(--rhc-color-layout);color:var(--rhc-color-foreground-on-primary);margin-block-start:var(--rhc-space-max-5xl);inline-size:100%}\n .inner{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;display:flex;gap:var(--rhc-space-max-3xl);flex-wrap:wrap;justify-content:space-between}\n .tagline{font-style:italic;font-weight:var(--rhc-text-font-weight-semi-bold);font-size:var(--rhc-text-font-size-lg);max-inline-size:18rem}\n .ministry{margin-block-start:var(--rhc-space-max-md);font-size:var(--rhc-text-font-size-sm);color:var(--rhc-color-foreground-on-primary)}\n .col h2{margin:0 0 var(--rhc-space-max-md);font-size:var(--rhc-text-font-size-sm);font-weight:var(--rhc-text-font-weight-bold);text-transform:uppercase;letter-spacing:.04em}\n .links{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--rhc-space-max-sm);font-size:var(--rhc-text-font-size-sm)}\n .links a{color:var(--rhc-color-foreground-on-primary);text-decoration:none}\n .links a:hover{text-decoration:underline}\n .meta{inline-size:100%;border-block-start:var(--rhc-border-width-sm) solid rgb(255 255 255 / 0.25);margin-block-start:var(--rhc-space-max-xl);padding-block-start:var(--rhc-space-max-lg);font-size:var(--rhc-text-font-size-sm);opacity:.85}\n \n", "extends": [] }, { "name": "SiteHeaderComponent", - "id": "component-SiteHeaderComponent-35a3fcd73778e62ff04f299edaa3cde5490b2800314762ac4b7fe01a68af79246c6fda70c45e0113ac7cb2ec0a81f2405c14a6c2a92a7a1f21f1b2f57ea1042f", + "id": "component-SiteHeaderComponent-4e99a966eef3f002465a6bb6a91c48f02cc0f39ed84dcd2ffcc04d6eb80ca2c878136183c30e68bd18ffd47d17cf0767639c39b8eba14d500220bb96ab81c592", "file": "src/app/shared/layout/site-header/site-header.component.ts", "encapsulation": [], "entryComponents": [], @@ -19182,7 +19182,7 @@ "selector": "app-site-header", "styleUrls": [], "styles": [ - "\n :host{display:block}\n /* Tier 1 — white brand bar */\n .brandbar{background:var(--rhc-color-wit);border-block-end:var(--rhc-border-width-sm) solid var(--rhc-color-border-subtle);inline-size:100%}\n .brandbar .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-lg) var(--rhc-space-max-2xl);box-sizing:border-box}\n .brand{display:flex;align-items:center;gap:var(--rhc-space-max-lg);color:var(--rhc-color-foreground-default);text-decoration:none}\n .mark{display:inline-block;inline-size:var(--rhc-space-max-md);block-size:var(--rhc-space-max-4xl);background:var(--rhc-color-lintblauw-700)}\n .name{font-weight:var(--rhc-text-font-weight-bold);line-height:var(--rhc-text-line-height-sm)}\n .sub{font-weight:var(--rhc-text-font-weight-regular);font-size:var(--rhc-text-font-size-sm);color:var(--rhc-color-foreground-subtle)}\n .session{margin-inline-start:auto;display:flex;align-items:center;gap:var(--rhc-space-max-lg);font-size:var(--rhc-text-font-size-sm)}\n .user{color:var(--rhc-color-foreground-subtle)}\n .logout{background:none;border:0;padding:0;cursor:pointer;color:var(--rhc-color-foreground-link);text-decoration:underline;font:inherit}\n .logout:hover{color:var(--rhc-color-foreground-link-hover)}\n /* Tier 2 — blue breadcrumb bar */\n .navbar{background:var(--rhc-color-lintblauw-700);color:var(--rhc-color-foreground-on-primary);inline-size:100%}\n .navbar .inner{max-inline-size:var(--app-content-max);margin-inline:auto;padding:var(--rhc-space-max-md) var(--rhc-space-max-2xl);box-sizing:border-box}\n " + "\n :host{display:block}\n /* Tier 1 — white brand bar */\n .brandbar{background:var(--rhc-color-wit);border-block-end:var(--rhc-border-width-sm) solid var(--rhc-color-border-subtle);inline-size:100%}\n .brandbar .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-lg) var(--rhc-space-max-2xl);box-sizing:border-box}\n .brand{display:flex;align-items:center;gap:var(--rhc-space-max-lg);color:var(--rhc-color-foreground-default);text-decoration:none}\n .mark{display:inline-block;inline-size:var(--rhc-space-max-md);block-size:var(--rhc-space-max-4xl);background:var(--rhc-color-layout)}\n .name{font-weight:var(--rhc-text-font-weight-bold);line-height:var(--rhc-text-line-height-sm)}\n .sub{font-weight:var(--rhc-text-font-weight-regular);font-size:var(--rhc-text-font-size-sm);color:var(--rhc-color-foreground-subtle)}\n .session{margin-inline-start:auto;display:flex;align-items:center;gap:var(--rhc-space-max-lg);font-size:var(--rhc-text-font-size-sm)}\n .user{color:var(--rhc-color-foreground-subtle)}\n .logout{background:none;border:0;padding:0;cursor:pointer;color:var(--rhc-color-foreground-link);text-decoration:underline;font:inherit}\n .logout:hover{color:var(--rhc-color-foreground-link-hover)}\n /* Tier 2 — blue breadcrumb bar */\n .navbar{background:var(--rhc-color-layout);color:var(--rhc-color-foreground-on-primary);inline-size:100%}\n .navbar .inner{max-inline-size:var(--app-content-max);margin-inline:auto;padding:var(--rhc-space-max-md) var(--rhc-space-max-2xl);box-sizing:border-box}\n " ], "template": "
\n
\n \n \n Rijksoverheid
BIG-register
\n
\n {{ subtitle() }}\n @if (session(); as s) {\n \n Ingelogd als {{ s.naam }}\n \n \n }\n
\n
\n@if (trail().length) {\n
\n
\n \n
\n
\n}\n", "templateUrl": [], @@ -19303,10 +19303,10 @@ "description": "

Organism: Rijksoverheid-style site header. Two tiers, like rijksoverheid.nl:\na white brand bar (wordmark + session/logout) over a lint-blue bar carrying\nthe breadcrumb. ponytail: text wordmark, not the licensed Rijksoverheid logo;\nno search box (no search feature yet).

\n", "rawdescription": "\nOrganism: Rijksoverheid-style site header. Two tiers, like rijksoverheid.nl:\na white brand bar (wordmark + session/logout) over a lint-blue bar carrying\nthe breadcrumb. ponytail: text wordmark, not the licensed Rijksoverheid logo;\nno search box (no search feature yet).", "type": "component", - "sourceCode": "import { Component, computed, inject, input } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { NavigationEnd, Router, RouterLink } from '@angular/router';\nimport { filter, map } from 'rxjs/operators';\nimport { SESSION_PORT } from '@shared/application/session.port';\nimport { BreadcrumbComponent } from '@shared/layout/breadcrumb/breadcrumb.component';\nimport { trailFor } from '@shared/layout/breadcrumb/breadcrumb-trail';\n\n/** Organism: Rijksoverheid-style site header. Two tiers, like rijksoverheid.nl:\n a white brand bar (wordmark + session/logout) over a lint-blue bar carrying\n the breadcrumb. ponytail: text wordmark, not the licensed Rijksoverheid logo;\n no search box (no search feature yet). */\n@Component({\n selector: 'app-site-header',\n imports: [RouterLink, BreadcrumbComponent],\n styles: [`\n :host{display:block}\n /* Tier 1 — white brand bar */\n .brandbar{background:var(--rhc-color-wit);border-block-end:var(--rhc-border-width-sm) solid var(--rhc-color-border-subtle);inline-size:100%}\n .brandbar .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-lg) var(--rhc-space-max-2xl);box-sizing:border-box}\n .brand{display:flex;align-items:center;gap:var(--rhc-space-max-lg);color:var(--rhc-color-foreground-default);text-decoration:none}\n .mark{display:inline-block;inline-size:var(--rhc-space-max-md);block-size:var(--rhc-space-max-4xl);background:var(--rhc-color-lintblauw-700)}\n .name{font-weight:var(--rhc-text-font-weight-bold);line-height:var(--rhc-text-line-height-sm)}\n .sub{font-weight:var(--rhc-text-font-weight-regular);font-size:var(--rhc-text-font-size-sm);color:var(--rhc-color-foreground-subtle)}\n .session{margin-inline-start:auto;display:flex;align-items:center;gap:var(--rhc-space-max-lg);font-size:var(--rhc-text-font-size-sm)}\n .user{color:var(--rhc-color-foreground-subtle)}\n .logout{background:none;border:0;padding:0;cursor:pointer;color:var(--rhc-color-foreground-link);text-decoration:underline;font:inherit}\n .logout:hover{color:var(--rhc-color-foreground-link-hover)}\n /* Tier 2 — blue breadcrumb bar */\n .navbar{background:var(--rhc-color-lintblauw-700);color:var(--rhc-color-foreground-on-primary);inline-size:100%}\n .navbar .inner{max-inline-size:var(--app-content-max);margin-inline:auto;padding:var(--rhc-space-max-md) var(--rhc-space-max-2xl);box-sizing:border-box}\n `],\n template: `\n
\n
\n \n \n Rijksoverheid
BIG-register
\n
\n {{ subtitle() }}\n @if (session(); as s) {\n \n Ingelogd als {{ s.naam }}\n \n \n }\n
\n
\n @if (trail().length) {\n
\n
\n \n
\n
\n }\n `,\n})\nexport class SiteHeaderComponent {\n subtitle = input($localize`:@@header.subtitle:Mijn omgeving`);\n\n private router = inject(Router);\n private sessionPort = inject(SESSION_PORT, { optional: true });\n\n readonly session = computed(() => this.sessionPort?.session() ?? null);\n private url = toSignal(\n this.router.events.pipe(filter((e) => e instanceof NavigationEnd), map(() => this.router.url)),\n { initialValue: this.router.url },\n );\n protected trail = computed(() => trailFor(this.url()));\n\n logout() {\n this.sessionPort?.logout();\n this.router.navigate(['/login']);\n }\n}\n", + "sourceCode": "import { Component, computed, inject, input } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { NavigationEnd, Router, RouterLink } from '@angular/router';\nimport { filter, map } from 'rxjs/operators';\nimport { SESSION_PORT } from '@shared/application/session.port';\nimport { BreadcrumbComponent } from '@shared/layout/breadcrumb/breadcrumb.component';\nimport { trailFor } from '@shared/layout/breadcrumb/breadcrumb-trail';\n\n/** Organism: Rijksoverheid-style site header. Two tiers, like rijksoverheid.nl:\n a white brand bar (wordmark + session/logout) over a lint-blue bar carrying\n the breadcrumb. ponytail: text wordmark, not the licensed Rijksoverheid logo;\n no search box (no search feature yet). */\n@Component({\n selector: 'app-site-header',\n imports: [RouterLink, BreadcrumbComponent],\n styles: [`\n :host{display:block}\n /* Tier 1 — white brand bar */\n .brandbar{background:var(--rhc-color-wit);border-block-end:var(--rhc-border-width-sm) solid var(--rhc-color-border-subtle);inline-size:100%}\n .brandbar .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-lg) var(--rhc-space-max-2xl);box-sizing:border-box}\n .brand{display:flex;align-items:center;gap:var(--rhc-space-max-lg);color:var(--rhc-color-foreground-default);text-decoration:none}\n .mark{display:inline-block;inline-size:var(--rhc-space-max-md);block-size:var(--rhc-space-max-4xl);background:var(--rhc-color-layout)}\n .name{font-weight:var(--rhc-text-font-weight-bold);line-height:var(--rhc-text-line-height-sm)}\n .sub{font-weight:var(--rhc-text-font-weight-regular);font-size:var(--rhc-text-font-size-sm);color:var(--rhc-color-foreground-subtle)}\n .session{margin-inline-start:auto;display:flex;align-items:center;gap:var(--rhc-space-max-lg);font-size:var(--rhc-text-font-size-sm)}\n .user{color:var(--rhc-color-foreground-subtle)}\n .logout{background:none;border:0;padding:0;cursor:pointer;color:var(--rhc-color-foreground-link);text-decoration:underline;font:inherit}\n .logout:hover{color:var(--rhc-color-foreground-link-hover)}\n /* Tier 2 — blue breadcrumb bar */\n .navbar{background:var(--rhc-color-layout);color:var(--rhc-color-foreground-on-primary);inline-size:100%}\n .navbar .inner{max-inline-size:var(--app-content-max);margin-inline:auto;padding:var(--rhc-space-max-md) var(--rhc-space-max-2xl);box-sizing:border-box}\n `],\n template: `\n
\n
\n \n \n Rijksoverheid
BIG-register
\n
\n {{ subtitle() }}\n @if (session(); as s) {\n \n Ingelogd als {{ s.naam }}\n \n \n }\n
\n
\n @if (trail().length) {\n
\n
\n \n
\n
\n }\n `,\n})\nexport class SiteHeaderComponent {\n subtitle = input($localize`:@@header.subtitle:Mijn omgeving`);\n\n private router = inject(Router);\n private sessionPort = inject(SESSION_PORT, { optional: true });\n\n readonly session = computed(() => this.sessionPort?.session() ?? null);\n private url = toSignal(\n this.router.events.pipe(filter((e) => e instanceof NavigationEnd), map(() => this.router.url)),\n { initialValue: this.router.url },\n );\n protected trail = computed(() => trailFor(this.url()));\n\n logout() {\n this.sessionPort?.logout();\n this.router.navigate(['/login']);\n }\n}\n", "assetsDirs": [], "styleUrlsData": "", - "stylesData": "\n :host{display:block}\n /* Tier 1 — white brand bar */\n .brandbar{background:var(--rhc-color-wit);border-block-end:var(--rhc-border-width-sm) solid var(--rhc-color-border-subtle);inline-size:100%}\n .brandbar .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-lg) var(--rhc-space-max-2xl);box-sizing:border-box}\n .brand{display:flex;align-items:center;gap:var(--rhc-space-max-lg);color:var(--rhc-color-foreground-default);text-decoration:none}\n .mark{display:inline-block;inline-size:var(--rhc-space-max-md);block-size:var(--rhc-space-max-4xl);background:var(--rhc-color-lintblauw-700)}\n .name{font-weight:var(--rhc-text-font-weight-bold);line-height:var(--rhc-text-line-height-sm)}\n .sub{font-weight:var(--rhc-text-font-weight-regular);font-size:var(--rhc-text-font-size-sm);color:var(--rhc-color-foreground-subtle)}\n .session{margin-inline-start:auto;display:flex;align-items:center;gap:var(--rhc-space-max-lg);font-size:var(--rhc-text-font-size-sm)}\n .user{color:var(--rhc-color-foreground-subtle)}\n .logout{background:none;border:0;padding:0;cursor:pointer;color:var(--rhc-color-foreground-link);text-decoration:underline;font:inherit}\n .logout:hover{color:var(--rhc-color-foreground-link-hover)}\n /* Tier 2 — blue breadcrumb bar */\n .navbar{background:var(--rhc-color-lintblauw-700);color:var(--rhc-color-foreground-on-primary);inline-size:100%}\n .navbar .inner{max-inline-size:var(--app-content-max);margin-inline:auto;padding:var(--rhc-space-max-md) var(--rhc-space-max-2xl);box-sizing:border-box}\n \n", + "stylesData": "\n :host{display:block}\n /* Tier 1 — white brand bar */\n .brandbar{background:var(--rhc-color-wit);border-block-end:var(--rhc-border-width-sm) solid var(--rhc-color-border-subtle);inline-size:100%}\n .brandbar .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-lg) var(--rhc-space-max-2xl);box-sizing:border-box}\n .brand{display:flex;align-items:center;gap:var(--rhc-space-max-lg);color:var(--rhc-color-foreground-default);text-decoration:none}\n .mark{display:inline-block;inline-size:var(--rhc-space-max-md);block-size:var(--rhc-space-max-4xl);background:var(--rhc-color-layout)}\n .name{font-weight:var(--rhc-text-font-weight-bold);line-height:var(--rhc-text-line-height-sm)}\n .sub{font-weight:var(--rhc-text-font-weight-regular);font-size:var(--rhc-text-font-size-sm);color:var(--rhc-color-foreground-subtle)}\n .session{margin-inline-start:auto;display:flex;align-items:center;gap:var(--rhc-space-max-lg);font-size:var(--rhc-text-font-size-sm)}\n .user{color:var(--rhc-color-foreground-subtle)}\n .logout{background:none;border:0;padding:0;cursor:pointer;color:var(--rhc-color-foreground-link);text-decoration:underline;font:inherit}\n .logout:hover{color:var(--rhc-color-foreground-link-hover)}\n /* Tier 2 — blue breadcrumb bar */\n .navbar{background:var(--rhc-color-layout);color:var(--rhc-color-foreground-on-primary);inline-size:100%}\n .navbar .inner{max-inline-size:var(--app-content-max);margin-inline:auto;padding:var(--rhc-space-max-md) var(--rhc-space-max-2xl);box-sizing:border-box}\n \n", "extends": [] }, { @@ -20744,16 +20744,6 @@ "rawdescription": "Used by the shell to find what to poll on return: still-in-flight uploads.", "description": "

Used by the shell to find what to poll on return: still-in-flight uploads.

\n" }, - { - "name": "initial", - "ctype": "miscellaneous", - "subtype": "variable", - "file": "src/app/brief/domain/brief.machine.ts", - "deprecated": false, - "deprecationMessage": "", - "type": "BriefState", - "defaultValue": "{ tag: 'loading' }" - }, { "name": "initial", "ctype": "miscellaneous", @@ -20774,6 +20764,16 @@ "type": "IntakeState", "defaultValue": "{ tag: 'Answering', answers: {}, cursor: 0, errors: {}, scholingThreshold: SCHOLING_THRESHOLD_DEFAULT }" }, + { + "name": "initial", + "ctype": "miscellaneous", + "subtype": "variable", + "file": "src/app/brief/domain/brief.machine.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "BriefState", + "defaultValue": "{ tag: 'loading' }" + }, { "name": "initial", "ctype": "miscellaneous", @@ -24759,50 +24759,6 @@ } ] }, - { - "name": "reduce", - "file": "src/app/brief/domain/brief.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "s", - "type": "BriefState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "m", - "type": "BriefMsg", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "BriefState", - "jsdoctags": [ - { - "name": "s", - "type": "BriefState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "m", - "type": "BriefMsg", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, { "name": "reduce", "file": "src/app/herregistratie/domain/herregistratie.machine.ts", @@ -24891,6 +24847,50 @@ } ] }, + { + "name": "reduce", + "file": "src/app/brief/domain/brief.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "args": [ + { + "name": "s", + "type": "BriefState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "m", + "type": "BriefMsg", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "BriefState", + "jsdoctags": [ + { + "name": "s", + "type": "BriefState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "m", + "type": "BriefMsg", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, { "name": "reduce", "file": "src/app/registratie/domain/change-request.machine.ts", @@ -27807,18 +27807,6 @@ "description": "

The server-owned geldigheidsvraag whose"ja" answer requires a Dutch-taalvaardigheid\nupload (proof of the confirmed B2 level). Stable id shared with the backend.

\n" } ], - "src/app/brief/domain/brief.machine.ts": [ - { - "name": "initial", - "ctype": "miscellaneous", - "subtype": "variable", - "file": "src/app/brief/domain/brief.machine.ts", - "deprecated": false, - "deprecationMessage": "", - "type": "BriefState", - "defaultValue": "{ tag: 'loading' }" - } - ], "src/app/herregistratie/domain/herregistratie.machine.ts": [ { "name": "initial", @@ -27867,6 +27855,18 @@ "description": "

The fixed step list. Number of steps never changes; questions reveal inline.

\n" } ], + "src/app/brief/domain/brief.machine.ts": [ + { + "name": "initial", + "ctype": "miscellaneous", + "subtype": "variable", + "file": "src/app/brief/domain/brief.machine.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "BriefState", + "defaultValue": "{ tag: 'loading' }" + } + ], "src/app/registratie/domain/change-request.machine.ts": [ { "name": "initial", diff --git a/src/app/auth/ui/login-form/login-form.component.ts b/src/app/auth/ui/login-form/login-form.component.ts index 1b39667..c9599a9 100644 --- a/src/app/auth/ui/login-form/login-form.component.ts +++ b/src/app/auth/ui/login-form/login-form.component.ts @@ -9,7 +9,11 @@ import { ButtonComponent } from '@shared/ui/button/button.component'; selector: 'app-login-form', imports: [FormsModule, FormFieldComponent, TextInputComponent, ButtonComponent], template: ` -
+ +
+
* verplichte velden
+
+ diff --git a/src/app/herregistratie/domain/herregistratie.machine.spec.ts b/src/app/herregistratie/domain/herregistratie.machine.spec.ts index ee8dc49..ea68058 100644 --- a/src/app/herregistratie/domain/herregistratie.machine.spec.ts +++ b/src/app/herregistratie/domain/herregistratie.machine.spec.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { ok, err } from '@shared/kernel/fp'; import { initialUpload } from '@shared/upload/upload.machine'; -import { initial, next, back, submit, resolve, reduce, WizardState } from './herregistratie.machine'; +import { initial, next, back, gaNaarStap, submit, resolve, reduce, WizardState } from './herregistratie.machine'; const editing1 = (uren: string, jaren = '5', punten = ''): WizardState => ({ tag: 'Editing', step: 1, draft: { uren, jaren, punten }, errors: {}, upload: initialUpload }); const editing2 = (uren: string, punten: string, jaren = '5'): WizardState => ({ tag: 'Editing', step: 2, draft: { uren, jaren, punten }, errors: {}, upload: initialUpload }); @@ -46,6 +46,17 @@ describe('wizard.machine', () => { expect(resolve(submitting, ok(undefined)).tag).toBe('Submitted'); expect(resolve(submitting, err('boom')).tag).toBe('Failed'); }); + + it('gaNaarStap jumps back to an earlier step, clearing errors', () => { + expect((gaNaarStap(editing3('4160', '200'), 1) as any).step).toBe(1); + }); + + it('gaNaarStap ignores a same/forward jump and jumps outside Editing', () => { + const e3 = editing3('4160', '200'); + expect(gaNaarStap(e3, 3)).toBe(e3); // same step -> no-op + const submitting = submit(e3); + expect(gaNaarStap(submitting, 1)).toBe(submitting); // not Editing -> no-op + }); }); describe('reduce (message-driven)', () => { diff --git a/src/app/herregistratie/domain/herregistratie.machine.ts b/src/app/herregistratie/domain/herregistratie.machine.ts index 388c920..c33d37e 100644 --- a/src/app/herregistratie/domain/herregistratie.machine.ts +++ b/src/app/herregistratie/domain/herregistratie.machine.ts @@ -87,6 +87,13 @@ export function back(s: WizardState): WizardState { return { ...s, step: (s.step - 1) as 1 | 2, errors: {} }; } +/** Jump back to an earlier step to correct data (controle → step N). Forward + jumps are not allowed (would skip validation). */ +export function gaNaarStap(s: WizardState, step: 1 | 2 | 3): WizardState { + if (s.tag !== 'Editing' || step >= s.step) return s; + return { ...s, step, errors: {} }; +} + /** Step 3 submit: parse everything + require documents; Submitting only with Valid. */ export function submit(s: WizardState): WizardState { if (s.tag !== 'Editing' || s.step !== 3) return s; @@ -121,6 +128,7 @@ export type WizardMsg = | { tag: 'SetField'; key: keyof Draft; value: string } | { tag: 'Next' } | { tag: 'Back' } + | { tag: 'GaNaarStap'; step: 1 | 2 | 3 } | { tag: 'Submit' } | { tag: 'Retry' } | { tag: 'SubmitConfirmed' } @@ -136,6 +144,8 @@ export function reduce(s: WizardState, m: WizardMsg): WizardState { return next(s); case 'Back': return back(s); + case 'GaNaarStap': + return gaNaarStap(s, m.step); case 'Submit': return submit(s); case 'Retry': diff --git a/src/app/herregistratie/domain/intake.machine.spec.ts b/src/app/herregistratie/domain/intake.machine.spec.ts index 75bbdc8..3007c32 100644 --- a/src/app/herregistratie/domain/intake.machine.spec.ts +++ b/src/app/herregistratie/domain/intake.machine.spec.ts @@ -8,6 +8,7 @@ import { currentStep, next, back, + gaNaarStap, submit, resolve, reduce, @@ -67,6 +68,19 @@ describe('navigation', () => { it('Back never goes below the first step', () => { expect(back(initial)).toBe(initial); }); + + it('gaNaarStap jumps back to an earlier step, clearing errors', () => { + const s = answering({ buitenlandGewerkt: 'nee' }, 2); + expect((gaNaarStap(s, 0) as any).cursor).toBe(0); + }); + + it('gaNaarStap ignores a same/forward jump and jumps outside Answering', () => { + const s = answering({ buitenlandGewerkt: 'nee' }, 1); + expect(gaNaarStap(s, 1)).toBe(s); // same step -> no-op + expect(gaNaarStap(s, 2)).toBe(s); // forward -> no-op + const submitting = submit(answering({ buitenlandGewerkt: 'nee', uren: '4160' }, 2)); + expect(gaNaarStap(submitting, 0)).toBe(submitting); // not Answering -> no-op + }); }); describe('submit', () => { diff --git a/src/app/herregistratie/domain/intake.machine.ts b/src/app/herregistratie/domain/intake.machine.ts index f3e8d1e..6c5dedc 100644 --- a/src/app/herregistratie/domain/intake.machine.ts +++ b/src/app/herregistratie/domain/intake.machine.ts @@ -156,6 +156,13 @@ export function back(s: IntakeState): IntakeState { return { ...s, cursor: s.cursor - 1, errors: {} }; } +/** Jump back to an earlier step to correct answers (review → step N). Forward + jumps are not allowed (would skip validation). */ +export function gaNaarStap(s: IntakeState, cursor: number): IntakeState { + if (s.tag !== 'Answering' || cursor < 0 || cursor >= s.cursor) return s; + return { ...s, cursor, errors: {} }; +} + export function submit(s: IntakeState): IntakeState { if (s.tag !== 'Answering') return s; const r = validateAll(s.answers, s.scholingThreshold); @@ -171,6 +178,7 @@ export type IntakeMsg = | { tag: 'SetAnswer'; key: keyof Answers; value: string } | { tag: 'Next' } | { tag: 'Back' } + | { tag: 'GaNaarStap'; cursor: number } | { tag: 'Submit' } | { tag: 'Retry' } | { tag: 'SubmitConfirmed' } @@ -186,6 +194,8 @@ export function reduce(s: IntakeState, m: IntakeMsg): IntakeState { return next(s); case 'Back': return back(s); + case 'GaNaarStap': + return gaNaarStap(s, m.cursor); case 'Submit': return submit(s); case 'Retry': diff --git a/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts b/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts index b341fb1..5833250 100644 --- a/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts +++ b/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts @@ -3,7 +3,8 @@ import { FormsModule } from '@angular/forms'; import { FormFieldComponent } from '@shared/ui/form-field/form-field.component'; import { TextInputComponent } from '@shared/ui/text-input/text-input.component'; import { AlertComponent } from '@shared/ui/alert/alert.component'; -import { WizardShellComponent, WizardError, WizardStatus } from '@shared/layout/wizard-shell/wizard-shell.component'; +import { WizardShellComponent, WizardError, WizardStatus, naarStapLabel } from '@shared/layout/wizard-shell/wizard-shell.component'; +import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component'; import { createStore } from '@shared/application/store'; import { whenTag } from '@shared/kernel/fp'; import { BigProfileStore } from '@registratie/application/big-profile.store'; @@ -22,12 +23,13 @@ import { UploadState, initialUpload, deliveryRefs } from '@shared/upload/upload. the dashboard shows "in behandeling" immediately. */ @Component({ selector: 'app-herregistratie-wizard', - imports: [FormsModule, FormFieldComponent, TextInputComponent, AlertComponent, WizardShellComponent, DocumentUploadComponent], + imports: [FormsModule, FormFieldComponent, TextInputComponent, AlertComponent, ConfirmationComponent, WizardShellComponent, DocumentUploadComponent], template: ` + (retry)="onRetry()" + (goToStep)="goToStep($event)"> @switch (step()) { @case (1) { @@ -71,7 +74,7 @@ import { UploadState, initialUpload, deliveryRefs } from '@shared/upload/upload. }
- Uw aanvraag tot herregistratie is ontvangen. +
`, @@ -127,7 +130,15 @@ export class HerregistratieWizardComponent { // --- Presentational wiring for the shared wizard shell --------------------- protected stepTitle = computed(() => this.stepTitles[this.step() - 1]); - protected primaryLabel = computed(() => (this.step() < 3 ? $localize`:@@wizard.volgende:Volgende` : $localize`:@@herregWizard.indienen:Herregistratie aanvragen`)); + protected primaryLabel = computed(() => { + const step = this.step(); + return step < 3 ? naarStapLabel(step + 1, this.stepLabels[step]) : $localize`:@@herregWizard.indienen:Herregistratie aanvragen`; + }); + + /** Stepper emits a 0-based index for an earlier (visited) step. */ + protected goToStep(index: number) { + this.dispatch({ tag: 'GaNaarStap', step: (index + 1) as 1 | 2 | 3 }); + } protected errorMessage = computed(() => $localize`:@@wizard.indienenMislukt:Indienen mislukt:` + ` ${this.failedError()}`); protected shellStatus = computed(() => { switch (this.state().tag) { diff --git a/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts b/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts index e3ad316..57f527e 100644 --- a/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts +++ b/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts @@ -6,7 +6,9 @@ import { RadioGroupComponent, JA_NEE } from '@shared/ui/radio-group/radio-group. import { ButtonComponent } from '@shared/ui/button/button.component'; import { AlertComponent } from '@shared/ui/alert/alert.component'; import { DataRowComponent } from '@shared/ui/data-row/data-row.component'; -import { WizardShellComponent, WizardError, WizardStatus } from '@shared/layout/wizard-shell/wizard-shell.component'; +import { ReviewSectionComponent } from '@shared/ui/review-section/review-section.component'; +import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component'; +import { WizardShellComponent, WizardError, WizardStatus, naarStapLabel } from '@shared/layout/wizard-shell/wizard-shell.component'; import { createStore } from '@shared/application/store'; import { whenTag } from '@shared/kernel/fp'; import { BigProfileStore } from '@registratie/application/big-profile.store'; @@ -32,12 +34,13 @@ import { IntakePolicyAdapter } from '@herregistratie/infrastructure/intake-polic sessionStorage so a page reload keeps the user's progress (cleared on tab close). */ @Component({ selector: 'app-intake-wizard', - imports: [FormsModule, FormFieldComponent, TextInputComponent, RadioGroupComponent, ButtonComponent, AlertComponent, DataRowComponent, WizardShellComponent], + imports: [FormsModule, FormFieldComponent, TextInputComponent, RadioGroupComponent, ButtonComponent, AlertComponent, DataRowComponent, ReviewSectionComponent, ConfirmationComponent, WizardShellComponent], template: ` + (retry)="onRetry()" + (goToStep)="dispatch({ tag: 'GaNaarStap', cursor: $event })"> @switch (step()) { @case ('buitenland') { @@ -81,12 +85,18 @@ import { IntakePolicyAdapter } from '@herregistratie/infrastructure/intake-polic } @case ('review') { Controleer uw antwoorden en dien de aanvraag in. -
+ @if (answers().buitenlandGewerkt === 'ja') { } + + @if (scholingZichtbaar()) { @@ -94,15 +104,16 @@ import { IntakePolicyAdapter } from '@herregistratie/infrastructure/intake-polic @if (answers().scholingGevolgd === 'ja') { } -
+ } }
- Uw aanvraag tot herregistratie is ontvangen. -
- Opnieuw beginnen -
+ +
+ Opnieuw beginnen +
+
`, @@ -155,7 +166,11 @@ export class IntakeWizardComponent { review: $localize`:@@intake.title.review:Controleren en indienen`, }; protected stepTitle = computed(() => this.stepTitles[this.step()]); - protected primaryLabel = computed(() => (this.step() === 'review' ? $localize`:@@intake.indienen:Aanvraag indienen` : $localize`:@@wizard.volgende:Volgende`)); + protected primaryLabel = computed(() => { + if (this.step() === 'review') return $localize`:@@intake.indienen:Aanvraag indienen`; + const next = this.cursor() + 1; + return naarStapLabel(next + 1, this.stepLabels[next]); + }); protected errorMessage = computed(() => $localize`:@@wizard.indienenMislukt:Indienen mislukt:` + ` ${this.failedError()}`); protected shellStatus = computed(() => { switch (this.state().tag) { diff --git a/src/app/registratie/ui/aanvraag-block/aanvraag-block.component.ts b/src/app/registratie/ui/aanvraag-block/aanvraag-block.component.ts index 3672fab..f3d1c8f 100644 --- a/src/app/registratie/ui/aanvraag-block/aanvraag-block.component.ts +++ b/src/app/registratie/ui/aanvraag-block/aanvraag-block.component.ts @@ -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 = { 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
  • a direct
      child). + styles: [`:host{display:contents}`], template: ` - -
      - {{ typeLabel() }} - -
      - - @switch (aanvraag().status.tag) { - @case ('Concept') { -

      Stap {{ stap().index }} van {{ stap().count }}

      - } - @case ('InBehandeling') { -

      Referentie {{ ref() }} · ingediend op {{ aanvraag().submittedAt | date: 'longDate' }}

      - @if (manual()) { -

      Uw aanvraag wordt handmatig beoordeeld in de backoffice.

      - } - } - @case ('Goedgekeurd') { -

      Referentie {{ ref() }}

      - } - @case ('Afgewezen') { -

      Referentie {{ ref() }}

      -

      {{ reden() }}

      - } - } - - @if (actions().length) { -
      - @if (actions().includes('resume')) { - Verder gaan - } - @if (actions().includes('cancel')) { - Annuleren - } + + @if (actions().includes('cancel')) { +
      + Annuleren
      } - +
      `, }) export class AanvraagBlockComponent { @@ -68,42 +42,30 @@ export class AanvraagBlockComponent { resume = output(); cancel = output(); + 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' }) : ''; +} diff --git a/src/app/registratie/ui/aanvraag-block/aanvraag-block.stories.ts b/src/app/registratie/ui/aanvraag-block/aanvraag-block.stories.ts index 77246d7..fb4c6d4 100644 --- a/src/app/registratie/ui/aanvraag-block/aanvraag-block.stories.ts +++ b/src/app/registratie/ui/aanvraag-block/aanvraag-block.stories.ts @@ -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 = { title: 'Organisms/Aanvraag Block', component: AanvraagBlockComponent, + decorators: [applicationConfig({ providers: [provideRouter([])] })], + render: (args) => ({ + props: args, + // A row is an
    • — the "applications" list styling needs the real list context. + template: `
      `, + }), }; export default meta; type Story = StoryObj; diff --git a/src/app/registratie/ui/address-fields/address-fields.component.ts b/src/app/registratie/ui/address-fields/address-fields.component.ts index 76a99c9..6d98137 100644 --- a/src/app/registratie/ui/address-fields/address-fields.component.ts +++ b/src/app/registratie/ui/address-fields/address-fields.component.ts @@ -22,7 +22,6 @@ export type AdresErrors = Partial>; styles: [` fieldset{border:0;margin:0;padding:0;min-inline-size:0} legend{padding:0;font-weight:var(--rhc-text-font-weight-semi-bold);margin-block-end:var(--rhc-space-max-md)} - app-form-field + app-form-field{display:block;margin-block-start:var(--rhc-space-max-lg)} `], template: `
      diff --git a/src/app/registratie/ui/change-request-form/change-request-form.component.ts b/src/app/registratie/ui/change-request-form/change-request-form.component.ts index 7789154..09f95d6 100644 --- a/src/app/registratie/ui/change-request-form/change-request-form.component.ts +++ b/src/app/registratie/ui/change-request-form/change-request-form.component.ts @@ -28,7 +28,10 @@ import { ApiClient } from '@shared/infrastructure/api-client';
    • } @else { Adreswijziging doorgeven - + +
      +
      * verplichte velden
      +
      -
      - -
      @if (aanvragen().length) {
      Mijn aanvragen -
      + @for (a of aanvragen(); track a.id) { } -
      +
      } @@ -68,7 +65,7 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
      -
      +
      @@ -100,19 +97,13 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
      Wat wilt u doen? -
        + @for (a of acties; track a.to) { -
      • - -

        {{ a.tekst }}

        - {{ a.actie }} → -
        -
      • + } -
      +
      -
  • `, }) @@ -158,21 +149,15 @@ export class DashboardPage { return tasksFromProfile(reg, this.eligible()); } - /** Portal sections (navigation, not actions). */ - protected readonly nav: NavItem[] = [ - { label: $localize`:@@dashboard.nav.overzicht:Overzicht`, to: '/dashboard' }, - { label: $localize`:@@dashboard.nav.gegevens:Mijn gegevens`, to: '/registratie' }, - { label: $localize`:@@dashboard.nav.herregistratie:Herregistratie`, to: '/herregistratie' }, - { label: $localize`:@@dashboard.nav.inschrijven:Inschrijven`, to: '/registreren' }, - { label: $localize`:@@dashboard.nav.concepten:Functionele patronen`, to: '/concepts' }, - { label: $localize`:@@dashboard.nav.brief:Brief opstellen`, to: '/brief' }, - ]; - - /** Primary transactional actions, as a card grid. */ + /** Primary transactional actions, as an "aanvragen" list (see CIBG's + componenten/aanvragen). The core portal sections live in the header nav now; + the teaching pages (concepts/brief) are only reachable from here. */ protected readonly acties = [ { to: '/registreren', titel: $localize`:@@dashboard.actie.inschrijven.titel:Inschrijven`, tekst: $localize`:@@dashboard.actie.inschrijven.tekst:Schrijf u in in het BIG-register via de registratiewizard.`, actie: $localize`:@@dashboard.actie.inschrijven.actie:Start inschrijving` }, { to: '/herregistratie', titel: $localize`:@@dashboard.actie.herregistratie.titel:Herregistratie aanvragen`, tekst: $localize`:@@dashboard.actie.herregistratie.tekst:Verleng uw registratie voor de komende periode.`, actie: $localize`:@@dashboard.actie.herregistratie.actie:Vraag aan` }, { to: '/intake', titel: $localize`:@@dashboard.actie.intake.titel:Herregistratie-intake`, tekst: $localize`:@@dashboard.actie.intake.tekst:Vragenlijst met vertakkingen.`, actie: $localize`:@@dashboard.actie.intake.actie:Start intake` }, { to: '/registratie', titel: $localize`:@@dashboard.actie.wijzigen.titel:Gegevens wijzigen`, tekst: $localize`:@@dashboard.actie.wijzigen.tekst:Bekijk uw gegevens of geef een wijziging door.`, actie: $localize`:@@dashboard.actie.wijzigen.actie:Bekijk gegevens` }, + { to: '/concepts', titel: $localize`:@@dashboard.actie.concepten.titel:Functionele patronen`, tekst: $localize`:@@dashboard.actie.concepten.tekst:Bekijk de FP/TEA-bouwstenen van deze POC.`, actie: $localize`:@@dashboard.actie.concepten.actie:Bekijk patronen` }, + { to: '/brief', titel: $localize`:@@dashboard.actie.brief.titel:Brief opstellen`, tekst: $localize`:@@dashboard.actie.brief.tekst:Stel een brief samen uit vaste en vrije onderdelen.`, actie: $localize`:@@dashboard.actie.brief.actie:Start brief` }, ]; } diff --git a/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts b/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts index 3f74130..e8b0c28 100644 --- a/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts +++ b/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts @@ -7,7 +7,9 @@ import { ButtonComponent } from '@shared/ui/button/button.component'; import { AlertComponent } from '@shared/ui/alert/alert.component'; import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component'; import { DataRowComponent } from '@shared/ui/data-row/data-row.component'; -import { WizardShellComponent, WizardError, WizardStatus } from '@shared/layout/wizard-shell/wizard-shell.component'; +import { ReviewSectionComponent } from '@shared/ui/review-section/review-section.component'; +import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component'; +import { WizardShellComponent, WizardError, WizardStatus, naarStapLabel } from '@shared/layout/wizard-shell/wizard-shell.component'; import { ASYNC } from '@shared/ui/async/async.component'; import { AddressFieldsComponent } from '@registratie/ui/address-fields/address-fields.component'; import { createStore } from '@shared/application/store'; @@ -54,7 +56,7 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid'; selector: 'app-registratie-wizard', imports: [ FormsModule, FormFieldComponent, TextInputComponent, RadioGroupComponent, ButtonComponent, - AlertComponent, SkeletonComponent, DataRowComponent, WizardShellComponent, + AlertComponent, SkeletonComponent, DataRowComponent, ReviewSectionComponent, ConfirmationComponent, WizardShellComponent, AddressFieldsComponent, DocumentUploadComponent, ...ASYNC, ], template: ` @@ -62,6 +64,7 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid'; [steps]="stepLabels" [current]="cursor()" [stepTitle]="stepTitle()" + i18n-processName="@@regWizard.processName" processName="Inschrijven in het BIG-register" [status]="shellStatus()" [primaryLabel]="primaryLabel()" [canGoBack]="cursor() > 0" @@ -71,7 +74,8 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid'; (primary)="onPrimary()" (back)="dispatch({ tag: 'Back' })" (cancel)="restart()" - (retry)="onRetry()"> + (retry)="onRetry()" + (goToStep)="dispatch({ tag: 'GaNaarStap', cursor: $event })"> @switch (step()) { @case ('adres') { @@ -119,7 +123,7 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid'; [ngModel]="draft().beroep ?? ''" (ngModelChange)="dispatch({ tag: 'DeclareerBeroep', beroep: $event })" /> } @else if (draft().beroep) { -
    +
    } @@ -156,31 +160,35 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid'; } @case ('controle') { Controleer uw gegevens en dien de registratie in. -
    + @if (draft().correspondentie === 'email') { } + + @for (item of samenvattingVragen(); track item.vraag) { } -
    -
    - Adres wijzigen - Diploma wijzigen -
    + } }
    - Uw registratie is ontvangen. Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie. -
    - Nieuwe registratie starten -
    + +

    Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.

    +
    + Nieuwe registratie starten +
    +
    `, @@ -243,7 +251,11 @@ export class RegistratieWizardComponent { protected failedError = computed(() => whenTag(this.state(), 'Mislukt')?.error ?? ''); // --- Presentational wiring for the shared wizard shell --------------------- - protected primaryLabel = computed(() => (this.step() === 'controle' ? $localize`:@@regWizard.indienen:Registratie indienen` : $localize`:@@wizard.volgende:Volgende`)); + protected primaryLabel = computed(() => { + if (this.step() === 'controle') return $localize`:@@regWizard.indienen:Registratie indienen`; + const next = this.cursor() + 1; + return naarStapLabel(next + 1, this.stepLabels[next]); + }); protected errorMessage = computed(() => $localize`:@@regWizard.indienenMislukt:Het indienen is niet gelukt:` + ` ${this.failedError()}`); protected shellStatus = computed(() => { switch (this.state().tag) { diff --git a/src/app/registratie/ui/registration-summary/registration-summary.component.ts b/src/app/registratie/ui/registration-summary/registration-summary.component.ts index 7626f6f..b83c7db 100644 --- a/src/app/registratie/ui/registration-summary/registration-summary.component.ts +++ b/src/app/registratie/ui/registration-summary/registration-summary.component.ts @@ -12,7 +12,7 @@ import { CardComponent } from '@shared/ui/card/card.component'; imports: [DatePipe, DataRowComponent, StatusBadgeComponent, CardComponent], template: ` -
    +
    diff --git a/src/app/shared/layout/breadcrumb/breadcrumb.component.ts b/src/app/shared/layout/breadcrumb/breadcrumb.component.ts index 9e7f957..350d13b 100644 --- a/src/app/shared/layout/breadcrumb/breadcrumb.component.ts +++ b/src/app/shared/layout/breadcrumb/breadcrumb.component.ts @@ -6,39 +6,27 @@ export interface BreadcrumbItem { 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. */ +/** Chrome: breadcrumb navigation, styled for the CIBG titlebar (`.titlebar .title`) — + plain links with a chevron `::after` from the CIBG Icons font, current page as an + unlinked, bold span. 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;font-size:var(--rhc-text-font-size-sm)} - .crumb{display:inline-flex;align-items:center;gap:var(--rhc-space-max-md)} - a{color:var(--rhc-color-foreground-link);text-decoration:underline} - a:hover{color:var(--rhc-color-foreground-link-hover)} - .current{color:var(--rhc-color-foreground-subtle)} - .sep{color:var(--rhc-color-foreground-subtle)} - /* Inverse: on the blue header bar, everything is white. */ - :host(.inverse) a, - :host(.inverse) .current, - :host(.inverse) .sep { color: var(--rhc-color-foreground-on-primary); } - `], + // CIBG's global "header nav" background rule matches ANY nav inside a
    + // — including this one, wherever it's mounted. Override it so the breadcrumb + // never carries its own background (it should show whatever's behind it, e.g. + // the titlebar's robijn fill). + styles: [`nav{background:none}`], template: ` `, }) diff --git a/src/app/shared/layout/breadcrumb/breadcrumb.stories.ts b/src/app/shared/layout/breadcrumb/breadcrumb.stories.ts index 0e88986..99a89cd 100644 --- a/src/app/shared/layout/breadcrumb/breadcrumb.stories.ts +++ b/src/app/shared/layout/breadcrumb/breadcrumb.stories.ts @@ -9,7 +9,8 @@ const meta: Meta = { decorators: [applicationConfig({ providers: [provideRouter([])] })], render: (args) => ({ props: args, - template: ``, + // Rendered inside a mock .titlebar .title so the story reflects the real chrome. + template: `
    `, }), }; export default meta; diff --git a/src/app/shared/layout/side-nav/side-nav.component.ts b/src/app/shared/layout/side-nav/side-nav.component.ts deleted file mode 100644 index fbd6fb1..0000000 --- a/src/app/shared/layout/side-nav/side-nav.component.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Component, input } from '@angular/core'; -import { RouterLink, RouterLinkActive } from '@angular/router'; - -export interface NavItem { - readonly label: string; - readonly to: string; -} - -/** Organism: vertical side navigation for the "Mijn omgeving" portal. The active - route is marked with `routerLinkActive`. Domain-free — the caller supplies the - items (English-named shared chrome). */ -@Component({ - selector: 'app-side-nav', - imports: [RouterLink, RouterLinkActive], - styles: [` - :host{display:block} - .list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--rhc-space-max-xs)} - .item{ - display:block; - padding:var(--rhc-space-max-md) var(--rhc-space-max-lg); - color:var(--rhc-color-foreground-default); - text-decoration:none; - border-radius:var(--rhc-border-radius-sm); - border-inline-start:var(--rhc-border-width-md) solid transparent; - } - .item:hover{background:var(--rhc-color-cool-grey-100)} - .item.active{ - background:var(--rhc-color-lintblauw-100); - border-inline-start-color:var(--rhc-color-lintblauw-700); - color:var(--rhc-color-lintblauw-700); - font-weight:var(--rhc-text-font-weight-semi-bold); - } - `], - template: ` - - `, -}) -export class SideNavComponent { - items = input.required(); - ariaLabel = input($localize`:@@sideNav.aria:Mijn omgeving`); -} diff --git a/src/app/shared/layout/side-nav/side-nav.stories.ts b/src/app/shared/layout/side-nav/side-nav.stories.ts deleted file mode 100644 index 5219ee5..0000000 --- a/src/app/shared/layout/side-nav/side-nav.stories.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/angular'; -import { applicationConfig } from '@storybook/angular'; -import { provideRouter } from '@angular/router'; -import { SideNavComponent } from './side-nav.component'; - -const meta: Meta = { - title: 'Layout/Side Nav', - component: SideNavComponent, - decorators: [applicationConfig({ providers: [provideRouter([])] })], - render: (args) => ({ - props: args, - template: `
    `, - }), -}; -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - items: [ - { label: 'Overzicht', to: '/dashboard' }, - { label: 'Mijn gegevens', to: '/registratie' }, - { label: 'Herregistratie', to: '/herregistratie' }, - { label: 'Inschrijven', to: '/registreren' }, - ], - }, -}; diff --git a/src/app/shared/layout/site-footer/site-footer.component.ts b/src/app/shared/layout/site-footer/site-footer.component.ts index 3cff534..abe61fa 100644 --- a/src/app/shared/layout/site-footer/site-footer.component.ts +++ b/src/app/shared/layout/site-footer/site-footer.component.ts @@ -8,7 +8,7 @@ import { Component } from '@angular/core'; selector: 'app-site-footer', styles: [` :host{display:block} - .bar{background:var(--rhc-color-donkerblauw-700);color:var(--rhc-color-foreground-on-primary);margin-block-start:var(--rhc-space-max-5xl);inline-size:100%} + .bar{background:var(--rhc-color-layout);color:var(--rhc-color-foreground-on-primary);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-3xl) var(--rhc-space-max-2xl);box-sizing:border-box;display:flex;gap:var(--rhc-space-max-3xl);flex-wrap:wrap;justify-content:space-between} .tagline{font-style:italic;font-weight:var(--rhc-text-font-weight-semi-bold);font-size:var(--rhc-text-font-size-lg);max-inline-size:18rem} .ministry{margin-block-start:var(--rhc-space-max-md);font-size:var(--rhc-text-font-size-sm);color:var(--rhc-color-foreground-on-primary)} @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; .links{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--rhc-space-max-sm);font-size:var(--rhc-text-font-size-sm)} .links a{color:var(--rhc-color-foreground-on-primary);text-decoration:none} .links a:hover{text-decoration:underline} - .meta{inline-size:100%;border-block-start:var(--rhc-border-width-sm) solid var(--rhc-color-lintblauw-600);margin-block-start:var(--rhc-space-max-xl);padding-block-start:var(--rhc-space-max-lg);font-size:var(--rhc-text-font-size-sm);opacity:.85} + .meta{inline-size:100%;border-block-start:var(--rhc-border-width-sm) solid rgb(255 255 255 / 0.25);margin-block-start:var(--rhc-space-max-xl);padding-block-start:var(--rhc-space-max-lg);font-size:var(--rhc-text-font-size-sm);opacity:.85} `], template: `