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
+33
View File
@@ -0,0 +1,33 @@
import { Box, Card, CardContent, Skeleton } from '@mui/material'
interface CardSkeletonProps {
lines?: number
hasHeader?: boolean
hasActions?: boolean
}
export function CardSkeleton({ lines = 3, hasHeader = true, hasActions = false }: CardSkeletonProps) {
return (
<Card>
<CardContent>
{hasHeader && (
<Box sx={{ mb: 1.5 }}>
<Skeleton variant="text" width="55%" height={22} />
<Skeleton variant="text" width="35%" height={16} />
</Box>
)}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}>
{Array.from({ length: lines }).map((_, i) => (
<Skeleton key={i} variant="text" width={i % 2 === 0 ? '100%' : '80%'} height={16} />
))}
</Box>
{hasActions && (
<Box sx={{ display: 'flex', gap: 1, mt: 2 }}>
<Skeleton variant="rectangular" width={80} height={32} sx={{ borderRadius: 1 }} />
<Skeleton variant="rectangular" width={80} height={32} sx={{ borderRadius: 1 }} />
</Box>
)}
</CardContent>
</Card>
)
}
+22
View File
@@ -0,0 +1,22 @@
import { Box, Skeleton } from '@mui/material'
interface PanelLoadingStateProps {
rows?: number
height?: number
}
export function PanelLoadingState({ rows = 4, height = 16 }: PanelLoadingStateProps) {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, p: 1 }}>
{Array.from({ length: rows }).map((_, i) => (
<Skeleton
key={i}
variant="rectangular"
width={i % 3 === 2 ? '60%' : i % 3 === 1 ? '85%' : '100%'}
height={height}
sx={{ borderRadius: 0.5 }}
/>
))}
</Box>
)
}
+37
View File
@@ -0,0 +1,37 @@
import { Box, Typography } from '@mui/material'
import { ShieldOff } from 'lucide-react'
import type { SxProps, Theme } from '@mui/material'
interface RestrictedStateProps {
message?: string
reason?: string
sx?: SxProps<Theme>
}
export function RestrictedState({ message, reason, sx }: RestrictedStateProps) {
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 1,
py: 4,
px: 2,
textAlign: 'center',
...sx,
}}
>
<ShieldOff size={28} color="#94a3b8" />
<Typography sx={{ fontWeight: 600, fontSize: '0.875rem', color: 'text.secondary' }}>
{message ?? 'Zugriff eingeschränkt'}
</Typography>
{reason && (
<Typography sx={{ fontSize: '0.75rem', color: 'text.disabled', maxWidth: 280 }}>
{reason}
</Typography>
)}
</Box>
)
}
+39
View File
@@ -0,0 +1,39 @@
import { Box, Button, Typography } from '@mui/material'
import { Lock } from 'lucide-react'
interface UnauthorizedStateProps {
message?: string
onLogin?: () => void
}
export function UnauthorizedState({ message, onLogin }: UnauthorizedStateProps) {
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 1.5,
py: 6,
px: 3,
textAlign: 'center',
}}
>
<Lock size={36} color="#94a3b8" />
<Typography sx={{ fontWeight: 600, fontSize: '1rem', color: 'text.primary' }}>
Nicht berechtigt
</Typography>
{message && (
<Typography sx={{ fontSize: '0.875rem', color: 'text.secondary', maxWidth: 360 }}>
{message}
</Typography>
)}
{onLogin && (
<Button variant="outlined" size="small" onClick={onLogin}>
Anmelden
</Button>
)}
</Box>
)
}
+4
View File
@@ -4,3 +4,7 @@ export { EmptyState } from './EmptyState'
export { ErrorState } from './ErrorState'
export { PageContainer } from './PageContainer'
export { SectionContainer } from './SectionContainer'
export { CardSkeleton } from './CardSkeleton'
export { PanelLoadingState } from './PanelLoadingState'
export { UnauthorizedState } from './UnauthorizedState'
export { RestrictedState } from './RestrictedState'