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>
<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}
</Typography>
{badge}
@@ -1,5 +1,5 @@
import { Box, Button, Chip, Tooltip, Typography } from '@mui/material'
import { Building2, CheckCircle2 } from 'lucide-react'
import { Box, Button, Chip, Typography } from '@mui/material'
import { Building2, CheckCircle2, MapPin } from 'lucide-react'
import { useSessionStore } from '../../stores/sessionStore'
import { useInquiryStore } from '../../stores/inquiryStore'
import { LocationPreview } from '../shared/LocationPreview'
@@ -34,13 +34,8 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel, overl
return <FutureAvailabilityCard vm={vm} />
}
// secondary chips — max 2 visible
const chips: Array<{ label: string; color?: string }> = []
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)
// secondary chips — max 1 visible (availability only), no overflow chip
const secondaryChip = vm.availabilityLabel ?? vm.fitOutLabel ?? null
return (
<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)',
display: 'flex',
flexDirection: 'column',
transition: 'border-color 0.15s',
'&:hover': { borderColor: '#c8c7c4' },
position: 'relative',
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 ── */}
@@ -67,15 +69,29 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel, overl
border: `1px solid ${theme.border}`,
}}>
<Typography sx={{
fontFamily: '"DM Serif Display", serif',
fontWeight: 400, fontSize: '1.625rem',
color: '#ffffff', lineHeight: 1,
fontWeight: 700, fontSize: '1.625rem',
color: '#ffffff', lineHeight: 1, letterSpacing: '-0.02em',
}}>
{vm.matchScore}%
</Typography>
</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' }}>
<Chip label={rt.label} size="small" sx={{ bgcolor: rt.color, color: 'white', fontWeight: 600, fontSize: 10, height: 20 }} />
{vm.resultType === 'VERIFIED_PORTFOLIO' && isPortfolioOwner && (
@@ -116,19 +132,13 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel, overl
</Typography>
)}
{/* Confidence + secondary chips */}
{/* Confidence + one secondary chip max */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mt: 1, alignItems: 'center' }}>
<Chip label={`${confPct}% Konfidenz`} size="small"
sx={{ bgcolor: confidenceHex(vm.confidenceScore), color: 'white', fontSize: 10, height: 20 }} />
{visibleChips.map((c, i) => (
<Chip key={i} label={c.label} size="small" variant="outlined"
{secondaryChip && (
<Chip label={secondaryChip} size="small" variant="outlined"
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>
@@ -145,7 +155,7 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel, overl
{vm.reasons.length > 0 && (
<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 }}>
<CheckCircle2 size={13} color="#1d6342" style={{ marginTop: 2, flexShrink: 0 }} />
<Box>
+65 -83
View File
@@ -1,23 +1,21 @@
import { memo } from 'react'
import { Alert, Box, Button, Chip, Divider, Tooltip, Typography } from '@mui/material'
import { MapPin, Building2, CheckCircle2, AlertTriangle } from 'lucide-react'
import { Alert, Box, Button, Chip, Typography } from '@mui/material'
import { MapPin, AlertTriangle } from 'lucide-react'
import { useSessionStore } from '../../stores/sessionStore'
import { useInquiryStore } from '../../stores/inquiryStore'
import { HeatBadge } from '../shared/HeatBadge'
import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme'
import { RESULT_TYPE_META, DS_TEXT, DS_BORDER } from '../../lib/ds'
import { confidenceHex } from '../../lib/utils'
import { ScoreInlineBreakdown } from './ScoreInlineBreakdown'
import { MatchCardRestrictedState } from './MatchCardRestrictedState'
import type { MatchCardViewModel } from './MatchCardViewModel'
interface Props {
vm: MatchCardViewModel
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 />
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 extraChips: string[] = []
if (vm.availabilityLabel) extraChips.push(vm.availabilityLabel)
if (vm.fitOutLabel) extraChips.push(vm.fitOutLabel)
if (vm.isDivisible && vm.minDivisibleUnitSqm) extraChips.push(`Teilbar ab ${vm.minDivisibleUnitSqm} `)
if (topRisk) extraChips.push(topRisk.level === 'CRITICAL' ? 'Kritisch' : 'Hohes Risiko')
const visibleExtra = extraChips.slice(0, 2)
const hiddenExtra = extraChips.slice(2)
// Compact metric line — area · rent · availability
const metricParts: string[] = []
if (vm.areaSqm) metricParts.push(`${vm.areaSqm.toLocaleString('de-CH')}`)
if (vm.rentPerSqm) metricParts.push(`CHF ${vm.rentPerSqm}//J`)
if (vm.availabilityLabel) metricParts.push(vm.availabilityLabel)
const metricLine = metricParts.join(' · ')
// 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 (
<Box sx={{
display: 'flex',
mb: 1.5,
borderRadius: 2,
overflow: 'hidden',
border: `1px solid ${DS_BORDER.default}`,
@@ -51,12 +51,19 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o
bgcolor: '#ffffff',
boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
opacity: vm.isStaleData ? 0.75 : 1,
transition: 'border-color 0.15s',
'&:hover': { borderColor: '#c8c7c4' },
transition: 'border-color 0.15s, transform 0.18s ease, box-shadow 0.18s ease',
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 ── */}
<Box sx={{ width: 120, flexShrink: 0, position: 'relative', bgcolor: '#f4f3f0' }}>
{/* ── Image strip — 180px, Ginesta proportion ── */}
<Box sx={{ width: 180, flexShrink: 0, position: 'relative', bgcolor: '#f4f3f0' }}>
{imageUrl ? (
<img
src={imageUrl}
@@ -73,7 +80,7 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o
</Box>
)}
{/* Score badge */}
{/* Score badge — top left */}
<Box sx={{
position: 'absolute', top: 8, left: 8,
bgcolor: scoreBadgeBg, borderRadius: '6px',
@@ -81,58 +88,50 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o
border: `1px solid ${SCORE_THEME[tier].border}`,
}}>
<Typography sx={{
fontFamily: '"DM Serif Display", serif',
fontWeight: 400, fontSize: '1.125rem',
color: '#ffffff', lineHeight: 1,
fontWeight: 700, fontSize: '1.125rem',
color: '#ffffff', lineHeight: 1, letterSpacing: '-0.01em',
}}>
{vm.matchScore}%
</Typography>
</Box>
{overlayTypeLabel && (
{/* Location pill — Ginesta-style, top right */}
<Box sx={{
position: 'absolute', bottom: 0, left: 0, right: 0,
px: 0.75, py: 0.5,
background: 'linear-gradient(to top, rgba(0,0,0,0.55) 0%, transparent 100%)',
position: 'absolute', top: 8, right: 8,
bgcolor: 'rgba(255,255,255,0.92)',
borderRadius: '20px',
px: 1, py: 0.35,
display: 'flex', alignItems: 'center', gap: 0.4,
backdropFilter: 'blur(4px)',
}}>
<Typography sx={{ fontSize: '0.6rem', fontWeight: 600, color: 'white', lineHeight: 1.3 }}>
{overlayTypeLabel}
<MapPin size={9} color="#64748b" />
<Typography sx={{ fontSize: '0.65rem', fontWeight: 500, color: '#374151', lineHeight: 1 }}>
{vm.locationLabel}
</Typography>
</Box>
)}
</Box>
{/* ── 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 && (
<Alert severity="warning" sx={{ mb: 1, py: 0.25, fontSize: '0.72rem' }}>{vm.disclaimer}</Alert>
)}
{/* Badges row */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, alignItems: 'center', mb: 0.75 }}>
<HeatBadge propertyId={vm.propertyId} size="sm" />
{/* Chips — result type + heat + confidence */}
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center', mb: 0.875 }}>
<Chip label={rt.label} size="small"
sx={{ bgcolor: rt.color, color: 'white', fontWeight: 600, fontSize: 10, height: 20 }} />
{vm.resultType === 'VERIFIED_PORTFOLIO' && isPortfolioOwner && (
<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 } }} />
)}
<HeatBadge propertyId={vm.propertyId} size="sm" />
<Chip label={`${confPct}% Konfidenz`} size="small"
sx={{ bgcolor: confidenceHex(vm.confidenceScore), color: 'white', fontSize: 10, height: 20 }} />
{visibleExtra.map((label, i) => (
<Chip key={i} label={label} size="small" variant="outlined"
sx={{ fontSize: 10, height: 20, borderColor: DS_BORDER.default, color: DS_TEXT.secondary }} />
))}
{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>
{vm.resultType === 'VERIFIED_PORTFOLIO' && isPortfolioOwner && (
<Chip label="Ihr Objekt" size="small"
sx={{ bgcolor: 'rgba(21,38,66,0.10)', color: '#152642', fontWeight: 700, fontSize: 9, height: 18 }} />
)}
</Box>
{/* Title + location */}
{/* Title */}
<Typography variant="subtitle2" sx={{ fontWeight: 700, lineHeight: 1.3, color: DS_TEXT.primary }} noWrap>
{vm.title}
</Typography>
@@ -141,40 +140,25 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o
{vm.propertySubtitle}
</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 && (
<Typography variant="body2" sx={{ color: DS_TEXT.secondary, fontWeight: 500, mt: 0.75, lineHeight: 1.5,
overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }}>
<Typography variant="body2" sx={{
color: DS_TEXT.secondary, fontWeight: 500, mt: 0.875, lineHeight: 1.5,
overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 1, WebkitBoxOrient: 'vertical',
}}>
{vm.explainabilitySummary}
</Typography>
)}
{vm.reasons.length > 0 && (
<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} />
</>
)}
{/* Critical risk only */}
{topRisk && topRisk.level === 'CRITICAL' && (
<Alert icon={<AlertTriangle size={13} />} severity="error"
sx={{ mt: 0.75, py: 0.25, fontSize: '0.72rem' }}>
@@ -182,8 +166,8 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o
</Alert>
)}
{/* Actions */}
<Box sx={{ display: 'flex', gap: 0.625, mt: 'auto', pt: 1, flexWrap: 'wrap' }}>
{/* 2 Buttons — Anfrage + Details */}
<Box sx={{ display: 'flex', gap: 0.625, mt: 'auto', pt: 1.25 }}>
<Button
size="small"
variant="contained"
@@ -201,18 +185,16 @@ export const MatchCardCompact = memo(function MatchCardCompact({ vm, imageUrl, o
>
Anfrage
</Button>
{vm.actions.map(a => (
{detailAction && (
<Button
key={a.id}
size="small"
variant={a.variant === 'primary' ? 'contained' : 'outlined'}
onClick={a.onClick}
disabled={a.disabled}
variant="outlined"
onClick={detailAction.onClick}
sx={{ textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1 }}
>
{a.label}
Details
</Button>
))}
)}
</Box>
</Box>
</Box>