18 KiB
| title | status | audience | language | last_updated | ||
|---|---|---|---|---|---|---|
| Scrum4Me — Styling & Design System | active |
|
nl | 2026-05-03 |
Scrum4Me — Styling & Design System
Versie: 0.1 — april 2026 Onderdeel van: CLAUDE.md context-set
Overzicht
Scrum4Me gebruikt Material Design 3 (MD3) als kleurfilosofie, geïmplementeerd via CSS custom properties in theme.css en direct bruikbaar als Tailwind utility classes. shadcn/ui levert alle UI-primitieven (Button, Dialog, Sheet, Badge, etc.) en is volledig compatibel met het MD3-kleurensysteem via de legacy-token-mapping.
Lees dit document voordat je een component schrijft. Gebruik nooit willekeurige Tailwind-kleuren zoals bg-blue-500 of bg-green-600 — gebruik altijd de semantische tokens uit dit systeem.
Setup
1. theme.css plaatsen
Kopieer het meegeleverde theme.css bestand naar:
app/globals.css ← importeer theme.css hier, of plak de inhoud direct
Of als apart bestand:
styles/theme.css
Importeer in app/globals.css:
@import './styles/theme.css';
2. shadcn/ui initialiseren
npx shadcn@latest init
Kies bij de setup:
- Style: Default
- Base color: Slate (wordt overschreven door theme.css)
- CSS variables: Yes
De theme.css overschrijft alle shadcn default-kleuren via CSS custom properties. Geen extra configuratie nodig.
3. Tailwind configuratie
theme.css registreert alle tokens via @theme inline — ze zijn direct beschikbaar als Tailwind utility classes:
// Werkt direct:
className="bg-primary text-primary-foreground"
className="bg-surface-container-low"
className="bg-status-done"
className="bg-priority-critical"
4. Dark mode
Dark mode werkt via de .dark class op <html>:
// components/theme-toggle.tsx
'use client'
import { useState, useEffect } from 'react'
export function ThemeToggle() {
const [isDark, setIsDark] = useState(false)
useEffect(() => {
const stored = localStorage.getItem('theme')
if (stored === 'dark') {
document.documentElement.classList.add('dark')
setIsDark(true)
}
}, [])
const toggle = () => {
document.documentElement.classList.toggle('dark')
const next = !isDark
setIsDark(next)
localStorage.setItem('theme', next ? 'dark' : 'light')
}
return (
<button onClick={toggle} className="text-muted-foreground hover:text-foreground">
{isDark ? '☀️' : '🌙'}
</button>
)
}
Kleurfilosofie
Drie hoofdrollen, elk met een semantische betekenis voor een Scrum-planner:
| Rol | Kleur | Betekenis | Gebruik in Scrum4Me |
|---|---|---|---|
| Primary | Blauw #0061a4 |
Productiviteit, vertrouwen | Primaire knoppen, actieve navigatie, Sprint Goal |
| Secondary | Paars #5b5e71 |
Planning, organisatie | Secundaire acties, filters, toolbar-items |
| Tertiary | Teal #006874 |
Voortgang, data | Voortgangsindicatoren, story-tellers, metrics |
Diepte wordt gecreëerd via tonal elevation (lichtere/donkerdere oppervlakken), niet via schaduwen.
Surface Elevation System
Gebruik deze hiërarchie consequent — nooit shadow-lg voor diepte:
HOOGSTE ELEVATIE (voorgrond)
surface-container-lowest → dialogs, modals, popovers
surface-container-low → kaarten, panelen
surface-container → standaard container
surface-container-high → geneste containers
surface-container-highest → achtergrondcontainers
LAAGSTE ELEVATIE (achtergrond)
background → app-achtergrond
In Scrum4Me specifiek
| Element | Surface token |
|---|---|
| App achtergrond | bg-background |
| Navigatiebalk | bg-surface-container-low |
| Gesplitst scherm (elk paneel) | bg-surface-container-low |
| PBI-rij | bg-surface-container |
| Geselecteerde PBI-rij | bg-primary-container |
| Story-blok | bg-surface-container-low border border-border |
| Story-blok (geselecteerd) | bg-primary-container border border-primary |
| Taakregel | bg-surface-container |
| Dialogs / modals | bg-surface-container-lowest |
| Slide-over (story detail) | bg-surface-container-lowest |
| Todo-item | bg-surface-container |
| Navigatiebar per paneel | bg-surface-container-highest |
Statuskleur mapping
Story- en taakstatus
Gebruik altijd icoon + tekst naast kleur (toegankelijkheid):
// Status badge component
const statusConfig = {
OPEN: {
label: 'Open',
className: 'bg-status-todo text-white',
},
IN_SPRINT: {
label: 'In Sprint',
className: 'bg-status-in-progress text-white',
},
DONE: {
label: 'Done',
className: 'bg-status-done text-white',
},
}
// Taakstatus
const taskStatusConfig = {
TO_DO: {
label: 'To Do',
className: 'bg-status-todo text-white',
},
IN_PROGRESS: {
label: 'In Progress',
className: 'bg-status-in-progress text-white',
},
DONE: {
label: 'Done',
className: 'bg-status-done text-white',
},
}
Prioriteitskleur mapping
const priorityConfig = {
1: {
label: 'Kritiek',
className: 'bg-priority-critical text-white',
borderClassName: 'border-l-4 border-priority-critical',
},
2: {
label: 'Hoog',
className: 'bg-priority-high text-white',
borderClassName: 'border-l-4 border-priority-high',
},
3: {
label: 'Middel',
className: 'bg-priority-medium text-white',
borderClassName: 'border-l-4 border-priority-medium',
},
4: {
label: 'Laag',
className: 'bg-priority-low text-white',
borderClassName: 'border-l-4 border-priority-low',
},
}
Story-activiteitenlog
const logTypeConfig = {
IMPLEMENTATION_PLAN: {
label: 'Implementatieplan',
className: 'bg-info-container text-info-container-foreground border-l-4 border-info',
},
TEST_RESULT: {
PASSED: {
label: 'Tests geslaagd',
className: 'bg-success-container text-success-container-foreground border-l-4 border-success',
},
FAILED: {
label: 'Tests mislukt',
className: 'bg-error-container text-error-container-foreground border-l-4 border-error',
},
},
COMMIT: {
label: 'Commit',
className: 'bg-secondary-container text-secondary-container-foreground border-l-4 border-secondary',
},
}
shadcn/ui componenten — gebruik in Scrum4Me
Alle shadcn-componenten gebruiken automatisch het MD3-kleurensysteem. Hieronder de aanbevolen varianten per context.
Button
import { Button } from '@/components/ui/button'
// Primaire actie (Sprint starten, PBI aanmaken, Opslaan)
<Button>Sprint starten</Button>
// Secundaire actie (Annuleren, Filters, Exporteren)
<Button variant="secondary">Annuleren</Button>
// Destructieve actie (Verwijderen, Archiveren)
<Button variant="destructive">Verwijderen</Button>
// Ghost (icon-knoppen in navigatiebar)
<Button variant="ghost" size="icon">
<PlusIcon className="h-4 w-4" />
</Button>
// Outline (minder urgente acties)
<Button variant="outline">Details bekijken</Button>
Badge (status en prioriteit)
import { Badge } from '@/components/ui/badge'
// Gebruik custom className voor MD3-kleuren
// shadcn Badge variant="secondary" is ook bruikbaar voor neutrale badges
<Badge className="bg-status-done text-white">Done</Badge>
<Badge className="bg-priority-critical text-white">Kritiek</Badge>
<Badge className="bg-status-in-progress text-white">In Sprint</Badge>
// Neutrale info badge (bijv. "3 taken")
<Badge variant="secondary">3 taken</Badge>
PBI-status (READY / BLOCKED / DONE): hergebruikt bestaande tokens —
status-todo voor READY, status-blocked voor BLOCKED, status-done voor
DONE. Centraal gedefinieerd in components/shared/pbi-status-select.tsx
(PBI_STATUS_LABELS, PBI_STATUS_COLORS); importeer die in plaats van
kleuren ad-hoc te kopiëren.
Dialog (bevestigingsdialogen)
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
// Standaard bevestigingsdialoog voor verwijderacties
<AlertDialogContent className="bg-surface-container-lowest">
<AlertDialogHeader>
<AlertDialogTitle>PBI verwijderen?</AlertDialogTitle>
<AlertDialogDescription>
Dit verwijdert ook alle gekoppelde stories en taken. Deze actie is niet ongedaan te maken.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Annuleren</AlertDialogCancel>
<AlertDialogAction className="bg-destructive text-destructive-foreground">
Verwijderen
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
Sheet (story detail slide-over)
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'
<Sheet>
<SheetContent
side="right"
className="w-[480px] bg-surface-container-lowest border-l border-border"
>
<SheetHeader>
<SheetTitle className="text-foreground">{story.title}</SheetTitle>
</SheetHeader>
{/* story detail inhoud */}
</SheetContent>
</Sheet>
Input en Textarea
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
// shadcn Input gebruikt --input-background automatisch uit theme.css
<Input
placeholder="PBI titel"
className="bg-input-background border-border focus:ring-primary"
/>
<Textarea
placeholder="Omschrijving (optioneel)"
className="bg-input-background border-border focus:ring-primary resize-none"
/>
Select (prioriteit dropdown)
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
<Select>
<SelectTrigger className="bg-input-background border-border">
<SelectValue placeholder="Prioriteit" />
</SelectTrigger>
<SelectContent className="bg-surface-container-lowest border-border">
<SelectItem value="1">
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-priority-critical" />
Kritiek
</span>
</SelectItem>
<SelectItem value="2">
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-priority-high" />
Hoog
</span>
</SelectItem>
<SelectItem value="3">
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-priority-medium" />
Middel
</span>
</SelectItem>
<SelectItem value="4">
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-priority-low" />
Laag
</span>
</SelectItem>
</SelectContent>
</Select>
Skeleton (loading states)
import { Skeleton } from '@/components/ui/skeleton'
// PBI lijst skeleton
function PbiListSkeleton() {
return (
<div className="space-y-2 p-4">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-12 w-full bg-surface-container-high" />
))}
</div>
)
}
// Story blokken skeleton
function StoryGridSkeleton() {
return (
<div className="flex flex-wrap gap-3 p-4">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className="h-24 w-[10%] min-w-[100px] bg-surface-container-high" />
))}
</div>
)
}
Tooltip (demo-gebruiker write-protection)
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
// Gebruik voor alle uitgeschakelde knoppen bij demo-gebruiker
function DemoProtectedButton({ children, isDemo, onClick, ...props }) {
if (!isDemo) {
return <Button onClick={onClick} {...props}>{children}</Button>
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button disabled {...props}>{children}</Button>
</span>
</TooltipTrigger>
<TooltipContent className="bg-surface-container-lowest border-border">
<p className="text-sm text-muted-foreground">Niet beschikbaar in demo-modus</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
Scrum4Me component patronen
PBI-rij
// Geselecteerd PBI heeft primary-container achtergrond
<div
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-lg cursor-pointer transition-colors border-l-4",
priorityConfig[pbi.priority].borderClassName,
isSelected
? "bg-primary-container text-primary-container-foreground"
: "bg-surface-container hover:bg-surface-container-high"
)}
onClick={() => setSelectedPbi(pbi.id)}
>
<span className="flex-1 text-sm font-medium truncate">{pbi.title}</span>
<Badge className={priorityConfig[pbi.priority].className}>
{priorityConfig[pbi.priority].label}
</Badge>
</div>
Story-blok
// ~10% schermbreedte, compacte weergave
<div
className={cn(
"relative flex flex-col gap-1 p-3 rounded-lg border cursor-pointer",
"min-w-[100px] w-[10%] h-24 text-xs",
"transition-colors hover:border-primary",
"bg-surface-container-low border-border"
)}
>
<span className="font-medium leading-tight line-clamp-2">{story.title}</span>
<div className="mt-auto flex items-center justify-between">
<span className={cn("px-1.5 py-0.5 rounded text-[10px] font-medium", statusConfig[story.status].className)}>
{statusConfig[story.status].label}
</span>
<span className={cn("w-2 h-2 rounded-full", `bg-priority-${priorityLabel}`)}>
</span>
</div>
</div>
Prioriteitsgroep scheidingslijn
// Visuele scheiding per prioriteitsgroep in PBI-lijst en story-grid
<div className="mt-4 mb-2">
<div className="flex items-center gap-2">
<span className={cn(
"text-xs font-semibold uppercase tracking-wider",
priority === 1 && "text-priority-critical",
priority === 2 && "text-priority-high",
priority === 3 && "text-priority-medium",
priority === 4 && "text-priority-low",
)}>
{priorityConfig[priority].label}
</span>
<div className={cn(
"flex-1 h-px",
priority === 1 && "bg-priority-critical/30",
priority === 2 && "bg-priority-high/30",
priority === 3 && "bg-priority-medium/30",
priority === 4 && "bg-priority-low/30",
)} />
<span className="text-xs text-muted-foreground">{count}</span>
</div>
</div>
Voortgangsindicator (story → taken)
// Gebruikt tertiary kleur voor voortgang
function StoryProgress({ done, total }: { done: number; total: number }) {
const pct = total === 0 ? 0 : Math.round((done / total) * 100)
return (
<div className="flex items-center gap-2 text-xs">
<div className="flex-1 h-1.5 bg-surface-container-highest rounded-full overflow-hidden">
<div
className="h-full bg-tertiary rounded-full transition-all"
style={{ width: `${pct}%` }}
/>
</div>
<span className="text-muted-foreground tabular-nums">
{done}/{total}
</span>
</div>
)
}
Activiteitenlog entry
function LogEntry({ entry }: { entry: StoryLog }) {
const config = entry.type === 'TEST_RESULT'
? logTypeConfig.TEST_RESULT[entry.status ?? 'PASSED']
: logTypeConfig[entry.type]
return (
<div className={cn("rounded-lg p-3 text-sm", config.className)}>
<div className="flex items-center justify-between mb-1">
<span className="font-medium text-xs uppercase tracking-wide">
{config.label}
</span>
<span className="text-xs opacity-70">
{formatDate(entry.created_at)}
</span>
</div>
<p className="text-sm leading-relaxed whitespace-pre-wrap">
{entry.content}
</p>
{entry.commit_hash && (
<a
href={`${repoUrl}/commit/${entry.commit_hash}`}
target="_blank"
rel="noopener noreferrer"
className="mt-1 inline-flex items-center gap-1 text-xs font-mono opacity-80 hover:opacity-100 underline"
>
{entry.commit_hash.slice(0, 7)} — {entry.commit_message}
</a>
)}
</div>
)
}
Sprint Goal banner
// Prominent bovenaan Sprint-schermen
<div className="bg-primary-container text-primary-container-foreground rounded-lg px-4 py-3 mb-4">
<span className="text-xs font-semibold uppercase tracking-wider opacity-70">
Sprint Goal
</span>
<p className="mt-0.5 font-medium">{sprint.sprint_goal}</p>
</div>
Toast notificaties (Sonner)
import { toast } from 'sonner'
// Success (aanmaken, opslaan)
toast.success('PBI aangemaakt')
toast.success('Story toegevoegd aan Sprint')
// Error (mislukte Server Action)
toast.error('Opslaan mislukt. Probeer opnieuw.')
// Info (neutrale melding)
toast.info('Sprint afgerond')
// Geen toast bij drag-and-drop (te frequent)
Regels (nooit overtreden)
❌ bg-blue-500, bg-green-600, bg-red-400 → gebruik semantische tokens
❌ shadow-lg, shadow-md → gebruik surface elevation
❌ opacity-50 op een primary button → gebruik -container variant
❌ Kleur alleen voor status (geen tekst) → altijd tekst + kleur
❌ Hardcoded hex-waarden in className → altijd via CSS token
❌ bg-white of bg-black → bg-background of bg-foreground
✅ bg-primary text-primary-foreground
✅ bg-surface-container-low
✅ bg-status-done + tekst "Done"
✅ bg-error-container text-error-container-foreground
✅ border-l-4 border-priority-critical
Bestandslocaties
styles/
theme.css ← bronbestand, niet aanpassen
app/
globals.css ← importeert theme.css
components/
ui/ ← shadcn/ui (auto-gegenereerd, niet aanpassen)
shared/
status-badge.tsx ← herbruikbare status badge
priority-badge.tsx ← herbruikbare prioriteit badge
demo-button.tsx ← Button met demo-protection tooltip
story-log.tsx ← activiteitenlog entries
story-progress.tsx ← voortgangsindicator
priority-group.tsx ← prioriteitsgroep scheidingslijn
Toegankelijkheid
- Alle kleurcombinaties voldoen aan WCAG AA (contrast ratio ≥ 4.5:1 voor normale tekst)
- Gebruik altijd tekst + kleur voor statusindicatoren, nooit kleur alleen
- Alle interactieve elementen hebben een zichtbare
focus:ring-primary - Dark mode is volledig ondersteund via de
.darkclass
Bijlage bij CLAUDE.md — lees beide voor je begint met bouwen.