fix(solo): TaskDetailDialog body scrollt + sticky header/footer

Story 6 zette entityDialogContentClasses op de buitenkant
(flex flex-col p-0 gap-0 max-h-[85vh]) maar de binnenkant van
TaskDetailContent gebruikte nog losse divs zonder shrink-0/flex-1
overflow-y-auto. Resultaat bij lange implementatieplannen: dialog
groeide tot voorbij de viewport, header zat niet vast en footer-margin
(-mx-4 -mb-4) brak omdat parent nu p-0 heeft.

Fix: header in shrink-0 div met px-6 pt-5 pb-4 + border-b; body in
entityDialogBodyClasses (flex-1 overflow-y-auto px-6 py-6 space-y-6);
footer in entityDialogFooterClasses + flex-wrap voor de variabele
job-status-knoppen. Plan-textarea krijgt max-h-[40vh] zodat een lang
plan niet meteen heel het body-gebied opvult.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-04 08:44:33 +02:00
parent e1f1f29db7
commit 61b3db195c

View file

@ -4,8 +4,12 @@ import { useRef, useState, useTransition } from 'react'
import Link from 'next/link'
import { toast } from 'sonner'
import { Markdown } from '@/components/markdown'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { entityDialogContentClasses } from '@/components/shared/entity-dialog-layout'
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
import {
entityDialogBodyClasses,
entityDialogContentClasses,
entityDialogFooterClasses,
} from '@/components/shared/entity-dialog-layout'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
@ -182,8 +186,8 @@ function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDe
return (
<>
<DialogHeader>
<div className="flex items-start gap-3 pr-8">
<div className="flex flex-col gap-1 px-6 pt-5 pb-4 border-b border-outline-variant shrink-0">
<div className="flex items-start gap-3">
<DialogTitle className="text-sm font-medium leading-snug flex-1">
{task.title}
</DialogTitle>
@ -200,78 +204,80 @@ function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDe
{task.story_code && <span className="font-mono mr-1">{task.story_code}</span>}
{task.story_title}
</p>
</DialogHeader>
</div>
<div className={entityDialogBodyClasses}>
{task.description && (
<div>
<p className="text-xs font-medium text-muted-foreground mb-1.5">Beschrijving</p>
<Markdown className="text-foreground">{task.description}</Markdown>
</div>
)}
{task.description && (
<div>
<p className="text-xs font-medium text-muted-foreground mb-1.5">Beschrijving</p>
<Markdown className="text-foreground">{task.description}</Markdown>
<p className="text-xs font-medium text-muted-foreground mb-1.5">Implementatieplan</p>
<DemoTooltip show={isDemo}>
<Textarea
value={localPlan}
onChange={(e) => setLocalPlan(e.target.value)}
onBlur={handleBlur}
placeholder="Voeg een implementatieplan toe…"
className="resize-none text-sm min-h-[120px] max-h-[40vh]"
readOnly={isDemo}
/>
</DemoTooltip>
<div className="flex justify-end mt-1 h-4">
{saveState === 'saving' && (
<span className="text-xs text-muted-foreground">Bezig met opslaan</span>
)}
{saveState === 'saved' && (
<span className="text-xs text-status-done">Opgeslagen</span>
)}
</div>
</div>
)}
<div>
<p className="text-xs font-medium text-muted-foreground mb-1.5">Implementatieplan</p>
<DemoTooltip show={isDemo}>
<Textarea
value={localPlan}
onChange={(e) => setLocalPlan(e.target.value)}
onBlur={handleBlur}
placeholder="Voeg een implementatieplan toe…"
className="resize-none text-sm min-h-[120px]"
readOnly={isDemo}
/>
</DemoTooltip>
<div className="flex justify-end mt-1 h-4">
{saveState === 'saving' && (
<span className="text-xs text-muted-foreground">Bezig met opslaan</span>
)}
{saveState === 'saved' && (
<span className="text-xs text-status-done">Opgeslagen</span>
)}
<div className="flex items-center gap-2">
<DemoTooltip show={isDemo}>
<button
type="button"
role="checkbox"
aria-checked={localVerifyOnly}
onClick={handleVerifyOnlyToggle}
disabled={isDemo || verifyOnlyPending}
className={cn(
'h-4 w-4 rounded border border-border flex items-center justify-center shrink-0',
'disabled:cursor-not-allowed disabled:opacity-50',
localVerifyOnly && 'bg-primary border-primary',
)}
>
{localVerifyOnly && (
<svg className="h-3 w-3 text-primary-foreground" viewBox="0 0 12 12" fill="none">
<path d="M2 6l3 3 5-5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)}
</button>
</DemoTooltip>
<span className="text-xs text-muted-foreground">Alleen verifiëren (niet implementeren)</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground shrink-0">Verify-gate:</span>
<DemoTooltip show={isDemo}>
<select
value={localVerifyRequired}
onChange={handleVerifyRequiredChange}
disabled={isDemo || verifyRequiredPending}
className="text-xs rounded-md border border-border bg-surface-container px-2 py-1 text-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:cursor-not-allowed disabled:opacity-50"
>
{(['ALIGNED', 'ALIGNED_OR_PARTIAL', 'ANY'] as const).map(v => (
<option key={v} value={v}>{VERIFY_REQUIRED_LABELS[v]}</option>
))}
</select>
</DemoTooltip>
</div>
</div>
<div className="flex items-center gap-2">
<DemoTooltip show={isDemo}>
<button
type="button"
role="checkbox"
aria-checked={localVerifyOnly}
onClick={handleVerifyOnlyToggle}
disabled={isDemo || verifyOnlyPending}
className={cn(
'h-4 w-4 rounded border border-border flex items-center justify-center shrink-0',
'disabled:cursor-not-allowed disabled:opacity-50',
localVerifyOnly && 'bg-primary border-primary',
)}
>
{localVerifyOnly && (
<svg className="h-3 w-3 text-primary-foreground" viewBox="0 0 12 12" fill="none">
<path d="M2 6l3 3 5-5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)}
</button>
</DemoTooltip>
<span className="text-xs text-muted-foreground">Alleen verifiëren (niet implementeren)</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground shrink-0">Verify-gate:</span>
<DemoTooltip show={isDemo}>
<select
value={localVerifyRequired}
onChange={handleVerifyRequiredChange}
disabled={isDemo || verifyRequiredPending}
className="text-xs rounded-md border border-border bg-surface-container px-2 py-1 text-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:cursor-not-allowed disabled:opacity-50"
>
{(['ALIGNED', 'ALIGNED_OR_PARTIAL', 'ANY'] as const).map(v => (
<option key={v} value={v}>{VERIFY_REQUIRED_LABELS[v]}</option>
))}
</select>
</DemoTooltip>
</div>
<div className="-mx-4 -mb-4 flex flex-wrap items-center gap-2 border-t bg-muted/50 px-4 py-3 rounded-b-xl">
<div className={cn(entityDialogFooterClasses, 'flex flex-wrap items-center gap-2')}>
<Link
href={`/products/${productId}/sprint/planning`}
className="text-xs text-primary hover:underline mr-auto"