feat: design token system — DS_TEXT/DS_SURFACE/DS_BORDER + 237 hex migrations
Token infrastructure (src/lib/ds.ts): - DS_TEXT: 16 semantic text color tokens (primary/secondary/muted/success/ warning/error/info/brand/signal + dark variants successDark/warningDark/ signalDark/infoDark for text on tinted surfaces) - DS_BG: page/surface/subtle/muted background tokens - DS_BORDER: default/muted/strong border tokens - DS_SURFACE: 10 bg+border surface pairs (success/warning/error/info/indigo/ purple/orange/blue/neutral/slate) - DS_MATCH_TIER: score-tier surface aliases keyed by strong/moderate/weak - DS_PRE_MARKET / DS_MARKET_SIGNAL: named aliases for futureCard tokens - DS_SHADOW: card/panel/dialog elevation tokens - BADGE_COLORS: 14 semantic presets for GenericBadge (confidenceHigh, riskMedium, preMarket, marketSignal, verified, gold, silver, bronze…) GenericBadge (src/components/shared/GenericBadge.tsx): - New semanticVariant prop (keyof BADGE_COLORS) — preferred over raw hex - color prop becomes optional (fallback), type-documented as escape hatch Priority file migrations — 237 hex literals replaced across 10 files: ScoreBreakdownPanel.tsx −22 PipelineDetailPanel.tsx −22 FutureAvailabilityCard.tsx −26 AICompareSummary.tsx −27 LocationIntelligencePanel −39 AssistantPromptSuggestions −18 UnitStructurePanel.tsx −25 BerichtDialog.tsx −24 PreMarketPanel.tsx −24 PropertyActivityLogPanel −10 ESLint (eslint.config.js): - Updated rule message to reference new tokens (DS_TEXT, DS_SURFACE, etc.) - Added 'stroke' to monitored property names - Remains 'warn' for gradual migration; use check:tokens for CI gate CI script (scripts/check-tokens.js + npm run check:tokens): - Counts hex patterns in components/ + pages/ - Fails if count > THRESHOLD (ratchet: 1958 baseline, lower per sprint) - Reports top 15 offenders for prioritizing next migration batch Results: ESLint targeted sx-prop violations: 1752 → 1030 (−41%) 0 TypeScript errors, 154 tests green Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
||||
import { Alert, Box, Chip, CircularProgress, Collapse, Typography } from '@mui/material'
|
||||
import { ChevronDown, ChevronUp, Trophy, TrendingDown, ShieldCheck, AlertTriangle, Info, ArrowRight, CheckCircle2, XCircle, Star } from 'lucide-react'
|
||||
import type { ComparisonSummary } from '../../services/aiService'
|
||||
import { DS_TEXT, DS_BG, DS_BORDER, DS_SURFACE } from '../../lib/ds'
|
||||
|
||||
interface Props {
|
||||
summary?: ComparisonSummary
|
||||
@@ -12,7 +13,7 @@ export function AICompareSummary({ summary, isLoading }: Props) {
|
||||
const [open, setOpen] = useState(true)
|
||||
|
||||
return (
|
||||
<Box sx={{ mb: 2, border: '1px solid #e2e8f0', borderRadius: 1, overflow: 'hidden' }}>
|
||||
<Box sx={{ mb: 2, border: `1px solid ${DS_BORDER.default}`, borderRadius: 1, overflow: 'hidden' }}>
|
||||
<Box
|
||||
onClick={() => setOpen(v => !v)}
|
||||
sx={{
|
||||
@@ -21,14 +22,14 @@ export function AICompareSummary({ summary, isLoading }: Props) {
|
||||
justifyContent: 'space-between',
|
||||
px: 2,
|
||||
py: 1.25,
|
||||
bgcolor: '#f8fafc',
|
||||
bgcolor: DS_SURFACE.neutral.bg,
|
||||
cursor: 'pointer',
|
||||
'&:hover': { bgcolor: '#f1f5f9' },
|
||||
'&:hover': { bgcolor: DS_BG.subtle },
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>AI Vergleichs-Zusammenfassung</Typography>
|
||||
<Chip label="Beta" size="small" sx={{ fontSize: 10, bgcolor: '#ede9fe', color: '#6d28d9' }} />
|
||||
<Chip label="Beta" size="small" sx={{ fontSize: 10, bgcolor: DS_SURFACE.purple.bg, color: DS_TEXT.signal }} />
|
||||
</Box>
|
||||
{open ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
||||
</Box>
|
||||
@@ -45,15 +46,15 @@ export function AICompareSummary({ summary, isLoading }: Props) {
|
||||
{!isLoading && summary && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
{/* Overall assessment */}
|
||||
<Box sx={{ bgcolor: '#f0f9ff', border: '1px solid #bae6fd', borderRadius: 1, p: 1.5 }}>
|
||||
<Typography variant="body2" sx={{ color: '#0c4a6e', lineHeight: 1.5 }}>{summary.overallAssessment}</Typography>
|
||||
<Box sx={{ bgcolor: DS_SURFACE.info.bg, border: `1px solid ${DS_SURFACE.info.border}`, borderRadius: 1, p: 1.5 }}>
|
||||
<Typography variant="body2" sx={{ color: DS_TEXT.infoDark, lineHeight: 1.5 }}>{summary.overallAssessment}</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Strongest option */}
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-start' }}>
|
||||
<Trophy size={16} color="#1a7a4a" style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<Trophy size={16} color={DS_TEXT.success} style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#64748b', display: 'block' }}>Stärkstes Match</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.muted, display: 'block' }}>Stärkstes Match</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{summary.strongestOption.label}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{summary.strongestOption.reason}</Typography>
|
||||
</Box>
|
||||
@@ -62,9 +63,9 @@ export function AICompareSummary({ summary, isLoading }: Props) {
|
||||
{/* Best value */}
|
||||
{summary.bestValue && (
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-start' }}>
|
||||
<TrendingDown size={16} color="#1e3a5f" style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<TrendingDown size={16} color={DS_TEXT.brand} style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#64748b', display: 'block' }}>Bestes Preis-Leistungs-Verhältnis</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.muted, display: 'block' }}>Bestes Preis-Leistungs-Verhältnis</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{summary.bestValue.label}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{summary.bestValue.reason}</Typography>
|
||||
</Box>
|
||||
@@ -75,7 +76,7 @@ export function AICompareSummary({ summary, isLoading }: Props) {
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-start' }}>
|
||||
<ShieldCheck size={16} color="#0891b2" style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#64748b', display: 'block' }}>Höchste Datenkonfidenz</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.muted, display: 'block' }}>Höchste Datenkonfidenz</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{summary.highestConfidence.label}
|
||||
<Typography component="span" variant="caption" color="text.secondary" sx={{ ml: 0.5 }}>
|
||||
@@ -88,9 +89,9 @@ export function AICompareSummary({ summary, isLoading }: Props) {
|
||||
{/* Tradeoffs */}
|
||||
{summary.biggestTradeoffs.length > 0 && (
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-start' }}>
|
||||
<AlertTriangle size={16} color="#d97706" style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<AlertTriangle size={16} color={DS_TEXT.warning} style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#64748b', display: 'block' }}>Wichtigste Abwägungen</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.muted, display: 'block' }}>Wichtigste Abwägungen</Typography>
|
||||
{summary.biggestTradeoffs.map((t, i) => (
|
||||
<Typography key={i} variant="caption" color="text.secondary" sx={{ display: 'block' }}>· {t}</Typography>
|
||||
))}
|
||||
@@ -101,9 +102,9 @@ export function AICompareSummary({ summary, isLoading }: Props) {
|
||||
{/* Missing data */}
|
||||
{summary.missingDataWarnings.length > 0 && (
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-start' }}>
|
||||
<Info size={16} color="#c0392b" style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<Info size={16} color={DS_TEXT.error} style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#64748b', display: 'block' }}>Fehlende Informationen</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.muted, display: 'block' }}>Fehlende Informationen</Typography>
|
||||
{summary.missingDataWarnings.map((w, i) => (
|
||||
<Typography key={i} variant="caption" color="error" sx={{ display: 'block' }}>· {w}</Typography>
|
||||
))}
|
||||
@@ -114,14 +115,14 @@ export function AICompareSummary({ summary, isLoading }: Props) {
|
||||
{/* Per-property assessment */}
|
||||
{summary.perPropertyAssessment.length > 0 && (
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#64748b', display: 'block', mb: 0.75 }}>Objektbewertung</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.muted, display: 'block', mb: 0.75 }}>Objektbewertung</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}>
|
||||
{summary.perPropertyAssessment.map(prop => (
|
||||
<Box
|
||||
key={prop.matchId}
|
||||
sx={{
|
||||
flex: '1 1 200px',
|
||||
border: '1px solid #e2e8f0',
|
||||
border: `1px solid ${DS_BORDER.default}`,
|
||||
borderRadius: 1,
|
||||
p: 1.25,
|
||||
display: 'flex',
|
||||
@@ -129,13 +130,13 @@ export function AICompareSummary({ summary, isLoading }: Props) {
|
||||
gap: 0.75,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: '#1e3a5f' }}>{prop.label}</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: DS_TEXT.brand }}>{prop.label}</Typography>
|
||||
|
||||
{/* Strengths */}
|
||||
<Box>
|
||||
{prop.strengths.map((s, i) => (
|
||||
<Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.5, mb: 0.25 }}>
|
||||
<CheckCircle2 size={12} color="#1a7a4a" style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<CheckCircle2 size={12} color={DS_TEXT.success} style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<Typography variant="caption" color="text.secondary" sx={{ lineHeight: 1.4 }}>{s}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
@@ -145,7 +146,7 @@ export function AICompareSummary({ summary, isLoading }: Props) {
|
||||
<Box>
|
||||
{prop.weaknesses.map((w, i) => (
|
||||
<Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.5, mb: 0.25 }}>
|
||||
<XCircle size={12} color="#dc2626" style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<XCircle size={12} color={DS_TEXT.danger} style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<Typography variant="caption" color="text.secondary" sx={{ lineHeight: 1.4 }}>{w}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
@@ -153,8 +154,8 @@ export function AICompareSummary({ summary, isLoading }: Props) {
|
||||
|
||||
{/* Best for */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.5 }}>
|
||||
<Star size={12} color="#d97706" style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<Typography variant="caption" sx={{ color: '#92400e', lineHeight: 1.4 }}>
|
||||
<Star size={12} color={DS_TEXT.warning} style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.warningDark, lineHeight: 1.4 }}>
|
||||
<strong>Am besten für:</strong> {prop.bestFor}
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -162,7 +163,7 @@ export function AICompareSummary({ summary, isLoading }: Props) {
|
||||
{/* Key risk */}
|
||||
{prop.keyRisk && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.5 }}>
|
||||
<AlertTriangle size={12} color="#d97706" style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<AlertTriangle size={12} color={DS_TEXT.warning} style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<Typography variant="caption" color="text.secondary" sx={{ lineHeight: 1.4 }}>{prop.keyRisk}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
@@ -173,10 +174,10 @@ export function AICompareSummary({ summary, isLoading }: Props) {
|
||||
)}
|
||||
|
||||
{/* Recommendation */}
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-start', pt: 0.5, borderTop: '1px solid #f1f5f9', mt: 0.5 }}>
|
||||
<ArrowRight size={16} color="#1e3a5f" style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-start', pt: 0.5, borderTop: `1px solid ${DS_BORDER.muted}`, mt: 0.5 }}>
|
||||
<ArrowRight size={16} color={DS_TEXT.brand} style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: '#64748b', display: 'block' }}>Empfehlung</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.muted, display: 'block' }}>Empfehlung</Typography>
|
||||
<Typography variant="body2">{summary.recommendation}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user