feat(frontend): visual design overhaul — Inter font, softer cards/shadows, red-themed profile banner, modern typography hierarchy, and refreshed color palette

This commit is contained in:
Matthias Hochmeister
2026-04-13 11:07:28 +02:00
parent 43ce1f930c
commit f4690cf185
7 changed files with 186 additions and 86 deletions

View File

@@ -4,6 +4,9 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<title>Feuerwehr Dashboard</title> <title>Feuerwehr Dashboard</title>
</head> </head>
<body> <body>

View File

@@ -23,28 +23,34 @@ const UserProfile: React.FC<UserProfileProps> = ({ user }) => {
<Paper <Paper
elevation={0} elevation={0}
sx={{ sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', background: 'linear-gradient(135deg, #d32f2f 0%, #9a0007 100%)',
color: 'white', color: 'white',
borderRadius: 2, borderRadius: 3,
px: 3, px: 3.5,
py: 1.5, py: 2,
}} }}
> >
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Avatar <Avatar
sx={{ sx={{
width: 40, width: 44,
height: 40, height: 44,
bgcolor: 'rgba(255,255,255,0.2)', bgcolor: 'rgba(255,255,255,0.2)',
fontSize: '1rem', fontSize: '1rem',
fontWeight: 'bold', fontWeight: 'bold',
border: '2px solid rgba(255,255,255,0.3)',
}} }}
> >
{initials.toUpperCase()} {initials.toUpperCase()}
</Avatar> </Avatar>
<Typography variant="h6" sx={{ fontWeight: 500 }}> <Box>
<Typography variant="h6" sx={{ fontWeight: 600, lineHeight: 1.3 }}>
{getGreeting()}, {firstName}! {getGreeting()}, {firstName}!
</Typography> </Typography>
<Typography variant="body2" sx={{ opacity: 0.8, fontSize: '0.75rem' }}>
{new Date().toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long' })}
</Typography>
</Box>
</Box> </Box>
</Paper> </Paper>
); );

View File

@@ -17,24 +17,28 @@ function WidgetGroup({ title, children, gridColumn }: WidgetGroupProps) {
<Box <Box
sx={{ sx={{
position: 'relative', position: 'relative',
border: '1px solid', borderRadius: 2,
borderColor: 'divider', p: 2.5,
borderRadius: 1, pt: 3.5,
p: 2,
pt: 3,
gridColumn, gridColumn,
bgcolor: 'rgba(0, 0, 0, 0.02)',
border: '1px solid rgba(0, 0, 0, 0.04)',
}} }}
> >
<Typography <Typography
sx={{ sx={{
position: 'absolute', position: 'absolute',
top: -10, top: -9,
left: 16, left: 20,
px: 1, px: 1.5,
py: 0.25,
backgroundColor: 'background.default', backgroundColor: 'background.default',
fontSize: '0.75rem', fontSize: '0.6875rem',
color: 'text.secondary', color: 'text.secondary',
fontWeight: 500, fontWeight: 600,
letterSpacing: '0.06em',
textTransform: 'uppercase',
borderRadius: 1,
}} }}
> >
{title} {title}

View File

@@ -129,8 +129,8 @@ export function DataTable<T>({
return ( return (
<Paper> <Paper>
{showToolbar && ( {showToolbar && (
<Box sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 2 }}> <Box sx={{ px: 2.5, py: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 2 }}>
{title && <Typography variant="h6">{title}</Typography>} {title && <Typography variant="h6" fontWeight={700}>{title}</Typography>}
<Box sx={{ flex: 1 }} /> <Box sx={{ flex: 1 }} />
{searchEnabled && ( {searchEnabled && (
<TextField <TextField
@@ -138,11 +138,11 @@ export function DataTable<T>({
placeholder={searchPlaceholder} placeholder={searchPlaceholder}
value={search} value={search}
onChange={(e) => { setSearch(e.target.value); setPage(0); }} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
sx={{ maxWidth: 300 }} sx={{ maxWidth: 280, '& .MuiOutlinedInput-root': { bgcolor: 'rgba(0,0,0,0.02)' } }}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
<SearchIcon fontSize="small" /> <SearchIcon fontSize="small" sx={{ color: 'text.disabled' }} />
</InputAdornment> </InputAdornment>
), ),
}} }}

View File

@@ -30,29 +30,32 @@ export const StatCard: React.FC<StatCardProps> = ({
) : ( ) : (
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box sx={{ flex: GOLDEN_RATIO }}> <Box sx={{ flex: GOLDEN_RATIO }}>
<Typography variant="caption" textTransform="uppercase" color="text.secondary"> <Typography variant="caption" textTransform="uppercase" color="text.secondary" sx={{ letterSpacing: '0.06em', fontWeight: 600 }}>
{title} {title}
</Typography> </Typography>
<Typography variant="h4" fontWeight={700}> <Typography variant="h3" fontWeight={800} sx={{ lineHeight: 1.1, mt: 0.5, letterSpacing: '-0.02em' }}>
{value} {value}
</Typography> </Typography>
{trend && ( {trend && (
<Box sx={{ display: 'inline-flex', alignItems: 'center', mt: 1, px: 1, py: 0.25, borderRadius: 1, bgcolor: trend.value >= 0 ? 'rgba(34,197,94,0.1)' : 'rgba(239,68,68,0.1)' }}>
<Typography <Typography
variant="caption" variant="caption"
fontWeight={600}
color={trend.value >= 0 ? 'success.main' : 'error.main'} color={trend.value >= 0 ? 'success.main' : 'error.main'}
> >
{trend.value >= 0 ? '↑' : '↓'} {Math.abs(trend.value)}% {trend.value >= 0 ? '↑' : '↓'} {Math.abs(trend.value)}%
{trend.label && ` ${trend.label}`} {trend.label && ` ${trend.label}`}
</Typography> </Typography>
</Box>
)} )}
</Box> </Box>
<Box sx={{ flex: 1, display: 'flex', justifyContent: 'center' }}> <Box sx={{ flex: 1, display: 'flex', justifyContent: 'center' }}>
<Box <Box
sx={{ sx={{
width: 56, width: 52,
height: 56, height: 52,
borderRadius: '50%', borderRadius: 3,
bgcolor: `${color}15`, bgcolor: `${color}12`,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',

View File

@@ -4,7 +4,6 @@ import {
Card, Card,
CardActionArea, CardActionArea,
CardContent, CardContent,
Divider,
Skeleton, Skeleton,
Typography, Typography,
Alert, Alert,
@@ -52,16 +51,24 @@ export const WidgetCard: React.FC<WidgetCardProps> = ({
<Box <Box
sx={noPadding ? { px: 2.5, pt: 2.5 } : undefined} sx={noPadding ? { px: 2.5, pt: 2.5 } : undefined}
> >
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}> <Box
<Box display="flex" alignItems="center" gap={1}> display="flex"
alignItems="center"
justifyContent="space-between"
mb={1.5}
>
<Box display="flex" alignItems="center" gap={0.75}>
{icon && (
<Box sx={{ color: 'text.secondary', display: 'flex', alignItems: 'center', '& > *': { fontSize: '1.1rem' } }}>
{icon} {icon}
<Typography variant="subtitle1" fontWeight={600}> </Box>
)}
<Typography variant="subtitle2" fontWeight={600} color="text.secondary" sx={{ textTransform: 'uppercase', fontSize: '0.6875rem', letterSpacing: '0.06em' }}>
{title} {title}
</Typography> </Typography>
</Box> </Box>
{action} {action}
</Box> </Box>
<Divider sx={{ mb: 2 }} />
</Box> </Box>
); );
@@ -113,8 +120,7 @@ export const WidgetCard: React.FC<WidgetCardProps> = ({
</Box> </Box>
{footer && ( {footer && (
<> <>
<Divider sx={{ mt: 2 }} /> <Box sx={{ mt: 2, pt: 1.5, borderTop: '1px solid', borderColor: 'divider', ...(noPadding ? { mx: 2.5, pb: 2.5 } : {}) }}>
<Box sx={{ pt: 1.5, ...(noPadding ? { px: 2.5, pb: 2.5 } : {}) }}>
{footer} {footer}
</Box> </Box>
</> </>

View File

@@ -24,24 +24,26 @@ const lightThemeOptions: ThemeOptions = {
primary: primaryRed, primary: primaryRed,
secondary: secondaryBlue, secondary: secondaryBlue,
background: { background: {
default: '#f5f5f5', default: '#f0f2f5',
paper: '#ffffff', paper: '#ffffff',
}, },
divider: 'rgba(0, 0, 0, 0.08)',
error: { error: {
main: '#f44336', main: '#ef4444',
}, },
warning: { warning: {
main: '#ff9800', main: '#f59e0b',
}, },
info: { info: {
main: '#2196f3', main: '#3b82f6',
}, },
success: { success: {
main: '#4caf50', main: '#22c55e',
}, },
}, },
typography: { typography: {
fontFamily: [ fontFamily: [
'Inter',
'-apple-system', '-apple-system',
'BlinkMacSystemFont', 'BlinkMacSystemFont',
'"Segoe UI"', '"Segoe UI"',
@@ -51,28 +53,30 @@ const lightThemeOptions: ThemeOptions = {
'sans-serif', 'sans-serif',
].join(','), ].join(','),
h1: { h1: {
fontSize: '2.5rem', fontSize: '2.25rem',
fontWeight: 600, fontWeight: 700,
lineHeight: 1.2, lineHeight: 1.2,
letterSpacing: '-0.02em',
}, },
h2: { h2: {
fontSize: '2rem', fontSize: '1.875rem',
fontWeight: 600, fontWeight: 700,
lineHeight: 1.3, lineHeight: 1.3,
letterSpacing: '-0.01em',
}, },
h3: { h3: {
fontSize: '1.75rem', fontSize: '1.5rem',
fontWeight: 600, fontWeight: 700,
lineHeight: 1.4, lineHeight: 1.4,
}, },
h4: { h4: {
fontSize: '1.5rem', fontSize: '1.25rem',
fontWeight: 600, fontWeight: 700,
lineHeight: 1.4, lineHeight: 1.4,
}, },
h5: { h5: {
fontSize: '1.25rem', fontSize: '1.125rem',
fontWeight: 600, fontWeight: 700,
lineHeight: 1.5, lineHeight: 1.5,
}, },
h6: { h6: {
@@ -80,34 +84,61 @@ const lightThemeOptions: ThemeOptions = {
fontWeight: 600, fontWeight: 600,
lineHeight: 1.6, lineHeight: 1.6,
}, },
body1: { subtitle1: {
fontSize: '1rem', fontSize: '0.9375rem',
fontWeight: 600,
lineHeight: 1.5, lineHeight: 1.5,
}, },
body1: {
fontSize: '0.9375rem',
lineHeight: 1.6,
},
body2: { body2: {
fontSize: '0.875rem', fontSize: '0.8125rem',
lineHeight: 1.43, lineHeight: 1.5,
},
caption: {
fontSize: '0.6875rem',
fontWeight: 500,
lineHeight: 1.5,
letterSpacing: '0.04em',
}, },
button: { button: {
textTransform: 'none', textTransform: 'none',
fontWeight: 500, fontWeight: 600,
fontSize: '0.8125rem',
}, },
}, },
spacing: 8, spacing: 8,
shape: { shape: {
borderRadius: 8, borderRadius: 10,
}, },
components: { components: {
MuiCssBaseline: {
styleOverrides: {
body: {
WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
},
},
},
MuiButton: { MuiButton: {
styleOverrides: { styleOverrides: {
root: { root: {
borderRadius: 8, borderRadius: 8,
padding: '8px 16px', padding: '8px 18px',
fontWeight: 600,
}, },
contained: { contained: {
boxShadow: 'none', boxShadow: 'none',
'&:hover': { '&:hover': {
boxShadow: '0 2px 4px rgba(0,0,0,0.2)', boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
},
},
outlined: {
borderWidth: 1.5,
'&:hover': {
borderWidth: 1.5,
}, },
}, },
}, },
@@ -115,11 +146,13 @@ const lightThemeOptions: ThemeOptions = {
MuiCard: { MuiCard: {
styleOverrides: { styleOverrides: {
root: { root: {
borderRadius: 12, borderRadius: 14,
boxShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)', border: '1px solid rgba(0, 0, 0, 0.06)',
transition: 'all 0.3s cubic-bezier(.25,.8,.25,1)', boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
transition: 'border-color 0.2s ease, box-shadow 0.2s ease',
'&:hover': { '&:hover': {
boxShadow: '0 4px 8px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)', borderColor: 'rgba(0, 0, 0, 0.12)',
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
}, },
}, },
}, },
@@ -127,23 +160,39 @@ const lightThemeOptions: ThemeOptions = {
MuiPaper: { MuiPaper: {
styleOverrides: { styleOverrides: {
root: { root: {
borderRadius: 8, borderRadius: 10,
},
elevation0: {
boxShadow: 'none',
}, },
elevation1: { elevation1: {
boxShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)', border: '1px solid rgba(0, 0, 0, 0.06)',
boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
}, },
elevation2: { elevation2: {
boxShadow: '0 3px 6px rgba(0,0,0,0.15), 0 2px 4px rgba(0,0,0,0.12)', boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
}, },
elevation3: { elevation3: {
boxShadow: '0 4px 8px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)', boxShadow: '0 4px 16px rgba(0,0,0,0.1)',
}, },
}, },
}, },
MuiAppBar: { MuiAppBar: {
styleOverrides: { styleOverrides: {
root: { root: {
boxShadow: '0 2px 4px rgba(0,0,0,0.1)', boxShadow: '0 1px 0 rgba(0,0,0,0.06)',
},
},
},
MuiChip: {
styleOverrides: {
root: {
fontWeight: 500,
borderRadius: 6,
},
sizeSmall: {
height: 22,
fontSize: '0.6875rem',
}, },
}, },
}, },
@@ -157,6 +206,10 @@ const lightThemeOptions: ThemeOptions = {
MuiOutlinedInput: { MuiOutlinedInput: {
styleOverrides: { styleOverrides: {
root: { root: {
borderRadius: 8,
'& .MuiOutlinedInput-notchedOutline': {
borderColor: 'rgba(0, 0, 0, 0.12)',
},
'& .MuiOutlinedInput-notchedOutline legend': { '& .MuiOutlinedInput-notchedOutline legend': {
fontSize: '0.75em', fontSize: '0.75em',
}, },
@@ -168,9 +221,11 @@ const lightThemeOptions: ThemeOptions = {
root: { root: {
'& .MuiTableCell-head': { '& .MuiTableCell-head': {
textTransform: 'uppercase', textTransform: 'uppercase',
fontSize: '0.75rem', fontSize: '0.6875rem',
fontWeight: 600, fontWeight: 600,
letterSpacing: '0.05em', letterSpacing: '0.06em',
color: 'rgba(0, 0, 0, 0.5)',
borderBottom: '2px solid rgba(0, 0, 0, 0.06)',
}, },
}, },
}, },
@@ -179,15 +234,37 @@ const lightThemeOptions: ThemeOptions = {
styleOverrides: { styleOverrides: {
root: { root: {
'&.MuiTableRow-hover:hover': { '&.MuiTableRow-hover:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.04)', backgroundColor: 'rgba(0, 0, 0, 0.02)',
}, },
}, },
}, },
}, },
MuiTableCell: {
styleOverrides: {
root: {
borderBottom: '1px solid rgba(0, 0, 0, 0.04)',
},
},
},
MuiDialog: { MuiDialog: {
styleOverrides: { styleOverrides: {
paper: { paper: {
borderRadius: 12, borderRadius: 14,
},
},
},
MuiDivider: {
styleOverrides: {
root: {
borderColor: 'rgba(0, 0, 0, 0.06)',
},
},
},
MuiTooltip: {
styleOverrides: {
tooltip: {
borderRadius: 6,
fontSize: '0.75rem',
}, },
}, },
}, },
@@ -201,20 +278,21 @@ const darkThemeOptions: ThemeOptions = {
primary: primaryRed, primary: primaryRed,
secondary: secondaryBlue, secondary: secondaryBlue,
background: { background: {
default: '#121212', default: '#0f1117',
paper: '#1e1e1e', paper: '#1a1d27',
}, },
divider: 'rgba(255, 255, 255, 0.08)',
error: { error: {
main: '#f44336', main: '#ef4444',
}, },
warning: { warning: {
main: '#ff9800', main: '#f59e0b',
}, },
info: { info: {
main: '#2196f3', main: '#3b82f6',
}, },
success: { success: {
main: '#4caf50', main: '#22c55e',
}, },
}, },
}; };