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:
Benjamin Sutter
2026-05-24 14:05:52 +02:00
parent e62391af66
commit 7934da7669
16 changed files with 445 additions and 243 deletions
+30 -29
View File
@@ -11,6 +11,7 @@ import { useMoveStage, useUpdateNotes, useLoseItem } from '../../hooks/usePipeli
import type { PipelineItem } from '../../domain/pipeline'
import { STAGES, NEXT_STAGE, MOCK_DOCS } from './pipelineConstants'
import { scoreColor, detailPath, getKiInsight } from './pipelineUtils'
import { DS_TEXT, DS_SURFACE, DS_BORDER, DS_PRE_MARKET, DS_COLORS } from '../../lib/ds'
// ── DetailPanel ───────────────────────────────────────────────────────────────
@@ -33,10 +34,10 @@ export function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: ()
<Box sx={{
width: { xs: '100%', md: 340 }, flexShrink: 0,
display: 'flex', flexDirection: 'column',
bgcolor: 'white', borderLeft: '1px solid #e2e8f0', overflow: 'hidden',
bgcolor: 'white', borderLeft: `1px solid ${DS_BORDER.default}`, overflow: 'hidden',
}}>
{/* Header */}
<Box sx={{ px: 2, py: 2, borderBottom: '1px solid #e2e8f0' }}>
<Box sx={{ px: 2, py: 2, borderBottom: `1px solid ${DS_BORDER.default}` }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 1 }}>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography variant="body1" sx={{ fontWeight: 700, lineHeight: 1.3, mb: 0.25 }}>{item.title}</Typography>
@@ -48,12 +49,12 @@ export function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: ()
</Typography>
{path && (
<Tooltip title="Vollständige Detailansicht öffnen">
<IconButton size="small" onClick={() => navigate(path)} sx={{ color: '#64748b', '&:hover': { color: '#1e3a5f' } }}>
<IconButton size="small" onClick={() => navigate(path)} sx={{ color: DS_TEXT.muted, '&:hover': { color: DS_TEXT.brand } }}>
<ExternalLink size={15} />
</IconButton>
</Tooltip>
)}
<IconButton size="small" onClick={onClose} sx={{ color: '#94a3b8' }}>
<IconButton size="small" onClick={onClose} sx={{ color: DS_TEXT.disabled }}>
<X size={16} />
</IconButton>
</Box>
@@ -61,7 +62,7 @@ export function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: ()
<Box sx={{ mt: 2 }}>
<Box sx={{ display: 'flex', gap: 0.25, mb: 1 }}>
{STAGES.slice(0, 5).map((s, idx) => (
<Box key={s.key} sx={{ flex: 1, height: 4, borderRadius: 2, bgcolor: idx <= progressIdx ? stageConfig.color : '#e2e8f0' }} />
<Box key={s.key} sx={{ flex: 1, height: 4, borderRadius: 2, bgcolor: idx <= progressIdx ? stageConfig.color : DS_BORDER.default }} />
))}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
@@ -74,10 +75,10 @@ export function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: ()
onClick={() => navigate(`/demand/anfragen?inquiry=${item.inquiryId}`)}
sx={{
height: 22, fontSize: '0.75rem', cursor: 'pointer',
bgcolor: '#eff6ff', color: '#1e3a5f', fontWeight: 600,
border: '1px solid #bfdbfe',
'& .MuiChip-icon': { color: '#1e3a5f' },
'&:hover': { bgcolor: '#dbeafe' },
bgcolor: DS_SURFACE.blue.bg, color: DS_TEXT.brand, fontWeight: 600,
border: `1px solid ${DS_SURFACE.blue.border}`,
'& .MuiChip-icon': { color: DS_TEXT.brand },
'&:hover': { bgcolor: DS_COLORS.futureCard.signal.badgeBg },
}}
/>
)}
@@ -87,7 +88,7 @@ export function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: ()
{/* Property / unit address */}
{item.propertyAddress && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mt: 1.25 }}>
<MapPin size={12} color="#64748b" />
<MapPin size={12} color={DS_TEXT.muted} />
<Typography variant="caption" color="text.secondary">{item.propertyAddress}</Typography>
</Box>
)}
@@ -97,24 +98,24 @@ export function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: ()
{/* KI */}
<Box sx={{ px: 2, pt: 2, pb: 1.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 1 }}>
<Sparkles size={13} color="#7c3aed" />
<Typography variant="caption" sx={{ fontWeight: 700, color: '#7c3aed', textTransform: 'uppercase', letterSpacing: 0.5, fontSize: '0.7rem' }}>
<Sparkles size={13} color={DS_PRE_MARKET.accent} />
<Typography variant="caption" sx={{ fontWeight: 700, color: DS_PRE_MARKET.accent, textTransform: 'uppercase', letterSpacing: 0.5, fontSize: '0.7rem' }}>
KI Einschätzung
</Typography>
</Box>
<Box sx={{ bgcolor: '#faf5ff', border: '1px solid #ddd6fe', borderRadius: 2, p: 1.5, mb: 1 }}>
<Typography variant="caption" sx={{ color: '#4c1d95', lineHeight: 1.6 }}>{ki.summary}</Typography>
<Box sx={{ bgcolor: DS_SURFACE.purple.bg, border: `1px solid ${DS_PRE_MARKET.border}`, borderRadius: 2, p: 1.5, mb: 1 }}>
<Typography variant="caption" sx={{ color: DS_TEXT.signalDark, lineHeight: 1.6 }}>{ki.summary}</Typography>
</Box>
{ki.positives.map((p, i) => (
<Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.75, mb: 0.375 }}>
<CheckCircle size={12} color="#1a7a4a" style={{ marginTop: 2, flexShrink: 0 }} />
<Typography variant="caption" sx={{ color: '#166534', lineHeight: 1.4 }}>{p}</Typography>
<CheckCircle size={12} color={DS_TEXT.success} style={{ marginTop: 2, flexShrink: 0 }} />
<Typography variant="caption" sx={{ color: DS_TEXT.successDark, lineHeight: 1.4 }}>{p}</Typography>
</Box>
))}
{ki.risks.map((r, i) => (
<Box key={i} sx={{ display: 'flex', alignItems: 'flex-start', gap: 0.75, mb: 0.375 }}>
<AlertTriangle size={12} color="#d97706" style={{ marginTop: 2, flexShrink: 0 }} />
<Typography variant="caption" sx={{ color: '#92400e', lineHeight: 1.4 }}>{r}</Typography>
<AlertTriangle size={12} color={DS_TEXT.warning} style={{ marginTop: 2, flexShrink: 0 }} />
<Typography variant="caption" sx={{ color: DS_TEXT.warningDark, lineHeight: 1.4 }}>{r}</Typography>
</Box>
))}
</Box>
@@ -124,7 +125,7 @@ export function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: ()
{/* Stage actions */}
{!isClosed && (
<Box sx={{ px: 2, py: 1.5 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: '#475569', textTransform: 'uppercase', letterSpacing: 0.5, fontSize: '0.7rem', display: 'block', mb: 1 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: DS_TEXT.secondary, textTransform: 'uppercase', letterSpacing: 0.5, fontSize: '0.7rem', display: 'block', mb: 1 }}>
Nächste Aktion
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
@@ -137,7 +138,7 @@ export function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: ()
</Button>
)}
<Button size="small" variant="outlined" onClick={() => loseItem(item.id)}
sx={{ color: '#c0392b', borderColor: '#c0392b', fontSize: '0.75rem', py: 0.5 }}>
sx={{ color: DS_TEXT.error, borderColor: DS_TEXT.error, fontSize: '0.75rem', py: 0.5 }}>
Ablehnen
</Button>
</Box>
@@ -149,8 +150,8 @@ export function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: ()
{/* Notes */}
<Box sx={{ px: 2, py: 1.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 1 }}>
<StickyNote size={13} color="#475569" />
<Typography variant="caption" sx={{ fontWeight: 700, color: '#475569', textTransform: 'uppercase', letterSpacing: 0.5, fontSize: '0.7rem' }}>
<StickyNote size={13} color={DS_TEXT.secondary} />
<Typography variant="caption" sx={{ fontWeight: 700, color: DS_TEXT.secondary, textTransform: 'uppercase', letterSpacing: 0.5, fontSize: '0.7rem' }}>
Notizen
</Typography>
</Box>
@@ -169,8 +170,8 @@ export function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: ()
{/* Documents */}
<Box sx={{ px: 2, py: 1.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 1 }}>
<FileText size={13} color="#475569" />
<Typography variant="caption" sx={{ fontWeight: 700, color: '#475569', textTransform: 'uppercase', letterSpacing: 0.5, fontSize: '0.7rem' }}>
<FileText size={13} color={DS_TEXT.secondary} />
<Typography variant="caption" sx={{ fontWeight: 700, color: DS_TEXT.secondary, textTransform: 'uppercase', letterSpacing: 0.5, fontSize: '0.7rem' }}>
Dokumente
</Typography>
</Box>
@@ -181,18 +182,18 @@ export function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: ()
{docs.map((doc, i) => (
<Box key={i} sx={{
display: 'flex', alignItems: 'center', gap: 1,
px: 1.5, py: 0.75, bgcolor: '#f8fafc',
borderRadius: 1.5, border: '1px solid #e2e8f0',
px: 1.5, py: 0.75, bgcolor: DS_SURFACE.neutral.bg,
borderRadius: 1.5, border: `1px solid ${DS_BORDER.default}`,
cursor: 'pointer', '&:hover': { bgcolor: '#f1f5f9' },
}}>
<FileText size={13} color="#475569" />
<Typography variant="caption" sx={{ flex: 1, color: '#1e293b' }} noWrap>{doc.name}</Typography>
<FileText size={13} color={DS_TEXT.secondary} />
<Typography variant="caption" sx={{ flex: 1, color: DS_TEXT.primary }} noWrap>{doc.name}</Typography>
<Typography variant="caption" color="text.secondary" sx={{ flexShrink: 0 }}>{doc.date}</Typography>
</Box>
))}
</Box>
)}
<Button size="small" variant="text" sx={{ mt: 0.75, color: '#1e3a5f', fontSize: '0.75rem', p: 0 }}>
<Button size="small" variant="text" sx={{ mt: 0.75, color: DS_TEXT.brand, fontSize: '0.75rem', p: 0 }}>
+ Dokument hochladen
</Button>
</Box>