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 {

View File

@@ -19,8 +19,8 @@ const meta: Meta<AanvraagBlockComponent> = {
decorators: [applicationConfig({ providers: [provideRouter([])] })],
render: (args) => ({
props: args,
// A row is an <li> — the "applications" list styling needs the real list context.
template: `<ul class="list-unstyled"><app-aanvraag-block [aanvraag]="aanvraag" /></ul>`,
// A row is an <li> — the keuzelijst styling needs the real list context.
template: `<ul class="keuzelijst__list"><app-aanvraag-block [aanvraag]="aanvraag" /></ul>`,
}),
};
export default meta;

View File

@@ -9,6 +9,7 @@ import { CardComponent } from '@shared/ui/card/card.component';
import { TaskListComponent } from '@shared/ui/task-list/task-list.component';
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
import { ChoiceListComponent } from '@shared/ui/choice-list/choice-list.component';
import { ASYNC } from '@shared/ui/async/async.component';
import { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';
import { RegistrationTableComponent } from '@registratie/ui/registration-table/registration-table.component';
@@ -25,7 +26,8 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
selector: 'app-dashboard-page',
imports: [
PageShellComponent, HeadingComponent, AlertComponent, SkeletonComponent,
DataRowComponent, CardComponent, TaskListComponent, ApplicationListComponent, ApplicationLinkComponent, ...ASYNC,
DataRowComponent, CardComponent, TaskListComponent, ApplicationListComponent, ApplicationLinkComponent,
ChoiceListComponent, ...ASYNC,
RegistrationSummaryComponent, RegistrationTableComponent, AanvraagBlockComponent,
],
template: `
@@ -33,12 +35,11 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
<div class="app-stack">
@if (aanvragen().length) {
<section>
<app-heading [level]="2" i18n="@@dashboard.mijnAanvragen">Mijn aanvragen</app-heading>
<app-application-list class="app-section">
<app-choice-list i18n-heading="@@dashboard.mijnAanvragen" heading="Mijn aanvragen" class="app-section">
@for (a of aanvragen(); track a.id) {
<app-aanvraag-block animate.enter="app-item-enter" animate.leave="app-item-leave" [aanvraag]="a" (resume)="resume(a)" (cancel)="cancelAanvraag(a)" />
}
</app-application-list>
</app-choice-list>
</section>
}
@@ -51,10 +52,10 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
@let tasks = tasksFor($any(p).registration);
<section>
<app-heading [level]="2" i18n="@@dashboard.watMoetIkRegelen">Wat moet ik regelen</app-heading>
@if (tasks.length) {
<app-task-list class="app-section" [tasks]="tasks" />
<app-task-list class="app-section" i18n-listHeading="@@dashboard.watMoetIkRegelen" listHeading="Wat moet ik regelen" [tasks]="tasks" />
} @else {
<app-heading [level]="2" i18n="@@dashboard.watMoetIkRegelen">Wat moet ik regelen</app-heading>
<p class="app-text-subtle" i18n="@@dashboard.nietsOpenstaan">U heeft op dit moment niets openstaan.</p>
}
</section>