feat(cibg): WP-12 — CIBG Datablock for application data

Adopt the vendored CIBG Datablock (.data-block / .block-wrapper) as the way to
show application data:
- New app-data-block molecule (grey surface + white panel + projected rows,
  optional heading, stacked variant, aria-label) + stories.
- data-row switches to a `div[app-data-row]` attribute selector so the <dl>'s
  direct child is a native <div> (HTML5.1 dl > div > dt+dd). This makes the
  definition list axe-clean — a bare custom element between <dl> and its dt/dd
  trips axe's definition-list rule regardless of display:contents, a defect the
  dashboard shipped live. Re-enables a11y on the data-row / review-section /
  registration-summary stories (previously disabled pending this rework).
- review-section folds onto app-data-block (drops its hand-carried classes).
- registration-summary + dashboard "Persoonsgegevens (BRP)" drop app-card and
  render as datablocks; both wizards' review rows + the beroep row convert to
  the div selector.

GREEN: lint, check:tokens, 178 tests, build, build-storybook, 136 axe stories.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 21:25:57 +02:00
parent 947d5fa90a
commit 82fc3c493d
12 changed files with 798 additions and 654 deletions

View File

@@ -0,0 +1,33 @@
import { Component, input } from '@angular/core';
import { HeadingComponent } from '@shared/ui/heading/heading.component';
/** Molecule: CIBG Huisstijl **Datablock** (designsystem.cibg.nl/componenten/datablock)
— THE way to show user/application data. A grey `.data-block` surface holds a white
`.block-wrapper` panel with a `<dl>` of projected `<app-data-row>`s. Use `stacked`
(`.data-block--stacked`) when labels/values are long and should stack. Replaces the
`app-card + dl` idiom for data views (the datablock carries its own surface, so it is
NOT wrapped in an `app-card` — see WP-12). When there is no visible `heading`, pass an
`ariaLabel` so the definition list is announced. */
@Component({
selector: 'app-data-block',
imports: [HeadingComponent],
template: `
@if (heading()) { <app-heading [level]="level()">{{ heading() }}</app-heading> }
<div class="data-block" [class.data-block--stacked]="stacked()">
<div class="block-wrapper">
<dl class="mb-0" [attr.aria-label]="ariaLabel() || null">
<ng-content />
</dl>
</div>
</div>
`,
})
export class DataBlockComponent {
heading = input('');
/** Heading level when `heading` is set (default h3, matching `app-card`). */
level = input<1 | 2 | 3 | 4 | 5>(3);
/** Stacks label above value (`.data-block--stacked`) for long content. */
stacked = input(false);
/** Accessible name for the `<dl>` when there is no visible heading. */
ariaLabel = input('');
}

View File

@@ -0,0 +1,26 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import { DataBlockComponent } from './data-block.component';
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
const meta: Meta<DataBlockComponent> = {
title: 'Molecules/Data Block',
component: DataBlockComponent,
decorators: [moduleMetadata({ imports: [DataRowComponent] })],
render: (args) => ({
props: args,
template: `
<app-data-block [heading]="heading" [stacked]="stacked" [ariaLabel]="ariaLabel">
<div app-data-row key="BIG-nummer" value="19012345601"></div>
<div app-data-row key="Naam" value="J. de Vries"></div>
<div app-data-row key="Beroep" value="Verpleegkundige"></div>
<div app-data-row key="Registratiedatum" value="1 maart 2018"></div>
</app-data-block>`,
}),
};
export default meta;
type Story = StoryObj<DataBlockComponent>;
export const Default: Story = { args: { heading: 'Persoonsgegevens (BRP)' } };
export const ZonderKop: Story = { args: { ariaLabel: 'Registratiegegevens' } };
export const Stacked: Story = { args: { heading: 'Toelichting', stacked: true } };

View File

@@ -1,12 +1,17 @@
import { Component, input } from '@angular/core';
/** Molecule: one key/value row in a CIBG Huisstijl "controlestap" data summary
(`dt.col-md-4` / `dd.col-md-8`). `display:contents` so the dt/dd become direct
flex children of the parent `<dl class="row">` — the Bootstrap grid classes
need that to lay out correctly. Wrap several in a `<dl class="row mb-0">`. */
/** Molecule: one key/value row inside a CIBG Huisstijl **Datablock** (see
`data-block.component.ts`) — the row primitive of the datablock/`controlestap`
data summary. Used on a `<div>` so the `<dl>`'s direct child is a native element
(the HTML5.1 `dl > div > dt + dd` grouping), which keeps the definition list
axe-clean — a bare custom element between `<dl>` and its `<dt>/<dd>` trips axe's
definition-list rule regardless of `display:contents`. The host is the Bootstrap
`.row`; `dt.col-md-4`/`dd.col-md-8` give the label/value widths. Wrap several in a
`<dl class="mb-0">` (a `<app-data-block>`). Project custom content (e.g. a badge)
into the `<dd>` via `<ng-content>`. */
@Component({
selector: 'app-data-row',
styles: [`:host{display:contents}`],
selector: 'div[app-data-row]',
host: { class: 'row' },
template: `
<dt class="col-md-4">{{ key() }}</dt>
<dd class="col-md-8"><ng-content>{{ value() }}</ng-content></dd>

View File

@@ -6,16 +6,11 @@ const meta: Meta<DataRowComponent> = {
component: DataRowComponent,
render: (args) => ({
props: args,
// Rows are dt.col-md-4/dd.col-md-8 flex children of a Bootstrap .row dl (CIBG controlestap).
template: `<dl class="row mb-0"><app-data-row [key]="key" [value]="value" /></dl>`,
// A row is a `.row` <div> grouping dt.col-md-4/dd.col-md-8 inside the datablock <dl>
// (HTML5.1 dl > div > dt+dd — a native div child keeps the definition list axe-clean).
template: `<dl class="mb-0"><div app-data-row [key]="key" [value]="value"></div></dl>`,
}),
args: { key: 'BIG-nummer', value: '19012345601' },
parameters: {
// Structural: app-data-row's host sits between the <dl> and its <dt>/<dd> —
// axe's definition-list rule needs them adjacent regardless of `display:contents`.
// WP-12 (CIBG Datablock) reworks this markup; see docs/backlog/WP-12-datablock.md.
a11y: { disable: true },
},
};
export default meta;
type Story = StoryObj<DataRowComponent>;

View File

@@ -1,11 +1,13 @@
import { Component, input, output } from '@angular/core';
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
/** Molecule: one section of a CIBG Huisstijl "controlestap" (wizard review step) —
a heading with a "Wijzigen" link, and the section's `<app-data-row>`s in a
`.data-block > .block-wrapper > dl.row`. Domain-free; the caller supplies the
a heading with a "Wijzigen" link, and the section's `<app-data-row>`s in a CIBG
Datablock (composes `<app-data-block>`). Domain-free; the caller supplies the
heading and decides what "Wijzigen" does (typically jump back to a step). */
@Component({
selector: 'app-review-section',
imports: [DataBlockComponent],
template: `
<div class="d-flex">
<h2>{{ heading() }}</h2>
@@ -15,13 +17,7 @@ import { Component, input, output } from '@angular/core';
</div>
}
</div>
<div class="data-block">
<div class="block-wrapper">
<dl class="row mb-0">
<ng-content />
</dl>
</div>
</div>
<app-data-block><ng-content /></app-data-block>
`,
})
export class ReviewSectionComponent {

View File

@@ -11,16 +11,11 @@ const meta: Meta<ReviewSectionComponent> = {
props: args,
template: `
<app-review-section [heading]="heading" [editLabel]="editLabel" [editAriaLabel]="editAriaLabel" [showEdit]="showEdit">
<app-data-row key="Straat en huisnummer" value="Dorpsstraat 1" />
<app-data-row key="Postcode" value="1234 AB" />
<app-data-row key="Woonplaats" value="Utrecht" />
<div app-data-row key="Straat en huisnummer" value="Dorpsstraat 1"></div>
<div app-data-row key="Postcode" value="1234 AB"></div>
<div app-data-row key="Woonplaats" value="Utrecht"></div>
</app-review-section>`,
}),
parameters: {
// Structural: app-data-row's host sits between the <dl> and its <dt>/<dd> —
// fixed by the WP-12 Datablock rework. See docs/backlog/WP-12-datablock.md.
a11y: { disable: true },
},
};
export default meta;
type Story = StoryObj<ReviewSectionComponent>;