feat(fp): WP-24 — letter canvas (edit on the letter)
Some checks failed
CI / frontend (push) Failing after 59s
CI / storybook-a11y (push) Successful in 4m22s
CI / backend (push) Successful in 1m18s
CI / codeql (csharp) (push) Failing after 1m47s
CI / codeql (javascript-typescript) (push) Failing after 1m20s
CI / api-client-drift (push) Successful in 1m42s
CI / e2e (push) Failing after 3h8m54s

One letter surface for every role: LetterCanvasComponent renders the
org template's letterhead/signature/footer around the case-type
sections, with editableRegions content|template|none. public/letter.css
is the FE⇄BE rendering contract (WP-25 inlines it verbatim).
letter-preview deleted — its read-only rendering absorbed into 'none'
mode. brief.machine.ts byte-identical; orgTemplate parses at the
adapter boundary and lives beside the machine in BriefStore.

Also fixes passage-picker multi-select (checkboxes all shared
id="undefined", so labels only toggled the first box) and keeps the
±page-break marks from drawing through canvas content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 11:47:51 +02:00
parent 5a610c10f0
commit c07a33ee3e
19 changed files with 3270 additions and 2126 deletions

View File

@@ -10,6 +10,7 @@ import {
unresolvedPlaceholders,
} from '@brief/domain/brief';
import { BriefMsg, BriefState, initial, reduce } from '@brief/domain/brief.machine';
import { OrgTemplate } from '@brief/domain/org-template';
import { BriefAdapter, BriefView } from '@brief/infrastructure/brief.adapter';
/** Transient action state (submit/approve/reject/send/resetDemo) — one tagged union
@@ -48,6 +49,11 @@ export class BriefStore {
/** Surfaced autosave state for the indicator + aria-live region. */
readonly saveState = signal<SaveState>({ tag: 'Idle' });
/** The org template the letter renders with (WP-24). Server-owned appearance data,
not letter state — held beside the machine, never inside it (`brief.machine.ts`
stays untouched by design). Set from every server view that carries it. */
readonly orgTemplate = signal<OrgTemplate | null>(null);
/** The load lifecycle as `RemoteData`, for `<app-async>` — the machine keeps
owning the letter's own domain lifecycle (draft/submitted/approved/…); this is
purely a projection of its loading/failed tags onto the shared async seam. */
@@ -87,8 +93,12 @@ export class BriefStore {
async load() {
const r = await this.adapter.load();
if (r.ok) this.store.dispatch({ tag: 'BriefLoaded', ...r.value });
else this.store.dispatch({ tag: 'BriefLoadFailed', reason: r.error });
if (r.ok) {
this.orgTemplate.set(r.value.orgTemplate);
this.store.dispatch({ tag: 'BriefLoaded', ...r.value });
} else {
this.store.dispatch({ tag: 'BriefLoadFailed', reason: r.error });
}
}
/** An edit: apply it optimistically in the pure reducer, then debounce-save. */
@@ -125,6 +135,7 @@ export class BriefStore {
this.saveState.set({ tag: 'Idle' });
if (r.ok) {
this.actionState.set({ tag: 'Idle' });
this.orgTemplate.set(r.value.orgTemplate);
this.store.dispatch({ tag: 'BriefLoaded', ...r.value });
} else {
this.actionState.set({ tag: 'Failed', error: r.error });
@@ -152,6 +163,8 @@ export class BriefStore {
}
private applyServerStatus(view: BriefView) {
// `send` pins the org-template version server-side — mirror whatever came back.
this.orgTemplate.set(view.orgTemplate);
const { brief, decisions } = view;
const s = brief.status;
switch (s.tag) {