fix(typography): remove DM Serif Display — all UI text now uses Inter

PageHeader titles, MatchCardCompact score badge, and IntelligenceMatchCard
score badge were the only 3 places using the serif override. All now use
the theme-defined Inter stack (fontWeight 600/700, letterSpacing tight).
PDF/print previews (BerichtDialog, LatentInquiryReportPreview) intentionally
keep their serif fonts — those are document renderers, not UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-06-10 23:53:15 +02:00
parent d945fb1ab6
commit 017835b083
3 changed files with 105 additions and 113 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ export function PageHeader({
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<Box> <Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="h6" sx={{ fontFamily: '"DM Serif Display", serif', fontWeight: 400, fontSize: '1.25rem', lineHeight: 1.3 }}> <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '1.25rem', lineHeight: 1.3 }}>
{title} {title}
</Typography> </Typography>
{badge} {badge}
@@ -1,5 +1,5 @@
import { Box, Button, Chip, Tooltip, Typography } from '@mui/material' import { Box, Button, Chip, Typography } from '@mui/material'
import { Building2, CheckCircle2 } from 'lucide-react' import { Building2, CheckCircle2, MapPin } from 'lucide-react'
import { useSessionStore } from '../../stores/sessionStore' import { useSessionStore } from '../../stores/sessionStore'
import { useInquiryStore } from '../../stores/inquiryStore' import { useInquiryStore } from '../../stores/inquiryStore'
import { LocationPreview } from '../shared/LocationPreview' import { LocationPreview } from '../shared/LocationPreview'
@@ -34,13 +34,8 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel, overl
return <FutureAvailabilityCard vm={vm} /> return <FutureAvailabilityCard vm={vm} />
} }
// secondary chips — max 2 visible // secondary chips — max 1 visible (availability only), no overflow chip
const chips: Array<{ label: string; color?: string }> = [] const secondaryChip = vm.availabilityLabel ?? vm.fitOutLabel ?? null
if (vm.availabilityLabel) chips.push({ label: vm.availabilityLabel })
if (vm.fitOutLabel) chips.push({ label: vm.fitOutLabel })
if (vm.isDivisible && vm.minDivisibleUnitSqm) chips.push({ label: `Teilbar ab ${vm.minDivisibleUnitSqm}` })
const visibleChips = chips.slice(0, 2)
const hiddenChips = chips.slice(2)
return ( return (
<Box sx={{ <Box sx={{
@@ -51,8 +46,15 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel, overl
boxShadow: '0 1px 3px rgba(0,0,0,0.06)', boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
transition: 'border-color 0.15s', position: 'relative',
'&:hover': { borderColor: '#c8c7c4' }, zIndex: 0,
transition: 'transform 0.18s ease, box-shadow 0.18s ease, border-color 0.15s',
'&:hover': {
borderColor: '#b0aead',
transform: 'scale(1.025)',
boxShadow: '0 10px 32px rgba(0,0,0,0.13)',
zIndex: 1,
},
}}> }}>
{/* ── Hero zone ── */} {/* ── Hero zone ── */}
@@ -67,15 +69,29 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel, overl
border: `1px solid ${theme.border}`, border: `1px solid ${theme.border}`,
}}> }}>
<Typography sx={{ <Typography sx={{
fontFamily: '"DM Serif Display", serif', fontWeight: 700, fontSize: '1.625rem',
fontWeight: 400, fontSize: '1.625rem', color: '#ffffff', lineHeight: 1, letterSpacing: '-0.02em',
color: '#ffffff', lineHeight: 1,
}}> }}>
{vm.matchScore}% {vm.matchScore}%
</Typography> </Typography>
</Box> </Box>
{/* Top-right badge strip */} {/* Location pill — Ginesta-style, top-left */}
<Box sx={{
position: 'absolute', top: 10, left: 10,
bgcolor: 'rgba(255,255,255,0.92)',
borderRadius: '20px',
px: 1, py: 0.35,
display: 'flex', alignItems: 'center', gap: 0.4,
backdropFilter: 'blur(4px)',
}}>
<MapPin size={9} color="#64748b" />
<Typography sx={{ fontSize: '0.65rem', fontWeight: 500, color: '#374151', lineHeight: 1 }}>
{vm.locationLabel}
</Typography>
</Box>
{/* Top-right badge strip — result type + owner + heat */}
<Box sx={{ position: 'absolute', top: 10, right: 10, display: 'flex', flexDirection: 'column', gap: 0.5, alignItems: 'flex-end' }}> <Box sx={{ position: 'absolute', top: 10, right: 10, display: 'flex', flexDirection: 'column', gap: 0.5, alignItems: 'flex-end' }}>
<Chip label={rt.label} size="small" sx={{ bgcolor: rt.color, color: 'white', fontWeight: 600, fontSize: 10, height: 20 }} /> <Chip label={rt.label} size="small" sx={{ bgcolor: rt.color, color: 'white', fontWeight: 600, fontSize: 10, height: 20 }} />
{vm.resultType === 'VERIFIED_PORTFOLIO' && isPortfolioOwner && ( {vm.resultType === 'VERIFIED_PORTFOLIO' && isPortfolioOwner && (
@@ -116,19 +132,13 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel, overl
</Typography> </Typography>
)} )}
{/* Confidence + secondary chips */} {/* Confidence + one secondary chip max */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mt: 1, alignItems: 'center' }}> <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mt: 1, alignItems: 'center' }}>
<Chip label={`${confPct}% Konfidenz`} size="small" <Chip label={`${confPct}% Konfidenz`} size="small"
sx={{ bgcolor: confidenceHex(vm.confidenceScore), color: 'white', fontSize: 10, height: 20 }} /> sx={{ bgcolor: confidenceHex(vm.confidenceScore), color: 'white', fontSize: 10, height: 20 }} />
{visibleChips.map((c, i) => ( {secondaryChip && (
<Chip key={i} label={c.label} size="small" variant="outlined" <Chip label={secondaryChip} size="small" variant="outlined"
sx={{ fontSize: 10, height: 20, borderColor: DS_BORDER.default, color: DS_TEXT.secondary }} /> sx={{ fontSize: 10, height: 20, borderColor: DS_BORDER.default, color: DS_TEXT.secondary }} />
))}
{hiddenChips.length > 0 && (
<Tooltip title={hiddenChips.map(c => c.label).join(' · ')} arrow>
<Chip label={`+${hiddenChips.length}`} size="small" variant="outlined"
sx={{ fontSize: 10, height: 20, borderColor: DS_BORDER.default, color: DS_TEXT.muted }} />
</Tooltip>
)} )}
</Box> </Box>
@@ -145,7 +155,7 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel, overl
{vm.reasons.length > 0 && ( {vm.reasons.length > 0 && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, mt: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, mt: 1 }}>
{vm.reasons.slice(0, 3).map((r, i) => ( {vm.reasons.slice(0, 2).map((r, i) => (
<Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.75 }}> <Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.75 }}>
<CheckCircle2 size={13} color="#1d6342" style={{ marginTop: 2, flexShrink: 0 }} /> <CheckCircle2 size={13} color="#1d6342" style={{ marginTop: 2, flexShrink: 0 }} />
<Box> <Box>
+69 -87
View File
@@ -1,23 +1,21 @@
import { memo } from 'react' import { memo } from 'react'
import { Alert, Box, Button, Chip, Divider, Tooltip, Typography } from '@mui/material' import { Alert, Box, Button, Chip, Typography } from '@mui/material'
import { MapPin, Building2, CheckCircle2, AlertTriangle } from 'lucide-react' import { MapPin, AlertTriangle } from 'lucide-react'
import { useSessionStore } from '../../stores/sessionStore' import { useSessionStore } from '../../stores/sessionStore'
import { useInquiryStore } from '../../stores/inquiryStore' import { useInquiryStore } from '../../stores/inquiryStore'
import { HeatBadge } from '../shared/HeatBadge' import { HeatBadge } from '../shared/HeatBadge'
import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme' import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme'
import { RESULT_TYPE_META, DS_TEXT, DS_BORDER } from '../../lib/ds' import { RESULT_TYPE_META, DS_TEXT, DS_BORDER } from '../../lib/ds'
import { confidenceHex } from '../../lib/utils' import { confidenceHex } from '../../lib/utils'
import { ScoreInlineBreakdown } from './ScoreInlineBreakdown'
import { MatchCardRestrictedState } from './MatchCardRestrictedState' import { MatchCardRestrictedState } from './MatchCardRestrictedState'
import type { MatchCardViewModel } from './MatchCardViewModel' import type { MatchCardViewModel } from './MatchCardViewModel'
interface Props { interface Props {
vm: MatchCardViewModel vm: MatchCardViewModel
imageUrl?: string imageUrl?: string
overlayTypeLabel?: string
} }
export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, overlayTypeLabel }: Props) { export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl }: Props) {
if (vm.isRestricted) return <MatchCardRestrictedState /> if (vm.isRestricted) return <MatchCardRestrictedState />
const { currentUser } = useSessionStore() const { currentUser } = useSessionStore()
@@ -32,18 +30,20 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o
const leftAccent = vm.isCompareSelected ? '#5b3f8a' : vm.isSelected ? '#152642' : 'transparent' const leftAccent = vm.isCompareSelected ? '#5b3f8a' : vm.isSelected ? '#152642' : 'transparent'
const extraChips: string[] = [] // Compact metric line — area · rent · availability
if (vm.availabilityLabel) extraChips.push(vm.availabilityLabel) const metricParts: string[] = []
if (vm.fitOutLabel) extraChips.push(vm.fitOutLabel) if (vm.areaSqm) metricParts.push(`${vm.areaSqm.toLocaleString('de-CH')}`)
if (vm.isDivisible && vm.minDivisibleUnitSqm) extraChips.push(`Teilbar ab ${vm.minDivisibleUnitSqm} `) if (vm.rentPerSqm) metricParts.push(`CHF ${vm.rentPerSqm}//J`)
if (topRisk) extraChips.push(topRisk.level === 'CRITICAL' ? 'Kritisch' : 'Hohes Risiko') if (vm.availabilityLabel) metricParts.push(vm.availabilityLabel)
const visibleExtra = extraChips.slice(0, 2) const metricLine = metricParts.join(' · ')
const hiddenExtra = extraChips.slice(2)
// Detail action — prefer the "Details →" action if available, fallback to first action
const detailAction = vm.actions.find(a => a.actionType === 'OPEN_DETAIL' && a.label.includes('Detail'))
?? vm.actions.find(a => a.actionType === 'OPEN_DETAIL')
return ( return (
<Box sx={{ <Box sx={{
display: 'flex', display: 'flex',
mb: 1.5,
borderRadius: 2, borderRadius: 2,
overflow: 'hidden', overflow: 'hidden',
border: `1px solid ${DS_BORDER.default}`, border: `1px solid ${DS_BORDER.default}`,
@@ -51,12 +51,19 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o
bgcolor: '#ffffff', bgcolor: '#ffffff',
boxShadow: '0 1px 3px rgba(0,0,0,0.06)', boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
opacity: vm.isStaleData ? 0.75 : 1, opacity: vm.isStaleData ? 0.75 : 1,
transition: 'border-color 0.15s', transition: 'border-color 0.15s, transform 0.18s ease, box-shadow 0.18s ease',
'&:hover': { borderColor: '#c8c7c4' }, position: 'relative',
zIndex: 0,
'&:hover': {
borderColor: '#b0aead',
transform: 'scale(1.012)',
boxShadow: '0 6px 20px rgba(0,0,0,0.10)',
zIndex: 1,
},
}}> }}>
{/* ── Image strip ── */} {/* ── Image strip — 180px, Ginesta proportion ── */}
<Box sx={{ width: 120, flexShrink: 0, position: 'relative', bgcolor: '#f4f3f0' }}> <Box sx={{ width: 180, flexShrink: 0, position: 'relative', bgcolor: '#f4f3f0' }}>
{imageUrl ? ( {imageUrl ? (
<img <img
src={imageUrl} src={imageUrl}
@@ -73,7 +80,7 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o
</Box> </Box>
)} )}
{/* Score badge */} {/* Score badge — top left */}
<Box sx={{ <Box sx={{
position: 'absolute', top: 8, left: 8, position: 'absolute', top: 8, left: 8,
bgcolor: scoreBadgeBg, borderRadius: '6px', bgcolor: scoreBadgeBg, borderRadius: '6px',
@@ -81,58 +88,50 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o
border: `1px solid ${SCORE_THEME[tier].border}`, border: `1px solid ${SCORE_THEME[tier].border}`,
}}> }}>
<Typography sx={{ <Typography sx={{
fontFamily: '"DM Serif Display", serif', fontWeight: 700, fontSize: '1.125rem',
fontWeight: 400, fontSize: '1.125rem', color: '#ffffff', lineHeight: 1, letterSpacing: '-0.01em',
color: '#ffffff', lineHeight: 1,
}}> }}>
{vm.matchScore}% {vm.matchScore}%
</Typography> </Typography>
</Box> </Box>
{overlayTypeLabel && ( {/* Location pill — Ginesta-style, top right */}
<Box sx={{ <Box sx={{
position: 'absolute', bottom: 0, left: 0, right: 0, position: 'absolute', top: 8, right: 8,
px: 0.75, py: 0.5, bgcolor: 'rgba(255,255,255,0.92)',
background: 'linear-gradient(to top, rgba(0,0,0,0.55) 0%, transparent 100%)', borderRadius: '20px',
}}> px: 1, py: 0.35,
<Typography sx={{ fontSize: '0.6rem', fontWeight: 600, color: 'white', lineHeight: 1.3 }}> display: 'flex', alignItems: 'center', gap: 0.4,
{overlayTypeLabel} backdropFilter: 'blur(4px)',
</Typography> }}>
</Box> <MapPin size={9} color="#64748b" />
)} <Typography sx={{ fontSize: '0.65rem', fontWeight: 500, color: '#374151', lineHeight: 1 }}>
{vm.locationLabel}
</Typography>
</Box>
</Box> </Box>
{/* ── Content ── */} {/* ── Content ── */}
<Box sx={{ flex: 1, p: 2, display: 'flex', flexDirection: 'column', minWidth: 0 }}> <Box sx={{ flex: 1, px: 2, py: 2.5, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
{vm.disclaimer && ( {vm.disclaimer && (
<Alert severity="warning" sx={{ mb: 1, py: 0.25, fontSize: '0.72rem' }}>{vm.disclaimer}</Alert> <Alert severity="warning" sx={{ mb: 1, py: 0.25, fontSize: '0.72rem' }}>{vm.disclaimer}</Alert>
)} )}
{/* Badges row */} {/* Chips — result type + heat + confidence */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, alignItems: 'center', mb: 0.75 }}> <Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center', mb: 0.875 }}>
<HeatBadge propertyId={vm.propertyId} size="sm" />
<Chip label={rt.label} size="small" <Chip label={rt.label} size="small"
sx={{ bgcolor: rt.color, color: 'white', fontWeight: 600, fontSize: 10, height: 20 }} /> sx={{ bgcolor: rt.color, color: 'white', fontWeight: 600, fontSize: 10, height: 20 }} />
{vm.resultType === 'VERIFIED_PORTFOLIO' && isPortfolioOwner && ( <HeatBadge propertyId={vm.propertyId} size="sm" />
<Chip icon={<Building2 size={9} color="#152642" />} label="Ihr Objekt" size="small"
sx={{ bgcolor: 'rgba(21,38,66,0.10)', color: '#152642', fontWeight: 700, fontSize: 9, height: 18, '& .MuiChip-icon': { ml: 0.5 } }} />
)}
<Chip label={`${confPct}% Konfidenz`} size="small" <Chip label={`${confPct}% Konfidenz`} size="small"
sx={{ bgcolor: confidenceHex(vm.confidenceScore), color: 'white', fontSize: 10, height: 20 }} /> sx={{ bgcolor: confidenceHex(vm.confidenceScore), color: 'white', fontSize: 10, height: 20 }} />
{visibleExtra.map((label, i) => ( {vm.resultType === 'VERIFIED_PORTFOLIO' && isPortfolioOwner && (
<Chip key={i} label={label} size="small" variant="outlined" <Chip label="Ihr Objekt" size="small"
sx={{ fontSize: 10, height: 20, borderColor: DS_BORDER.default, color: DS_TEXT.secondary }} /> sx={{ bgcolor: 'rgba(21,38,66,0.10)', color: '#152642', fontWeight: 700, fontSize: 9, height: 18 }} />
))}
{hiddenExtra.length > 0 && (
<Tooltip title={hiddenExtra.join(' · ')} arrow>
<Chip label={`+${hiddenExtra.length}`} size="small" variant="outlined"
sx={{ fontSize: 10, height: 20, borderColor: DS_BORDER.default, color: DS_TEXT.muted }} />
</Tooltip>
)} )}
</Box> </Box>
{/* Title + location */} {/* Title */}
<Typography variant="subtitle2" sx={{ fontWeight: 700, lineHeight: 1.3, color: DS_TEXT.primary }} noWrap> <Typography variant="subtitle2" sx={{ fontWeight: 700, lineHeight: 1.3, color: DS_TEXT.primary }} noWrap>
{vm.title} {vm.title}
</Typography> </Typography>
@@ -141,40 +140,25 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o
{vm.propertySubtitle} {vm.propertySubtitle}
</Typography> </Typography>
)} )}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.4, mt: 0.25 }}>
<MapPin size={11} color="#9a9a9a" />
<Typography variant="caption" sx={{ color: DS_TEXT.muted }}>
{vm.locationLabel}
</Typography>
</Box>
{/* Metric line — Ginesta-style */}
{metricLine && (
<Typography sx={{ fontSize: '0.75rem', color: '#94a3b8', mt: 0.5, letterSpacing: 0.1 }}>
{metricLine}
</Typography>
)}
{/* Explainability summary — 1 line */}
{vm.explainabilitySummary && ( {vm.explainabilitySummary && (
<Typography variant="body2" sx={{ color: DS_TEXT.secondary, fontWeight: 500, mt: 0.75, lineHeight: 1.5, <Typography variant="body2" sx={{
overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }}> color: DS_TEXT.secondary, fontWeight: 500, mt: 0.875, lineHeight: 1.5,
overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 1, WebkitBoxOrient: 'vertical',
}}>
{vm.explainabilitySummary} {vm.explainabilitySummary}
</Typography> </Typography>
)} )}
{vm.reasons.length > 0 && ( {/* Critical risk only */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.35, mt: 0.75 }}>
{vm.reasons.slice(0, 2).map((r, i) => (
<Box key={i} sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<CheckCircle2 size={11} color="#1d6342" style={{ flexShrink: 0 }} />
<Typography sx={{ fontSize: '0.72rem', color: DS_TEXT.secondary, lineHeight: 1.3 }}>
{r.label}
</Typography>
</Box>
))}
</Box>
)}
{vm.scoreBreakdown && vm.allFactors && vm.allFactors.length > 0 && (
<>
<Divider sx={{ my: 0.75 }} />
<ScoreInlineBreakdown scoreBreakdown={vm.scoreBreakdown} allFactors={vm.allFactors} />
</>
)}
{topRisk && topRisk.level === 'CRITICAL' && ( {topRisk && topRisk.level === 'CRITICAL' && (
<Alert icon={<AlertTriangle size={13} />} severity="error" <Alert icon={<AlertTriangle size={13} />} severity="error"
sx={{ mt: 0.75, py: 0.25, fontSize: '0.72rem' }}> sx={{ mt: 0.75, py: 0.25, fontSize: '0.72rem' }}>
@@ -182,8 +166,8 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o
</Alert> </Alert>
)} )}
{/* Actions */} {/* 2 Buttons — Anfrage + Details */}
<Box sx={{ display: 'flex', gap: 0.625, mt: 'auto', pt: 1, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', gap: 0.625, mt: 'auto', pt: 1.25 }}>
<Button <Button
size="small" size="small"
variant="contained" variant="contained"
@@ -201,18 +185,16 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o
> >
Anfrage Anfrage
</Button> </Button>
{vm.actions.map(a => ( {detailAction && (
<Button <Button
key={a.id}
size="small" size="small"
variant={a.variant === 'primary' ? 'contained' : 'outlined'} variant="outlined"
onClick={a.onClick} onClick={detailAction.onClick}
disabled={a.disabled}
sx={{ textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1 }} sx={{ textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1 }}
> >
{a.label} Details
</Button> </Button>
))} )}
</Box> </Box>
</Box> </Box>
</Box> </Box>