New `brief` context — a letter-composition feature with a drafter/approver approval workflow, built as a teaching vertical slice on the repo's existing FP + Elm + atomic-design patterns (see plan in ~/.claude/plans). Domain (pure): - Rich text as a serialisable value tree (placeholders are first-class nodes), moved to @shared/kernel/rich-text.ts so the shared editor can use it. - lintPlaceholders: a pure, total content -> Diagnostic[] linter, derived never stored. - brief.machine.ts: status sum-type with guarded transitions; frozen-snapshot = deep value copy; derived diagnostics/editability. Full specs. Backend (.NET stub): - BriefStore + seed, GET/PUT /brief and submit/approve/reject/send endpoints, role via X-Role header (mirrors X-Admin), transition + approver!=drafter guards, audit logging. Regenerated typed client via gen:api. +6 backend tests. Seam: - brief.adapter.ts maps flat wire unions <-> domain discriminated unions at the parse boundary (+ spec). UI (atomic): - shared atoms: checkbox, placeholder-chip; molecule: rich-text-editor (no-dep contenteditable, DOM<->RichTextBlock round-trip tested). - brief/ui: letter-block, passage-picker, diagnostics-panel, rejection-comments, letter-section, letter-composer, letter-preview, brief.page + /brief route. - Dev-only ?role=drafter|approver toggle + roleInterceptor; dashboard nav link. Enforcement: @brief/* alias + eslint layer boundary (brief depends only on shared). Also included (same session): - Value-object specs (postcode/uren/big-nummer) — closes the "domain must have a spec" gap. - src/docs/ Storybook MDX foundation pages (atomic design, tokens, FP-in-UI). - .storybook/tsconfig.json: add @angular/localize to types (Storybook was fully broken — $localize unresolved — dev + build). Verified: 168 FE tests, 68 backend tests, lint/build/check:tokens green, Storybook boots, end-to-end HTTP smoke (self-approve 403, approver 200, full flow). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
49 lines
2.0 KiB
TypeScript
49 lines
2.0 KiB
TypeScript
import { Component, computed, input, output } from '@angular/core';
|
|
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
|
import { Diagnostic } from '@brief/domain/placeholders';
|
|
|
|
/** Molecule: lists all letter diagnostics grouped by severity. Errors block
|
|
save/send; warnings (deprecated, unresolved-at-send) are surfaced but allowed.
|
|
Fed by a `computed()` over the letter content — never stored. */
|
|
@Component({
|
|
selector: 'app-diagnostics-panel',
|
|
imports: [AlertComponent],
|
|
styles: [`
|
|
:host{display:block}
|
|
ul{margin:0;padding-inline-start:1.1rem;display:grid;gap:0.15rem}
|
|
button{background:none;border:0;padding:0;color:var(--rhc-color-foreground-link);cursor:pointer;text-align:start;text-decoration:underline}
|
|
`],
|
|
template: `
|
|
@if (errors().length) {
|
|
<app-alert type="error">
|
|
<strong>{{ errorsTitle() }}</strong>
|
|
<ul>
|
|
@for (d of errors(); track $index) { <li><button type="button" (click)="locate.emit(d)">{{ d.message }}</button></li> }
|
|
</ul>
|
|
</app-alert>
|
|
}
|
|
@if (warnings().length) {
|
|
<app-alert type="warning">
|
|
<strong>{{ warningsTitle() }}</strong>
|
|
<ul>
|
|
@for (d of warnings(); track $index) { <li><button type="button" (click)="locate.emit(d)">{{ d.message }}</button></li> }
|
|
</ul>
|
|
</app-alert>
|
|
}
|
|
@if (!errors().length && !warnings().length) {
|
|
<app-alert type="ok">{{ cleanText() }}</app-alert>
|
|
}
|
|
`,
|
|
})
|
|
export class DiagnosticsPanelComponent {
|
|
diagnostics = input<readonly Diagnostic[]>([]);
|
|
locate = output<Diagnostic>();
|
|
|
|
errorsTitle = input($localize`:@@brief.diag.errors:Op te lossen voor indienen/versturen:`);
|
|
warningsTitle = input($localize`:@@brief.diag.warnings:Aandachtspunten:`);
|
|
cleanText = input($localize`:@@brief.diag.clean:Geen problemen gevonden in de velden.`);
|
|
|
|
protected errors = computed(() => this.diagnostics().filter((d) => d.severity === 'error'));
|
|
protected warnings = computed(() => this.diagnostics().filter((d) => d.severity === 'warning'));
|
|
}
|