feat(fp): WP-13 — CIBG-gap register + hygiene + MDX

Mark every hand-rolled shared/ui surface with a `// CIBG-GAP EXTENSION:`
comment + `cibgGap` story parameter (skeleton, spinner, rich-text-editor,
wizard-shell's error summary, application-link's non-navigating row,
debug-state, status-badge, card, placeholder-chip) so deviations from the
CIBG design system are auditable. Add the register MDX
(Foundations/CIBG Gap Register), cross-linked from ADR-0003. Delete the
near-identity upload-status-banner wrapper; its one consumer now uses
<app-alert> directly (a story added to keep the info-banner state covered).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 22:42:13 +02:00
parent 69880efd38
commit 9d58f597ea
27 changed files with 1676 additions and 1735 deletions

View File

@@ -2,6 +2,9 @@ import { Component, input, output } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { RouterLink } from '@angular/router';
// CIBG-GAP EXTENSION: Aanvragen (non-navigating row) — the vendored
// `.dashboard-block.applications li a` chain only styles `<a>`; `.static-row`
// mirrors it from tokens for the non-navigating case, see cibg-gaps.mdx.
/** Molecule: one row in a CIBG Huisstijl "aanvragen" list
(designsystem.cibg.nl/componenten/aanvragen) — a white card-link styled by the
vendored `.dashboard-block.applications li a` chain (bg, chevron, link-blue `h3`),

View File

@@ -12,6 +12,15 @@ const meta: Meta<ApplicationLinkComponent> = {
// Rows are <li>s in the "aanvragen" list — a real <ul> gives them their layout.
template: `<div class="dashboard-block applications"><ul class="list-unstyled"><li app-application-link [heading]="heading" [subtitle]="subtitle" [status]="status" [cta]="cta" [to]="to" [clickable]="clickable"></li></ul></div>`,
}),
parameters: {
cibgGap: true,
docs: {
description: {
component:
'CIBG-gap extension (non-navigating row only, see NietInteractief) — see Foundations/CIBG Gap Register.',
},
},
},
};
export default meta;
type Story = StoryObj<ApplicationLinkComponent>;

View File

@@ -1,6 +1,9 @@
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.

View File

@@ -12,6 +12,10 @@ const meta: Meta<CardComponent> = {
</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>;

View File

@@ -6,6 +6,8 @@ import { BigProfileStore } from '@registratie/application/big-profile.store';
import { map } from '@shared/application/remote-data';
import { maskBsn, redactProfile } from './mask';
// CIBG-GAP EXTENSION: n/a — devtool, no corresponding CIBG concept; deliberately
// off-theme by design (see the ponytail note below), see cibg-gaps.mdx.
/**
* Dev-only "show the current Model" panel (Elm-debugger style, read-only).
* Observes the root singletons and renders them via the json pipe. Never a

View File

@@ -5,6 +5,10 @@ import { DebugStateComponent } from './debug-state.component';
const meta: Meta<DebugStateComponent> = {
title: 'Devtools/State debug',
component: DebugStateComponent,
parameters: {
cibgGap: true,
docs: { description: { component: 'CIBG-gap extension — see Foundations/CIBG Gap Register.' } },
},
};
export default meta;
type Story = StoryObj<DebugStateComponent>;

View File

@@ -1,5 +1,7 @@
import { Component, computed, input } from '@angular/core';
// CIBG-GAP EXTENSION: n/a — no vendored inline-chip/tag class; hand-rolled
// brace-wrapped chip, see cibg-gaps.mdx.
/** Atom: a highlighted, non-editable placeholder chip for READ-ONLY rendering
(preview, diagnostics). Distinct styling for auto-resolvable vs manual fields and
for linter error/warning states. Domain-free and presentational — the caller

View File

@@ -8,6 +8,10 @@ const meta: Meta<PlaceholderChipComponent> = {
props: args,
template: `<app-placeholder-chip [label]="label" [autoResolvable]="autoResolvable" [state]="state"></app-placeholder-chip>`,
}),
parameters: {
cibgGap: true,
docs: { description: { component: 'CIBG-gap extension — see Foundations/CIBG Gap Register.' } },
},
};
export default meta;
type Story = StoryObj<PlaceholderChipComponent>;

View File

@@ -12,6 +12,10 @@ export interface PlaceholderOption {
readonly autoResolvable?: boolean;
}
// CIBG-GAP EXTENSION: Tekstgebied — CIBG has no rich-text/WYSIWYG pattern (a
// contenteditable editor with formatting + placeholder chips); hand-rolled
// surface (toolbar + chip styling), see cibg-gaps.mdx. Buttons still use the
// vendored .btn-ghost class (WP-10).
/**
* Molecule: a minimal no-dependency WYSIWYG editor over a `RichTextBlock`.
*

View File

@@ -34,6 +34,10 @@ const meta: Meta<RichTextEditorComponent> = {
props: args,
template: `<app-rich-text-editor [content]="content" [placeholders]="placeholders" [editable]="editable"></app-rich-text-editor>`,
}),
parameters: {
cibgGap: true,
docs: { description: { component: 'CIBG-gap extension — see Foundations/CIBG Gap Register.' } },
},
};
export default meta;
type Story = StoryObj<RichTextEditorComponent>;

View File

@@ -1,5 +1,7 @@
import { Component, OnDestroy, OnInit, computed, input, signal } from '@angular/core';
// CIBG-GAP EXTENSION: Laadindicatie — no vendored loading-skeleton class exists
// (verified absent from huisstijl.min.css); hand-rolled shimmer, see cibg-gaps.mdx.
/** Atom: skeleton placeholder (grey shimmer). Delay-gated so it never flashes
on fast responses. Render `count` lines shaped roughly like the content. */
@Component({

View File

@@ -5,6 +5,10 @@ const meta: Meta<SkeletonComponent> = {
title: 'Atoms/Skeleton',
component: SkeletonComponent,
args: { delay: 0 },
parameters: {
cibgGap: true,
docs: { description: { component: 'CIBG-gap extension — see Foundations/CIBG Gap Register.' } },
},
};
export default meta;
type Story = StoryObj<SkeletonComponent>;

View File

@@ -1,5 +1,7 @@
import { Component, OnDestroy, OnInit, input, signal } from '@angular/core';
// CIBG-GAP EXTENSION: Laadindicatie — no vendored loading-spinner class exists
// (verified absent from huisstijl.min.css); hand-rolled, see cibg-gaps.mdx.
/** Atom: spinner that only appears after `delay` ms — fast responses never
flash a spinner, slow ones get feedback. */
@Component({

View File

@@ -4,6 +4,10 @@ import { SpinnerComponent } from './spinner.component';
const meta: Meta<SpinnerComponent> = {
title: 'Atoms/Spinner',
component: SpinnerComponent,
parameters: {
cibgGap: true,
docs: { description: { component: 'CIBG-gap extension — see Foundations/CIBG Gap Register.' } },
},
};
export default meta;
type Story = StoryObj<SpinnerComponent>;

View File

@@ -1,5 +1,7 @@
import { Component, input } from '@angular/core';
// CIBG-GAP EXTENSION: n/a — deliberate custom surface, not Bootstrap's `.badge`
// (whose pill padding/colour don't fit a status dot); see cibg-gaps.mdx.
/** Atom: a coloured dot + label. Purely presentational and domain-free — the
caller decides what colour and label mean (e.g. via registration.policy).
This keeps the shared UI kernel free of any domain knowledge. */

View File

@@ -4,6 +4,10 @@ import { StatusBadgeComponent } from './status-badge.component';
const meta: Meta<StatusBadgeComponent> = {
title: 'Shared UI/Status Badge',
component: StatusBadgeComponent,
parameters: {
cibgGap: true,
docs: { description: { component: 'CIBG-gap extension — see Foundations/CIBG Gap Register.' } },
},
};
export default meta;
type Story = StoryObj<StatusBadgeComponent>;

View File

@@ -1,14 +1,14 @@
import { Component, input, output } from '@angular/core';
import type { DeliveryChannel, UploadState } from '@shared/upload/upload.machine';
import { AlertComponent } from '@shared/ui/alert/alert.component';
import { DocumentCategoryComponent } from '../document-category/document-category.component';
import { UploadStatusBannerComponent } from '../upload-status-banner/upload-status-banner.component';
/** Organism: the full document-upload step — the category list, or a load-error
banner. Pure UI: re-exposes the category events, tagging each with its category
where the parent needs it. The container wires these to the upload reducer. */
@Component({
selector: 'app-document-upload',
imports: [DocumentCategoryComponent, UploadStatusBannerComponent],
imports: [DocumentCategoryComponent, AlertComponent],
styles: [
`
:host {
@@ -20,10 +20,10 @@ import { UploadStatusBannerComponent } from '../upload-status-banner/upload-stat
],
template: `
@if (state().categoriesError) {
<app-upload-status-banner type="error" [message]="state().categoriesError!" />
<app-alert type="error">{{ state().categoriesError }}</app-alert>
} @else {
@if (state().backgroundSyncAvailable === false && state().categories.length > 0) {
<app-upload-status-banner type="info" [message]="foregroundOnlyMessage" />
<app-alert type="info">{{ foregroundOnlyMessage }}</app-alert>
}
@for (c of state().categories; track c.categoryId) {
<app-document-category

View File

@@ -56,3 +56,7 @@ export const Default: Story = { args: { state } };
export const LoadError: Story = {
args: { state: { ...state, categoriesError: 'De categorieën konden niet worden geladen.' } },
};
export const ForegroundOnly: Story = {
args: { state: { ...state, backgroundSyncAvailable: false } },
};

View File

@@ -1,25 +0,0 @@
import { Component, computed, input } from '@angular/core';
import { AlertComponent } from '@shared/ui/alert/alert.component';
type BannerType = 'info' | 'warning' | 'error';
type AlertType = 'info' | 'ok' | 'warning' | 'error';
/** Molecule: a polite, announced status banner. Wraps the alert atom and maps the
banner type to an alert type. Pure UI: the container computes the message. */
@Component({
selector: 'app-upload-status-banner',
imports: [AlertComponent],
template: `
<div aria-live="polite">
<app-alert [type]="alertType()">{{ message() }}</app-alert>
</div>
`,
})
export class UploadStatusBannerComponent {
message = input.required<string>();
type = input<BannerType>('info');
protected readonly alertType = computed<AlertType>(() =>
this.type() === 'info' ? 'info' : this.type(),
);
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { UploadStatusBannerComponent } from './upload-status-banner.component';
const meta: Meta<UploadStatusBannerComponent> = {
title: 'Molecules/UploadStatusBanner',
component: UploadStatusBannerComponent,
render: (args) => ({
props: args,
template: `<app-upload-status-banner [message]="message" [type]="type" />`,
}),
};
export default meta;
type Story = StoryObj<UploadStatusBannerComponent>;
export const Info: Story = {
args: { type: 'info', message: 'Uw documenten worden geüpload.' },
};
export const Error: Story = {
args: { type: 'error', message: 'De categorieën konden niet worden geladen.' },
};