feat(brief): letter composition + two-person approval (teaching slice)
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>
This commit is contained in:
39
src/app/shared/ui/checkbox/checkbox.component.ts
Normal file
39
src/app/shared/ui/checkbox/checkbox.component.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Component, forwardRef, input } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
/** Atom: a labelled checkbox wired as a form control (ngModel/reactive). Native
|
||||
input for full keyboard + screen-reader support; the label is the click target. */
|
||||
@Component({
|
||||
selector: 'app-checkbox',
|
||||
styles: [`
|
||||
:host{display:block}
|
||||
label{display:inline-flex;align-items:center;gap:var(--rhc-space-max-md);cursor:pointer}
|
||||
input{inline-size:1.1rem;block-size:1.1rem}
|
||||
`],
|
||||
template: `
|
||||
<label>
|
||||
<input type="checkbox" [id]="checkboxId()" [checked]="value" [disabled]="disabled"
|
||||
(change)="onToggle($event)" (blur)="onTouched()" />
|
||||
<span>{{ label() }}</span>
|
||||
</label>
|
||||
`,
|
||||
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CheckboxComponent), multi: true }],
|
||||
})
|
||||
export class CheckboxComponent implements ControlValueAccessor {
|
||||
checkboxId = input<string>();
|
||||
label = input('');
|
||||
|
||||
value = false;
|
||||
disabled = false;
|
||||
onChange: (v: boolean) => void = () => {};
|
||||
onTouched: () => void = () => {};
|
||||
|
||||
onToggle(e: Event) {
|
||||
this.value = (e.target as HTMLInputElement).checked;
|
||||
this.onChange(this.value);
|
||||
}
|
||||
writeValue(v: boolean) { this.value = !!v; }
|
||||
registerOnChange(fn: (v: boolean) => void) { this.onChange = fn; }
|
||||
registerOnTouched(fn: () => void) { this.onTouched = fn; }
|
||||
setDisabledState(d: boolean) { this.disabled = d; }
|
||||
}
|
||||
15
src/app/shared/ui/checkbox/checkbox.stories.ts
Normal file
15
src/app/shared/ui/checkbox/checkbox.stories.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { CheckboxComponent } from './checkbox.component';
|
||||
|
||||
const meta: Meta<CheckboxComponent> = {
|
||||
title: 'Atoms/Checkbox',
|
||||
component: CheckboxComponent,
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<app-checkbox [label]="label" [checkboxId]="checkboxId"></app-checkbox>`,
|
||||
}),
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<CheckboxComponent>;
|
||||
|
||||
export const Default: Story = { args: { label: 'Standaard aanhef', checkboxId: 'cb-1' } };
|
||||
Reference in New Issue
Block a user