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
+54
View File
@@ -0,0 +1,54 @@
import { Paper, Table, TableBody, TableContainer } from '@mui/material'
import type { SxProps, Theme } from '@mui/material'
import type { ReactNode } from 'react'
import { LoadingRows } from './LoadingRows'
import { EmptyTableState } from './EmptyTableState'
interface DataTableShellProps {
toolbar?: ReactNode
children: ReactNode
loading?: boolean
loadingRows?: number
loadingCols?: number
empty?: boolean
emptySlot?: ReactNode
emptyColSpan?: number
stickyHeader?: boolean
size?: 'small' | 'medium'
sx?: SxProps<Theme>
}
export function DataTableShell({
toolbar,
children,
loading = false,
loadingRows = 5,
loadingCols = 6,
empty = false,
emptySlot,
emptyColSpan = 6,
stickyHeader = false,
size = 'small',
sx,
}: DataTableShellProps) {
return (
<Paper variant="outlined" sx={{ borderRadius: 1.5, overflow: 'hidden', ...sx }}>
{toolbar}
<TableContainer>
<Table size={size} stickyHeader={stickyHeader}>
{!loading && children}
{loading && (
<TableBody>
<LoadingRows rows={loadingRows} cols={loadingCols} />
</TableBody>
)}
{!loading && empty && (
<TableBody>
{emptySlot ?? <EmptyTableState colSpan={emptyColSpan} />}
</TableBody>
)}
</Table>
</TableContainer>
</Paper>
)
}
+38
View File
@@ -0,0 +1,38 @@
import { Box, TableCell, TableRow, Typography } from '@mui/material'
import { Inbox } from 'lucide-react'
interface EmptyTableStateProps {
colSpan: number
title?: string
description?: string
}
export function EmptyTableState({ colSpan, title, description }: EmptyTableStateProps) {
return (
<TableRow>
<TableCell colSpan={colSpan} sx={{ border: 'none' }}>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 1,
py: 5,
color: 'text.disabled',
}}
>
<Inbox size={32} color="#cbd5e1" />
<Typography sx={{ fontWeight: 500, fontSize: '0.875rem', color: 'text.secondary' }}>
{title ?? 'Keine Einträge gefunden'}
</Typography>
{description && (
<Typography sx={{ fontSize: '0.8125rem', color: 'text.disabled', textAlign: 'center', maxWidth: 360 }}>
{description}
</Typography>
)}
</Box>
</TableCell>
</TableRow>
)
}
+50
View File
@@ -0,0 +1,50 @@
import { Box, Chip, Typography } from '@mui/material'
import type { SxProps, Theme } from '@mui/material'
interface ActiveFilter {
key: string
label: string
onRemove: () => void
}
interface FilterBarProps {
filters: ActiveFilter[]
onClearAll?: () => void
sx?: SxProps<Theme>
}
export function FilterBar({ filters, onClearAll, sx }: FilterBarProps) {
if (filters.length === 0) return null
return (
<Box sx={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 0.5, ...sx }}>
{filters.map(f => (
<Chip
key={f.key}
label={f.label}
size="small"
onDelete={f.onRemove}
sx={{ fontSize: '0.75rem', height: 24 }}
/>
))}
{onClearAll && filters.length > 1 && (
<Typography
component="button"
onClick={onClearAll}
sx={{
fontSize: '0.75rem',
color: 'text.secondary',
cursor: 'pointer',
background: 'none',
border: 'none',
p: 0,
ml: 0.5,
'&:hover': { color: 'primary.main', textDecoration: 'underline' },
}}
>
Alle löschen
</Typography>
)}
</Box>
)
}
+22
View File
@@ -0,0 +1,22 @@
import { Skeleton, TableCell, TableRow } from '@mui/material'
interface LoadingRowsProps {
rows?: number
cols?: number
}
export function LoadingRows({ rows = 5, cols = 6 }: LoadingRowsProps) {
return (
<>
{Array.from({ length: rows }).map((_, ri) => (
<TableRow key={ri}>
{Array.from({ length: cols }).map((_, ci) => (
<TableCell key={ci}>
<Skeleton variant="text" height={18} width={ci === 0 ? '80%' : ci === cols - 1 ? '40%' : '65%'} />
</TableCell>
))}
</TableRow>
))}
</>
)
}
+51
View File
@@ -0,0 +1,51 @@
import { Box, Chip, Typography } from '@mui/material'
import type { SxProps, Theme } from '@mui/material'
import type { ReactNode } from 'react'
interface TableToolbarProps {
title?: string
count?: number
filterSlot?: ReactNode
actions?: ReactNode
sx?: SxProps<Theme>
}
export function TableToolbar({ title, count, filterSlot, actions, sx }: TableToolbarProps) {
return (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 2,
py: 1.25,
borderBottom: '1px solid',
borderColor: 'divider',
gap: 1,
flexWrap: 'wrap',
...sx,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap', flex: 1 }}>
{title && (
<Typography sx={{ fontWeight: 600, fontSize: '0.875rem' }}>
{title}
</Typography>
)}
{count !== undefined && (
<Chip
label={count}
size="small"
sx={{ height: 20, fontSize: '0.7rem', bgcolor: 'rgba(30,58,95,0.08)', color: '#1e3a5f' }}
/>
)}
{filterSlot}
</Box>
{actions && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexShrink: 0 }}>
{actions}
</Box>
)}
</Box>
)
}
+5
View File
@@ -0,0 +1,5 @@
export { DataTableShell } from './DataTableShell'
export { TableToolbar } from './TableToolbar'
export { FilterBar } from './FilterBar'
export { EmptyTableState } from './EmptyTableState'
export { LoadingRows } from './LoadingRows'