refactor: replace hardcoded hex colors with design tokens across priority components

- Add DS_COLORS.heat, DS_COLORS.futureCard, INQUIRY_STATUS_META tokens to ds.ts
- Eliminate 4 duplicate RESULT_TYPE_META / TYPE_META constants (now all use ds.ts)
- Eliminate 3 duplicate scoreColor / SCORE_COLOR functions (now all use matchScoreHex)
- HeatBadge: use DS_COLORS.heat.VERY_HOT / HOT tokens
- FutureAvailabilityCard: use DS_COLORS.futureCard.controlled / signal tokens
- IntelligenceMatchCard: import RESULT_TYPE_META from ds.ts, remove primary override
- MatchScoreDisplay: replace local scoreColor() with matchScoreHex()
- CompareColumnHeader: remove local TYPE_META + SCORE_COLOR, use ds.ts + utils.ts
- CompareTableBody: import RESULT_TYPE_META + matchScoreHex, fix #7c3aed → token
- compareUtils: remove TYPE_META + SCORE_COLOR, use dataQualityHex in scoreBar
- Anfragen + AnfragenInquiryItem: deduplicate STATUS_CONFIG → INQUIRY_STATUS_META
- Anfragen: #1e3a5f → 'primary.main', KI alert → futureCard.controlled tokens
- AnfragenMessageBubble: own-message bubble → 'primary.main', AI bubble → tokens
- ScoreBreakdownPanel: #1e3a5f → 'primary.main', #6d28d9 → futureCard.controlled
- ESLint: add warn rule against new hex colors in component sx props

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 13:02:01 +02:00
parent c972392b78
commit 7f090a48ff
13 changed files with 130 additions and 112 deletions
@@ -4,6 +4,7 @@ import { useNavigate } from 'react-router'
import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme'
import { floorLabel, SOURCE_META, ASSET_TYPE_LABELS, getOpportunityHeadline } from './futureAvailabilityCardHelpers'
import { SignalQualityDots } from './SignalQualityDots'
import { DS_COLORS } from '../../lib/ds'
import type { MatchCardViewModel } from './MatchCardViewModel'
// ── Future Availability Card ──────────────────────────────────────────────────
@@ -12,13 +13,12 @@ export function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
const isControlled = vm.signalIsControlled ?? false
const navigate = useNavigate()
// PRE-MARKET VERIFIED: soft purple / institutional premium
// MARKET SIGNAL: slate blue / analytical
const accentColor = isControlled ? '#7c3aed' : '#1d4ed8'
const headerBg = isControlled ? '#faf5ff' : '#eff6ff'
const borderColor = isControlled ? '#e9d5ff' : '#bfdbfe'
const badgeBg = isControlled ? '#ede9fe' : '#dbeafe'
const badgeColor = isControlled ? '#5b21b6' : '#1e40af'
const cardColors = isControlled ? DS_COLORS.futureCard.controlled : DS_COLORS.futureCard.signal
const accentColor = cardColors.accent
const headerBg = cardColors.headerBg
const borderColor = cardColors.border
const badgeBg = cardColors.badgeBg
const badgeColor = cardColors.badgeText
const assetLabel = vm.assetType ? (ASSET_TYPE_LABELS[vm.assetType] ?? vm.assetType) : 'Fläche'
const headline = getOpportunityHeadline(vm.signalType, assetLabel)
@@ -168,7 +168,7 @@ export function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.35 }}>
{(vm.signalMarketIndicators ?? []).slice(0, 3).map((ind, i) => (
<Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.6 }}>
<Box sx={{ width: 4, height: 4, borderRadius: '50%', bgcolor: '#1d4ed8', mt: '5px', flexShrink: 0 }} />
<Box sx={{ width: 4, height: 4, borderRadius: '50%', bgcolor: DS_COLORS.futureCard.signal.indicator, mt: '5px', flexShrink: 0 }} />
<Typography sx={{ fontSize: '0.72rem', color: '#475569', lineHeight: 1.3 }}>{ind}</Typography>
</Box>
))}
@@ -210,7 +210,7 @@ export function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1,
...(a.variant === 'primary' && {
bgcolor: accentColor,
'&:hover': { bgcolor: isControlled ? '#6d28d9' : '#1e40af' },
'&:hover': { bgcolor: cardColors.accentHover },
}),
}}
>
@@ -224,7 +224,9 @@ export function FutureAvailabilityCard({ vm }: { vm: MatchCardViewModel }) {
onClick={() => navigate(`/demand/property/${vm.propertyId}${vm.unitId ? `?unit=${vm.unitId}` : ''}`)}
sx={{
textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1,
bgcolor: '#7c3aed', '&:hover': { bgcolor: '#6d28d9' }, ml: 'auto',
bgcolor: DS_COLORS.futureCard.controlled.accent,
'&:hover': { bgcolor: DS_COLORS.futureCard.controlled.accentHover },
ml: 'auto',
}}
>
Zur Einheit
@@ -7,18 +7,10 @@ import { useSessionStore } from '../../stores/sessionStore'
import { LocationPreview } from '../shared/LocationPreview'
import { HeatBadge } from '../shared/HeatBadge'
import { getScoreTier, SCORE_THEME } from '../shared/scoreTheme'
import { RESULT_TYPE_META } from '../../lib/ds'
import type { MatchCardViewModel } from './MatchCardViewModel'
import { FutureAvailabilityCard } from './FutureAvailabilityCard'
// ── Constants ─────────────────────────────────────────────────────────────────
const RESULT_TYPE_META: Record<string, { label: string; color: string }> = {
VERIFIED_PORTFOLIO: { label: 'Plattform', color: '#1e3a5f' },
EXTERNAL_MARKET: { label: 'Plattform', color: '#1e3a5f' },
MAISON_WORK: { label: 'Maison Work', color: '#0369a1' },
FUTURE_AVAILABILITY: { label: 'Future Availability', color: '#7c3aed' },
}
// ── Main component ────────────────────────────────────────────────────────────
interface Props {
@@ -163,10 +155,6 @@ export function IntelligenceMatchCard({ vm, imageUrl, lat, lng, cityLabel }: Pro
disabled={a.disabled}
sx={{
textTransform: 'none', fontSize: '0.7rem', py: 0.375, px: 1,
...(a.variant === 'primary' && {
bgcolor: '#1e3a5f',
'&:hover': { bgcolor: '#162d4a' },
}),
}}
>
{a.label}
@@ -1,22 +1,17 @@
import { Box, Typography } from '@mui/material'
import { matchScoreHex } from '../../lib/utils'
interface Props {
score: number
size?: 'sm' | 'md' | 'lg'
}
function scoreColor(score: number): string {
if (score >= 78) return '#1a7a4a'
if (score >= 52) return '#d97706'
return '#c0392b'
}
const FONT_SIZES: Record<string, string> = { sm: '1.25rem', md: '1.75rem', lg: '2.5rem' }
export function MatchScoreDisplay({ score, size = 'md' }: Props) {
return (
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.25, flexShrink: 0 }}>
<Typography sx={{ fontWeight: 800, fontSize: FONT_SIZES[size], color: scoreColor(score), lineHeight: 1 }}>
<Typography sx={{ fontWeight: 800, fontSize: FONT_SIZES[size], color: matchScoreHex(score), lineHeight: 1 }}>
{score}
</Typography>
<Typography variant="caption" color="text.secondary">/100</Typography>