Neon in der Nacht: fast schwarze Flaechen mit blauem Stich, violettes Leuchten, Blaustahl statt Neutralgrau und Kanten von 2 px - scharf genug fuer HUD-Elemente, aber nicht eckig, das ist schon Pixel Art. Schatten sind hier kein Schatten, sondern ein Schein: farbige Kontur plus weicher Halo in der Markenfarbe. Schrift ist Chakra Petch, die Feldbeschriftungen laufen gesperrt. Der Akzent ist bewusst Violett und nicht Neon-Cyan: auf brand-500 und brand-600 steht 69 Mal weisse Schrift, und auf Cyan waere die nicht zu lesen. Cyan sitzt deshalb nur in --color-brand-accent und in den Statusfarben, wo dunkler Text darauf steht. Der helle Modus ist der Konzern bei Tageslicht: kuehles Blaugrau, dasselbe Violett nur dunkler, und aus dem Leuchten wird ein knapper kalter Schatten - ein Halo auf hellem Grund sieht nach Fehler aus, nicht nach Neon. Es waren vier Zeilen ausserhalb von index.css: die Whitelist in userFields.js, die Liste in src/types.ts mit ihrer Beschriftung, die Whitelist im Inline-Skript und die Schrift im Google-Fonts-Aufruf. Keine Komponente wurde angefasst. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
213 lines
9.9 KiB
Markdown
213 lines
9.9 KiB
Markdown
# 05 Frontend
|
||
|
||
Eine einzige React-Anwendung ohne Router: `App.tsx` hält die aktuelle Ansicht in
|
||
einem Zustand und rendert sie. Das genügt, weil die App hinter einer Anmeldung
|
||
sitzt und keine tiefen Links braucht.
|
||
|
||
## Menü und Ansichten
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
SB["Seitenleiste"] --> K["Kategorien"]
|
||
K --> D["Dashboard<br/>(Startansicht, auch über das Logo)"]
|
||
K --> AQ["Akquise"]
|
||
AQ --> AQ1["Eingang<br/>Funde der letzten 14 Tage"]
|
||
AQ --> AQ2["Archiv<br/>älter; nach 90 Tagen gelöscht"]
|
||
AQ --> AQ3["Papierkorb<br/>von Hand gelöscht; 90 Tage"]
|
||
AQ2 -. "Zurück in den Eingang<br/>(Funddatum = heute)" .-> AQ1
|
||
AQ3 -. "Wiederherstellen<br/>(Funddatum bleibt)" .-> AQ2
|
||
K --> O["Opportunities"]
|
||
K --> C["Interviews"]
|
||
K --> L["Kontakte"]
|
||
L --> L1["Alle Kontakte"]
|
||
L --> L2["Aus Mails<br/>Vorbereich, Zähler im Menü"]
|
||
K --> M["Mails"]
|
||
M --> M1["Postfach<br/>Reiter: Postfach, Kontakt-Vorschläge"]
|
||
M --> M2["Papierkorb<br/>im CRM gelöschte Mails"]
|
||
K --> SC["Scanner"]
|
||
SC --> SC1["Dashboard"]
|
||
SC --> SC2["Unpassend"]
|
||
SC --> SC3["Suchagenten"]
|
||
SC --> SC4["Börsen-Konten"]
|
||
SB --> E["Einstellungen"]
|
||
E --> E1["Nutzerverwaltung (admin)"]
|
||
E --> E2["Mein Konto"]
|
||
E --> E3["Postfach (admin)"]
|
||
E --> E4["Scanner (admin)"]
|
||
E --> E5["Whisper"]
|
||
```
|
||
|
||
*Postfach* und *Kontakt-Vorschläge* sind Reiter **innerhalb** der Ansicht
|
||
*Mails*, keine Menüpunkte - sie gehören zusammen und wechseln oft hin und her.
|
||
|
||
Die Menüpunkte *Kontakte* und *Scanner* klappen ihre Unterpunkte auf; der
|
||
Kopfeintrag ist dabei selbst eine Ansicht (Kontaktliste bzw. Scanner-Dashboard).
|
||
Bei eingeklappter Leiste (Symbolbreite) erscheinen nur die Symbole, die
|
||
Unterpunkte kommen mit den Beschriftungen zurück. Beide Gruppen entstehen aus
|
||
derselben Struktur `MenuGroup` in `Sidebar.tsx`.
|
||
|
||
*Aus Mails* trägt einen Zähler mit der Anzahl der offenen Funde - er kommt aus
|
||
`App.tsx`, das die Kontakte ohnehin geladen hat.
|
||
|
||
## Zustand und Datenfluss
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
subgraph Kontexte["Kontexte in main.tsx"]
|
||
TH["ThemeProvider<br/>hell/dunkel/System"]
|
||
TO["ToastProvider"]
|
||
AU["AuthProvider<br/>JWT, Benutzer"]
|
||
WH["WhisperProvider<br/>Modell, Worker"]
|
||
end
|
||
|
||
APP["App.tsx<br/>currentView, settingsTab, focusLeadId"]
|
||
NAV["NavigationProvider<br/>goTo(view, {tab, leadId})"]
|
||
DATA["loadData()<br/>leads, connections, events, users, opportunities"]
|
||
|
||
Kontexte --> APP
|
||
APP --> NAV
|
||
APP --> DATA
|
||
DATA --> V1["Opportunities"]
|
||
NOTE["Akquise lädt selbst<br/>acquisitionsApi, nicht über loadData()"]
|
||
DATA --> V2["Interviews"]
|
||
DATA --> V3["Kontakte<br/>mail_pool = false"]
|
||
DATA --> V5["Aus Mails<br/>mail_pool = true + Kontakte zum Abgleich"]
|
||
DATA --> V4["Mails<br/>leads + opportunities für den Zuordnungsdialog"]
|
||
NAV --> DF["DictationField<br/>springt nach Whisper"]
|
||
NAV --> ML["Mails<br/>springt zum Kontakt einer Zuordnung"]
|
||
|
||
SCV["Scanner-Ansichten"] -->|"laden selbst"| API["/api/scanner"]
|
||
MV["Mailansicht"] -->|"lädt selbst"| MAPI["/api/mail"]
|
||
```
|
||
|
||
Die CRM-Daten werden **einmal zentral** geladen und als Props durchgereicht; die
|
||
Scanner- und Mailansichten laden ihre eigenen Daten selbst - der Scanner, weil er
|
||
eine andere Datenbank und eine Auffrischung im 30-Sekunden-Takt hat; die Mails,
|
||
weil sie blättern, suchen und filtern und dabei nicht die ganze Liste im Speicher
|
||
halten sollen. Kontakte und Opportunities bekommt die Mailansicht dagegen als
|
||
Props: der Zuordnungsdialog braucht sie beide zum Vorschlagen.
|
||
|
||
`/api/leads` liefert beide Bestände in einem Aufruf; `App.tsx` teilt sie in
|
||
`leads` (Kontakte) und `mailPoolLeads` (Vorbereich „Aus Mails") auf. Alle
|
||
anderen Ansichten bekommen ausschließlich `leads` - ein Fund aus einer Mail soll
|
||
in keiner Auswahlliste auftauchen. Die Ansicht *Aus Mails* braucht beides: sie
|
||
vergleicht jeden Fund gegen den Kontaktbestand und markiert Dubletten.
|
||
|
||
`goTo('Kontakte', { leadId })` ist der Sprung aus einer Mailzuordnung zum
|
||
Kontakt. `App.tsx` hält die Id in `focusLeadId`, die Kontaktansicht öffnet den
|
||
Kontakt und meldet mit `onFocusHandled()` zurück - der Effekt hängt an `leads`
|
||
und wartet damit auf einen gerade erst angelegten Kontakt.
|
||
|
||
`reload={() => loadData(true)}` lädt still nach - ohne Ladeanzeige, damit ein
|
||
Speichern im Dialog nicht die ganze Liste flackern lässt.
|
||
|
||
## Komponentenkarte
|
||
|
||
```
|
||
src/
|
||
App.tsx Layout, Datenladen, Ansichtswechsel
|
||
types.ts Gemeinsame Typen, Farbpalette, Rechenhilfen
|
||
lib/
|
||
api.ts Fetch-Wrapper (JWT, 401-Behandlung) + CRM-Endpunkte
|
||
scannerApi.ts Typisierte Endpunkte unter /api/scanner
|
||
mailApi.ts Endpunkte unter /api/mail, UTC-Datumshelfer
|
||
auth.tsx Anmeldung, Benutzer, Rolle
|
||
theme.tsx Hell/Dunkel/System
|
||
navigation.tsx Ansichtswechsel für tiefe Komponenten
|
||
whisper.tsx Modell, Worker-Lebenszyklus
|
||
dictation.tsx Aufnahme-Hook
|
||
audio.ts Aufnahme zu 16-kHz-Mono-PCM
|
||
imageCompress.ts Bilder vor dem Upload verkleinern
|
||
components/
|
||
Sidebar.tsx Menü inkl. aufklappbarer Gruppen (Kontakte, Scanner)
|
||
Header.tsx Titel, Untertitel, Design-Auswahl, Hell/Dunkel-Schalter
|
||
opportunities/ Kachel, Liste, Pipeline (Drag & Drop), Formular
|
||
calendar/ Monat/Woche/Tag, Termine (Drag & Drop), @-Mentions
|
||
leads/ Kachel, Liste, Netzwerkdiagramm (d3-force),
|
||
Vorbereich "Aus Mails" mit Dublettenabgleich
|
||
mail/ Postfachliste, Detail, Zuordnung, Kontakt-Vorschläge
|
||
scanner/ Dashboard, Unpassend, Suchagenten, Börsen-Konten
|
||
settings/ Reiter: Nutzerverwaltung, Konto, Postfach, Scanner, Whisper
|
||
whisper/ Einrichtung der Spracherkennung
|
||
ui/ Modal, Avatare, Diktatfeld
|
||
```
|
||
|
||
## Themes ohne `dark:`-Varianten
|
||
|
||
Die Darstellung hat **zwei unabhängige Achsen**, beide als Klasse am `<html>`:
|
||
|
||
| Achse | Klasse | Werte |
|
||
| --- | --- | --- |
|
||
| Farbwelt | `theme-perinco`, `theme-pixel`, `theme-outdoor`, `theme-cyberpunk` | perinco ist der Standard |
|
||
| Modus | `dark` bzw. keine | hell, dunkel, oder der Systemeinstellung folgen |
|
||
|
||
`src/index.css` tauscht darin nur Variablenwerte: `canvas`, `surface`, `ink-*`,
|
||
`brand-*`, die getönten Statusfarben, dazu `--radius-*`, `--shadow-*` und die
|
||
Schriften. Die Komponenten benutzen durchgängig diese Skalen und folgen damit
|
||
automatisch - **kein `dark:` und keine themespezifische Klasse im Markup**.
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
U["Benutzer wählt<br/>im Header"] --> DB["crm_users.theme<br/>crm_users.theme_mode"]
|
||
U --> LS["localStorage<br/>nur Zwischenspeicher"]
|
||
DB -->|"beim Anmelden"| H["Klassen am html<br/>theme-*, dark"]
|
||
LS -->|"vor dem ersten Frame"| H
|
||
H --> V["Variablen in index.css<br/>Farben, Radien, Schatten, Schrift"]
|
||
V --> W["Utility-Klassen der Komponenten"]
|
||
SYS["Systemeinstellung"] -.->|"ohne eigene Wahl"| H
|
||
```
|
||
|
||
Die Wahl gehört dem **Benutzer**, nicht dem Browser: sie steht in `crm_users`
|
||
und kommt mit `/api/auth/me`. Wer sich an einem anderen Rechner anmeldet, findet
|
||
sein Design wieder, und zwei Benutzer an derselben Maschine kommen sich nicht in
|
||
die Quere. Der `localStorage` bleibt daneben als Zwischenspeicher liegen, damit
|
||
das Inline-Skript in `index.html` die Klassen vor dem ersten Frame setzen kann -
|
||
sonst blitzt beim Laden das Standarddesign auf. Das hat genau eine sichtbare
|
||
Folge: melden sich zwei Leute an demselben Rechner an, sieht der zweite für einen
|
||
Augenblick das Design des ersten.
|
||
|
||
Der `ThemeProvider` steht deshalb in `main.tsx` **unter** dem `AuthProvider` -
|
||
er liest den Benutzer.
|
||
|
||
### Was ein neues Theme können muss
|
||
|
||
Die Rollen der Skalen sind über die ganze App gleich; ein Theme, das sie
|
||
verletzt, macht Text unlesbar. Nachgezählt im Markup:
|
||
|
||
| Skala | Rolle | Verwendungen |
|
||
| --- | --- | --- |
|
||
| `ink-50`, `ink-100` | Flächen und Hover | 102 × `bg-` |
|
||
| `ink-200` | **Rahmen** | 224 × `border-`, 1 × `bg-` |
|
||
| `ink-300`–`ink-900` | Text; `ink-400` trägt die Hinweistexte | 241 × `text-ink-400` |
|
||
| `ink-950` | nur unter Dialog-Overlays, bleibt immer dunkel | 5 × `bg-` |
|
||
| `brand-50`, `brand-100` | getönte Flächen | 70 × `bg-` |
|
||
| `brand-200`–`brand-400` | Rahmen | 76 × `border-` |
|
||
| `brand-500`, `brand-600` | gefüllte Knöpfe - **weisse Schrift darauf** | 100 × `bg-` |
|
||
| `brand-600`–`brand-800` | Text auf getönten Flächen | 108 × `text-` |
|
||
|
||
Daraus folgen zwei harte Regeln:
|
||
|
||
1. **`brand-500` und `brand-600` müssen dunkel genug für weisse Schrift sein.**
|
||
`text-white` steht 69 × im Markup, immer auf einer Markenfläche. Ein helles
|
||
Theme kann dort kein Pastell verwenden.
|
||
2. **Der helle Block eines Themes braucht `:not(.dark)`.** `html.theme-x` hat
|
||
eine Klasse mehr in der Spezifität als `.dark` und würde den Dunkelmodus
|
||
sonst überstimmen.
|
||
|
||
Was ein Theme über Farben hinaus ändern kann: Eckenradien (`--radius-*`, weil
|
||
Tailwind v4 `rounded-xl` zu `var(--radius-xl)` auflöst), Schatten (`--shadow-*`)
|
||
und die Schriften (`--font-sans` für Fliesstext, `--font-display` für
|
||
Überschriften und Feldbeschriftungen - letztere tragen dafür die Klasse
|
||
`field-label`). `rounded-full` ist die eine Ausnahme: dort schreibt Tailwind
|
||
einen festen Wert, deshalb hat Pixel Art dafür eine eigene Regel.
|
||
|
||
Drei Werte bleiben in jedem Theme und Modus gleich: `ink-950` (liegt nur unter
|
||
Dialog-Overlays), `text-white` (steht immer auf farbigen Flächen) und die 39
|
||
festen Hex-Werte für Phasenfarben, Benutzerfarben und Diagramme - die folgen
|
||
bewusst keinem Theme, weil sie Bedeutung tragen und nicht Dekoration sind.
|
||
|
||
Eine bewusste Ausnahme: der **Mailinhalt** bleibt auch im Dunkelmodus auf weissem
|
||
Grund. Fremdes Mail-HTML bringt seine eigenen Farben mit; ein dunkler Hintergrund
|
||
darunter macht schwarze Schrift auf schwarzer Fläche. Der Rahmen liest sich damit
|
||
wie ein Blatt Papier im dunklen Fenster - gewollt.
|