Scrum4Me/docs/patterns/dialog.md
Janpeter Visser 7e45bbdbc0
docs: AI-optimized docs restructure (Phases 1–8) (#61)
* docs(dialog-pattern): add generic entity-dialog spec

Introduceert docs/patterns/dialog.md als bron-of-truth voor elke
create/edit/detail-dialog in Scrum4Me, ongeacht het achterliggende
dataobject. Bevat 14 secties: uitgangspunten, stack, component-
architectuur, layout, validatie, drielaagse demo-policy, submission,
dialog-gedrag, theming, footer, triggers/URL-state, per-entiteit
profile-template, out-of-scope, en een verificatie-checklist.

Registreert het patroon in CLAUDE.md "Implementatiepatronen"-tabel
zodat Claude (en mensen) de spec verplicht raadplegen voor elke
nieuwe dialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(dialog-pattern): convert task spec + add pbi/story entity-profiles

Reduceert docs/scrum4me-task-dialog.md van 507 naar ~140 regels: alle
gedeelde regels verhuisd naar docs/patterns/dialog.md, dit document
bevat nu alleen Task-specifieke velden, URL-pattern, status-veld,
server actions, triggers en bewuste out-of-scope-keuzes.

Voegt twee nieuwe entity-profielen toe voor bestaande dialogen:
- docs/scrum4me-pbi-dialog.md (PbiDialog: state-based, code+title-rij,
  PbiStatusSelect, geen delete in v1)
- docs/scrum4me-story-dialog.md (StoryDialog: state-based, header met
  status/priority badges, inline activity-log, demo-readonly-fallback,
  inline-delete-confirm i.p.v. AlertDialog)

Beide profielen documenteren expliciet de "Bekende gaps t.o.v.
generieke spec" zodat opvolgende PR's de afwijkingen kunnen
rechtzetten of bewust kunnen accorderen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Added pdevelopment docs

* docs(plans): add docs-restructure plan for AI-optimized lookup

Audit of existing 39 doc files (~10.700 lines) and a phased restructure
proposal aimed at minimising the tokens an AI agent has to read to find
the right reference. Captures resolved decisions on language (English),
ADR template (Nygard default with MADR escape-hatch), index generator
(node script), and folder taxonomy. Proposal status — fase 1 to follow.

* docs(adr): add ADR scaffolding (templates, README, meta-ADR)

Set up docs/adr/ as the canonical home for architecture decisions:

- templates/nygard.md — default four-section format (Status, Context,
  Decision, Consequences) for one-way-door decisions.
- templates/madr.md — MADR v4 with YAML front-matter and explicit
  Considered Options for decisions where rejected alternatives matter.
- README.md — naming convention (NNNN-kebab-case), template-selection
  guidance (Nygard default; MADR for auth, queue mechanics, agent
  integration), status lifecycle, and ADR roster.
- 0000-record-architecture-decisions.md — meta-ADR establishing the
  practice itself, in Nygard format.

Backfilling existing implicit decisions (base-ui-over-radix, float
sort_order, demo-user three-layer policy, etc.) is fase 6 of the
docs-restructure plan.

* feat(docs): add docs index generator + initial INDEX.md

scripts/generate-docs-index.mjs walks docs/**/*.md, parses YAML
front-matter (or first H1 fallback) and a Nygard-style ## Status
section, then writes docs/INDEX.md with grouped tables for ADRs,
Specs, Plans (with archive subsection), Patterns, and Other.

Pure Node 20 (no external deps); idempotent — running it twice
produces byte-identical output. Excludes adr/templates/, the ADR
README, INDEX.md itself, and any *_*.md sidecar file.

Wire-up:
- package.json: docs:index → node scripts/generate-docs-index.mjs

Initial run indexed 35 docs across the existing structure; the
generated INDEX.md is committed so the table is reviewable in the
PR before hooking generation into a pre-commit step.

* chore: ignore Obsidian vault and personal sidecar files

Add .obsidian/ (Obsidian vault config) and _*.md (personal sidecar
notes) to .gitignore so the docs/ tree can serve as canonical source
of truth while still being usable as an Obsidian vault for personal
authoring. The docs index generator already excludes the same _*.md
pattern from INDEX.md.

* docs(plans): add PBI bulk-create spec for docs-restructure

Machine-parseable spec for an executor that calls the scrum4me MCP
(create_pbi → create_story → create_task) to seed the docs-restructure
work into the DB.

- Section 1 (Context) is the PBI description; serves as task-context
  via mcp__scrum4me__get_claude_context.
- Section 2 lists the 6 resolved decisions (English, MD3+styling
  merged, solo-paneel merged, .Plans archived, Nygard ADR default,
  node index script).
- Section 3 records what already shipped on this branch so the
  executor doesn't duplicate the ADR scaffolding or index generator.
- Section 4 carries the structured YAML graph: 1 PBI, 8 stories
  (one per phase), 39 tasks. product_id is REPLACE_ME — fill before
  running.
- YAML validated with PyYAML; field schema sanity-checked.

* docs(junk-cleanup): remove stub patterns/test.md

* docs(junk-cleanup): archive .Plans/ to docs/plans/archive/

* docs(front-matter): add YAML front-matter to docs/ root

* docs(front-matter): add YAML front-matter to patterns/

* docs(front-matter): add YAML front-matter to plans + agent files

* docs(index): regenerate INDEX.md after front-matter pass

* docs(naming): drop scrum4me- prefix from doc filenames

* docs(naming): lowercase API.md and MD3 filenames

* docs(naming): rename plan file to kebab-case ASCII

* docs(naming): rename middleware.md to proxy.md (next 16)

* docs(naming): polish CLAUDE.md doc-index after renames

* docs(taxonomy): scaffold topical folders under docs/

* docs(taxonomy): move spec files into docs/specs/

* docs(taxonomy): move design/api/qa/backlog/assets into folders

* docs(taxonomy): move agent-instruction-audit into decisions/

* docs(split): break architecture.md into 6 topical files

* docs(split): merge solo-paneel-spec into specs/functional.md

* docs(split): merge md3-color-scheme into design/styling

* docs(trim): extract branch/commit rules into runbook

* docs(trim): extract MCP integration into runbook

* docs(adr): add 0001-base-ui-over-radix

* docs(adr): add 0002-float-sort-order

* docs(adr): add 0003-one-branch-per-milestone

* docs(adr): add 0004-status-enum-mapping

* docs(adr): add 0005-iron-session-over-nextauth

* docs(adr): add 0006-demo-user-three-layer-policy

* docs(adr): add 0007-claude-question-channel-design

* docs(adr): add 0008-agent-instructions-in-claude-md + update README index

* docs(index): regenerate after ADR 0001-0008

* docs(glossary): add docs/glossary.md

* chore(docs): regenerate INDEX.md in pre-commit hook

* docs(readme): link INDEX + glossary + agent instructions

* feat(docs): add doc-link checker script

* chore(docs): wire docs:check-links and docs npm scripts

* ci(docs): block merge on broken doc links

* docs(links): fix broken cross-references after restructure

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:21:59 +02:00

396 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: "Entity Dialog"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
when_to_read: "Before building any create/edit/detail dialog component."
---
# Pattern — Entity Dialog
Deze pagina is **bindend** voor elke create/edit/detail-dialog in Scrum4Me, ongeacht het achterliggende dataobject (PBI, Story, Task, Todo, Sprint, Product, User, of toekomstige entiteiten). Een nieuwe dialog die hier niet aan voldoet, hoort niet gemerged te worden.
> **Doel:** elke dialog voelt identiek aan voor de gebruiker, hergebruikt dezelfde primitives, en heeft de drielaagse demo-policy + auth-scoping standaard ingebakken.
Voor entity-specifieke afwijkingen of velden: schrijf één begeleidende doc per entiteit (zie sectie [§ Per-entiteit profile](#per-entiteit-profile-verplicht)). Voorbeeld: `docs/specs/dialogs/task.md` is het Task-profiel.
---
## 1 — Verplichte uitgangspunten
| # | Regel | Bron / waarom |
|---|---|---|
| 1.1 | Bouw op `components/ui/dialog.tsx` (de bestaande shadcn/`@base-ui/react`-wrapper). **Geen** directe imports van dialog-primitives uit `@base-ui/react`. | Voorkomt twee parallelle dialog-implementaties met inconsistente animatie/focus-trap/theming |
| 1.2 | Gebruik composition via de **`render`-prop** (zie `CLAUDE.md` "UI Library Conventions"). Nooit Radix' `asChild`. | Project gebruikt `@base-ui/react`, niet Radix |
| 1.3 | Mode (`create` vs `edit` vs `detail`) wordt afgeleid uit één input — een prop, een `state`-object of een `searchParam`. **Niet** twee aparte componenten. | Voorkomt code-duplicatie en inconsistente labels/footer-layouts |
| 1.4 | Auth-scoping op elke server action via `productAccessFilter(userId)` (of het scope-helper-equivalent). Cross-tenant writes mogen onmogelijk zijn. | `CLAUDE.md` "Toegangsmodel" + `docs/patterns/server-action.md` |
| 1.5 | **Drielaagse demo-policy** (verplicht — zie § 6) op elke write-actie. | `CLAUDE.md` "Demo-check" + `docs/architecture.md#demo-user-policy` |
| 1.6 | Validatie via één gedeeld zod-schema (`lib/schemas/<entity>.ts`) — gebruikt door zowel form als server action. | `CLAUDE.md` "Validatie" |
| 1.7 | Foutcodes volgen de project-conventie (§ 5). | `CLAUDE.md` "Foutcodes API" |
| 1.8 | Geen willekeurige Tailwind-kleuren (`bg-blue-500` etc.). Alleen MD3-tokens uit `app/styles/theme.css`. | `docs/design/styling.md` |
---
## 2 — Stack & dependencies
Toegestane runtime-deps voor dialog-werk (al aanwezig of standaard pattern):
| Doel | Voorkeur | Acceptabele alternatief |
|---|---|---|
| Form-state | `react-hook-form` + `@hookform/resolvers/zod` | `useActionState` + `useFormStatus` (Server Actions, native React) |
| Auto-grow textarea | `react-textarea-autosize` | — |
| Markdown-rendering (preview) | `react-markdown` + `remark-gfm` (via bestaande `<Markdown>`-wrapper) | — |
| Toasts | `sonner` | — |
| Iconen | `lucide-react` | — |
> **Per-dialog mag je kiezen tussen `react-hook-form` of `useActionState`.** Beide patronen draaien al in deze codebase. Kies één per dialog en blijf consistent binnen dat bestand. Mix ze niet.
Verboden in dialog-context (v1):
- `material-color-utilities` (dynamic color valt buiten v1)
- Nieuwe form-libraries — geen `formik`, `final-form`, etc.
---
## 3 — Component-architectuur
### 3.1 Reusables (`components/entity-dialog/` of `components/shared/`)
Deze primitives kennen géén entity-specifieke types en mogen door élke dialog gebruikt worden:
| Primitive | Locatie | Verantwoordelijkheid |
|---|---|---|
| `<Dialog>` + `<DialogContent>` etc. | `components/ui/dialog.tsx` | Shell, motion, focus-trap, backdrop |
| `<PrioritySelect>` / `<PrioritySegmented>` | `components/shared/priority-select.tsx` | P1-P4 — identiek over alle entiteiten |
| `<DemoTooltip>` | `components/shared/demo-tooltip.tsx` | Wrapper rond write-knoppen voor demo-modus (laag 3 van 3) |
| Auto-grow textarea | (toe te voegen wanneer nodig in `components/shared/`) | Wrapper rond `react-textarea-autosize` met char-counter + markdown-hint |
| Dirty-close-guard | (gedeelde AlertDialog-flow) | "Wijzigingen niet opgeslagen — weggooien?" |
| `<Markdown>` | `components/markdown.tsx` | `react-markdown` + `remark-gfm` voor description/criteria-preview |
> Wanneer je een primitive twee keer kopieert tussen entity-dialogs, **promote 'm meteen** naar `components/shared/` (of `components/entity-dialog/`). Drie keer is te laat.
### 3.2 Entity-specifieke laag (`components/<domain>/<entity>-dialog.tsx`)
Per entiteit één wrapper-bestand dat:
1. De juiste form/body rendert
2. De juiste server actions koppelt (`save<Entity>Action`, `delete<Entity>Action`)
3. Entiteit-specifieke labels levert ("Story bewerken", "PBI aanmaken", etc.)
Een entity-dialog bevat **geen** layout-mechanica, motion-config of dirty-check zelf — die komen uit § 3.1.
---
## 4 — Layout & responsive gedrag
Identiek voor élke dialog (geen entity-specifieke variaties tenzij expliciet beargumenteerd in het entity-profile):
| Breakpoint | Breedte | Hoogte |
|---|---|---|
| Mobiel (< 640px) | full-screen | full-screen |
| Tablet (6401024px) | `90vw` | `max-h-[85vh]` |
| Desktop (≥ 1024px) | `max-w-[50vw]`, `min-w-[480px]` | `max-h-[85vh]` |
Verplicht:
- Padding `p-6` rondom (24px)
- Veld-spacing in body `space-y-6` (24px)
- **Sticky** header (titel + close) en **sticky** footer (knoppen)
- Body scrollt onafhankelijk; geneste scrolls vermijden
- Footer heeft `border-t` in `outline-variant`
---
## 5 — Validatie & foutcodes
### 5.1 zod-schema
Eén `lib/schemas/<entity>.ts` per entiteit. Geïmporteerd door zowel form als server action geen aparte definities.
### 5.2 Foutcodes (verplicht)
| Code | Wanneer | UI-respons |
|---|---|---|
| **422** | zod-validatiefout (server-side dubbelcheck) | `fieldErrors` mappen naar `form.setError()`, géén toast |
| **403** | demo-sessie probeert te schrijven, of cross-tenant write geblokkeerd | toast "Niet toegestaan in demo-modus" of "Geen toegang", form blijft open |
| **400** | malformed JSON-body (`request.json()` faalt) alleen bij REST-route-handlers | toast "Ongeldige aanvraag" |
| **500** | onverwachte serverfout | toast met "Opnieuw proberen"-knop, form-state behouden |
> Field-level errors zijn **alleen** geldig bij `code: 422`. Bij andere codes is `fieldErrors` ongedefinieerd.
### 5.3 Field-level rendering
- Errors **onder** het veld, in `text-error`, met `border-error` op het input-element
- Géén toast voor field-level errors
- Submit-knop **blijft enabled** bij errors klik scrollt naar eerste error-veld + focus
- react-hook-form mode: `onTouched` (eerste validatie bij blur, daarna onChange)
---
## 6 — Drielaagse demo-policy (verplicht voor write-dialogs)
Elke dialog die schrijft (create / edit / delete) MOET door alle drie de lagen heen:
1. **Middleware-guard** in `proxy.ts` blokkeert demo-sessies op write-routes vóór de server action loopt. Returnt **403**.
2. **`session.isDemo`-check** binnen elke `save<Entity>Action` / `delete<Entity>Action` zelf defense-in-depth voor het geval een actie buiten een proxy-route loopt. Returnt **403**.
3. **`<DemoTooltip show={isDemo}>`** rond de submit- en delete-knoppen UI-laag: knoppen `disabled` met tooltip "Demo-modus: opslaan uitgeschakeld".
> Eén laag missen = bug. Reviewers moeten alle drie de lagen kunnen aanwijzen in de PR.
---
## 7 — Submission-flow
### 7.1 Server Action (template)
```ts
// actions/<entity>.ts
'use server'
export async function save<Entity>Action(
input: <Entity>Input,
context: { /* ids voor revalidatePath en scope */ },
): Promise<Save<Entity>Result> {
const session = await getSession()
if (!session.userId) return { ok: false, code: 403, error: 'forbidden' }
if (session.isDemo) return { ok: false, code: 403, error: 'demo_readonly' }
const scope = await productAccessFilter(session.userId) // verplicht
const parsed = <entity>Schema.safeParse(input)
if (!parsed.success) {
return { ok: false, code: 422, error: 'validation', fieldErrors: parsed.error.flatten().fieldErrors }
}
// ... Prisma write binnen `scope` ...
// revalidatePath(...) op de context-route
return { ok: true, <entity>: row }
}
type Save<Entity>Result =
| { ok: true; <entity>: <Entity> }
| { ok: false; code: 422; error: 'validation'; fieldErrors: Record<string, string[]> }
| { ok: false; code: 403; error: 'demo_readonly' | 'forbidden' }
| { ok: false; code: 500; error: 'server_error' }
```
### 7.2 Revalidation
`revalidatePath` op de **context-route** waarin de dialog werd geopend, niet op een statisch path. Context wordt door de aanroepende client meegegeven (geen hard-coded paths in de action).
### 7.3 Submit-flow
- Synchroon (geen optimistic update in v1, behalve waar het store-patroon `usePlannerStore` al bestaat)
- Tijdens submit: cancel- en submit-knop disabled, spinner of "…" in submit-knop, velden **blijven enabled**
- Server saniteert en valideert opnieuw met hetzelfde zod-schema
---
## 8 — Dialog-gedrag (UX-regels)
### 8.1 Sluiten met dirty state
- Form niet aangeraakt Esc / backdrop-klik / Cancel sluiten **direct**
- Form `isDirty` Esc / backdrop-klik / Cancel triggeren `AlertDialog`: *"Wijzigingen niet opgeslagen — weggooien?"*
### 8.2 Keyboard shortcuts
| Toets | Actie |
|---|---|
| **Esc** | Sluiten (met dirty-check) |
| **Cmd/Ctrl + Enter** | Submit vanuit elk veld |
| **Enter in `<input type=text>`** | **Geen** submit (alleen Cmd/Ctrl+Enter) |
| **Enter in `<textarea>`** | Newline (default browser, niet overriden) |
| **Tab** | Top-naar-bottom door velden, dan Cancel Submit (en Delete in edit-mode) |
### 8.3 Focus management
- Bij openen: focus op het eerste tekstveld (meestal `title` / `name`)
- Edit-mode: cursor aan het einde van bestaande waarde, **geen auto-select** (anders typt user per ongeluk weg)
- Bij sluiten: focus terug naar het element dat de dialog opende (`@base-ui/react` doet dit by default niet breken)
- Bij submit-error: focus naar eerste error-veld
### 8.4 Motion (MD3-conform)
- Open: 250ms, easing `cubic-bezier(0.2, 0, 0, 1)`, scale 0.951 + opacity 01
- Close: 200ms, easing `cubic-bezier(0.4, 0, 1, 1)`
### 8.5 Backdrop
`rgba(0,0,0,0.4)` (iets sterker dan MD3-default 0.32 voor contrast op zowel licht als donker).
---
## 9 — Theming (MD3-tokens)
| Surface | Token |
|---|---|
| Dialog-background | `surface-container-high` + `shadow-2xl` (getemperde opacity) |
| Form input-background | `surface-container-low`, geen shadow |
| Footer top-border | `outline-variant` |
| Submit-knop (filled) | `primary` background + `on-primary` tekst |
| Cancel-knop (text) | geen background + `primary` tekst |
| Delete-knop (tonal error) | `error-container` background + `on-error-container` tekst |
Density: **comfortable** (geen compact). Single-line input `h-14` (56px MD3-default).
Typography:
- `headline-small` (24px) dialog-titel
- `body-large` (16px) form-input tekst
- `body-medium` (14px) helptext, counter
---
## 10 — Footer
### 10.1 Edit-mode
```
[ Verwijderen ] [ Annuleren ] [ Opslaan ]
tonal (error-container) text filled (primary)
```
### 10.2 Create-mode
```
[ Annuleren ] [ Aanmaken ]
text filled (primary)
```
### 10.3 Detail-mode (read-only)
```
[ Sluiten ]
filled (primary)
```
### 10.4 Delete-flow (verplicht patroon)
1. Klik "Verwijderen" `AlertDialog`: *"Weet je zeker? Dit kan niet ongedaan worden."*
2. Bevestigen `delete<Entity>Action` (zelfde auth-scoping én demo-checks als save) `revalidatePath` op context-route dialog sluit toast "<Entity> verwijderd"
3. Geen undo in v1
---
## 11 — Triggers & URL-state
Elke dialog wordt geopend vanuit een context-pagina (sprint, backlog, dashboard, …). Twee patronen zijn toegestaan:
### 11.1 URL-based (voorkeur voor deep-link-bare dialogen)
```
?new<Entity>=1 → create-dialog open
?edit<Entity>=<id> → edit-dialog open
```
Kies dit patroon wanneer de dialog gedeeld of gebookmarkt moet kunnen worden, of wanneer Suspense + skeleton voor edit-mode gewenst is.
Sluiten = dezelfde route opnieuw pushen zonder de query-params.
### 11.2 State-based (voor pure UI-dialogen binnen één client-component)
```tsx
const [dialogState, setDialogState] = useState<DialogState | null>(null)
<EntityDialog state={dialogState} onClose={() => setDialogState(null)} />
```
Kies dit patroon voor dialogen die altijd binnen één parent-component leven en geen deep-linking nodig hebben (bv. PBI-dialog binnen PbiList, Story-dialog binnen StoryPanel).
> Mix de twee patronen niet binnen één entity-dialog. Documenteer in het entity-profile welk patroon gekozen is en waarom.
### 11.3 Server-side fetch in edit-mode
Bij URL-based pattern: server component fetcht het record vóór render — **inclusief auth-scoping**. Bestaat het record niet of valt het buiten scope → toast + redirect naar context-route zonder query-param.
Voor state-based pattern: het record komt al uit de parent (in-memory store of server-prop), dus geen aparte fetch nodig.
---
## 12 — Per-entiteit profile (verplicht)
Voor elke entiteit met een dialog hoort één profiel-doc te bestaan: `docs/<<entity>-dialog>.md` (of vergelijkbaar). Het profiel **vult de generieke spec aan** en bevat **alleen** de entity-specifieke onderdelen.
### Verplichte secties van het entity-profile
```md
# <Entity>Dialog Profiel
> Volgt `docs/patterns/dialog.md`. Dit document beschrijft alleen de afwijkingen en entity-specifieke keuzes.
## Velden
| Veld | Type | Mode | Validatie |
|---|---|---|---|
| ... | ... | create / edit / both | ... |
## URL- of state-pattern
- Gekozen: <URL-based | state-based>
- Reden: <korte motivatie>
- Routes (indien URL-based): `?new<Entity>=1` op `<route>`, `?edit<Entity>=<id>` op `<route>`
## Status-veld (indien aanwezig)
- Enum: ...
- Dot-kleur-mapping: ...
- Default bij create: ...
## Server actions
- `save<Entity>Action` — locatie, context-arg, revalidate-paden
- `delete<Entity>Action` — locatie, context-arg, revalidate-paden
## Speciale gedragingen (alleen indien de generieke spec niet volstaat)
- ... (bv. story-log-tab, claude-question-historie, file-upload, etc.)
## Bewust NIET in v1
- ...
```
> Geen entity-profile = geen merge. Reviewers checken op het bestaan van het profiel-bestand bij een PR die een nieuwe dialog introduceert.
---
## 13 — Bewust **niet** in deze pattern (out of scope)
Deze items zijn over het algemeen niet welkom in een dialog. Een entity-profile mag ze toevoegen, maar moet dan de afweging documenteren:
- ❌ Bulk-edit (meerdere records tegelijk)
- ❌ Drag-and-drop binnen de dialog (drag hoort op de lijst-view)
- ❌ Tabs voor secties — alleen spacing-gebaseerde groepering
- ❌ Section-headers — implicit via spacing, geen labels
- ❌ Sub-entiteiten (parent-child binnen dezelfde dialog)
- ❌ File uploads (uitzondering: avatar — eigen pattern)
- ❌ Comments / activity log binnen de dialog (mag wel als read-only side-panel; zie `StoryLog`)
- ❌ Optimistic locking — last-write-wins binnen scope
- ❌ Cmd+K / quick-create zonder dialog
- ❌ Templates voor terugkerende records
- ❌ Telemetrie / per-veld analytics
---
## 14 — Verificatie-checklist (per nieuwe dialog)
Reviewer en bouwer lopen deze door vóór merge:
- [ ] Bouwt op `components/ui/dialog.tsx` (geen directe `@base-ui/react`-imports)
- [ ] Composition via `render`-prop waar nodig
- [ ] Layout volgt § 4 (responsive breakpoints, padding, sticky header/footer)
- [ ] zod-schema in `lib/schemas/<entity>.ts`, gedeeld door form en action
- [ ] Server action heeft `productAccessFilter(userId)` + `session.isDemo`-check
- [ ] `proxy.ts` heeft een guard voor de bijbehorende write-route(s) (laag 1)
- [ ] Submit/Delete-knoppen omwikkeld met `<DemoTooltip show={isDemo}>` (laag 3)
- [ ] Foutcodes 422/403/500 correct teruggemapt naar UI (zie § 5)
- [ ] Dirty-close-guard actief
- [ ] Cmd/Ctrl+Enter submit, Esc sluit (met dirty-check)
- [ ] Focus management: opent op eerste veld, geen auto-select in edit-mode
- [ ] Motion + backdrop conform § 8.48.5
- [ ] Alleen MD3-tokens; geen `bg-blue-500`-style classes
- [ ] Footer-layout per mode conform § 10
- [ ] Delete-flow: AlertDialog → action → toast
- [ ] URL- of state-pattern gekozen + gedocumenteerd in entity-profile
- [ ] Entity-profile bestaat in `docs/` en is up-to-date
- [ ] Test gedekt: server action met validatie + demo + scope, en component-test op render + submit-flow
---
## 15 — Referenties
- `CLAUDE.md` — UI Library Conventions, Demo-check, Foutcodes API, Validatie
- `docs/design/styling.md` — MD3-tokens, kleurklassen
- `docs/architecture.md` — Demo user policy, scope-helpers
- `docs/patterns/server-action.md` — Server Action template (auth + Zod)
- `docs/patterns/zustand-optimistic.md` — voor lijst-views die de dialog aanroepen
- `docs/specs/dialogs/task.md` — voorbeeld-profile voor entiteit "Task"