docs: introduce generic entity-dialog pattern + entity-profiles (#45)

* 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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-02 13:09:25 +02:00 committed by GitHub
parent 6c6c8b96b7
commit 55a1ee035c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1241 additions and 469 deletions

387
docs/patterns/dialog.md Normal file
View file

@ -0,0 +1,387 @@
# 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/scrum4me-task-dialog.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/scrum4me-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/scrum4me-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.95→1 + opacity 0→1
- 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/<scrum4me-<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/scrum4me-styling.md` — MD3-tokens, kleurklassen
- `docs/scrum4me-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/scrum4me-task-dialog.md` — voorbeeld-profile voor entiteit "Task"