feat(dashboard): restyle Mijn aanvragen / Wat moet ik regelen as a CIBG keuzelijst

Both sections offer a set of choices the user picks between to proceed, matching
designsystem.cibg.nl/componenten/keuzelijst rather than the "aanvragen" row pattern
("Wat wilt u doen?" keeps that look — it's a static nav list, not a choice list).

- New shared/ui molecules: choice-list (heading + keuzelijst__list, wired via
  aria-labelledby per CIBG's a11y guidance) and choice-link (one keuzelijst__link
  choice; routerLink, imperative-clickable, or a plain non-interactive block).
- choice-link's non-interactive block needed a `--static` modifier: CIBG's
  `.keuzelijst__link:after`/`:hover`/`:focus` key off the bare class (keuzelijst
  assumes every item is a link), unlike `.applications li a::after` which is scoped
  to the anchor — without it, a non-actionable aanvraag row inherited a chevron and
  hover accent it shouldn't have.
- task-list.component.ts now composes choice-list/choice-link internally; public
  API unchanged except a new required `listHeading` input (the heading moves inside
  the list for the aria-labelledby link, so dashboard.page.ts stops rendering it
  separately — same fix applied to "Mijn aanvragen").
- aanvraag-block.component.ts moves from application-link to choice-link, combining
  its separate status/subtitle text into one instructions paragraph (keuzelijst has
  no cta field — the row itself is the action). Only a resumable Concept renders as
  a real choice; InBehandeling/Goedgekeurd/Afgewezen stay non-interactive, unchanged
  from before.

Verified: lint/check:tokens/build green, 178 tests pass, build-storybook succeeds,
and manually driven end-to-end (dashboard renders both sections as keuzelijst cards,
confirmed via screenshot that non-actionable rows have no chevron after the fix).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 15:54:02 +02:00
parent 6257d7ede3
commit 7ac14557dd
10 changed files with 2225 additions and 875 deletions

View File

@@ -1,6 +1,6 @@
import { Component, computed, input, output } from '@angular/core';
import { ButtonComponent } from '@shared/ui/button/button.component';
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
import { ChoiceLinkComponent } from '@shared/ui/choice-link/choice-link.component';
import { Aanvraag, AanvraagType } from '@registratie/domain/aanvraag';
import { blockActions } from '@registratie/domain/block-actions';
@@ -11,29 +11,27 @@ const TYPE_LABELS: Record<AanvraagType, string> = {
};
/** Organism: one application on the dashboard's "Mijn aanvragen" list, rendered as
a CIBG Huisstijl "aanvragen" row (application-link). Which text/actions it shows
a CIBG Huisstijl "keuzelijst" choice (choice-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: [ButtonComponent, ApplicationLinkComponent],
// display:contents — see application-link.component.ts (keeps <li> a direct <ul> child).
imports: [ButtonComponent, ChoiceLinkComponent],
// display:contents — see choice-link.component.ts (keeps <li> a direct <ul> child).
styles: [`:host{display:contents}`],
template: `
<app-application-link
<app-choice-link
[heading]="typeLabel()"
[status]="statusText()"
[subtitle]="subtitle()"
[cta]="actions().includes('resume') ? verderGaan : ''"
[instructions]="instructions()"
[clickable]="actions().includes('resume')"
(activate)="resume.emit()">
@if (actions().includes('cancel')) {
<div applicationActions>
<div choiceActions>
<app-button variant="subtle" (click)="cancel.emit()" i18n="@@aanvraagBlock.annuleren">Annuleren</app-button>
</div>
}
</app-application-link>
</app-choice-link>
`,
})
export class AanvraagBlockComponent {
@@ -42,13 +40,11 @@ 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 → row text (the UI's mapping, not a domain rule).
protected statusText = computed(() => {
// Status-tag → the choice's description line (the UI's mapping, not a domain rule).
private 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:`;
@@ -57,13 +53,19 @@ export class AanvraagBlockComponent {
case 'Afgewezen': return $localize`:@@aanvraagBlock.afgewezen:Referentie ${s.referentie}:ref:`;
}
});
// Secondary note under the status line, when there's one to show.
protected subtitle = computed(() => {
// Secondary note appended to the status line, when there's one to show.
private subtitle = computed(() => {
const s = this.aanvraag().status;
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 '';
});
// The keuzelijst has one description paragraph — combine status + secondary note.
protected instructions = computed(() => {
const status = this.statusText();
const sub = this.subtitle();
return sub ? `${status} ${sub}` : status;
});
}
function formatNL(iso?: string): string {