Neben dem Design des Hauses ("perinco") gibt es "Pixel Art" und
"Outdoor", jedes in hell und dunkel. Die Wahl steht neben dem
Hell/Dunkel-Schalter im Kopf der App.
Sie gehoert dem BENUTZER und nicht dem Browser: crm_users.theme und
crm_users.theme_mode, gesetzt ueber PATCH /api/auth/me, das jeder fuer
sich selbst darf. 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 als Zwischenspeicher daneben, damit
das Inline-Skript in index.html die Klassen vor dem ersten Frame setzen
kann - ohne das blitzt beim Laden das Standarddesign auf. Das hat eine
sichtbare Folge: melden sich zwei Leute an demselben Rechner an, sieht
der zweite fuer einen Augenblick das Design des ersten.
Der ThemeProvider steht deshalb in main.tsx jetzt unter dem AuthProvider
- er liest den Benutzer.
Keine Komponente wurde umgefaerbt. Im Markup steht nirgends eine feste
Farbe, sondern durchgaengig ink-*, brand-*, canvas und surface; Tailwind
v4 loest auch --radius-*, --shadow-* und die Schriften ueber Variablen
auf. Ein Theme tauscht also nur Werte, und deshalb kann Pixel Art alle
380 abgerundeten Ecken eckig machen, ohne eine Klasse anzufassen.
Zwei Dinge waren Voraussetzung:
- Der helle Block eines Themes braucht :not(.dark). html.theme-x hat
eine Klasse mehr in der Spezifitaet als .dark und wuerde den
Dunkelmodus sonst ueberstimmen.
- Auf brand-500/600 steht 69 Mal weisse Schrift. Jedes Theme braucht
dort einen ausreichend dunklen Ton - ein helles Pastell waere
unlesbar.
Die Pixelschrift (Silkscreen) sitzt nur in --font-display, also in den
Ueberschriften und den Feldbeschriftungen; labelClass tragt dafuer die
neue Klasse "field-label". Der Fliesstext bleibt Nunito Sans: die Listen
haben bis zu acht Spalten bei 11-13 px, und eine Pixelschrift waere darin
nicht mehr zu lesen. Outdoor bringt Cabin fuer beides mit.
server/test/userFields.test.mjs prueft die Werte und dass die Liste der
Farbwelten in userFields.js und src/types.ts nicht auseinanderlaeuft -
sonst zeigt die Auswahl ein Design an, dessen Speichern das Backend
abweist, und niemand weiss warum.
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` | 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.
|