feat(fp): WP-15 — missing stories: shell + brief components

Add the 7 stories CLAUDE.md's testing rule ("UI is exercised via Storybook
stories") was missing: shared/layout/shell (Design System/Templates/Shell)
and all six previously-unstoried brief components (passage-picker,
rejection-comments, diagnostics-panel, letter-block, letter-preview,
letter-section — Domein/Brief/*), each with a default state plus the
meaningful variants (locked/editable, findings/clean, show/entry, etc).
Every *.component.ts in the repo now has a co-located story; *.page.ts
files stay unstoried, matching the existing norm.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 23:37:58 +02:00
parent ac1f0b6aeb
commit 0cfb01f12c
10 changed files with 1695 additions and 1393 deletions

View File

@@ -0,0 +1,33 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { Diagnostic } from '@brief/domain/placeholders';
import { DiagnosticsPanelComponent } from './diagnostics-panel.component';
const location = { blockId: 'local-2', paragraphIndex: 0, nodeIndex: 1 };
const errorAndWarning: Diagnostic[] = [
{
severity: 'error',
code: 'unknown-placeholder',
placeholderKey: 'onbekend_veld',
location,
message: 'Onbekend veld "onbekend_veld" — controleer de spelling.',
},
{
severity: 'warning',
code: 'unresolved-at-send',
placeholderKey: 'reden_besluit',
location,
message: '"Reden besluit" is nog niet ingevuld.',
},
];
const meta: Meta<DiagnosticsPanelComponent> = {
title: 'Domein/Brief/Diagnostics Panel',
component: DiagnosticsPanelComponent,
args: { locate: () => {} },
};
export default meta;
type Story = StoryObj<DiagnosticsPanelComponent>;
export const Findings: Story = { args: { diagnostics: errorAndWarning } };
export const Clean: Story = { args: { diagnostics: [] } };

View File

@@ -0,0 +1,47 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { LetterBlock } from '@brief/domain/brief';
import { LetterBlockComponent } from './letter-block.component';
const passageBlock: LetterBlock = {
type: 'passage',
blockId: 'local-1',
sourcePassageId: 'p1',
sourceVersion: 1,
edited: false,
content: { paragraphs: [{ nodes: [{ type: 'text', text: 'Geachte heer/mevrouw,' }] }] },
};
const freeTextBlock: LetterBlock = {
type: 'freeText',
blockId: 'local-2',
content: {
paragraphs: [
{
nodes: [
{ type: 'text', text: 'Wij hebben besloten om reden ' },
{ type: 'placeholder', key: 'reden_besluit' },
{ type: 'text', text: '.' },
],
},
],
},
};
const placeholders = [{ key: 'reden_besluit', label: 'Reden besluit' }];
const meta: Meta<LetterBlockComponent> = {
title: 'Domein/Brief/Letter Block',
component: LetterBlockComponent,
args: {
block: passageBlock,
placeholders,
contentChanged: () => {},
removed: () => {},
moved: () => {},
},
};
export default meta;
type Story = StoryObj<LetterBlockComponent>;
export const ReadOnlyPassage: Story = { args: { editable: false } };
export const EditableFreeText: Story = { args: { block: freeTextBlock, editable: true } };

View File

@@ -0,0 +1,88 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { Brief } from '@brief/domain/brief';
import { Diagnostic } from '@brief/domain/placeholders';
import { LetterPreviewComponent } from './letter-preview.component';
const brief: Brief = {
briefId: 'b1',
beroep: 'arts',
templateId: 't1',
drafterId: 'demo-drafter',
status: { tag: 'draft' },
placeholders: [
{ key: 'naam_zorgverlener', label: 'Naam zorgverlener', autoResolvable: true },
{ key: 'reden_besluit', label: 'Reden besluit', autoResolvable: false },
],
sections: [
{
sectionKey: 'aanhef',
title: 'Aanhef',
required: true,
locked: true,
blocks: [
{
type: 'passage',
blockId: 'local-1',
sourcePassageId: 'p1',
sourceVersion: 1,
edited: false,
content: {
paragraphs: [
{
nodes: [
{ type: 'text', text: 'Geachte heer/mevrouw ' },
{ type: 'placeholder', key: 'naam_zorgverlener' },
{ type: 'text', text: ',' },
],
},
],
},
},
],
},
{
sectionKey: 'kern',
title: 'Kern van het besluit',
required: true,
locked: false,
blocks: [
{
type: 'freeText',
blockId: 'local-2',
content: {
paragraphs: [
{
nodes: [
{ type: 'text', text: 'Wij hebben besloten om reden ' },
{ type: 'placeholder', key: 'reden_besluit' },
{ type: 'text', text: '.' },
],
},
],
},
},
],
},
],
};
const diagnostics: Diagnostic[] = [
{
severity: 'warning',
code: 'unresolved-at-send',
placeholderKey: 'reden_besluit',
location: { blockId: 'local-2', paragraphIndex: 0, nodeIndex: 1 },
message: '"Reden besluit" is nog niet ingevuld.',
},
];
const meta: Meta<LetterPreviewComponent> = {
title: 'Domein/Brief/Letter Preview',
component: LetterPreviewComponent,
args: { brief, diagnostics },
};
export default meta;
type Story = StoryObj<LetterPreviewComponent>;
export const Default: Story = {};
export const ZonderBevindingen: Story = { args: { diagnostics: [] } };

View File

@@ -0,0 +1,55 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { LetterSection, LibraryPassage } from '@brief/domain/brief';
import { LetterSectionComponent } from './letter-section.component';
const section: LetterSection = {
sectionKey: 'kern',
title: 'Kern van het besluit',
required: true,
locked: false,
blocks: [
{
type: 'freeText',
blockId: 'local-2',
content: {
paragraphs: [
{
nodes: [
{ type: 'text', text: 'Wij hebben besloten om reden ' },
{ type: 'placeholder', key: 'reden_besluit' },
{ type: 'text', text: '.' },
],
},
],
},
},
],
};
const emptySection: LetterSection = { ...section, blocks: [] };
const passages: LibraryPassage[] = [
{
passageId: 'p2',
scope: 'beroep',
beroep: 'arts',
sectionKey: 'kern',
label: 'Toelichting arts',
version: 1,
content: { paragraphs: [{ nodes: [{ type: 'text', text: 'Als arts ...' }] }] },
},
];
const placeholders = [{ key: 'reden_besluit', label: 'Reden besluit' }];
const meta: Meta<LetterSectionComponent> = {
title: 'Domein/Brief/Letter Section',
component: LetterSectionComponent,
args: { section, availablePassages: passages, placeholders, edit: () => {} },
};
export default meta;
type Story = StoryObj<LetterSectionComponent>;
export const ReadOnly: Story = { args: { editable: false } };
export const Editable: Story = { args: { editable: true } };
export const EditableEmpty: Story = { args: { section: emptySection, editable: true } };

View File

@@ -0,0 +1,37 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { LibraryPassage } from '@brief/domain/brief';
import { PassagePickerComponent } from './passage-picker.component';
const passages: LibraryPassage[] = [
{
passageId: 'p1',
scope: 'global',
sectionKey: 'aanhef',
label: 'Standaard aanhef',
version: 1,
content: { paragraphs: [{ nodes: [{ type: 'text', text: 'Geachte heer/mevrouw,' }] }] },
},
{
passageId: 'p2',
scope: 'beroep',
beroep: 'arts',
sectionKey: 'aanhef',
label: 'Aanhef, arts-specifiek',
version: 1,
content: { paragraphs: [{ nodes: [{ type: 'text', text: 'Geachte collega,' }] }] },
},
];
const meta: Meta<PassagePickerComponent> = {
title: 'Domein/Brief/Passage Picker',
component: PassagePickerComponent,
render: (args) => ({
props: args,
template: `<app-passage-picker [passages]="passages" (insert)="insert($event)" />`,
}),
args: { passages, insert: () => {} },
};
export default meta;
type Story = StoryObj<PassagePickerComponent>;
export const Default: Story = {};

View File

@@ -0,0 +1,16 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { RejectionCommentsComponent } from './rejection-comments.component';
const meta: Meta<RejectionCommentsComponent> = {
title: 'Domein/Brief/Rejection Comments',
component: RejectionCommentsComponent,
args: { reject: () => {} },
};
export default meta;
type Story = StoryObj<RejectionCommentsComponent>;
export const Show: Story = {
args: { mode: 'show', comments: 'Graag de aanhef formeler.' },
};
export const Entry: Story = { args: { mode: 'entry' } };
export const EntryBusy: Story = { args: { mode: 'entry', busy: true } };

View File

@@ -0,0 +1,16 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { applicationConfig } from '@storybook/angular';
import { provideRouter } from '@angular/router';
import { ShellComponent } from './shell.component';
const meta: Meta<ShellComponent> = {
title: 'Design System/Templates/Shell',
component: ShellComponent,
decorators: [applicationConfig({ providers: [provideRouter([])] })],
};
export default meta;
type Story = StoryObj<ShellComponent>;
// No route matches, so <router-outlet> renders nothing — this story is about the
// persistent chrome (skip-link, header, footer), not routed page content.
export const Default: Story = {};