feat(design-system): F003 – complete design system foundation

Design tokens:
- lib/ds.ts: DS_COLORS (resultType/confidence/risk/availability/freshness/dataQuality), scoreToConfidenceLevel(), scoreToDataQualityLevel()
- lib/constants.ts: added CONFIDENCE_LABELS, DATA_QUALITY_LABELS

Badge system (src/components/badges/):
- ResultTypeBadge, ConfidenceBadge, RiskBadge, AvailabilityBadge, FreshnessBadge, DataQualityBadge
- All use DS_COLORS, MUI Chip size=small, lucide icons, German labels from constants

Score components (src/components/scores/):
- ConfidenceScore: numeric % with color from utils.confidenceHex
- DataQualityScore: LinearProgress + qualitative label
- ScoreBar: horizontal bar with label/value/weight
- ScoreBreakdownMini: ScoreFactor[] mapped to ScoreBars with Tooltip explanations

Card system (src/components/cards/):
- DecisionCard: header+badges+score+body+actions, selected/loading/restricted states
- CompactCard: single-row 48px card with leading/trailing slots
- MetricCard: KPI card with label/value/delta/icon (extracted pattern from SupplyDashboard)

Panel system (src/components/panels/):
- InfoPanel: outlined Paper with icon+title header
- DetailPanel: Accordion sections in bordered Paper
- WarningPanel: MUI Alert with warning/critical severity

Table components (src/components/tables/):
- DataTableShell: Paper+TableContainer+Table with loading/empty/toolbar slots
- TableToolbar: flex row with title/count/filters/actions
- FilterBar: deletable Chips with clear-all
- EmptyTableState: centered empty row spanning all cols
- LoadingRows: Skeleton cell rows

Form components (src/components/forms/):
- TextInput: MUI TextField wrapper with onChange(string) signature
- SelectField: generic SelectField<T extends string> with MUI Select
- PriorityChipGroup: togglable Chip set (single/multi select)
- FieldWithConfidence: label+value+ConfidenceBadge for AI-parsed fields

UI additions (src/components/ui/):
- CardSkeleton, PanelLoadingState, UnauthorizedState, RestrictedState

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-15 11:34:25 +02:00
parent 5ef8b9d69d
commit 70c0d79d8c
38 changed files with 1385 additions and 0 deletions
@@ -0,0 +1,53 @@
import { Box, Typography } from '@mui/material'
import type { SxProps, Theme } from '@mui/material'
import type { ReactNode } from 'react'
import type { ConfidenceLevel } from '../../domain/enums'
import { ConfidenceBadge } from '../badges/ConfidenceBadge'
interface FieldWithConfidenceProps {
label: string
value: ReactNode
confidence?: number
confidenceLevel?: ConfidenceLevel
layout?: 'row' | 'column'
sx?: SxProps<Theme>
}
export function FieldWithConfidence({
label,
value,
confidence,
confidenceLevel,
layout = 'column',
sx,
}: FieldWithConfidenceProps) {
const showBadge = confidence !== undefined || confidenceLevel !== undefined
if (layout === 'row') {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, ...sx }}>
<Typography sx={{ fontSize: '0.75rem', color: 'text.secondary', minWidth: 80 }}>
{label}
</Typography>
<Box sx={{ flex: 1 }}>{value}</Box>
{showBadge && (
<ConfidenceBadge level={confidenceLevel} score={confidence} />
)}
</Box>
)
}
return (
<Box sx={sx}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.25 }}>
<Typography sx={{ fontSize: '0.75rem', color: 'text.secondary' }}>
{label}
</Typography>
{showBadge && (
<ConfidenceBadge level={confidenceLevel} score={confidence} />
)}
</Box>
<Box sx={{ fontSize: '0.875rem' }}>{value}</Box>
</Box>
)
}
@@ -0,0 +1,64 @@
import { Box, Chip, Typography } from '@mui/material'
import type { SxProps, Theme } from '@mui/material'
interface ChipOption {
value: string
label: string
color?: string
}
interface PriorityChipGroupProps {
label?: string
options: ChipOption[]
value: string[]
onChange: (value: string[]) => void
exclusive?: boolean
sx?: SxProps<Theme>
}
export function PriorityChipGroup({ label, options, value, onChange, exclusive = false, sx }: PriorityChipGroupProps) {
function toggle(optValue: string) {
if (exclusive) {
onChange(value.includes(optValue) ? [] : [optValue])
} else {
onChange(
value.includes(optValue)
? value.filter(v => v !== optValue)
: [...value, optValue],
)
}
}
return (
<Box sx={sx}>
{label && (
<Typography sx={{ fontSize: '0.75rem', color: 'text.secondary', mb: 0.75, fontWeight: 500 }}>
{label}
</Typography>
)}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{options.map(opt => {
const selected = value.includes(opt.value)
const accent = opt.color ?? '#1e3a5f'
return (
<Chip
key={opt.value}
label={opt.label}
size="small"
clickable
onClick={() => toggle(opt.value)}
sx={{
fontSize: '0.75rem',
bgcolor: selected ? accent : 'transparent',
color: selected ? '#fff' : 'text.secondary',
border: '1px solid',
borderColor: selected ? accent : 'divider',
'&:hover': { bgcolor: selected ? accent : 'rgba(0,0,0,0.04)' },
}}
/>
)
})}
</Box>
</Box>
)
}
+55
View File
@@ -0,0 +1,55 @@
import { FormControl, FormHelperText, InputLabel, MenuItem, Select } from '@mui/material'
import type { SxProps, Theme } from '@mui/material'
interface SelectOption<T extends string = string> {
value: T
label: string
}
interface SelectFieldProps<T extends string = string> {
label: string
value: T | ''
onChange: (value: T) => void
options: SelectOption<T>[]
error?: string
helperText?: string
required?: boolean
disabled?: boolean
size?: 'small' | 'medium'
fullWidth?: boolean
sx?: SxProps<Theme>
}
export function SelectField<T extends string = string>({
label,
value,
onChange,
options,
error,
helperText,
required,
disabled,
size = 'small',
fullWidth = true,
sx,
}: SelectFieldProps<T>) {
const labelId = `select-${label.replace(/\s+/g, '-').toLowerCase()}`
return (
<FormControl fullWidth={fullWidth} size={size} error={!!error} disabled={disabled} required={required} sx={sx}>
<InputLabel id={labelId}>{label}</InputLabel>
<Select
labelId={labelId}
value={value}
label={label}
onChange={e => onChange(e.target.value as T)}
>
{options.map(opt => (
<MenuItem key={opt.value} value={opt.value}>
{opt.label}
</MenuItem>
))}
</Select>
{(error ?? helperText) && <FormHelperText>{error ?? helperText}</FormHelperText>}
</FormControl>
)
}
+52
View File
@@ -0,0 +1,52 @@
import { TextField } from '@mui/material'
import type { SxProps, Theme } from '@mui/material'
interface TextInputProps {
label: string
value: string
onChange: (value: string) => void
error?: string
helperText?: string
placeholder?: string
required?: boolean
disabled?: boolean
multiline?: boolean
rows?: number
size?: 'small' | 'medium'
fullWidth?: boolean
sx?: SxProps<Theme>
}
export function TextInput({
label,
value,
onChange,
error,
helperText,
placeholder,
required,
disabled,
multiline,
rows,
size = 'small',
fullWidth = true,
sx,
}: TextInputProps) {
return (
<TextField
label={label}
value={value}
onChange={e => onChange(e.target.value)}
error={!!error}
helperText={error ?? helperText}
placeholder={placeholder}
required={required}
disabled={disabled}
multiline={multiline}
rows={rows}
size={size}
fullWidth={fullWidth}
sx={sx}
/>
)
}
+4
View File
@@ -0,0 +1,4 @@
export { TextInput } from './TextInput'
export { SelectField } from './SelectField'
export { PriorityChipGroup } from './PriorityChipGroup'
export { FieldWithConfidence } from './FieldWithConfidence'