Files
atomic-design-poc/docs/project/archive/backlog/WP-27-brief-ux-layer.md
T
ehoandClaude Opus 5 12f17d9d73 docs: archive the finished backlogs (RD-30)
Two backlog trees are complete: `docs/project/backlog/` (75 files, every
WP done) and `docs/project/refactor-backlog-setup/` (the arc before it).
Move both under `docs/project/archive/` with `git mv`, so history stays
intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them,
because it points at the now-archived backlog README.

Add `docs/project/archive/README.md`. It states that these trees are
historical and names the two directories that are still live.

Repoint every inbound reference named in RD-30's Files table: CLAUDE.md,
the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the
`document-feature` and `new-ssp` skills, and the readable-codebase PLAN,
README, and RD-19 ticket. Fix two upward-relative links inside the moved
WP files (WP-68, WP-69) that gained a directory level and would otherwise
break. Repoint `.prettierignore`'s two agent-prompt exclusions to their
new path, so prettier keeps leaving those files' exact wording alone.

Mark RD-30 done and check off its acceptance criteria; flip its README
row to done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:00:38 +02:00

4.9 KiB
Raw Blame History

WP-27 — Brief UX layer (undo/redo, standaardbrief, search, diff badges)

Status: done Phase: 6 — Brief v2 (edit-on-the-letter, org templates, server-rendered preview)

Why

PRD Brief v2 §7: the working-day features that make the composer pleasant daily. Several are nearly free because state is one immutable value — that's the teaching payload: undo/redo is a shell-side snapshot list, the rejection diff is a pure function over two values. Say so in code comments and stories.

Read first

  • PRD Brief v2 §7 (and the plan-review trim recorded below)
  • src/app/brief/application/brief.store.ts (autosave + SaveState already exist)
  • src/app/brief/domain/brief.machine.ts — the Seed Msg (undo/redo's restore path)

Decisions (pre-made, don't relitigate)

  • Trim agreed at plan review. IN: undo/redo, autosave retry affordance, standaardbrief, passage search, canvas zoom controls, Ctrl+Z/Ctrl+Shift+Z, block-level rejection-diff badges. OUT (deferred, one line each in Out of scope): soft lock/takeover, case-context panel, 401 autosave grace, per-user usage counts, shortcut-overlay dialog, inline character-level text diff.
  • Undo/redo is shell state, not machine state: a past/future: Brief[] stack in BriefStore (cap 50; push on edit(); clear future on a new edit); restore dispatches the existing Seed Msg — zero machine changes — then scheduleSave(). (Superseded by WP-31: the hand-rolled stack was later extracted into the shared createHistory<T>(cap) helper — BriefStore now holds this.history = createHistory<Brief>(50) and calls .record/.undo/.redo/.clear on it; same cap-50, push-on-edit, clear-on-new-edit semantics, just generalized for reuse.)
  • Standaardbrief: backend seeds IsDefault on 23 kern passages (LibraryPassageDto gains the flag); one button, visible only while the kern section is empty, dispatches the existing PassagesInserted with the default set — one Msg, one undo step.
  • Passage search is a client-side filter in the picker (label + content match) — the library is small; no server search, no usage tracking.
  • Rejection diff: pure diffBlocks(before, after): BlockDiff[] in domain/brief-diff.ts (added/removed/changed by blockId); the "before" snapshot is captured shell-side when the Rejected dispatch happens (POC limit: lost on reload — comment it). Rendered as "gewijzigd sinds afwijzing" badges on the canvas; the approver gets a "Toon wijzigingen" toggle on resubmission.
  • Autosave retry: SaveState.Error already exists; add the "Opnieuw proberen" button that calls the existing flush path. No new state.

Files

  • src/app/brief/application/brief.store.ts (+spec: history bounds, clear-on-edit, redo, rejection snapshot)
  • src/app/brief/domain/brief-diff.ts (new, +spec)
  • backend/src/BigRegister.Api/Data/BriefStore.cs (IsDefault seed) + Contracts/Dtos.cs (LibraryPassageDto) + gen:api + adapter parse
  • src/app/brief/ui/passage-picker/* (search input)
  • src/app/brief/ui/letter-canvas/* (diff badges, standaardbrief button, zoom controls)
  • src/app/brief/ui/brief.page.ts (undo/redo buttons + keydown listener, retry button)

Steps

  1. diffBlocks + spec (added/removed/changed/unchanged; changed = same blockId, different content).
  2. Store: history + undo/redo + rejection snapshot (+spec).
  3. Backend IsDefault + gen:api + parse.
  4. UI: standaardbrief button, search, zoom, badges, keyboard, retry.
  5. Stories for the new states (axe).

Acceptance criteria

  • Remove a block → Ctrl+Z restores it → Ctrl+Shift+Z re-removes; buttons mirror; history capped at 50; a new edit clears redo; restore re-triggers autosave.
  • Empty kern + "Standaardbrief invoegen" → default passages inserted as one undo step; button gone once kern is non-empty.
  • Search filters passages by label and content.
  • Reject → edit → resubmit: approver toggles "Toon wijzigingen", changed/added/ removed blocks are badged (block granularity).
  • Autosave failure shows "Niet opgeslagen — opnieuw proberen"; retry works; content never lost locally.
  • Full GREEN.

Verification

GREEN one-liner; store + diff specs; manual reject→edit→diff walk with two roles.

Out of scope (deferred, per plan review)

Soft lock/heartbeat/takeover (real session infra, no FP teaching value here). Case-context panel (no case data exists). 401 autosave grace (auth is faked). Per-user passage usage counts (bookkeeping, demos nothing). Shortcut overlay dialog (no modal component exists; not worth building one). Inline character-level diff (block granularity carries the teaching point).

Risks

Undo history holds Brief snapshots — deep-frozen immutable values, so sharing is safe, but never push non-content dispatches (status transitions, Seed itself) into history or undo will replay workflow state. The rejection snapshot lives in memory only — document it where it's captured.