Files
property-match/src/lib/ds.ts
T
Benjamin SutterandClaude Opus 5 d8f8f728cc refactor(startseite): Meetingraum als Fotoszene mit Live-Auslastung
Die gezeichnete Raumkulisse ist ersetzt. Aus Farbflächen konstruierte Wände
bleiben neben fotografierten Agentenporträts erkennbar gezeichnet — die
Bildwirkung muss aus dem Foto kommen, nicht aus Geometrie. Entfallen sind
damit die beiden SVG-Stadtansichten, die CSS-3D-Flächen und die Farbwelt
DS_MEETING_ROOM.

Neu trägt die Szene die Basler Aussicht aus dem Auftrag als Foto (§5), mit
abgestuftem Verlauf für Lesbarkeit und Übergang zur Objektliste. Ein Austausch
des Motivs ist ein Einzeiler in `roomBackdrop.ts`.

Mehrwert statt Kulisse: Jedes Namensschild zeigt die aktuelle Auslastung
seines Agenten — überfällige Fristen, Termine der Woche, offene Exposés, neue
Marktchancen, Objekte mit Datenlücken, offene Freigaben. Darüber fasst ein
Anriss zusammen, wie viele Bereiche heute Aufmerksamkeit brauchen. Damit
beantwortet die Startseite die Frage, mit der sie geöffnet wird, statt sie
auf sechs Zahlen zu verteilen.

- `agentWorkloadService` hält die fachlichen Festlegungen: Zeitfenster,
  Dringlichkeitsschwellen und Formulierungen. Der Stichzeitpunkt ist ein
  Parameter, nicht die Systemuhr — dieselbe Datenlage ergibt dasselbe
  Ergebnis, und der Dienst ist ohne Renderer prüfbar (10 neue Tests).
- `useAgentWorkload` sammelt nur die Bestände ein.
- Noras Zahl wird nie «dringend»: sie schlägt vor, sie entscheidet nicht.
- Die Schilder bemessen ihre Höchstbreite an der Szenenbreite (`cqw`), nicht
  am Fenster. Am Fenster gemessen überlappten sie auf schmalen Laptops, weil
  die Szene um das Seitenmenü schmaler ist.

Geprüft: 423 Tests, Typecheck, Build, ESLint, Token-Schwelle (889/925).
Im Browser 1280–1920 px: keine Überlappungen, Titel und «Filter & Übersicht»
beim Einstieg sichtbar, alle sechs Agentenklicks auf der richtigen Subpage,
Tablet-Ansicht mit allen sechs Agenten, keine Konsolenfehler, keine 404.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:07:21 +02:00

311 lines
13 KiB
TypeScript

import type { ConfidenceLevel, DataQualityLevel } from '../domain/enums'
import { CONF_HIGH, CONF_MEDIUM, DQ_HIGH, DQ_MEDIUM } from './constants'
// ── Semantic color tokens ─────────────────────────────────────────────────────
export const DS_COLORS = {
resultType: {
VERIFIED_PORTFOLIO: { bg: 'rgba(21,38,66,0.10)', fg: '#152642' },
MAISON_WORK: { bg: 'rgba(45,90,138,0.10)', fg: '#2d5a8a' },
FUTURE_AVAILABILITY: { bg: 'rgba(91,63,138,0.10)', fg: '#5b3f8a' },
},
confidence: {
VERY_HIGH: { bg: 'rgba(29,99,66,0.12)', fg: '#1d6342' },
HIGH: { bg: 'rgba(29,99,66,0.09)', fg: '#1d6342' },
MEDIUM: { bg: 'rgba(21,38,66,0.10)', fg: '#152642' },
LOW: { bg: 'rgba(161,98,7,0.12)', fg: '#a16207' },
VERY_LOW: { bg: 'rgba(185,28,28,0.12)', fg: '#b91c1c' },
},
risk: {
LOW: { bg: 'rgba(29,99,66,0.10)', fg: '#1d6342' },
MEDIUM: { bg: 'rgba(161,98,7,0.10)', fg: '#a16207' },
HIGH: { bg: 'rgba(185,28,28,0.10)', fg: '#b91c1c' },
CRITICAL: { bg: 'rgba(127,0,0,0.12)', fg: '#7f1d1d' },
},
availability: {
AVAILABLE_NOW: { bg: 'rgba(29,99,66,0.10)', fg: '#1d6342' },
AVAILABLE_SOON: { bg: 'rgba(161,98,7,0.10)', fg: '#a16207' },
FUTURE_SIGNAL: { bg: 'rgba(91,63,138,0.10)', fg: '#5b3f8a' },
OCCUPIED: { bg: 'rgba(100,116,139,0.10)', fg: '#475569' },
UNKNOWN: { bg: '#f4f3f0', fg: '#9a9a9a' },
},
freshness: {
FRESH: { bg: 'rgba(29,99,66,0.10)', fg: '#1d6342' },
STALE: { bg: 'rgba(161,98,7,0.10)', fg: '#a16207' },
OUTDATED: { bg: 'rgba(185,28,28,0.10)', fg: '#b91c1c' },
},
dataQuality: {
HIGH: { bg: 'rgba(29,99,66,0.10)', fg: '#1d6342' },
MEDIUM: { bg: 'rgba(161,98,7,0.10)', fg: '#a16207' },
LOW: { bg: 'rgba(185,28,28,0.10)', fg: '#b91c1c' },
INCOMPLETE: { bg: 'rgba(127,0,0,0.12)', fg: '#7f1d1d' },
},
heat: {
VERY_HOT: { bg: '#fef3c7', border: '#fcd34d', icon: '#a16207', text: '#92400e' },
HOT: { bg: '#fff7ed', border: '#fed7aa', icon: '#a16207', text: '#92400e' },
},
futureCard: {
controlled: {
accent: '#5b3f8a',
accentHover: '#4a3070',
headerBg: '#faf7ff',
border: '#e9d5ff',
badgeBg: '#ede9fe',
badgeText: '#5b21b6',
},
signal: {
accent: '#2d5a8a',
accentHover: '#1e4068',
headerBg: '#f0f6ff',
border: '#bfdbfe',
badgeBg: '#dbeafe',
badgeText: '#1e40af',
indicator: '#2d5a8a',
},
},
} as const
// ── Text color tokens ─────────────────────────────────────────────────────────
export const DS_TEXT = {
primary: '#141414',
secondary: '#5c5c5c',
muted: '#9a9a9a',
disabled: '#c4c4c4',
inverted: '#ffffff',
success: '#1d6342',
warning: '#a16207',
error: '#b91c1c',
danger: '#b91c1c',
info: '#2d5a8a',
brand: '#152642',
signal: '#5b3f8a',
successDark: '#166534',
warningDark: '#92400e',
signalDark: '#4c1d95',
infoDark: '#0c4a6e',
brandDark: '#0e1c30',
} as const
// ── Background tokens ─────────────────────────────────────────────────────────
export const DS_BG = {
page: '#f9f8f6',
surface: '#ffffff',
subtle: '#f4f3f0',
muted: '#e8e7e4',
} as const
// ── Border tokens ─────────────────────────────────────────────────────────────
export const DS_BORDER = {
default: '#e8e7e4',
muted: '#f2f1ee',
strong: '#c8c7c4',
} as const
// ── Surface tokens ────────────────────────────────────────────────────────────
export const DS_SURFACE = {
success: { bg: '#f0fdf4', border: '#bbf7d0' },
warning: { bg: '#fffbeb', border: '#fde68a' },
error: { bg: '#fef2f2', border: '#fecaca' },
info: { bg: '#f0f6ff', border: '#bfdbfe' },
indigo: { bg: '#eef2ff', border: '#e0e7ff' },
purple: { bg: '#faf7ff', border: '#e9d5ff' },
orange: { bg: '#fff7ed', border: '#fed7aa' },
blue: { bg: '#f0f6ff', border: '#bfdbfe' },
neutral: { bg: '#f9f8f6', border: '#e8e7e4' },
slate: { bg: '#f4f3f0', border: '#e8e7e4' },
} as const
// ── Neutrale Skala ────────────────────────────────────────────────────────────
/**
* Die kühle Graustufe, auf der Tabellen, Trennlinien und Nebentexte liegen.
*
* Sie war über rund 200 Dateien als Hex-Literal verstreut — dieselben zehn
* Werte, immer wieder abgetippt. Die Stufen entsprechen exakt den bisher
* verwendeten Werten; die Zusammenführung ändert kein einziges Pixel, macht
* aber einen späteren Themenwechsel überhaupt erst möglich.
*
* Abgrenzung zu `DS_BG`/`DS_BORDER`: jene tragen den warmen Grundton der
* Anwendung (Papier), diese Skala den kühlen für Datenflächen.
*/
export const DS_SLATE = {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
} as const
/** Weitere neutrale Einzelwerte ohne Platz in der Skala. */
export const DS_NEUTRAL = {
white: '#ffffff',
offWhite: '#fafafa',
paper: '#fbfaf8',
paperTint: '#f0f4f8',
stone: '#b0aead',
graphite: '#374151',
sidebar: '#0f1923',
gold: '#b8975a',
} as const
// ── Markenfarben ──────────────────────────────────────────────────────────────
/**
* Das dunkle Blau der Anwendung samt seinen Hover-Stufen.
*
* `hover` und `hoverAlt` unterscheiden sich um zwei Prozent Helligkeit — sie
* sind historisch gewachsen und stehen beide im Code. Sie bleiben getrennt,
* damit die Zusammenführung nichts sichtbar verschiebt; wer sie vereinheitlichen
* will, tut das ab jetzt an einer Stelle statt an vierzig.
*/
export const DS_BRAND = {
main: '#152642',
hover: '#16304d',
hoverAlt: '#162d4a',
dark: '#0e1c30',
darker: '#1a3050',
muted: '#5a87a3',
light: '#64b5f6',
} as const
// ── Akzentfarben nach Bedeutung ───────────────────────────────────────────────
/**
* Semantische Akzente. Jede Gruppe führt Vorder-, Flächen- und Randfarbe
* derselben Bedeutung, damit ein Zustand nicht auf zwei Bildschirmen zwei
* verschiedene Grüntöne bekommt.
*/
export const DS_ACCENT = {
success: {
main: '#1a7a4a', strong: '#15803d', bright: '#16a34a', emerald: '#10b981',
dark: '#155f3a', darkAlt: '#15643c',
bg: '#f0fdf4', bgAlt: '#dcfce7', border: '#86efac',
},
warning: {
main: '#d97706', strong: '#ea580c', amber: '#f59e0b', burnt: '#c2410c',
dark: '#b45309', darker: '#854d0e', darkest: '#7a4f00', brown: '#7c3d12',
bg: '#fffbeb', bgAlt: '#fff7ed', border: '#fcd34d', borderSoft: '#fef3c7',
},
danger: {
main: '#c0392b', strong: '#dc2626', bright: '#ef4444', dark: '#a93226', darkest: '#7f1d1d',
bg: '#fef2f2', bgAlt: '#fff1f2', border: '#fca5a5',
},
violet: {
main: '#7c3aed', strong: '#6d28d9', light: '#8b5cf6',
dark: '#5b21b6', darkest: '#4c1d95',
bg: '#faf5ff', bgAlt: '#f5f3ff', border: '#c4b5fd', borderSoft: '#ddd6fe', tint: '#ede9fe',
},
indigo: {
main: '#4f46e5', strong: '#4338ca', dark: '#3730a3', light: '#6366f1',
bg: '#eef2ff', border: '#e0e7ff',
},
blue: {
main: '#1d4ed8', strong: '#2563eb', dark: '#1e40af', darkest: '#1e3a8a',
bg: '#eff6ff', bgAlt: '#f0f9ff', border: '#bfdbfe', borderSoft: '#dbeafe',
},
cyan: {
main: '#0891b2', deep: '#0369a1', dark: '#075985',
bg: '#e0f2fe', border: '#bae6fd',
},
} as const
// ── Match tier surface ────────────────────────────────────────────────────────
export const DS_MATCH_TIER = {
strong: DS_SURFACE.success,
moderate: DS_SURFACE.warning,
weak: DS_SURFACE.error,
} as const
// ── Pre-market & market signal aliases ────────────────────────────────────────
export const DS_PRE_MARKET = DS_COLORS.futureCard.controlled
export const DS_MARKET_SIGNAL = DS_COLORS.futureCard.signal
// ── Badge color presets ───────────────────────────────────────────────────────
export const BADGE_COLORS = {
confidenceHigh: '#1d6342',
confidenceMedium: '#a16207',
confidenceLow: '#b91c1c',
riskHigh: '#b91c1c',
riskMedium: '#a16207',
riskLow: '#1d6342',
preMarket: '#5b3f8a',
marketSignal: '#2d5a8a',
verified: '#152642',
info: '#2d5a8a',
muted: '#9a9a9a',
gold: '#b8975a',
silver: '#7a8a9a',
bronze: '#7a6050',
} as const
// ── Shadow tokens ─────────────────────────────────────────────────────────────
export const DS_SHADOW = {
card: '0 1px 3px rgba(0,0,0,0.06)',
panel: '0 3px 6px rgba(0,0,0,0.07)',
dialog: '0 16px 24px rgba(0,0,0,0.10)',
} as const
// ── Inquiry status metadata ───────────────────────────────────────────────────
export const INQUIRY_STATUS_META: Record<string, { label: string; fg: string; bg: string }> = {
new: { label: 'Neu', fg: '#b91c1c', bg: '#fef2f2' },
in_progress: { label: 'Aktiv', fg: '#a16207', bg: '#fffbeb' },
answered: { label: 'Beantwortet', fg: '#1d6342', bg: '#f0fdf4' },
archived: { label: 'Archiviert', fg: '#9a9a9a', bg: '#f9f8f6' },
}
// ── Score → qualitative level helpers ────────────────────────────────────────
export function scoreToConfidenceLevel(score: number): ConfidenceLevel {
if (score >= CONF_HIGH) return 'VERY_HIGH'
if (score >= 0.75) return 'HIGH'
if (score >= CONF_MEDIUM) return 'MEDIUM'
if (score >= 0.35) return 'LOW'
return 'VERY_LOW'
}
export function scoreToDataQualityLevel(score: number): DataQualityLevel {
if (score >= DQ_HIGH) return 'HIGH'
if (score >= DQ_MEDIUM) return 'MEDIUM'
if (score > 0) return 'LOW'
return 'INCOMPLETE'
}
// ── Result type meta ──────────────────────────────────────────────────────────
export const RESULT_TYPE_META: Record<string, { label: string; color: string; bg: string }> = {
VERIFIED_PORTFOLIO: { label: 'Verifiziertes Objekt', color: '#152642', bg: 'rgba(21,38,66,0.10)' },
MAISON_WORK: { label: 'Maison Work', color: '#2d5a8a', bg: 'rgba(45,90,138,0.10)' },
FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#5b3f8a', bg: 'rgba(91,63,138,0.10)' },
}
// ── Kanalherkunft der Leads bei Nora (Runde 5, §12) ──────────────────────────
/**
* Badge je Kanal. Nach dem Muster von `RESULT_TYPE_META`: Beschriftung und
* Farben stehen zusammen, damit derselbe Kanal in Liste, Filter und Detail nie
* unterschiedlich aussieht.
*
* Der Schlüssel ist `LeadChannel` aus `domain/unifiedLead.ts`; hier steht er
* bewusst als `string`, damit die Designschicht nicht auf das Domänenmodell
* zeigt — die Abhängigkeit läuft immer nur in die andere Richtung.
*/
export const LEAD_CHANNEL_META: Record<string, { label: string; color: string; bg: string }> = {
KI_SIGNAL: { label: 'KI Signal', color: '#5b3f8a', bg: 'rgba(91,63,138,0.10)' },
NETZWERK: { label: 'Netzwerk', color: '#1a7a4a', bg: 'rgba(26,122,74,0.10)' },
CRM: { label: 'CRM', color: '#2d5a8a', bg: 'rgba(45,90,138,0.10)' },
}