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
@@ -7,6 +7,7 @@ import { useNavigate } from 'react-router'
import { useProperties } from '../../hooks/useProperties'
import { getCityIntelligence } from '../../lib/locationIntelligence'
import type { Property } from '../../domain/property'
import { DS_TEXT, DS_SURFACE, DS_BORDER } from '../../lib/ds'
import { SoftFactorBar } from './SoftFactorBar'
import { KpiTile } from './KpiTile'
import { NEW_PROJECTS } from './locationIntelligenceConstants'
@@ -63,31 +64,31 @@ export function LocationIntelligencePanel({ property }: Props) {
label="Leerstandsquote"
value={`${intel.vacancyRatePct}%`}
sub={intel.vacancyRatePct < 3 ? 'Angespannter Markt' : intel.vacancyRatePct < 5 ? 'Ausgeglichen' : 'Käufermarkt'}
color={intel.vacancyRatePct < 3 ? '#c0392b' : intel.vacancyRatePct < 5 ? '#d97706' : '#1a7a4a'}
color={intel.vacancyRatePct < 3 ? DS_TEXT.error : intel.vacancyRatePct < 5 ? DS_TEXT.warning : DS_TEXT.success}
/>
<KpiTile
label="Mietpreis-Trend"
value={`${rentTrendPositive ? '+' : ''}${intel.rentTrend12m}%`}
sub="letzte 12 Monate"
color={rentTrendPositive ? '#c0392b' : '#1a7a4a'}
color={rentTrendPositive ? DS_TEXT.error : DS_TEXT.success}
/>
<KpiTile
label="Kaufkraft-Index"
value={`${intel.purchasingPowerIndex}`}
sub="CH-Mittel = 100"
color={intel.purchasingPowerIndex >= 115 ? '#1a7a4a' : intel.purchasingPowerIndex >= 95 ? '#d97706' : '#c0392b'}
color={intel.purchasingPowerIndex >= 115 ? DS_TEXT.success : intel.purchasingPowerIndex >= 95 ? DS_TEXT.warning : DS_TEXT.error}
/>
<KpiTile
label="Ø Vermietungsdauer"
value={`${intel.avgDaysOnMarket}T`}
sub="Tage auf dem Markt"
color={intel.avgDaysOnMarket < 40 ? '#c0392b' : intel.avgDaysOnMarket < 65 ? '#d97706' : '#1a7a4a'}
color={intel.avgDaysOnMarket < 40 ? DS_TEXT.error : intel.avgDaysOnMarket < 65 ? DS_TEXT.warning : DS_TEXT.success}
/>
</Box>
{/* Rent trend interpretation */}
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, p: 1.5, bgcolor: rentTrendPositive ? '#fff8f0' : '#f0fdf4', borderRadius: 1.5, mb: 2 }}>
<Box sx={{ color: rentTrendPositive ? '#d97706' : '#1a7a4a', mt: 0.1 }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, p: 1.5, bgcolor: rentTrendPositive ? '#fff8f0' : DS_SURFACE.success.bg, borderRadius: 1.5, mb: 2 }}>
<Box sx={{ color: rentTrendPositive ? DS_TEXT.warning : DS_TEXT.success, mt: 0.1 }}>
{rentTrendPositive ? <TrendingUp size={16} /> : <TrendingDown size={16} />}
</Box>
<Typography variant="body2" sx={{ color: rentTrendPositive ? '#92400e' : '#14532d' }}>
@@ -99,29 +100,29 @@ export function LocationIntelligencePanel({ property }: Props) {
{/* Tax + demand */}
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mb: 2 }}>
<Box sx={{ flex: 1, p: 1.5, bgcolor: '#f8fafc', borderRadius: 1.5, border: '1px solid #e2e8f0', minWidth: 140 }}>
<Box sx={{ flex: 1, p: 1.5, bgcolor: DS_SURFACE.neutral.bg, borderRadius: 1.5, border: `1px solid ${DS_BORDER.default}`, minWidth: 140 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }}>
<Percent size={13} color="#64748b" />
<Percent size={13} color={DS_TEXT.muted} />
<Typography variant="caption" color="text.secondary">Steuerindex Kanton</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: intel.taxIndexCanton <= 80 ? '#1a7a4a' : intel.taxIndexCanton <= 105 ? '#d97706' : '#c0392b' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: intel.taxIndexCanton <= 80 ? DS_TEXT.success : intel.taxIndexCanton <= 105 ? DS_TEXT.warning : DS_TEXT.error }}>
{intel.taxIndexCanton} <Typography component="span" variant="caption" color="text.secondary">(CH = 100)</Typography>
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.25 }}>
{intel.taxIndexCanton <= 75 ? 'Sehr steuerattraktiv' : intel.taxIndexCanton <= 95 ? 'Günstige Steuerlast' : intel.taxIndexCanton <= 110 ? 'Durchschnittlich' : 'Hohe Steuerlast'}
</Typography>
</Box>
<Box sx={{ flex: 1, p: 1.5, bgcolor: '#f8fafc', borderRadius: 1.5, border: '1px solid #e2e8f0', minWidth: 140 }}>
<Box sx={{ flex: 1, p: 1.5, bgcolor: DS_SURFACE.neutral.bg, borderRadius: 1.5, border: `1px solid ${DS_BORDER.default}`, minWidth: 140 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }}>
<Activity size={13} color="#64748b" />
<Activity size={13} color={DS_TEXT.muted} />
<Typography variant="caption" color="text.secondary">Nachfragestärke</Typography>
</Box>
<Chip
label={{ LOW: 'Gering', MEDIUM: 'Mittel', HIGH: 'Hoch', VERY_HIGH: 'Sehr hoch' }[intel.demandStrength]}
size="small"
sx={{
bgcolor: { LOW: '#f1f5f9', MEDIUM: '#fef3c7', HIGH: '#dcfce7', VERY_HIGH: '#f0fdf4' }[intel.demandStrength],
color: { LOW: '#64748b', MEDIUM: '#d97706', HIGH: '#16a34a', VERY_HIGH: '#1a7a4a' }[intel.demandStrength],
bgcolor: { LOW: '#f1f5f9', MEDIUM: '#fef3c7', HIGH: '#dcfce7', VERY_HIGH: DS_SURFACE.success.bg }[intel.demandStrength],
color: { LOW: DS_TEXT.muted, MEDIUM: DS_TEXT.warning, HIGH: '#16a34a', VERY_HIGH: DS_TEXT.success }[intel.demandStrength],
fontWeight: 600, fontSize: 11,
}}
/>
@@ -133,12 +134,12 @@ export function LocationIntelligencePanel({ property }: Props) {
{/* Industry clusters */}
<Box sx={{ mb: 2 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#475569', display: 'block', mb: 0.75 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.secondary, display: 'block', mb: 0.75 }}>
Dominante Branchen-Cluster
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{intel.dominantIndustryClusters.map(c => (
<Chip key={c} label={c} size="small" sx={{ bgcolor: '#eff6ff', color: '#1e3a5f', fontSize: 11, height: 22 }} />
<Chip key={c} label={c} size="small" sx={{ bgcolor: '#eff6ff', color: DS_TEXT.brand, fontSize: 11, height: 22 }} />
))}
</Box>
</Box>
@@ -147,8 +148,8 @@ export function LocationIntelligencePanel({ property }: Props) {
{intel.plannedInfrastructure.length > 0 && (
<Box sx={{ mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 0.75 }}>
<Train size={13} color="#64748b" />
<Typography variant="caption" sx={{ fontWeight: 600, color: '#475569' }}>
<Train size={13} color={DS_TEXT.muted} />
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.secondary }}>
Geplante Infrastruktur-Projekte
</Typography>
</Box>
@@ -173,7 +174,7 @@ export function LocationIntelligencePanel({ property }: Props) {
{/* ── Soft Factors ── */}
{hasSoftFactors && (
<Box sx={{ mb: 1.5 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#475569', display: 'block', mb: 1 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.secondary, display: 'block', mb: 1 }}>
KI-berechnete Standortqualität
</Typography>
<SoftFactorBar
@@ -221,7 +222,7 @@ export function LocationIntelligencePanel({ property }: Props) {
{comparables.length > 0 && (
<>
<Divider sx={{ my: 1.5 }} />
<Typography variant="caption" sx={{ fontWeight: 600, color: '#475569', display: 'block', mb: 0.75 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.secondary, display: 'block', mb: 0.75 }}>
Vergleichbare Angebote in {city}
</Typography>
{comparables.map(p => (
@@ -231,7 +232,7 @@ export function LocationIntelligencePanel({ property }: Props) {
sx={{
display: 'flex', alignItems: 'center', gap: 1.5, py: 0.75,
borderBottom: '1px solid #f1f5f9', cursor: 'pointer',
'&:hover': { bgcolor: '#f8fafc', borderRadius: 0.5 },
'&:hover': { bgcolor: DS_SURFACE.neutral.bg, borderRadius: 0.5 },
}}
>
<Building2 size={13} color="#3b82f6" />
@@ -6,7 +6,7 @@ import type { FutureSignal } from '../../domain/futureSignal'
import { CREDIBILITY_LABELS, HARD_KEYS, factorLabel } from './scoreBreakdownConstants'
import { CriterionRow } from './CriterionRow'
import { criterionScoreColor, criterionScoreTextColor } from '../../lib/utils'
import { DS_COLORS } from '../../lib/ds'
import { DS_COLORS, DS_TEXT, DS_SURFACE } from '../../lib/ds'
import { SCORE_STRONG, SCORE_MODERATE } from '../../lib/constants'
// ── Standard breakdown (VERIFIED_PORTFOLIO / EXTERNAL / MAISON) ──────────────
@@ -46,7 +46,7 @@ function StandardBreakdown({ match, taxCalculatorUrl }: StandardBreakdownProps)
{hardFactors.map((f, i) => <CriterionRow key={i} factor={f} maxWeight={maxHardWeight} />)}
<Box sx={{ display: 'flex', justifyContent: 'space-between', bgcolor: '#eef2ff', px: 1.25, py: 0.75, borderRadius: 1, mt: 0.25, mb: hasAllFactors && softFactors.length > 0 ? 2 : 1.5 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', bgcolor: DS_SURFACE.indigo.bg, px: 1.25, py: 0.75, borderRadius: 1, mt: 0.25, mb: hasAllFactors && softFactors.length > 0 ? 2 : 1.5 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: 'primary.main' }}>
Hart-Kriterien {sb.hardMatchScore}/100 × 60%
</Typography>
@@ -57,11 +57,11 @@ function StandardBreakdown({ match, taxCalculatorUrl }: StandardBreakdownProps)
{/* Soft factors: full list when allFactors present, otherwise summary row */}
{!hasAllFactors && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', bgcolor: '#f8fafc', px: 1.25, py: 0.75, borderRadius: 1, mb: 1.5 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#475569' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', bgcolor: DS_SURFACE.neutral.bg, px: 1.25, py: 0.75, borderRadius: 1, mb: 1.5 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.secondary }}>
Soft-Faktoren {sb.softFactorScore}/100 × 40%
</Typography>
<Typography variant="caption" sx={{ fontWeight: 800, color: '#475569' }}>
<Typography variant="caption" sx={{ fontWeight: 800, color: DS_TEXT.secondary }}>
{softContrib} Pkt
</Typography>
</Box>
@@ -70,15 +70,15 @@ function StandardBreakdown({ match, taxCalculatorUrl }: StandardBreakdownProps)
{hasAllFactors && softFactors.length > 0 && (
<>
<Divider sx={{ mb: 2 }} />
<Typography variant="caption" sx={{ fontWeight: 700, color: '#475569', textTransform: 'uppercase', letterSpacing: 0.6, display: 'block', mb: 1.25 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: DS_TEXT.secondary, textTransform: 'uppercase', letterSpacing: 0.6, display: 'block', mb: 1.25 }}>
Soft-Faktoren
</Typography>
{softFactors.map((f, i) => <CriterionRow key={i} factor={f} maxWeight={maxSoftWeight} />)}
<Box sx={{ display: 'flex', justifyContent: 'space-between', bgcolor: '#f8fafc', px: 1.25, py: 0.75, borderRadius: 1, mt: 0.25, mb: 1.5 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#475569' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', bgcolor: DS_SURFACE.neutral.bg, px: 1.25, py: 0.75, borderRadius: 1, mt: 0.25, mb: 1.5 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.secondary }}>
Soft-Score {sb.softFactorScore}/100 × 40%
</Typography>
<Typography variant="caption" sx={{ fontWeight: 800, color: '#475569' }}>
<Typography variant="caption" sx={{ fontWeight: 800, color: DS_TEXT.secondary }}>
{softContrib} Pkt
</Typography>
</Box>
@@ -87,10 +87,10 @@ function StandardBreakdown({ match, taxCalculatorUrl }: StandardBreakdownProps)
{/* Tax link */}
{taxCalculatorUrl && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5, p: 1.25, bgcolor: '#f0f9ff', borderRadius: 1, border: '1px solid #bae6fd' }}>
<ExternalLink size={13} color="#0369a1" style={{ flexShrink: 0 }} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5, p: 1.25, bgcolor: DS_SURFACE.info.bg, borderRadius: 1, border: `1px solid ${DS_SURFACE.info.border}` }}>
<ExternalLink size={13} color={DS_TEXT.info} style={{ flexShrink: 0 }} />
<Box>
<Typography variant="caption" sx={{ color: '#0369a1', fontWeight: 600, display: 'block', lineHeight: 1.2 }}>
<Typography variant="caption" sx={{ color: DS_TEXT.info, fontWeight: 600, display: 'block', lineHeight: 1.2 }}>
Steuerlast in Bewertung eingeflossen
</Typography>
<Link
@@ -98,7 +98,7 @@ function StandardBreakdown({ match, taxCalculatorUrl }: StandardBreakdownProps)
target="_blank"
rel="noopener noreferrer"
underline="hover"
sx={{ fontSize: '0.72rem', color: '#0369a1' }}
sx={{ fontSize: '0.72rem', color: DS_TEXT.info }}
>
Steuerrechner Gemeinde öffnen
</Link>
@@ -109,11 +109,11 @@ function StandardBreakdown({ match, taxCalculatorUrl }: StandardBreakdownProps)
<Divider sx={{ mb: 1.5 }} />
{/* Formula row — always visible */}
<Box sx={{ bgcolor: '#f8fafc', px: 1.25, py: 0.75, borderRadius: 1, mb: 1.25 }}>
<Typography variant="caption" sx={{ color: '#64748b', display: 'block', mb: 0.4, fontWeight: 500 }}>
<Box sx={{ bgcolor: DS_SURFACE.neutral.bg, px: 1.25, py: 0.75, borderRadius: 1, mb: 1.25 }}>
<Typography variant="caption" sx={{ color: DS_TEXT.muted, display: 'block', mb: 0.4, fontWeight: 500 }}>
Berechnung
</Typography>
<Typography variant="caption" sx={{ fontFamily: 'monospace', color: '#1e293b', display: 'block', lineHeight: 1.6 }}>
<Typography variant="caption" sx={{ fontFamily: 'monospace', color: DS_TEXT.primary, display: 'block', lineHeight: 1.6 }}>
Hart {sb.hardMatchScore} × 60% + Soft {sb.softFactorScore} × 40%
{' = '}{hardContrib} + {softContrib} = {baseSum}
</Typography>
@@ -122,7 +122,7 @@ function StandardBreakdown({ match, taxCalculatorUrl }: StandardBreakdownProps)
{/* Total */}
<Box sx={{
display: 'flex', alignItems: 'baseline', justifyContent: 'space-between',
bgcolor: baseSum >= SCORE_STRONG ? '#f0fdf4' : baseSum >= SCORE_MODERATE ? '#fffbeb' : '#fef2f2',
bgcolor: baseSum >= SCORE_STRONG ? DS_SURFACE.success.bg : baseSum >= SCORE_MODERATE ? DS_SURFACE.warning.bg : DS_SURFACE.error.bg,
p: 1.5, borderRadius: 1,
}}>
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>Gesamt-Score</Typography>
@@ -176,11 +176,11 @@ function SignalScoreDerivation({ match, signal }: SignalScoreDerivationProps) {
<Box key={i} sx={{ mb: 1.25 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.6, mb: 0.4 }}>
{isPositive
? <CheckCircle2 size={12} color="#1a7a4a" style={{ flexShrink: 0 }} />
: <X size={12} color="#c0392b" style={{ flexShrink: 0 }} />
? <CheckCircle2 size={12} color={DS_TEXT.success} style={{ flexShrink: 0 }} />
: <X size={12} color={DS_TEXT.error} style={{ flexShrink: 0 }} />
}
<Box sx={{ flex: 1, display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#1e293b' }}>{factorLabel(f.criterion)}</Typography>
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.primary }}>{factorLabel(f.criterion)}</Typography>
<Typography variant="caption" sx={{ fontWeight: 700, color: criterionScoreTextColor(f.score), ml: 1 }}>{f.score}/100</Typography>
</Box>
</Box>
@@ -197,9 +197,9 @@ function SignalScoreDerivation({ match, signal }: SignalScoreDerivationProps) {
)
})}
<Box sx={{ display: 'flex', justifyContent: 'space-between', bgcolor: '#f8fafc', px: 1.25, py: 0.75, borderRadius: 1, mt: 0.5 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#475569' }}>Basis-Score</Typography>
<Typography variant="caption" sx={{ fontWeight: 800, color: '#1e293b' }}>{sb.hardMatchScore}/100</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', bgcolor: DS_SURFACE.neutral.bg, px: 1.25, py: 0.75, borderRadius: 1, mt: 0.5 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.secondary }}>Basis-Score</Typography>
<Typography variant="caption" sx={{ fontWeight: 800, color: DS_TEXT.primary }}>{sb.hardMatchScore}/100</Typography>
</Box>
</Box>
@@ -214,20 +214,20 @@ function SignalScoreDerivation({ match, signal }: SignalScoreDerivationProps) {
{isVerifiedContract ? (
<>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 0.75 }}>
<ShieldCheck size={13} color="#1a7a4a" style={{ flexShrink: 0 }} />
<Typography variant="caption" sx={{ color: '#1a7a4a', lineHeight: 1.3 }}>
<ShieldCheck size={13} color={DS_TEXT.success} style={{ flexShrink: 0 }} />
<Typography variant="caption" sx={{ color: DS_TEXT.success, lineHeight: 1.3 }}>
Vertragsende aus ERP verifiziert keine Schätzung
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 1 }}>
<ShieldCheck size={13} color="#1a7a4a" style={{ flexShrink: 0 }} />
<Typography variant="caption" sx={{ color: '#1a7a4a', lineHeight: 1.3 }}>
<ShieldCheck size={13} color={DS_TEXT.success} style={{ flexShrink: 0 }} />
<Typography variant="caption" sx={{ color: DS_TEXT.success, lineHeight: 1.3 }}>
Verwaltung hat Freigabe erteilt
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', bgcolor: '#f0fdf4', px: 1.25, py: 0.75, borderRadius: 1, border: '1px solid #bbf7d0' }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#1a7a4a' }}>Signal-Abschlag</Typography>
<Typography variant="caption" sx={{ fontWeight: 800, color: '#1a7a4a' }}>keiner</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', bgcolor: DS_SURFACE.success.bg, px: 1.25, py: 0.75, borderRadius: 1, border: `1px solid ${DS_SURFACE.success.border}` }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.success }}>Signal-Abschlag</Typography>
<Typography variant="caption" sx={{ fontWeight: 800, color: DS_TEXT.success }}>keiner</Typography>
</Box>
</>
) : (
@@ -235,18 +235,18 @@ function SignalScoreDerivation({ match, signal }: SignalScoreDerivationProps) {
{probPct !== null && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', mb: 0.5 }}>
<Typography variant="caption" color="text.secondary">Eintretenswahrscheinlichkeit: {probPct}%</Typography>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#c0392b' }}>{Math.round(signalDeduction * 0.7)} Pkt.</Typography>
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.error }}>{Math.round(signalDeduction * 0.7)} Pkt.</Typography>
</Box>
)}
{credLabel && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', mb: 1 }}>
<Typography variant="caption" color="text.secondary">Quellenqualität: {credLabel}</Typography>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#d97706' }}>{Math.round(signalDeduction * 0.3)} Pkt.</Typography>
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.warning }}>{Math.round(signalDeduction * 0.3)} Pkt.</Typography>
</Box>
)}
<Box sx={{ display: 'flex', justifyContent: 'space-between', bgcolor: '#fff7ed', px: 1.25, py: 0.75, borderRadius: 1, border: '1px solid #fed7aa' }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: '#d97706' }}>Signal-Abschlag</Typography>
<Typography variant="caption" sx={{ fontWeight: 800, color: '#d97706' }}>{signalDeduction} Punkte</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', bgcolor: DS_SURFACE.orange.bg, px: 1.25, py: 0.75, borderRadius: 1, border: `1px solid ${DS_SURFACE.orange.border}` }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: DS_TEXT.warning }}>Signal-Abschlag</Typography>
<Typography variant="caption" sx={{ fontWeight: 800, color: DS_TEXT.warning }}>{signalDeduction} Punkte</Typography>
</Box>
</>
)}
@@ -255,7 +255,7 @@ function SignalScoreDerivation({ match, signal }: SignalScoreDerivationProps) {
<Divider sx={{ mb: 1.5 }} />
{/* Gesamt */}
<Box sx={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', bgcolor: '#f8fafc', p: 1.5, borderRadius: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', bgcolor: DS_SURFACE.neutral.bg, p: 1.5, borderRadius: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>Gesamt-Score</Typography>
<Typography
variant="h4"