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:
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user