Scrum4Me/docs/styling.md

18 KiB

title status audience language last_updated
Scrum4Me — Styling & Design System active
ai-agent
contributor
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 .dark class

Bijlage bij CLAUDE.md — lees beide voor je begint met bouwen.