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>
This commit is contained in:
Janpeter Visser 2026-05-02 02:28:42 +02:00
parent b3b5b910c0
commit e6c3dc11b3
3 changed files with 372 additions and 468 deletions

120
docs/scrum4me-pbi-dialog.md Normal file
View file

@ -0,0 +1,120 @@
# PbiDialog Profiel
> Volgt **`docs/patterns/dialog.md`** (de generieke spec voor élke entity-dialog in Scrum4Me).
> Dit document beschrijft alleen de PBI-specifieke afwijkingen en keuzes — alle gedeelde regels (layout, motion, demo-policy, foutcodes, validatie, theming, dialog-gedrag) staan in de generieke spec en worden hier niet herhaald.
> **Belangrijk:** als een regel in dit profiel botst met de generieke spec, wint de generieke spec. Documenteer hier de afwijking + reden, of pas de generieke spec aan.
---
## Velden
| Veld | Type | Mode | Validatie |
|---|---|---|---|
| `code` | `string \| null` | beide | optional, max 30 chars, mono-font, placeholder `auto` op create (server kent dan zelf een code toe) |
| `title` | `string` (required) | beide | trim, 1-200 chars |
| `priority` | `int` (1-4, P1 = hoogste) | beide | int 1-4, default 2 (kan via `defaultPriority`-prop bij create) |
| `status` | `PbiStatusApi` enum | beide | enum, default `'ready'` |
| `description` | `string \| null` | beide | optional, max 2000 chars, plain textarea (geen markdown rendering binnen de dialog) |
`PbiStatusApi` enum (lowercase, mapped via `lib/task-status.ts`): zie `<PbiStatusSelect>` voor de waarden.
### Veld-specifiek gedrag
- **Code + Titel** in één rij (`grid-cols-[6rem_1fr]`)
- **Prioriteit + Status** in één rij (`grid-cols-2`)
- **Prioriteit** via `<PrioritySelect>` (gedeelde primitive, géén segmented buttons in deze dialog)
- **Status** via `<PbiStatusSelect>` (PBI-specifieke wrapper rond gedeelde select)
- **Description** is `<Textarea rows={3} resize-none>` — géén auto-grow, géén markdown-hint, géén char-counter (afwijking van generieke spec; rationale: PBI-descriptions zijn doorgaans kort en richtinggevend)
---
## URL- of state-pattern
- **Gekozen:** state-based (`state: PbiDialogState | null` prop, gerendeerd binnen `PbiList`)
- **Reden:** PBI-dialog leeft altijd binnen `PbiList` op de product-backlog-pagina; deep-linking is niet vereist en zou een tweede edit-flow toevoegen.
- **State-shape:**
```ts
type PbiDialogState =
| { mode: 'create'; productId: string; defaultPriority?: number }
| { mode: 'edit'; pbi: PbiDialogPbi; productId: string }
```
- **Sluiten:** `onClose()` callback uit de parent — `setState(null)` in `PbiList`.
---
## Status-veld
- **Default bij create:** `'ready'` (PBI-default state)
- **Geen verberging in create-mode** — anders dan TaskDialog wordt status hier wél getoond bij create, omdat een PBI zonder expliciete status onhandig is voor backlog-grooming
---
## Server actions
| Actie | Locatie | Form-binding | Revalidatie |
|---|---|---|---|
| `createPbiAction` | `actions/pbis.ts` | via `useActionState` + `<form action>` (FormData) | server-side `revalidatePath` op product-backlog |
| `updatePbiAction` | `actions/pbis.ts` | idem | idem |
| ~~`deletePbiAction`~~ | **(ontbreekt)** | n.v.t. | n.v.t. |
Beide acties moeten de drielaagse demo-policy volgen (zie § Bekende gaps).
---
## Speciale gedragingen
### Form-state via `useActionState`
PbiDialog gebruikt het `useActionState` + `useFormStatus`-patroon (Server Actions / native React), niet `react-hook-form`. Dit is een toegestaan alternatief volgens de generieke spec § 2. Field-errors worden gemapt via een lokale `fieldError(field)`-helper die `result.error` als `Record<string, string[]>` interpreteert wanneer 'm geen string is.
### `key`-prop op `<form>`
Het `<form>`-element heeft `key={isEdit ? pbi!.id : 'create'}` — dit reset native form-state (defaultValues) wanneer de dialog tussen create en edit wisselt of wanneer een ander record bewerkt wordt.
### Hidden inputs voor server-binding
`priority` en `status` worden via `<input type="hidden">` doorgegeven aan de Server Action (de UI-controls zijn JS-state, niet directe form-fields).
---
## Triggers
- **Create-trigger:** `+ PBI`-knop in `PanelNavBar` van `PbiList``setPbiDialogState({ mode: 'create', ... })`
- **Edit-trigger:** edit-icoon op een PBI-rij in `PbiList``setPbiDialogState({ mode: 'edit', pbi, ... })`
---
## Bekende gaps t.o.v. generieke spec
> Deze items wijken af van `docs/patterns/dialog.md` en horen in een vervolg-PR rechtgezet (niet onderdeel van de huidige docs-introductie).
- ❌ **Geen `<DemoTooltip>`** rond submit-knop — laag 3 van de drielaagse demo-policy ontbreekt voor PBI-create/update. Dat betekent dat een demo-user de knop kan klikken; de server action blokkeert nog steeds (laag 2), maar de UX is suboptimaal.
- ❌ **Geen delete-knop / `deletePbiAction`** — alleen create + update. Of dat bewust is (PBI's worden nooit verwijderd, alleen status veranderd) of een gat, moet expliciet worden besloten en in dit profiel vastgelegd.
- ❌ **Geen dirty-close-guard** — Esc / backdrop / Cancel sluiten direct, ook met onopgeslagen wijzigingen. Generieke spec § 8.1 vereist een AlertDialog bij `isDirty`.
- ❌ **Geen Cmd/Ctrl+Enter shortcut** — alleen klik op submit-knop.
- ❌ **Geen char-counter / markdown-hint** op description — bewust weggelaten omdat PBI-descriptions kort zijn, maar verdient expliciete bevestiging.
- ⚠️ **Layout wijkt af** van de generieke responsive-tabel: `sm:max-w-md` i.p.v. de `max-w-[50vw]` / `90vw` / full-screen-progressie uit § 4.
---
## Bewust NIET in v1
Specifiek voor PbiDialog (boven op de algemene out-of-scope-lijst in `docs/patterns/dialog.md` § 13):
- ❌ Inline aanmaken van child-stories binnen de PBI-dialog (gebeurt via StoryDialog vanuit `StoryPanel`)
- ❌ Bulk-status-update over meerdere PBI's
- ❌ PBI-templates / kopiëren
---
## Referenties
- `components/backlog/pbi-dialog.tsx` — implementatie
- `actions/pbis.ts` — server actions
- `components/shared/priority-select.tsx` — gedeelde priority-control
- `components/shared/pbi-status-select.tsx` — PBI-status-select
- `lib/task-status.ts``PbiStatusApi`-mapper
- `docs/patterns/dialog.md` — generieke spec (bron-of-truth)
- `docs/scrum4me-architecture.md` — datamodel `Pbi`
- `docs/scrum4me-styling.md` — MD3-tokens, status- en priority-kleuren

View file

@ -0,0 +1,163 @@
# StoryDialog Profiel
> Volgt **`docs/patterns/dialog.md`** (de generieke spec voor élke entity-dialog in Scrum4Me).
> Dit document beschrijft alleen de Story-specifieke afwijkingen en keuzes — alle gedeelde regels (layout, motion, demo-policy, foutcodes, validatie, theming, dialog-gedrag) staan in de generieke spec en worden hier niet herhaald.
> **Belangrijk:** als een regel in dit profiel botst met de generieke spec, wint de generieke spec. Documenteer hier de afwijking + reden, of pas de generieke spec aan.
---
## Velden
| Veld | Type | Mode | Validatie |
|---|---|---|---|
| `code` | `string \| null` | beide | optional, max 30 chars, mono-font, placeholder `auto` op create |
| `title` | `string` (required) | beide | trim, 1-200 chars |
| `priority` | `int` (1-4, P1 = hoogste) | beide | int 1-4, default 2 (overschrijfbaar via `defaultPriority`-prop bij create) |
| `description` | `string \| null` | beide | optional, plain textarea, placeholder `Als… wil ik… zodat…` (user-story-template) |
| `acceptance_criteria` | `string \| null` | beide | optional, plain textarea, placeholder `- Gegeven… Als… Dan…` (Gherkin-template) |
| `status` | `StoryStatus` enum | alleen edit | read-only badge in header, niet bewerkbaar in deze dialog |
`StoryStatus` enum: `OPEN | IN_SPRINT | DONE` (uppercase in DB).
### Veld-specifiek gedrag
- **Code + Titel** in één rij (`grid-cols-[6rem_1fr]`)
- **Prioriteit** via `<PrioritySelect>` (gedeelde primitive)
- **Description** als `<Textarea rows={3} resize-none>` — géén auto-grow, géén markdown-hint binnen de dialog (afwijking van generieke spec; rationale: stories zijn meestal één zin)
- **Acceptatiecriteria** idem — géén auto-grow, géén char-counter
- **Status** wordt **niet bewerkt** vanuit deze dialog. Status verandert via lijst-acties (sleep naar sprint = IN_SPRINT, taak-completion = DONE). Read-only badge in dialog-header.
---
## URL- of state-pattern
- **Gekozen:** state-based (`state: StoryDialogState | null` prop, gerendeerd binnen `StoryPanel`)
- **Reden:** StoryDialog leeft binnen `StoryPanel` met live-store-data (geselecteerde PBI bepaalt zichtbare stories); deep-linking zou parallelle data-fetch-paden vereisen.
- **State-shape:**
```ts
type StoryDialogState =
| { mode: 'create'; pbiId: string; productId: string; defaultPriority?: number }
| { mode: 'edit'; story: Story; productId: string }
```
- **Sluiten:** `onClose()` callback uit de parent — `setState(null)` in `StoryPanel`.
---
## Status-veld
- **Niet bewerkbaar in deze dialog** — alleen weergegeven als badge in de header (edit-mode)
- **Default bij create:** `OPEN` (server-default, niet expliciet gezet vanuit form)
- Status-overgangen lopen via:
- `OPEN → IN_SPRINT` — drag-and-drop naar een sprint of `sprint-id` zetten via story-actions
- `IN_SPRINT → DONE` — alle taken op `DONE` zetten triggert auto-promotion (zie story-status-logic in `actions/stories.ts`)
---
## Server actions
| Actie | Locatie | Form-binding | Revalidatie |
|---|---|---|---|
| `createStoryAction` | `actions/stories.ts` | via `useActionState` + `<form action>` (FormData) | server-side `revalidatePath` op product-backlog |
| `updateStoryAction` | `actions/stories.ts` | idem | idem |
| `deleteStoryAction` | `actions/stories.ts` | aangeroepen vanuit `useTransition` (geen form) | server-side `revalidatePath` |
| `getStoryLogsAction` | `actions/stories.ts` | aangeroepen on-mount in edit-mode | n.v.t. (read-only) |
Alle write-acties zijn drielaags afgedekt (proxy-guard + server-action-check + DemoTooltip op submit-knop).
---
## Speciale gedragingen
### Header-presentatie (afwijking van generieke spec)
In edit-mode toont de dialog-header **drie elementen** boven op de standaard titel:
1. Story-titel als dialog-title (groot)
2. Story-code als monospace-badge rechtsboven (klein)
3. Twee badges direct onder de titel: priority-badge (kleur via `PRIORITY_COLORS`) en status-badge (kleur via `STATUS_COLORS`)
Generieke spec gaat uit van een sobere header met alleen `headline-small` titel + optioneel een `created_at`-meta-string. StoryDialog wijkt hier bewust van af omdat status + priority belangrijke context zijn voor de gebruiker bij het openen van een story (vaak wisselt iemand vlot tussen meerdere stories).
### Demo-modus = read-only weergave
Wanneer `isDemo === true` én `isEdit === true`, wordt het form **vervangen** door een read-only weergave:
- `description` via gedeelde `<Markdown>`-wrapper (`react-markdown` + `remark-gfm`)
- `acceptance_criteria` als plain whitespace-pre-line tekst
In create-mode is er voor demo-users niets te tonen — de dialog wordt alsnog geopend maar de submit-knop is `disabled` met `<DemoTooltip>`.
> Dit "read-only-fallback"-patroon is uniek voor StoryDialog tot nu toe. Het zou geadopteerd kunnen worden door andere edit-dialogs zodra demo-flow-vereisten dat rechtvaardigen.
### Activity-log (StoryLog) inline
In edit-mode wordt onder het form een `<StoryLog>`-paneel getoond met de chronologische logs van deze story (commit-hashes, status-transitions, etc.). Logs worden lazy-fetched via `getStoryLogsAction(story.id)` zodra de dialog opent.
Dit is een **read-only side-panel** en valt binnen de uitzondering die de generieke spec § 13 maakt voor `<StoryLog>`-style activity-rendering.
### Delete-flow (afwijking van generieke spec)
Generieke spec § 10.4 vereist een **`AlertDialog`** voor delete-confirmatie. StoryDialog gebruikt in plaats daarvan een **inline-confirm** in dezelfde footer-rij:
```
[ Weet je het zeker? Taken worden ook verwijderd. [Verwijderen] [Annuleren] ]
```
Een `AlertDialog` zou een tweede modale laag toevoegen die in deze context onhandig voelt (de dialog zelf is al een interruptive overlay). De inline-confirm is een **bewuste afwijking** van de generieke spec.
### Form-state via `useActionState`
Net als PbiDialog gebruikt StoryDialog `useActionState` + `useFormStatus`, niet `react-hook-form`. Dit is een toegestaan alternatief volgens de generieke spec § 2.
### `key`-prop op `<form>`
Het `<form>` heeft `key={isEdit ? story!.id : 'create'}` — reset native form-state bij record-wissel of mode-switch.
---
## Triggers
- **Create-trigger:** `+ Story`-knop in `PanelNavBar` van `StoryPanel``setStoryDialogState({ mode: 'create', pbiId, productId, defaultPriority: 2 })`
- **Edit-trigger:** edit-icoon op een story-card in `StoryPanel``setStoryDialogState({ mode: 'edit', story, productId })`
- **Empty-state-trigger:** `Maak je eerste story aan`-knop in `EmptyPanel` (zelfde state als create-trigger)
---
## Bekende gaps t.o.v. generieke spec
> Deze items wijken af van `docs/patterns/dialog.md` en horen in een vervolg-PR rechtgezet (niet onderdeel van de huidige docs-introductie).
- ❌ **Geen dirty-close-guard** — Esc / backdrop / Cancel sluiten direct, ook met onopgeslagen wijzigingen. Generieke spec § 8.1 vereist een AlertDialog bij `isDirty`.
- ❌ **Geen Cmd/Ctrl+Enter shortcut** — alleen klik op submit-knop.
- ❌ **Geen char-counter / markdown-hint** op description / acceptance_criteria — bewust weggelaten, maar verdient expliciete bevestiging als design-keuze.
- ⚠️ **Inline-delete-confirm** in plaats van AlertDialog (zie § Speciale gedragingen). Bewuste afwijking; de generieke spec mag deze variant expliciet toestaan, of dit profile moet als precedent gelden voor toekomstige dialogen.
- ⚠️ **Header-layout** met meerdere badges wijkt af van de sobere header in § 4. Bewuste afwijking — context-zwaar bij story-wisselen.
- ⚠️ **Layout wijkt af** van de generieke responsive-tabel: `sm:max-w-lg` met eigen `max-h-[90vh]` + `flex flex-col` i.p.v. de exacte `max-w-[50vw]` / `90vw` / full-screen-progressie uit § 4.
---
## Bewust NIET in v1
Specifiek voor StoryDialog (boven op de algemene out-of-scope-lijst in `docs/patterns/dialog.md` § 13):
- ❌ Status bewerken vanuit de dialog (gebeurt via lijst-acties / drag-and-drop / auto-promotion)
- ❌ Inline aanmaken van child-tasks (gebeurt via TaskDialog vanuit `TaskPanel`)
- ❌ Bulk-edit over meerdere stories
- ❌ Story-templates
- ❌ Linking aan externe issues (GitHub / Linear) — staat op v1.1+ roadmap
---
## Referenties
- `components/backlog/story-dialog.tsx` — implementatie
- `actions/stories.ts` — server actions (incl. `getStoryLogsAction`)
- `components/shared/priority-select.tsx` — gedeelde priority-control
- `components/shared/story-log.tsx` — activity-log paneel
- `components/shared/demo-tooltip.tsx` — demo-policy laag 3
- `components/markdown.tsx` — gedeelde markdown-wrapper
- `lib/task-status.ts` — status-enum-mapper
- `docs/patterns/dialog.md` — generieke spec (bron-of-truth)
- `docs/scrum4me-architecture.md` — datamodel `Story`
- `docs/scrum4me-styling.md` — MD3-tokens, status- en priority-kleuren

View file

@ -1,506 +1,127 @@
# Scrum4Me — TaskDialog Spec
# TaskDialog Profiel
> Volledige design-spec voor de add/update task dialog van de inspannings monitor app.
> Resultaat van een grill-me sessie (15 vragen, alle beslissingen vastgelegd).
> Volgt **`docs/patterns/dialog.md`** (de generieke spec voor élke entity-dialog in Scrum4Me).
> Dit document beschrijft alleen de Task-specifieke afwijkingen en keuzes — alle gedeelde regels (layout, motion, demo-policy, foutcodes, validatie, theming, dialog-gedrag) staan in de generieke spec en worden hier niet herhaald.
---
## Stack
- **Framework:** Next.js (App Router)
- **ORM:** Prisma
- **UI components:** shadcn/ui — wrappers rond `@base-ui/react` (zoals expliciet vastgelegd in `CLAUDE.md`)
- **Styling:** Tailwind CSS
- **Form:** react-hook-form + @hookform/resolvers/zod
- **Design language:** Material Design 3 als theming-laag (geen MUI components)
- **Theming:** `material-color-utilities` voor dynamic color, `next-themes` voor dark mode
- **Icons:** Lucide
- **Markdown rendering:** `react-markdown` + `remark-gfm`
- **Toasts:** sonner (shadcn default)
> **Composition-regel:** dit project gebruikt `@base-ui/react`, niet Radix. Composition gebeurt via de **`render`-prop**, niet via `asChild`. Zie ook `CLAUDE.md` "UI Library Conventions".
>
> ```tsx
> // ✅ goed
> <TooltipTrigger render={<button />}>...</TooltipTrigger>
> // ❌ fout — geeft TS-errors
> <TooltipTrigger asChild><button>...</button></TooltipTrigger>
> ```
> **Dialog-primitive:** bouw de TaskDialog op de bestaande wrapper in `components/ui/dialog.tsx` (shadcn rond `@base-ui/react`). **Geen** directe imports uit `@base-ui/react` voor dialog-primitives in deze feature — anders krijg je twee parallelle dialog-implementaties die uit de pas gaan lopen qua animatie, focus-trap en theming.
---
## Dependency-impact
De volgende packages staan **nog niet** in `package.json` en moeten direct als runtime-`dependencies` worden toegevoegd voordat de eerste commit van deze feature gemerged wordt (CLAUDE.md "Dependencies"-regel). Voeg ze in dezelfde change toe waarin ze geïmporteerd worden, en vermeld ze in de docs-sync.
| Package | Doel | Scope |
|---|---|---|
| `react-hook-form` | form-state management voor TaskDialog | runtime |
| `@hookform/resolvers` | zod-resolver voor `react-hook-form` | runtime |
| `react-textarea-autosize` | auto-grow textareas voor `description` / `implementation_plan` | runtime |
| `react-markdown` | markdown rendering elders in de app (taakdetail, hover-card) | runtime |
| `remark-gfm` | GFM-extensies (tabellen, taken, strikethrough) | runtime |
| `@tailwindcss/typography` | `prose`-classes voor markdown-styling | runtime (Tailwind v4 plugin) |
**Bewust niet meegenomen:**
- `material-color-utilities` — dynamic color valt buiten v1 (zie Theming hieronder).
- `nuqs` — start met **native `searchParams`**; als de URL-state-handling te omslachtig wordt, dan pas `nuqs` als losse refactor-task introduceren. Niet in deze feature mengen.
Reeds aanwezig en gebruikt: `@base-ui/react`, `next-themes`, `lucide-react`, `sonner`, `zod`, `prisma`.
---
## Component-API
Eén component `TaskDialog`, mode afgeleid uit `task?: Task` prop:
```tsx
<TaskDialog task={editTask} /> // task undefined = create mode, task aanwezig = edit mode
```
Open/close-state komt uit de URL via `nuqs` of `searchParams`. Taken leven binnen de context van een sprint of een PBI/story — er is **geen** zelfstandige `/tasks`-route:
```
/sprint/<sprintId>?newTask=1 → create-dialog open binnen sprint-context
/sprint/<sprintId>?editTask=<taskId> → edit-dialog open binnen sprint-context
/products/<productId>/backlog?newTask=1 → create-dialog open binnen backlog-context
/products/<productId>/backlog?editTask=<taskId>
```
Dialog sluit door dezelfde route opnieuw te pushen zonder de `newTask` / `editTask` query-params (bv. `router.push(\`/sprint/\${sprintId}\`)`).
---
## Velden die de dialog gebruikt
De dialog leest en schrijft uitsluitend deze velden van het `Task`-record. Het volledige datamodel valt buiten scope van deze spec.
| Veld | Type | Mode |
|---|---|---|
| `title` | `string` (required) | beide |
| `description` | `string \| null` | beide |
| `implementation_plan` | `string \| null` | beide |
| `priority` | `int` (1-4, P1 = hoogste) | beide |
| `status` | `TaskStatus` enum | alleen edit (default `TO_DO` op create, niet getoond) |
| `created_at` | `Date` | alleen edit, read-only metadata in header |
`TaskStatus` enum-waarden: `TO_DO | IN_PROGRESS | REVIEW | DONE`.
---
## Layout & responsive gedrag
| Breakpoint | Breedte | Hoogte |
|---|---|---|
| Mobiel (<640px) | full-screen | full-screen |
| Tablet (640-1024px) | `90vw` | `max-h-[85vh]` |
| Desktop (≥1024px) | `max-w-[50vw]`, `min-w-[480px]` | `max-h-[85vh]` |
- Padding: `p-6` rondom
- Veld-spacing binnen blok: `space-y-6` (24px)
- Sticky header (titel + close) en sticky footer (knoppen)
- Body scrollt als content de `max-h` overschrijdt
- Footer heeft top-border in `outline-variant` kleur
> **Belangrijk:** als een regel in dit profiel botst met de generieke spec, wint de generieke spec. Documenteer hier de afwijking + reden, of pas de generieke spec aan.
---
## Velden
In volgorde van boven naar beneden:
| Veld | Control | Mode | Validatie |
| Veld | Type | Mode | Validatie |
|---|---|---|---|
| `title` | `Input` (single-line) | beide | required, trim, 1-120 chars |
| `description` | `Textarea` (auto-grow, 3-6 regels) | beide | optional, max 2.000 chars, markdown |
| `implementation_plan` | `Textarea` (auto-grow, 5-12 regels) | beide | optional, max 10.000 chars, markdown |
| `priority` | Segmented buttons (P1/P2/P3/P4) | beide | int 1-4, default 3 |
| `status` | `Select` met gekleurde dot | alleen edit | enum, default TO_DO |
| `title` | `string` (required) | beide | trim, 1-120 chars |
| `description` | `string \| null` | beide | optional, max 2.000 chars, markdown |
| `implementation_plan` | `string \| null` | beide | optional, max 10.000 chars, markdown |
| `priority` | `int` (1-4, P1 = hoogste) | beide | int 1-4, default 3 |
| `status` | `TaskStatus` enum | alleen edit (default `TO_DO` op create, niet getoond) | enum |
| `created_at` | `Date` | alleen edit | read-only metadata in header |
Verberg `status` in create-mode (default = TO_DO is genoeg).
`TaskStatus` enum: `TO_DO | IN_PROGRESS | REVIEW | DONE`.
### Auto-grow textareas
Gebruik `react-textarea-autosize`. Bereikt het veld zijn max-regels, dan `overflow-y-auto` (interne scroll). De **dialog-body** scrollt onafhankelijk; je krijgt zelden geneste scrolls.
### Veld-specifiek gedrag
### Karakter-counter
Alleen tonen vanaf 75% van de limiet. Klein, rechtsonder in het veld, `muted-foreground` kleur. Bv. `1547 / 2000`.
### Markdown hint
Onder elk textarea: `Markdown ondersteund (lijstjes, **vet**, \`code\`)` — klein, muted.
### Priority segmented buttons
```
[ P1 Critical ] [ P2 High ] [ P3 Medium ] [ P4 Low ]
error tertiary primary outline
```
- Lager getal = hoger prio (industriestandaard, Linear/Jira-conform)
- Default geselecteerd: P3 Medium
- Geen 0-waarde toestaan
### Status select (alleen edit)
- TO_DO — grijze dot
- IN_PROGRESS — blauwe dot
- REVIEW — paarse dot
- DONE — groene dot
### `created_at` als header-metadata
In edit-mode tonen in de dialog-header naast de titel:
```
Taak bewerken Aangemaakt: 23 apr 2026
```
Klein, `muted-foreground`, niet als form-veld.
- **Auto-grow textareas** (`description`, `implementation_plan`) via `react-textarea-autosize`. Max 6 regels (description) / 12 regels (implementation_plan), daarna `overflow-y-auto`.
- **Karakter-counter** vanaf 75% van de limiet, klein, rechtsonder, `text-muted-foreground`. Bv. `1547 / 2000`.
- **Markdown-hint** onder elk textarea: `Markdown ondersteund (lijstjes, **vet**, \`code\`)`.
- **Priority** als segmented buttons via `<PrioritySelect>` / `<PrioritySegmented>`. Default P3 (Medium).
- **Status** met gekleurde dot:
- `TO_DO` — grijs
- `IN_PROGRESS``status-in-progress` (blauw)
- `REVIEW` — paars
- `DONE``status-done` (groen)
- **`created_at` als header-metadata** in edit-mode, naast de titel: `Aangemaakt: 23 apr 2026`. Klein, `muted-foreground`, géén form-veld.
---
## Validatie
## URL- of state-pattern
- **Gedeeld zod-schema** in `lib/schemas/task.ts`, geïmporteerd door zowel form als server action
- react-hook-form mode: `onTouched` (eerste validatie bij blur, daarna onChange)
- Errors onder het veld, in error-color, met label en outline van het veld in dezelfde kleur
- Geen toasts voor field-level errors
- Submit-button blijft enabled bij errors — klik scrollt naar eerste error-veld + focus
```ts
// lib/schemas/task.ts (richtlijn)
export const taskSchema = z.object({
title: z.string().trim().min(1, "Verplicht").max(120),
description: z.string().max(2000).optional(),
implementation_plan: z.string().max(10000).optional(),
priority: z.number().int().min(1).max(4),
status: z.nativeEnum(TaskStatus).optional(), // alleen in edit
});
```
- **Gekozen:** URL-based (`searchParams`)
- **Reden:** TaskDialog wordt geopend vanuit twee context-pagina's (sprint-detail en product-backlog) en moet deep-linkable zijn voor share/refresh-scenario's. Suspense + skeleton voor edit-mode loading is gewenst.
- **Routes:**
```
/sprint/<sprintId>?newTask=1 → create
/sprint/<sprintId>?editTask=<taskId> → edit
/products/<productId>/backlog?newTask=1 → create
/products/<productId>/backlog?editTask=<taskId> → edit
```
- **Sluiten:** `router.push(<base-route>)` zonder query-params.
- **Server-side fetch in edit-mode:** server component fetcht de taak vóór render mét `productAccessFilter(userId)`. Bestaat de taak niet of valt 'm buiten scope → toast + redirect naar de context-route.
- Optioneel: `nuqs` als de query-state-handling te omslachtig wordt — pas introduceren als losse refactor-task, niet inline.
---
## Submission
## Status-veld
### Auth-scoping (verplicht)
Elke server action — zowel `saveTask` als `deleteTask` — moet de operatie scope-en op de huidige user. Cross-tenant writes voorkomen via `productAccessFilter(userId)` (of het project-equivalent), zodat een user geen task kan schrijven of verwijderen die niet onder zijn product-scope valt.
> Concreet: de Prisma-mutatie staat nóóit alleen op `where: { id: taskId }`. De scope wordt verplicht gecombineerd in elke `update`/`delete`/`create`-call.
### Demo read-only enforcement (drie lagen — ST-1110)
Elke write-flow moet door deze drie lagen:
1. **Middleware-guard in `proxy.ts`** — blokkeert demo-sessies op write-routes vóór de server action überhaupt loopt. Returnt **403**.
2. **`session.isDemo`-check in de server action zelf** — defense-in-depth voor het geval een write-flow buiten een proxy-route loopt (bv. directe action-invocation). Returnt **403**.
3. **`<DemoTooltip>` op de save- en delete-knoppen** — UI-laag: knoppen zijn zichtbaar disabled met tooltip "Demo-modus: opslaan uitgeschakeld". Vermijdt onnodige round-trips.
### Server Action
```ts
// app/actions/tasks.ts
"use server"
export async function saveTask(
input: TaskInput,
context: { sprintId?: string; productId?: string }, // voor revalidatePath en scope
): Promise<SaveTaskResult> {
const session = await getSession();
if (session.isDemo) return { ok: false, code: 403, error: "demo_readonly" };
const scope = await productAccessFilter(session.userId); // verplicht
// ... validate met taskSchema → Prisma write binnen `scope`
}
type SaveTaskResult =
| { ok: true; task: Task }
| { 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" }
```
### Foutcodes (volgens `CLAUDE.md` "Foutcodes API")
| Code | Wanneer | UI-respons |
|---|---|---|
| **422** | zod-validatiefout (server-side dubbelcheck) | `fieldErrors` mappen naar `form.setError()`, geen toast |
| **403** | demo-sessie probeert te schrijven, of cross-tenant write geblokkeerd | toast "Niet toegestaan in demo-modus" / "Geen toegang", form blijft open |
| **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.
### Revalidation
`revalidatePath` op de **context-route** waarin de dialog werd geopend, niet op een statische `/tasks`-path:
```ts
if (context.sprintId) revalidatePath(`/sprint/${context.sprintId}`);
if (context.productId) revalidatePath(`/products/${context.productId}/backlog`);
```
De aanroepende client geeft de relevante `sprintId` of `productId` mee als argument bij elke save/delete. Geen hard-coded paths in de action zelf.
### Flow
- Synchroon (geen optimistic update in v1)
- Tijdens submit: cancel- en save-knop disabled, spinner in save-knop met "Opslaan...", velden blijven enabled
- Server saniteert en valideert opnieuw met hetzelfde zod-schema
- Field-level server errors (bv. unique constraint op title binnen scope) → `code: 422` met `fieldErrors`, terugmappen naar `form.setError()`
### Error handling
- **422** → field errors inline tonen, geen toast
- **403** → toast met passende boodschap, form blijft open, ingevulde waarden behouden
- **500 / netwerk** → toast met "Opnieuw proberen"-knop, form-state behouden, knoppen weer enabled
Verberg `status` in **create-mode** (default = `TO_DO` is genoeg). Toon alleen in edit-mode als `<Select>` met gekleurde dot per optie.
---
## Dialog-gedrag
## Server actions
### 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?"*
| Actie | Locatie | Context-arg | Revalidatie |
|---|---|---|---|
| `saveTask` | `app/actions/tasks.ts` | `{ sprintId?: string; productId?: string }` | `revalidatePath('/sprint/<sprintId>')` óf `revalidatePath('/products/<productId>/backlog')` afhankelijk van context |
| `deleteTask` | `app/actions/tasks.ts` | idem | idem |
### Keyboard shortcuts
- **Esc** — sluit (met dirty-check)
- **Cmd/Ctrl+Enter** — submit vanuit elk veld
- **Enter in title-input** — submit niet (alleen Cmd/Ctrl+Enter)
- **Enter in textarea** — newline (default browser behavior, niet overriden)
- **Tab** — title → description → implementation_plan → priority → (status) → cancel → save
### Focus management
- Bij openen: focus op `title`-input
- Edit-mode: cursor aan einde van bestaande titel, **geen auto-select** (anders typt user per ongeluk de titel 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
### 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)`
### Backdrop
Scrim `rgba(0,0,0,0.4)` (iets sterker dan MD3-default 0.32 voor betere contrast op licht/donker).
Beide acties volgen de drielaagse demo-policy + auth-scoping uit `docs/patterns/dialog.md` § 67.
---
## Footer
## Speciale gedragingen
### Edit-mode
```
[ Verwijderen ] [ Annuleren ] [ Opslaan ]
tonal (error-container) text filled (primary)
```
### Triggers (bestaande UI vervangen)
### Create-mode
```
[ Annuleren ] [ Aanmaken ]
text filled (primary)
```
Deze TaskDialog is de **enige** create/edit-flow voor taken in beide contexten (sprint én backlog). Bestaande inline-edit-paden in `components/sprint/task-list.tsx` en het backlog-equivalent worden vervangen, niet ernaast geplaatst.
### Delete-flow
- Klik op "Verwijderen" → `AlertDialog`: *"Weet je zeker? Dit kan niet ongedaan worden."*
- Bevestigen → `deleteTask` server action (zelfde auth-scoping en demo-checks als `saveTask`) → `revalidatePath` op de context-route (`/sprint/<sprintId>` of `/products/<productId>/backlog`) → dialog sluit → toast "Taak verwijderd"
- Geen undo in v1
- **Create-trigger:** filled button `+ Nieuwe taak` in tasklist-header → zet `?newTask=1` op huidige route
- **Edit-trigger:** klik op de hele rij in de tasklist (geen apart edit-icoon) → zet `?editTask=<id>` op huidige route
- **Loading edit-mode:** Suspense met minimale skeleton (3 grijze balken), `200ms`-delay zodat snelle fetches geen flicker tonen
---
### Markdown-rendering elders
## Triggers (hoe komt de user erbij?)
De dialog wordt vanuit twee context-pagina's geopend: een sprint-detail (`/sprint/<sprintId>`) of een product-backlog (`/products/<productId>/backlog`).
> **Vervangt bestaande create/edit-flows.** Deze TaskDialog is de **enige** flow voor het aanmaken en bewerken van taken in beide contexten. Bestaande inline-edit-paden in `components/sprint/task-list.tsx` (en eventueel in de backlog) worden door deze dialog vervangen — niet er naast geplaatst. De huidige task-row-rendering wordt aangepast om bij klik de dialog te openen via `?editTask=<id>`; geen aparte edit-icon, geen inline form. Een eventuele "+ Nieuwe taak"-knop in de bestaande tasklist-header wordt eveneens omgeleid naar `?newTask=1` op dezelfde route.
- **Create:** filled button `+ Nieuwe taak` rechtsboven in de tasklist-header van de huidige context (FAB op mobiel optioneel later). Klik zet de juiste query-param (`?newTask=1`) op de huidige route.
- **Edit:** klik op de hele rij in de tasklist (geen apart edit-icoon). Klik zet `?editTask=<id>` op de huidige route.
- **Loading edit-mode:** Suspense met minimale skeleton (3 grijze balken voor inputs), `200ms` delay zodat snelle fetches geen flicker tonen
### Server-fetch
Bij `?editTask=<id>`: server component fetcht de taak vóór render — **inclusief auth-scoping** via `productAccessFilter(userId)` zodat een user nooit een task uit een ander product kan openen via een geraden ID. Bestaat de taak niet of valt 'm buiten scope → toast + redirect naar de context-route zonder query-param (bv. `/sprint/<sprintId>`).
---
## Theming (Material Design 3 tokens)
> **Bron-of-truth in v1:** de bestaande **statische** tokens in `app/styles/theme.css` zijn canoniek. De TaskDialog **consumeert** deze tokens en voegt er geen nieuwe aan toe. Dynamic color (`material-color-utilities`) valt **buiten v1** — niet introduceren in deze feature.
### Color
- TaskDialog gebruikt de bestaande MD3-tokens uit `app/styles/theme.css`: `--primary`, `--on-primary`, `--surface-container`, `--surface-container-high`, `--surface-container-low`, `--error-container`, `--on-error-container`, `--outline-variant`, plus de project-specifieke `--status-*` en `--priority-*` tokens
- Eventueel ontbrekende tokens (bv. een specifieke `surface-container-high` als die er nog niet is) worden in **dezelfde commit** als de feature aan `theme.css` toegevoegd, niet ad-hoc per component gehard-codeerd
- **Verboden:** willekeurige Tailwind-kleuren (`bg-blue-500`, etc.). Altijd semantische tokens — zie `docs/scrum4me-styling.md`
### Dark mode
- `next-themes` is al in de stack; TaskDialog erft automatisch de actieve kleurmodus via de bestaande tokens
- Geen extra setup nodig in deze feature
### Surface elevation
Hybrid (tonal surface + zachte shadow):
- Dialog: `surface-container-high` background + `shadow-2xl` met getemperde opacity
- Form inputs: `surface-container-low` background, geen shadow
- Geen pure tonal-only (voelt te plat op desktop)
### Buttons
- **Filled** (Save/Aanmaken): `primary` background, `on-primary` tekst
- **Text** (Cancel): geen background, `primary` tekst
- **Tonal error** (Delete): `error-container` background, `on-error-container` tekst
### Density
Comfortable (geen compact):
- Single-line input-hoogte: 56px (MD3 outlined text field default)
- Veld-spacing: 24px (`space-y-6`)
- Dialog-padding: 24px alle kanten (`p-6`)
### Typography
- **Font:** Inter via `next/font/google` (geen Roboto-dwang)
- **Schaal (beperkt):**
- `headline-small` (24px) — dialog-titel
- `body-large` (16px) — form-input tekst
- `body-medium` (14px) — helptext, counter
- Geen Material-specifieke letter-spacing tweaks; Inter-defaults voldoen
### Iconen
Lucide (shadcn default). Geen Material Symbols importeren — ~150kb winst en visueel neutraal genoeg om in MD3-themed app te passen.
---
## Markdown rendering (buiten de dialog)
Voor weergave van `description` en `implementation_plan` elders in de app (taakdetail, hover-card, etc.):
```tsx
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
<ReactMarkdown
remarkPlugins={[remarkGfm]}
disallowedElements={["script", "iframe"]}
className="prose prose-sm dark:prose-invert"
>
{content}
</ReactMarkdown>
```
- Tailwind Typography (`prose prose-sm`) voor styling
- `remark-gfm` voor tabellen, taken, strikethrough
- `react-markdown` saniteert by default; `disallowedElements` als extra defense-in-depth
---
## Hergebruik & generalisatie
De TaskDialog is de eerste van naar verwachting meerdere entity-dialogs (PBI, Story, Todo volgen logisch). Bouw daarom **vanaf dag 1** een dunne scheiding tussen generic shell+primitives en entity-specifieke form-body. Dit is geen speculatieve abstractie: de breuklijn tussen "dialog-mechanica" en "welke velden horen bij deze entiteit" is natuurlijk en levert per nieuwe entiteit ~70% codebesparing op.
### Wat generic wordt (`components/entity-dialog/`)
| Component | Waarom generic |
|---|---|
| `entity-dialog.tsx` | Shell: sticky header/footer, responsive layout, motion, backdrop, dirty-close-guard, keyboard-shortcuts, focus-management. Slot-props voor body en footer-actions. |
| `priority-segmented.tsx` | P1-P4 segmented buttons; `priority: int 1-4` is identiek over Task / PBI / Story / Todo. |
| `auto-grow-textarea.tsx` | Wrapper rond `react-textarea-autosize` met char-counter (vanaf 75%) en markdown-hint. Generic — neemt min/max regels en max-chars als props. |
| `dirty-close-guard.tsx` | AlertDialog "Wijzigingen niet opgeslagen — weggooien?" — entity-agnostisch. |
Deze primitives importeren **alleen** uit `components/ui/*` en hebben geen kennis van Task / Story / PBI.
### Wat entity-specifiek blijft (`components/tasks/`)
| Component | Waarom niet generic |
|---|---|
| `task-dialog.tsx` | Dunne wrapper: kiest body, koppelt `saveTask`/`deleteTask`, levert label-strings ("Taak bewerken" / "Aangemaakt: …"). Geen mechanica meer in dit bestand. |
| `task-form.tsx` | Velden zijn task-specifiek (`title`, `description`, `implementation_plan`, `priority`, `status`). Andere entiteiten (Story heeft `acceptance_criteria`, PBI heeft alleen `description`) krijgen elk hun eigen `*-form.tsx`. |
| `task-status-select.tsx` | `TaskStatus` enum met 4 specifieke waarden + dot-kleurmapping. `StoryStatus` (`OPEN | IN_SPRINT | DONE`) en `PbiStatus` (`OPEN | IN_SPRINT | DONE` + `BLOCKED`) hebben andere enums en horen bij eigen select-componenten. |
### Wat **niet** abstraheren in v1
- **URL-state pattern**`?newTask=1` / `?editTask=<id>` per route. Een toekomstige PBI-dialog krijgt `?newPbi=1` / `?editPbi=<id>` op zijn eigen routes. Copy-paste tussen 2-3 pages is goedkoper dan een generic helper die je later toch moet generaliseren.
- **Save/delete-flows** — auth-scoping, demo-checks en revalidatePath verschillen subtiel per entiteit (verschillende productAccessFilter-paden, verschillende context-routes). Per entiteit een eigen actions-file in `app/actions/<entity>.ts`.
### Per-entiteit kostenplaatje
Wanneer er straks een PbiDialog of StoryDialog gebouwd wordt, kost dat alleen:
1. `components/<entity>/<entity>-form.tsx` — de velden + zod-schema
2. `components/<entity>/<entity>-status-select.tsx` — als de entiteit een status-veld heeft
3. `components/<entity>/<entity>-dialog.tsx` — dunne wrapper rond `EntityDialog` met de juiste form en save/delete-handler
4. `app/actions/<entity>.ts` — server actions
5. URL-state uitbreiding op de relevante page(s)
Geen herhaling van layout, motion, dirty-check, keyboard-shortcuts, of segmented/textarea-primitives.
---
## Bewust NIET in v1
Om scope te bewaken:
- ❌ Bulk-edit (meerdere taken tegelijk)
- ❌ Drag-and-drop herorderen
- ❌ Sub-tasks / parent-child relaties
- ❌ Tags / labels / categorieën
- ❌ Due dates / reminders
- ❌ Attachments / file uploads
- ❌ Comments / activity log
- ❌ Sharing / collaboration
- ❌ Undo na delete (toast met undo-actie)
- ❌ Cmd+K keyboard-driven creation zonder dialog
- ❌ Templates voor terugkerende taken
- ❌ Time tracking (uren-registratie) — wel relevant voor inspannings-monitor, maar apart feature
- ❌ Telemetrie / analytics
- ❌ Optimistic locking — niet geïmplementeerd in v1 (last-write-wins binnen scope)
- ❌ Tabs voor secties — alleen spacing-gebaseerde groepering
- ❌ Section-headers — implicit via spacing, geen labels
> Heroverweeg deze keuzes pas als de app groeit. Niet om je te beperken, maar om elke "ja maar moeten we niet ook…"-impuls een bewuste afweging te maken.
---
## File structuur (richtlijn)
```
app/
├── sprint/
│ └── [id]/
│ └── page.tsx # leest searchParams, rendert TaskDialog
├── products/
│ └── [id]/
│ └── backlog/
│ └── page.tsx # leest searchParams, rendert TaskDialog
├── actions/
│ └── tasks.ts # saveTask, deleteTask server actions (auth-scoped)
components/
├── ui/
│ ├── dialog.tsx # bestaande @base-ui/react-wrapper
│ └── demo-tooltip.tsx # wrapper voor save/delete-knoppen in demo-mode
├── entity-dialog/ # GENERIC — geen kennis van Task/Story/PBI
│ ├── entity-dialog.tsx # shell: header/footer/motion/dirty-check/keyboard
│ ├── priority-segmented.tsx # P1-P4 segmented buttons
│ ├── auto-grow-textarea.tsx # textarea met counter + markdown-hint
│ └── dirty-close-guard.tsx # AlertDialog bij dirty close
├── tasks/ # ENTITY-SPECIFIEK
│ ├── task-dialog.tsx # dunne wrapper rond EntityDialog
│ ├── task-form.tsx # task-velden + react-hook-form binding
│ └── task-status-select.tsx # TaskStatus enum + dot-kleuren
lib/
├── schemas/
│ └── task.ts # gedeeld zod-schema (form + server action)
├── auth/
│ └── product-access-filter.ts # scope-helper, gedeeld door page-fetches en actions
proxy.ts # demo-readonly middleware-guard (laag 1 van 3)
```
`description` en `implementation_plan` worden buiten de dialog (taakdetail, hover-card) gerenderd via de gedeelde `<Markdown>`-wrapper (`react-markdown` + `remark-gfm`). Niet in de dialog zelf.
---
## Implementatie-volgorde (suggestie)
1. Dependencies toevoegen aan `package.json` (zie "Dependency-impact"); commit als `chore(ST-XXX): add deps for task dialog`
2. zod-schema in `lib/schemas/task.ts`
3. `productAccessFilter` helper checken/uitbreiden in `lib/auth/`
4. Server actions (`saveTask`, `deleteTask`) met **auth-scoping én demo-check** (laag 2) — testen via thunk
5. `proxy.ts` middleware-guard voor demo-routes (laag 1) — alleen als nog niet aanwezig voor deze routes
6. Eventueel ontbrekende MD3-tokens aanvullen in `app/styles/theme.css` (geen dynamic color in v1)
7. `<DemoTooltip>`-wrapper component (laag 3)
8. TaskDialog — create-mode eerst (minder edge cases), bovenop bestaande `components/ui/dialog.tsx`-wrapper
9. Edit-mode toevoegen (status field, delete-knop, `created_at`-metadata)
10. URL-state via native `searchParams` binnen sprint en backlog routes (geen `nuqs` in v1)
11. **Bestaande task-row / tasklist-trigger refactoren**`components/sprint/task-list.tsx` (en backlog-equivalent) klikbaar maken zodat ze de dialog openen via query-param; oude inline-edit-paden verwijderen
12. Suspense + skeleton voor edit-mode loading + scope-check op fetch
13. Dirty-check + AlertDialog
Hergebruik dit als checklist bij het bouwen of refactoren van TaskDialog:
1. Dependencies in `package.json` (zie `docs/patterns/dialog.md` § 2)
2. zod-schema in `lib/schemas/task.ts` — gedeeld door form en action
3. `productAccessFilter`-helper checken in `lib/auth/`
4. `saveTask` / `deleteTask` in `app/actions/tasks.ts` met auth-scoping + demo-check (laag 2)
5. `proxy.ts`-guard voor demo-write-routes (laag 1) — alleen als nog niet aanwezig
6. Eventueel ontbrekende MD3-tokens in `app/styles/theme.css` aanvullen
7. `<DemoTooltip>` rond submit/delete-knoppen (laag 3)
8. TaskDialog — create-mode eerst (minder edge cases)
9. Edit-mode toevoegen (status, delete, `created_at`-metadata)
10. URL-state via native `searchParams` op beide context-pagina's
11. Bestaande task-row trigger refactoren (klikbaar maken naar dialog)
12. Suspense + skeleton voor edit-mode + scope-check op fetch
13. Dirty-close-guard
14. Keyboard shortcuts (Cmd+Enter)
15. Markdown rendering elders (out-of-scope voor dialog zelf, maar related)
---
## Bewust NIET in v1
Specifiek voor TaskDialog (boven op de algemene out-of-scope-lijst in `docs/patterns/dialog.md` § 13):
- ❌ Sub-tasks / parent-child relaties tussen taken
- ❌ Tags / labels / categorieën op taken
- ❌ Due dates / reminders per taak
- ❌ Time tracking (uren-registratie) — wel relevant voor inspannings-monitor, eigen feature
- ❌ Sharing / collaboration per taak
- ❌ Templates voor terugkerende taken
---
## Referenties
- `docs/patterns/dialog.md` — generieke spec (bron-of-truth voor alles wat hier niet beschreven is)
- `docs/scrum4me-architecture.md` — datamodel `Task`
- `docs/scrum4me-styling.md` — MD3-tokens, status- en priority-kleuren
- `lib/task-status.ts` — enum-mapper DB ↔ API