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:
@@ -54,7 +54,7 @@ for its existing violations, so every WP ends green.
|
|||||||
| [WP-12](WP-12-datablock.md) | CIBG Datablock for application data | 2 · CIBG | done |
|
| [WP-12](WP-12-datablock.md) | CIBG Datablock for application data | 2 · CIBG | done |
|
||||||
| [WP-13](WP-13-cibg-gap-register.md) | CIBG-gap register + hygiene + MDX | 2 · CIBG | done |
|
| [WP-13](WP-13-cibg-gap-register.md) | CIBG-gap register + hygiene + MDX | 2 · CIBG | done |
|
||||||
| [WP-14](WP-14-storybook-taxonomy.md) | Storybook taxonomy reorg + Layers MDX | 3 · Storybook | done |
|
| [WP-14](WP-14-storybook-taxonomy.md) | Storybook taxonomy reorg + Layers MDX | 3 · Storybook | done |
|
||||||
| [WP-15](WP-15-missing-stories.md) | Missing stories: shell + brief components | 3 · Storybook | todo |
|
| [WP-15](WP-15-missing-stories.md) | Missing stories: shell + brief components | 3 · Storybook | done |
|
||||||
| [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | todo |
|
| [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | todo |
|
||||||
| [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | todo |
|
| [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | todo |
|
||||||
| [WP-18](WP-18-abac-capability-spine.md) | ABAC capability spine (Principal + capabilities, phase P1) | 5 · productie-volwassenheid | done |
|
| [WP-18](WP-18-abac-capability-spine.md) | ABAC capability spine (Principal + capabilities, phase P1) | 5 · productie-volwassenheid | done |
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
# WP-15 — Missing stories: shell + brief components
|
# WP-15 — Missing stories: shell + brief components
|
||||||
|
|
||||||
Status: todo
|
Status: done (<pending commit>)
|
||||||
Phase: 3 — Storybook as curriculum
|
Phase: 3 — Storybook as curriculum
|
||||||
Depends on: WP-14 (titles), WP-01 (axe gate covers the new stories automatically)
|
Depends on: WP-14 (titles), WP-01 (axe gate covers the new stories automatically)
|
||||||
|
|
||||||
|
> **Note:** fixture duplication across the four brief stories that need `Brief`/
|
||||||
|
> `LetterSection`/`LetterBlock` shapes (letter-block, letter-preview, letter-section, plus
|
||||||
|
> the pre-existing letter-composer) didn't bite enough to justify the shared-fixtures
|
||||||
|
> escape hatch — each story only builds the minimal slice it actually renders (letter-block
|
||||||
|
> needs one block, not a whole `Brief`), so the co-located fixtures stayed small and
|
||||||
|
> non-duplicative in practice. A pre-existing, unrelated axe finding on
|
||||||
|
> `text-input--invalid` (informational only — `test-storybook:ci` doesn't fail on it) shows
|
||||||
|
> up in the run; it predates this WP and isn't caused by anything here.
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
"UI is exercised via Storybook stories" (CLAUDE.md §5) — but 7 components have none:
|
"UI is exercised via Storybook stories" (CLAUDE.md §5) — but 7 components have none:
|
||||||
@@ -44,11 +53,12 @@ invisible to the axe gate.
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] Every component in `src/app` has ≥1 story (verify: list components without a
|
- [x] Every component in `src/app` has ≥1 story (verify: list components without a
|
||||||
co-located `*.stories.ts`; expect zero, pages excepted if that's the existing
|
co-located `*.stories.ts`; expect zero, pages excepted if that's the existing
|
||||||
norm — note the norm in this file when checked).
|
norm — note the norm in this file when checked). **Confirmed norm:** `*.page.ts`
|
||||||
- [ ] All new stories pass the axe gate (or carry a justified skip).
|
files (9 of them) have never had stories; every `*.component.ts` now does.
|
||||||
- [ ] Titles follow WP-14.
|
- [x] All new stories pass the axe gate (or carry a justified skip).
|
||||||
|
- [x] Titles follow WP-14.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
|
|||||||
2774
documentation.json
2774
documentation.json
File diff suppressed because one or more lines are too long
@@ -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: [] } };
|
||||||
47
src/app/brief/ui/letter-block/letter-block.stories.ts
Normal file
47
src/app/brief/ui/letter-block/letter-block.stories.ts
Normal 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 } };
|
||||||
88
src/app/brief/ui/letter-preview/letter-preview.stories.ts
Normal file
88
src/app/brief/ui/letter-preview/letter-preview.stories.ts
Normal 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: [] } };
|
||||||
55
src/app/brief/ui/letter-section/letter-section.stories.ts
Normal file
55
src/app/brief/ui/letter-section/letter-section.stories.ts
Normal 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 } };
|
||||||
37
src/app/brief/ui/passage-picker/passage-picker.stories.ts
Normal file
37
src/app/brief/ui/passage-picker/passage-picker.stories.ts
Normal 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 = {};
|
||||||
@@ -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 } };
|
||||||
16
src/app/shared/layout/shell/shell.stories.ts
Normal file
16
src/app/shared/layout/shell/shell.stories.ts
Normal 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 = {};
|
||||||
Reference in New Issue
Block a user