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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { TextInput } from './TextInput'
|
||||
export { SelectField } from './SelectField'
|
||||
export { PriorityChipGroup } from './PriorityChipGroup'
|
||||
export { FieldWithConfidence } from './FieldWithConfidence'
|
||||
Reference in New Issue
Block a user