fix(cibg): address fieldset padding + atomic-design audit cleanup
Fix: address-fields shipped a scoped `fieldset { padding:0; margin:0 }` that
tied on specificity with CIBG's `.form-horizontal fieldset` and, injected later,
won — flattening the grey-box padding on "Mijn gegevens" and the registratie
wizard address step. Removed the override (kept the legend rule).
Audit cleanup (minimise atoms, converge or document near-duplicates):
- delete dead `app-card` (+ story; zero consumers, superseded by app-data-block)
and the unused `.app-info-box` utility
- review-section raw <h2> -> app-heading; registratie-wizard lone <dl> -> app-data-block
- keep application-link's raw <h3> (vendored `.applications li a h3` chain needs
the bare element) with an inline justification
- document the deliberate "don't merge these" verdicts in a new Convergence
decisions table in atomic-design.mdx; record the card removal in cibg-gaps.mdx
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -19,14 +19,11 @@ export type AdresErrors = Partial<Record<keyof AdresValue, string>>;
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-address-fields',
|
selector: 'app-address-fields',
|
||||||
imports: [FormsModule, FormFieldComponent, TextInputComponent],
|
imports: [FormsModule, FormFieldComponent, TextInputComponent],
|
||||||
|
// No scoped `fieldset` reset here: the fieldset must keep CIBG's
|
||||||
|
// `.form-horizontal fieldset` grey-box padding/margin. A local `fieldset{padding:0}`
|
||||||
|
// would tie on specificity and (injected later) win, flattening the padding.
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
fieldset {
|
|
||||||
border: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
min-inline-size: 0;
|
|
||||||
}
|
|
||||||
legend {
|
legend {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-weight: var(--rhc-text-font-weight-semi-bold);
|
font-weight: var(--rhc-text-font-weight-semi-bold);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ButtonComponent } from '@shared/ui/button/button.component';
|
|||||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||||
|
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||||
import { ReviewSectionComponent } from '@shared/ui/review-section/review-section.component';
|
import { ReviewSectionComponent } from '@shared/ui/review-section/review-section.component';
|
||||||
import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component';
|
import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component';
|
||||||
import {
|
import {
|
||||||
@@ -67,6 +68,7 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
|
|||||||
AlertComponent,
|
AlertComponent,
|
||||||
SkeletonComponent,
|
SkeletonComponent,
|
||||||
DataRowComponent,
|
DataRowComponent,
|
||||||
|
DataBlockComponent,
|
||||||
ReviewSectionComponent,
|
ReviewSectionComponent,
|
||||||
ConfirmationComponent,
|
ConfirmationComponent,
|
||||||
WizardShellComponent,
|
WizardShellComponent,
|
||||||
@@ -216,14 +218,14 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
|
|||||||
</app-form-field>
|
</app-form-field>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
} @else if (draft().beroep) {
|
} @else if (draft().beroep) {
|
||||||
<dl class="mb-0 app-section">
|
<app-data-block class="app-section">
|
||||||
<div
|
<div
|
||||||
app-data-row
|
app-data-row
|
||||||
i18n-key="@@regWizard.beroepAfgeleid"
|
i18n-key="@@regWizard.beroepAfgeleid"
|
||||||
key="Beroep (afgeleid uit diploma)"
|
key="Beroep (afgeleid uit diploma)"
|
||||||
[value]="draft().beroep ?? ''"
|
[value]="draft().beroep ?? ''"
|
||||||
></div>
|
></div>
|
||||||
</dl>
|
</app-data-block>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (actieveVragen(data).length) {
|
@if (actieveVragen(data).length) {
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ import { RouterLink } from '@angular/router';
|
|||||||
<ng-content select="[applicationActions]" />
|
<ng-content select="[applicationActions]" />
|
||||||
<ng-template #body>
|
<ng-template #body>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
<!-- Raw <h3>, not <app-heading>: the vendored ".applications li a h3" chain styles
|
||||||
|
the bare h3 (link-blue); an app-heading host wrapper would sit between and can
|
||||||
|
break that selector. Documented in atomic-design.mdx (convergence decisions). -->
|
||||||
<h3 class="h3">{{ heading() }}</h3>
|
<h3 class="h3">{{ heading() }}</h3>
|
||||||
@if (subtitle()) {
|
@if (subtitle()) {
|
||||||
<div class="subtitle">{{ subtitle() }}</div>
|
<div class="subtitle">{{ subtitle() }}</div>
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import { Component, input } from '@angular/core';
|
|
||||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
|
||||||
|
|
||||||
// CIBG-GAP EXTENSION: n/a — no vendored generic-card class; `.app-card` is a
|
|
||||||
// hand-rolled surface (NOT Bootstrap's `.card`), see cibg-gaps.mdx. Prefer the
|
|
||||||
// vendored Datablock (WP-12, `app-data-block`) for application/user data.
|
|
||||||
/** Molecule: a content card. Standardises the repeated card surface (white,
|
|
||||||
subtle border, rounded, padded) so pages compose cards instead of hand-rolling
|
|
||||||
a hand-rolled card surface. Optional heading; the rest is projected.
|
|
||||||
Local class is .app-card (NOT Bootstrap's .card, whose padding-on-body layout differs). */
|
|
||||||
@Component({
|
|
||||||
selector: 'app-card',
|
|
||||||
imports: [HeadingComponent],
|
|
||||||
styles: [
|
|
||||||
`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
block-size: 100%;
|
|
||||||
}
|
|
||||||
.app-card {
|
|
||||||
background: var(--rhc-color-wit);
|
|
||||||
border: var(--rhc-border-width-sm) solid var(--rhc-color-border-subtle);
|
|
||||||
border-radius: var(--rhc-border-radius-md);
|
|
||||||
padding: var(--rhc-space-max-xl);
|
|
||||||
block-size: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.app-card > * + * {
|
|
||||||
margin-block-start: var(--rhc-space-max-md);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
],
|
|
||||||
template: `
|
|
||||||
<section class="app-card">
|
|
||||||
@if (heading()) {
|
|
||||||
<app-heading [level]="level()">{{ heading() }}</app-heading>
|
|
||||||
}
|
|
||||||
<ng-content />
|
|
||||||
</section>
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
export class CardComponent {
|
|
||||||
heading = input<string>();
|
|
||||||
level = input<1 | 2 | 3 | 4 | 5>(3);
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/angular';
|
|
||||||
import { CardComponent } from './card.component';
|
|
||||||
|
|
||||||
const meta: Meta<CardComponent> = {
|
|
||||||
title: 'Design System/Molecules/Card',
|
|
||||||
component: CardComponent,
|
|
||||||
render: (args) => ({
|
|
||||||
props: args,
|
|
||||||
template: `
|
|
||||||
<app-card [heading]="heading" [level]="level">
|
|
||||||
<p class="rhc-paragraph">Een kaart groepeert samenhangende inhoud op een schone, omkaderde vlak.</p>
|
|
||||||
</app-card>`,
|
|
||||||
}),
|
|
||||||
args: { heading: 'Persoonsgegevens (BRP)', level: 3 },
|
|
||||||
parameters: {
|
|
||||||
cibgGap: true,
|
|
||||||
docs: { description: { component: 'CIBG-gap extension — see Foundations/CIBG Gap Register.' } },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<CardComponent>;
|
|
||||||
|
|
||||||
export const Default: Story = {};
|
|
||||||
export const ZonderKop: Story = { args: { heading: undefined } };
|
|
||||||
@@ -4,10 +4,11 @@ import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
|||||||
/** Molecule: CIBG Huisstijl **Datablock** (designsystem.cibg.nl/componenten/datablock)
|
/** Molecule: CIBG Huisstijl **Datablock** (designsystem.cibg.nl/componenten/datablock)
|
||||||
— THE way to show user/application data. A grey `.data-block` surface holds a white
|
— 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`
|
`.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
|
(`.data-block--stacked`) when labels/values are long and should stack. This is the
|
||||||
`app-card + dl` idiom for data views (the datablock carries its own surface, so it is
|
single data surface (a generic white `app-card` used to exist but was unused and
|
||||||
NOT wrapped in an `app-card` — see WP-12). When there is no visible `heading`, pass an
|
removed — see WP-12); the datablock carries its own surface, so it is not nested in
|
||||||
`ariaLabel` so the definition list is announced. */
|
another one. When there is no visible `heading`, pass an `ariaLabel` so the definition
|
||||||
|
list is announced. */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-data-block',
|
selector: 'app-data-block',
|
||||||
imports: [HeadingComponent],
|
imports: [HeadingComponent],
|
||||||
@@ -26,7 +27,7 @@ import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
|||||||
})
|
})
|
||||||
export class DataBlockComponent {
|
export class DataBlockComponent {
|
||||||
heading = input('');
|
heading = input('');
|
||||||
/** Heading level when `heading` is set (default h3, matching `app-card`). */
|
/** Heading level when `heading` is set (default h3). */
|
||||||
level = input<1 | 2 | 3 | 4 | 5>(3);
|
level = input<1 | 2 | 3 | 4 | 5>(3);
|
||||||
/** Stacks label above value (`.data-block--stacked`) for long content. */
|
/** Stacks label above value (`.data-block--stacked`) for long content. */
|
||||||
stacked = input(false);
|
stacked = input(false);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component, input, output } from '@angular/core';
|
import { Component, input, output } from '@angular/core';
|
||||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||||
|
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||||
|
|
||||||
/** Molecule: one section of a CIBG Huisstijl "controlestap" (wizard review step) —
|
/** 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 CIBG
|
a heading with a "Wijzigen" link, and the section's `<app-data-row>`s in a CIBG
|
||||||
@@ -7,10 +8,10 @@ import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
|||||||
heading and decides what "Wijzigen" does (typically jump back to a step). */
|
heading and decides what "Wijzigen" does (typically jump back to a step). */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-review-section',
|
selector: 'app-review-section',
|
||||||
imports: [DataBlockComponent],
|
imports: [DataBlockComponent, HeadingComponent],
|
||||||
template: `
|
template: `
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<h2>{{ heading() }}</h2>
|
<app-heading [level]="2">{{ heading() }}</app-heading>
|
||||||
@if (showEdit()) {
|
@if (showEdit()) {
|
||||||
<div class="ms-auto">
|
<div class="ms-auto">
|
||||||
<a href="#" [attr.aria-label]="editAriaLabel() || editLabel()" (click)="onEdit($event)">{{
|
<a href="#" [attr.aria-label]="editAriaLabel() || editLabel()" (click)="onEdit($event)">{{
|
||||||
|
|||||||
@@ -99,3 +99,27 @@ A new page should be **composition of existing blocks**. Adding a new building b
|
|||||||
exception, not the reflex — if you reach for one, that is a signal to check whether an
|
exception, not the reflex — if you reach for one, that is a signal to check whether an
|
||||||
existing atom/molecule already covers it. Fewer primitives → less to test, less to learn,
|
existing atom/molecule already covers it. Fewer primitives → less to test, less to learn,
|
||||||
one place to fix a bug.
|
one place to fix a bug.
|
||||||
|
|
||||||
|
## Convergence decisions — pairs that look duplicated but stay separate
|
||||||
|
|
||||||
|
Periodically we audit for near-duplicate blocks. Some collapse into one; a few **look**
|
||||||
|
similar but earn their separation. This table records the "don't merge these" verdicts so
|
||||||
|
the next person doesn't spend an afternoon re-deciding. (Deliberate CIBG-specific deviations
|
||||||
|
live in [CIBG gaps](?path=/docs/foundations-cibg-gaps--docs); the FE⇄DS "same shape, different
|
||||||
|
context" cases in [Layers](?path=/docs/foundations-layers--docs).)
|
||||||
|
|
||||||
|
| Pair | Why kept separate |
|
||||||
|
| --- | --- |
|
||||||
|
| `choice-link` vs `application-link` | Share the same `to`/`clickable`/`activate` navigation triad, but bind **different vendored patterns** — CIBG _Keuzelijst_ (`.keuzelijst__link`, `.stretched-link`) vs _Aanvragen_ (`.dashboard-block.applications li a`) — with different list/host semantics (`app-choice-link` renders an inner `<li>`; `application-link` **is** the `<li>`). Merging would fight the vendored CSS. Extract the shared triad into a mixin only if it grows. |
|
||||||
|
| `text-input` / `radio-group` / `checkbox` | Share only the standard Angular **ControlValueAccessor** boilerplate (the `writeValue`/`registerOn*`/`setDisabledState` block). They render genuinely different controls, so they stay three atoms. A base CVA class is the only DRY move — a refactor, not a component merge, and not worth it at three. |
|
||||||
|
| `button variant="subtle"` (`.btn-link`) vs `app-link` | A subtle button _looks_ like a link but is an **action** (`<button>`, emits click); `app-link` is **navigation** (`<a routerLink>`). Different semantics and a11y roles → keep both. |
|
||||||
|
| `shell` / `page-shell` / `wizard-shell` | Three distinct jobs that **compose**, not overlap: persistent app chrome (mounted once) → routed page body → the wizard form/step frame. |
|
||||||
|
| Raw `<h3>` in `application-link` vs the `heading` atom | The vendored `.applications li a h3` chain styles the **bare `<h3>`**; wrapping it in the `app-heading` host element would sit between the anchor and the h3 and can break that selector. This is the one sanctioned raw-heading; everywhere else uses `<app-heading [level]>`. |
|
||||||
|
|
||||||
|
Single-consumer shared blocks (e.g. `placeholder-chip`, `rich-text-editor`, `checkbox`, the
|
||||||
|
`task-list`/`choice-list`/`choice-link` family) currently have one consumer each. They stay in
|
||||||
|
`shared` as design-system primitives; relocate one into its consuming context only if it stays
|
||||||
|
single-consumer long-term. That is a watch-item, not a merge.
|
||||||
|
|
||||||
|
The last audit also **removed** a genuinely dead block — a generic white `app-card` with zero
|
||||||
|
consumers (superseded by the grey `app-data-block` as the single data surface).
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ placed above the `@Component` decorator, plus `parameters: { cibgGap: true }` an
|
|||||||
| `application-link` (non-navigating row) | Aanvragen | The vendored `.dashboard-block.applications li a` chain only styles `<a>`; `.static-row` mirrors it from tokens for the informational (non-link) case. |
|
| `application-link` (non-navigating row) | Aanvragen | The vendored `.dashboard-block.applications li a` chain only styles `<a>`; `.static-row` mirrors it from tokens for the informational (non-link) case. |
|
||||||
| `debug-state` | n/a | Dev-only tool, deliberately off-theme — see the component's own `ponytail:` note. |
|
| `debug-state` | n/a | Dev-only tool, deliberately off-theme — see the component's own `ponytail:` note. |
|
||||||
| `status-badge` | n/a | Deliberate custom status dot, not Bootstrap's `.badge` (pill padding/colour don't fit). |
|
| `status-badge` | n/a | Deliberate custom status dot, not Bootstrap's `.badge` (pill padding/colour don't fit). |
|
||||||
| `card` (`.app-card`) | n/a | No vendored generic-card class; prefer the vendored **Datablock** (`app-data-block`, WP-12) for application/user data. |
|
|
||||||
| `placeholder-chip` | n/a | No vendored inline-chip/tag class. |
|
| `placeholder-chip` | n/a | No vendored inline-chip/tag class. |
|
||||||
|
|
||||||
Not a gap: `confirmation` renders entirely with vendored `.confirmation*` classes (no `styles:
|
Not a gap: `confirmation` renders entirely with vendored `.confirmation*` classes (no `styles:
|
||||||
@@ -45,6 +44,11 @@ it in their own header comment — no marker needed, they don't hand-roll surfac
|
|||||||
`upload-status-banner` (a 23-line near-identity wrapper over `app-alert` with one consumer) was
|
`upload-status-banner` (a 23-line near-identity wrapper over `app-alert` with one consumer) was
|
||||||
deleted; its consumer (`document-upload`) now uses `<app-alert>` directly.
|
deleted; its consumer (`document-upload`) now uses `<app-alert>` directly.
|
||||||
|
|
||||||
|
`card` (`.app-card`, a generic white surface) was deleted — it had zero consumers; the grey
|
||||||
|
vendored **Datablock** (`app-data-block`) is the single data surface. The convergence verdicts
|
||||||
|
for the pairs we deliberately keep separate live in
|
||||||
|
[Atomic Design → Convergence decisions](?path=/docs/foundations-atomic-design--docs).
|
||||||
|
|
||||||
## Keeping this register honest
|
## Keeping this register honest
|
||||||
|
|
||||||
No automated check diffs this table against the markers in code (skipped as not worth a CI
|
No automated check diffs this table against the markers in code (skipped as not worth a CI
|
||||||
|
|||||||
@@ -128,13 +128,6 @@ body {
|
|||||||
color: var(--rhc-color-foreground-subtle);
|
color: var(--rhc-color-foreground-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bordered info box (outline, no fill) — e.g. a "Vragen?" contact block. */
|
|
||||||
.app-info-box {
|
|
||||||
border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);
|
|
||||||
border-radius: var(--rhc-border-radius-md);
|
|
||||||
padding: var(--rhc-space-max-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Route transitions (withViewTransitions): cross-fade the routed CONTENT only.
|
/* Route transitions (withViewTransitions): cross-fade the routed CONTENT only.
|
||||||
The chrome gets its own stable view-transition-name so it's lifted out of the
|
The chrome gets its own stable view-transition-name so it's lifted out of the
|
||||||
`root` snapshot and stays put while the content fades. */
|
`root` snapshot and stays put while the content fades. */
|
||||||
|
|||||||
Reference in New Issue
Block a user