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:
@@ -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} m²` })
|
||||
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>
|
||||
|
||||
@@ -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} m²`)
|
||||
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')} m²`)
|
||||
if (vm.rentPerSqm) metricParts.push(`CHF ${vm.rentPerSqm}/m²/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>
|
||||
|
||||
Reference in New Issue
Block a user