* chore(ST-1112): add deps for task dialog Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1112): add shared zod schema for task dialog Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1112): add missing MD3 tokens for task dialog outline-variant, on-error-container, status-review (light + dark) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1112): add saveTask and deleteTask server actions for TaskDialog Unified create/edit action (saveTask) replaces separate formData-based actions for the new TaskDialog. Uses shared zod schema, structured SaveTaskResult union type, and context-aware revalidatePath for both sprint and backlog routes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1112): add TaskDialog component (create & edit mode) Builds the full TaskDialog on top of the existing @base-ui/react Dialog primitive. Covers create mode, edit mode (status field + created_at metadata + delete), dirty-check AlertDialog, delete confirm AlertDialog, Cmd+Enter submit, and per-field char counters. Uses react-hook-form + zodResolver against the shared taskSchema. Priority and status are extracted to PrioritySegmented and StatusSelect sub-components using MD3 tokens throughout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1112): refactor task-list to open TaskDialog via URL params Replaces inline create/edit forms with router.push navigation: - Clicking a task row → ?editTask=<id> - "+ Taak" button → ?newTask=1&storyId=<storyId> Removes CreateTaskForm, EditSubmitButton, updateTaskAction, and createTaskAction from the component. Status toggle and DnD remain unchanged. Rows now have cursor-pointer and keyboard a11y. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1112): wire TaskDialog into sprint page via searchParams Sprint page now reads ?newTask, ?storyId, and ?editTask query params. For edit mode: fetches the task server-side with productAccessFilter scope (invalid/foreign IDs redirect to closePath). Renders TaskDialog when either param is present. closePath is the sprint route without query params. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1112): add Suspense skeleton for edit-mode task loading Extracts task fetch into EditTaskLoader (async server component) so the sprint board renders immediately while the task loads. TaskDialogSkeleton shows 3 grey bars during the fetch. Invalid or out-of-scope task IDs redirect to closePath. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1112): render description as markdown in task-detail-dialog Solo task detail now renders description via react-markdown + remark-gfm with prose styling. Sanitizes script/iframe elements. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(ST-1112): add saveTask/deleteTask server action tests Covers all three demo-policy layers and cross-tenant scope: demo blocked (403), unauthenticated blocked, validation 422, edit cross-tenant forbidden, create cross-tenant forbidden, and happy-path for both edit and create. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1112): add updateTaskStatusWithStoryPromotion helper Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1112): wire story-promotion into saveTask and PATCH /api/tasks/:id Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(ST-1112): add task-dialog doc and architecture note Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: extend allowed tools in settings.local.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1113): add 200ms animation-delay to TaskDialogSkeleton to prevent flicker Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1114): add DirtyCloseGuard reusable component for dirty-form close confirmation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1114): add shared Markdown wrapper, apply to task-detail and story-dialog Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: allow grep -E pattern in settings.local.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
24 KiB
Scrum4Me — TaskDialog Spec
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).
Stack
- Framework: Next.js (App Router)
- ORM: Prisma
- UI components: shadcn/ui — wrappers rond
@base-ui/react(zoals expliciet vastgelegd inCLAUDE.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-utilitiesvoor dynamic color,next-themesvoor 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 derender-prop, niet viaasChild. Zie ookCLAUDE.md"UI Library Conventions".// ✅ 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/reactvoor 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 nativesearchParams; als de URL-state-handling te omslachtig wordt, dan pasnuqsals 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:
<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-6rondom - Veld-spacing binnen blok:
space-y-6(24px) - Sticky header (titel + close) en sticky footer (knoppen)
- Body scrollt als content de
max-hoverschrijdt - Footer heeft top-border in
outline-variantkleur
Velden
In volgorde van boven naar beneden:
| Veld | Control | 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 |
Verberg status in create-mode (default = TO_DO is genoeg).
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.
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.
Validatie
- 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
// 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
});
Submission
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 elkeupdate/delete/create-call.
Demo read-only enforcement (drie lagen — ST-1110)
Elke write-flow moet door deze drie lagen:
- Middleware-guard in
proxy.ts— blokkeert demo-sessies op write-routes vóór de server action überhaupt loopt. Returnt 403. 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.<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
// 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 isfieldErrorsongedefinieerd.
Revalidation
revalidatePath op de context-route waarin de dialog werd geopend, niet op een statische /tasks-path:
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: 422metfieldErrors, terugmappen naarform.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
Dialog-gedrag
Sluiten met dirty state
- Form niet aangeraakt → Esc / backdrop-klik / Cancel sluiten direct
- Form
isDirty→ Esc / backdrop-klik / Cancel triggerenAlertDialog: "Wijzigingen niet opgeslagen — weggooien?"
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/reactdoet 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).
Footer
Edit-mode
[ Verwijderen ] [ Annuleren ] [ Opslaan ]
tonal (error-container) text filled (primary)
Create-mode
[ Annuleren ] [ Aanmaken ]
text filled (primary)
Delete-flow
- Klik op "Verwijderen" →
AlertDialog: "Weet je zeker? Dit kan niet ongedaan worden." - Bevestigen →
deleteTaskserver action (zelfde auth-scoping en demo-checks alssaveTask) →revalidatePathop de context-route (/sprint/<sprintId>of/products/<productId>/backlog) → dialog sluit → toast "Taak verwijderd" - Geen undo in v1
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=1op dezelfde route.
- Create: filled button
+ Nieuwe taakrechtsboven 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),
200msdelay 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.csszijn 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-highals die er nog niet is) worden in dezelfde commit als de feature aantheme.csstoegevoegd, niet ad-hoc per component gehard-codeerd - Verboden: willekeurige Tailwind-kleuren (
bg-blue-500, etc.). Altijd semantische tokens — ziedocs/scrum4me-styling.md
Dark mode
next-themesis 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-highbackground +shadow-2xlmet getemperde opacity - Form inputs:
surface-container-lowbackground, geen shadow - Geen pure tonal-only (voelt te plat op desktop)
Buttons
- Filled (Save/Aanmaken):
primarybackground,on-primarytekst - Text (Cancel): geen background,
primarytekst - Tonal error (Delete):
error-containerbackground,on-error-containertekst
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-titelbody-large(16px) — form-input tekstbody-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.):
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-gfmvoor tabellen, taken, strikethroughreact-markdownsaniteert by default;disallowedElementsals 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 |
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:
components/<entity>/<entity>-form.tsx— de velden + zod-schemacomponents/<entity>/<entity>-status-select.tsx— als de entiteit een status-veld heeftcomponents/<entity>/<entity>-dialog.tsx— dunne wrapper rondEntityDialogmet de juiste form en save/delete-handlerapp/actions/<entity>.ts— server actions- 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)
Implementatie-volgorde (suggestie)
- Dependencies toevoegen aan
package.json(zie "Dependency-impact"); commit alschore(ST-XXX): add deps for task dialog - zod-schema in
lib/schemas/task.ts productAccessFilterhelper checken/uitbreiden inlib/auth/- Server actions (
saveTask,deleteTask) met auth-scoping én demo-check (laag 2) — testen via thunk proxy.tsmiddleware-guard voor demo-routes (laag 1) — alleen als nog niet aanwezig voor deze routes- Eventueel ontbrekende MD3-tokens aanvullen in
app/styles/theme.css(geen dynamic color in v1) <DemoTooltip>-wrapper component (laag 3)- TaskDialog — create-mode eerst (minder edge cases), bovenop bestaande
components/ui/dialog.tsx-wrapper - Edit-mode toevoegen (status field, delete-knop,
created_at-metadata) - URL-state via native
searchParamsbinnen sprint en backlog routes (geennuqsin v1) - 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 - Suspense + skeleton voor edit-mode loading + scope-check op fetch
- Dirty-check + AlertDialog
- Keyboard shortcuts (Cmd+Enter)
- Markdown rendering elders (out-of-scope voor dialog zelf, maar related)