diff --git a/docs/02-datenmodell.md b/docs/02-datenmodell.md
index 38e43e5..97e4ffd 100644
--- a/docs/02-datenmodell.md
+++ b/docs/02-datenmodell.md
@@ -35,6 +35,8 @@ erDiagram
enum role "admin | user"
varchar color "Terminfarbe, NULL = aus Id abgeleitet"
longtext avatar "Data-URL"
+ varchar theme "Farbwelt, NULL = perinco"
+ varchar theme_mode "light | dark, NULL = folgt dem System"
varchar password_hash "bcrypt"
tinyint is_active
}
diff --git a/docs/05-frontend.md b/docs/05-frontend.md
index 49f8de6..7adc15a 100644
--- a/docs/05-frontend.md
+++ b/docs/05-frontend.md
@@ -120,7 +120,7 @@ src/
imageCompress.ts Bilder vor dem Upload verkleinern
components/
Sidebar.tsx Menü inkl. aufklappbarer Gruppen (Kontakte, Scanner)
- Header.tsx Titel, Untertitel, Dark-Mode-Schalter
+ 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),
@@ -132,26 +132,79 @@ src/
ui/ Modal, Avatare, Diktatfeld
```
-## Dark Mode ohne `dark:`-Varianten
+## Themes ohne `dark:`-Varianten
-Die Klasse `dark` auf `` tauscht in `src/index.css` nur die Werte der
-Variablen `canvas`, `surface`, `ink-*`, `brand-*` und der getönten Statusfarben.
-Die Komponenten benutzen durchgängig diese Skalen und folgen damit automatisch -
-kein `dark:` im Markup.
+Die Darstellung hat **zwei unabhängige Achsen**, beide als Klasse am ``:
+
+| 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
- T["Umschalter im Header"] --> P["localStorage perinco_crm_theme"]
- P --> H["class 'dark' auf html"]
- H --> V["Variablen in index.css canvas, surface, ink-*, brand-*"]
- V --> U["Utility-Klassen der Komponenten"]
+ U["Benutzer wählt im Header"] --> DB["crm_users.theme crm_users.theme_mode"]
+ U --> LS["localStorage nur Zwischenspeicher"]
+ DB -->|"beim Anmelden"| H["Klassen am html theme-*, dark"]
+ LS -->|"vor dem ersten Frame"| H
+ H --> V["Variablen in index.css Farben, Radien, Schatten, Schrift"]
+ V --> W["Utility-Klassen der Komponenten"]
SYS["Systemeinstellung"] -.->|"ohne eigene Wahl"| H
```
-Ein Inline-Skript in `index.html` setzt die Klasse vor dem ersten Frame, sonst
-blitzt beim Laden die helle Variante auf. Drei Werte bleiben in beiden Modi
-gleich: `ink-950` (liegt nur unter Dialog-Overlays), `text-white` (steht immer auf
-farbigen Flächen) und `brand-500` (das CI-Grün der Buttons).
+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
diff --git a/docs/07-anleitung-benutzer.md b/docs/07-anleitung-benutzer.md
index ce967e2..d41fc22 100644
--- a/docs/07-anleitung-benutzer.md
+++ b/docs/07-anleitung-benutzer.md
@@ -32,7 +32,7 @@ die Scanner-Einstellungen. Alles andere ist für beide Rollen gleich.
```
┌──────────────┬───────────────────────────────────────────┐
-│ Kategorien │ Titel der Ansicht [Hell/Dunkel] │
+│ Kategorien │ Titel der Ansicht [Design][Hell/Dunkel] │
│ Dashboard │ │
│ Akquise ▾ │ │
│ Eingang │ │
@@ -64,8 +64,25 @@ die Scanner-Einstellungen. Alles andere ist für beide Rollen gleich.
das ist auch die Ansicht, mit der die Anwendung startet.
* Der Knopf mit den drei Strichen oben links klappt das Menü auf Symbolbreite
zusammen - hilfreich auf kleinen Bildschirmen.
-* Rechts oben schaltet zwischen hellem und dunklem Design. Ein **Doppelklick**
- auf den Schalter folgt wieder der Systemeinstellung.
+* Rechts oben stehen zwei Schalter. Der rechte wechselt zwischen **hell und
+ dunkel**; ein **Doppelklick** darauf folgt wieder der Systemeinstellung.
+* Links daneben steht das **Design**. Es gibt drei zur Wahl, jedes in hell und
+ dunkel:
+
+ | Design | Aussehen |
+ | --- | --- |
+ | **perinco** | Das Design des Hauses: Blattgrün auf neutralem Grau |
+ | **Pixel Art** | 8-bit: eckige Kanten, harte Schatten, Pixelschrift in den Überschriften |
+ | **Outdoor** | Wanderkarte: Sandpapier, Bergsee-Türkis, weiche Ecken |
+
+ > **Die Wahl gehört dir, nicht dem Rechner.** Sie wird an deinem Benutzerkonto
+ > gespeichert. Meldest du dich an einem anderen Rechner an, ist dein Design
+ > wieder da - und wenn Kai an derselben Maschine arbeitet, sieht er weiter
+ > sein eigenes. Wer nichts umstellt, bleibt bei perinco.
+
+ Beim Fließtext bleibt Pixel Art bewusst bei der normalen Schrift: die Listen
+ haben bis zu acht Spalten, und eine Pixelschrift wäre darin nicht mehr zu
+ lesen.

* Die Menüpunkte **Akquise**, **Opportunities**, **Kontakte**, **Mails** und
diff --git a/docs/08-anleitung-entwickler.md b/docs/08-anleitung-entwickler.md
index 5fa3988..8b825fa 100644
--- a/docs/08-anleitung-entwickler.md
+++ b/docs/08-anleitung-entwickler.md
@@ -220,8 +220,12 @@ Browser-Download.
* **Deutsch** in Bezeichnern, Kommentaren, Meldungen. Fachbegriffe bleiben
englisch (`Match`, `Opportunity`, `Lead`).
* **Kommentare erklären das Warum.** Was der Code tut, steht im Code.
-* **Kein `dark:` im Markup.** Nur die Skalen `ink-*`, `brand-*`, `canvas`,
- `surface` benutzen, den Rest macht `index.css`.
+* **Kein `dark:` und keine Theme-Klasse im Markup.** Nur die Skalen `ink-*`,
+ `brand-*`, `canvas`, `surface` benutzen, den Rest macht `index.css`. Das ist
+ nicht Kosmetik: daran hängen drei Farbwelten in je zwei Modi, und eine feste
+ Farbe im Markup bricht fünf davon. Die Rollen der Skalen und die Regeln für ein
+ neues Theme stehen in [05 Frontend](05-frontend.md#themes-ohne-dark-varianten) -
+ wichtigste davon: auf `brand-500`/`brand-600` steht weisse Schrift.
* **Gemeinsame Klassen** statt kopierter Utility-Ketten: `inputClass`,
`labelClass` aus `components/leads/leadUi.tsx`, dazu `cardClass`,
`primaryButtonClass` aus `components/scanner/scannerUi.tsx`.
diff --git a/docs/09-quellcode.md b/docs/09-quellcode.md
index e2b90e7..c3cf22a 100644
--- a/docs/09-quellcode.md
+++ b/docs/09-quellcode.md
@@ -247,7 +247,7 @@ Uhrzeit, dieses Jahr Tag und Monat, sonst mit Jahr.
| Datei | Hält | Eigenheit |
| --- | --- | --- |
| `auth.tsx` | Benutzer, `isAdmin`, `login`, `logout`, `updateUser` | prüft beim Start das gespeicherte Token gegen `/auth/me` |
-| `theme.tsx` | hell/dunkel/System | Wahl in `localStorage`, Doppelklick löst sie wieder |
+| `theme.tsx` | Farbwelt (perinco/Pixel Art/Outdoor) und Modus (hell/dunkel/System) | Beides gehört dem Benutzer und steht in `crm_users`; `localStorage` ist nur der Zwischenspeicher fürs Inline-Skript in `index.html`. Doppelklick auf den Modus-Schalter folgt wieder dem System |
| `navigation.tsx` | `currentView`, `goTo(view, { tab, leadId })` | erspart es, Callbacks durch vier Ebenen zu reichen; `leadId` öffnet den Kontakt direkt |
| `whisper.tsx` | Modellstatus, Worker, Fortschritt, `transcribe` | ein Worker für die ganze App |
diff --git a/index.html b/index.html
index b4990ed..e8f75bc 100644
--- a/index.html
+++ b/index.html
@@ -7,25 +7,43 @@
perinco CRM
+
diff --git a/server/package.json b/server/package.json
index 52a725e..7e9c18f 100644
--- a/server/package.json
+++ b/server/package.json
@@ -13,7 +13,7 @@
"dev": "node --watch src/server.js",
"migrate": "node src/migrate.js",
"scanner:import": "node src/scanner/importLegacy.js",
- "test": "node test/mail.test.mjs && node test/acquisition.test.mjs"
+ "test": "node test/mail.test.mjs && node test/acquisition.test.mjs && node test/userFields.test.mjs"
},
"dependencies": {
"bcryptjs": "^2.4.3",
diff --git a/server/src/routes/auth.js b/server/src/routes/auth.js
index 5e03c34..f2bf9cd 100644
--- a/server/src/routes/auth.js
+++ b/server/src/routes/auth.js
@@ -2,7 +2,12 @@ import express from 'express';
import bcrypt from 'bcryptjs';
import { pool } from '../db.js';
import { signToken, requireAuth } from '../middleware/auth.js';
-import { normalizeAvatar, normalizeColor } from '../userFields.js';
+import {
+ normalizeAvatar,
+ normalizeColor,
+ normalizeTheme,
+ normalizeThemeMode,
+} from '../userFields.js';
const router = express.Router();
@@ -15,6 +20,9 @@ function publicUser(row) {
role: row.role,
color: row.color ?? null,
avatar: row.avatar ?? null,
+ // Persoenliche Darstellung; null heisst "Standard" bzw. "folgt dem System".
+ theme: row.theme ?? null,
+ theme_mode: row.theme_mode ?? null,
is_active: Boolean(row.is_active),
created_at: row.created_at,
updated_at: row.updated_at,
@@ -59,10 +67,11 @@ router.get('/me', requireAuth, (req, res) => {
res.json({ user: publicUser(req.user) });
});
-// PATCH /api/auth/me { color?, avatar? }
-// Jeder darf seine eigene Terminfarbe und sein Foto setzen - dafuer braucht
-// es keine Adminrechte. null bzw. "" setzt das Feld zurueck (Farbe: automatisch
-// aus der Palette, Foto: Initiale).
+// PATCH /api/auth/me { color?, avatar?, theme?, theme_mode? }
+// Jeder darf seine eigene Terminfarbe, sein Foto und seine Darstellung setzen -
+// dafuer braucht es keine Adminrechte. null bzw. "" setzt das Feld zurueck
+// (Farbe: automatisch aus der Palette, Foto: Initiale, Design: perinco, Modus:
+// folgt dem System).
router.patch('/me', requireAuth, async (req, res, next) => {
const fields = [];
const values = [];
@@ -71,6 +80,8 @@ router.patch('/me', requireAuth, async (req, res, next) => {
for (const [field, normalize] of [
['color', normalizeColor],
['avatar', normalizeAvatar],
+ ['theme', normalizeTheme],
+ ['theme_mode', normalizeThemeMode],
]) {
if (req.body?.[field] === undefined) continue;
const result = normalize(req.body[field]);
diff --git a/server/src/routes/users.js b/server/src/routes/users.js
index 5d2a211..7e26668 100644
--- a/server/src/routes/users.js
+++ b/server/src/routes/users.js
@@ -31,7 +31,8 @@ function normalizeEmail(value) {
router.get('/directory', async (req, res, next) => {
try {
const [rows] = await pool.query(
- `SELECT id, username, email, name, role, color, avatar, is_active, created_at, updated_at
+ `SELECT id, username, email, name, role, color, avatar, theme, theme_mode, is_active,
+ created_at, updated_at
FROM crm_users ORDER BY name ASC`
);
res.json(rows.map(publicUser));
@@ -65,7 +66,8 @@ function duplicateMessage(err) {
router.get('/', async (req, res, next) => {
try {
const [rows] = await pool.query(
- `SELECT id, username, email, name, role, color, avatar, is_active, created_at, updated_at
+ `SELECT id, username, email, name, role, color, avatar, theme, theme_mode, is_active,
+ created_at, updated_at
FROM crm_users ORDER BY role DESC, name ASC`
);
res.json(rows.map(publicUser));
@@ -105,7 +107,8 @@ router.post('/', async (req, res, next) => {
[username, email, name, role, optional.color, optional.avatar, hash, isActive]
);
const [rows] = await pool.query(
- `SELECT id, username, email, name, role, color, avatar, is_active, created_at, updated_at
+ `SELECT id, username, email, name, role, color, avatar, theme, theme_mode, is_active,
+ created_at, updated_at
FROM crm_users WHERE id = ?`,
[result.insertId]
);
@@ -194,7 +197,8 @@ router.patch('/:id', async (req, res, next) => {
await pool.query(`UPDATE crm_users SET ${fields.join(', ')} WHERE id = ?`, values);
const [rows] = await pool.query(
- `SELECT id, username, email, name, role, color, avatar, is_active, created_at, updated_at
+ `SELECT id, username, email, name, role, color, avatar, theme, theme_mode, is_active,
+ created_at, updated_at
FROM crm_users WHERE id = ?`,
[id]
);
diff --git a/server/src/schema.sql b/server/src/schema.sql
index 996fcc1..44bcae0 100644
--- a/server/src/schema.sql
+++ b/server/src/schema.sql
@@ -24,6 +24,16 @@ ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS color VARCHAR(9) NULL AFTER role;
-- Benutzerfoto als Data-URL oder externe URL - wie bei crm_leads.avatar.
ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS avatar LONGTEXT NULL AFTER color;
+-- Persoenliche Darstellung. Sie haengt am Benutzer und nicht am Browser: wer
+-- sich an einem anderen Rechner anmeldet, findet sein Design wieder, und zwei
+-- Benutzer am selben Rechner kommen sich nicht in die Quere.
+-- theme = Farbwelt ('perinco', 'pixel', 'outdoor'); NULL = 'perinco'
+-- theme_mode = 'light' oder 'dark'; NULL = der Systemeinstellung folgen
+-- Bewusst VARCHAR und kein ENUM: ein weiteres Theme soll eine Zeile im
+-- Frontend sein und keine Datenbankmigration. Geprueft wird in userFields.js.
+ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS theme VARCHAR(32) NULL AFTER avatar;
+ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS theme_mode VARCHAR(16) NULL AFTER theme;
+
CREATE TABLE IF NOT EXISTS crm_leads (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
salutation VARCHAR(32) NULL,
diff --git a/server/src/userFields.js b/server/src/userFields.js
index 6432356..6e36051 100644
--- a/server/src/userFields.js
+++ b/server/src/userFields.js
@@ -34,3 +34,39 @@ export function normalizeAvatar(value) {
}
return { value: avatar };
}
+
+/*
+ * Persoenliche Darstellung.
+ *
+ * Die Liste steht hier und nicht in der Datenbank (kein ENUM): ein weiteres
+ * Theme soll ein Block in src/index.css und eine Zeile hier sein, keine
+ * Migration. Das Gegenstueck fuer das Frontend steht in src/types.ts - wer hier
+ * etwas ergaenzt, muss dort mitziehen (server/test/userFields.test.mjs prueft,
+ * dass beide Listen gleich bleiben).
+ *
+ * Ein unbekannter Wert wird abgewiesen und nicht stillschweigend auf den
+ * Standard gesetzt: sonst faellt ein Tippfehler im Frontend nie auf, die
+ * Oberflaeche zeigt weiter das alte Design und niemand weiss, warum.
+ */
+export const THEMES = ['perinco', 'pixel', 'outdoor'];
+export const THEME_MODES = ['light', 'dark'];
+
+/** Farbwelt; null bzw. '' bedeutet "perinco", der Standard. */
+export function normalizeTheme(value) {
+ if (value === null || value === undefined || String(value).trim() === '') return { value: null };
+ const theme = String(value).trim().toLowerCase();
+ if (!THEMES.includes(theme)) {
+ return { error: `Unbekanntes Design (erlaubt: ${THEMES.join(', ')}).` };
+ }
+ return { value: theme };
+}
+
+/** Hell oder dunkel; null bzw. '' bedeutet "der Systemeinstellung folgen". */
+export function normalizeThemeMode(value) {
+ if (value === null || value === undefined || String(value).trim() === '') return { value: null };
+ const mode = String(value).trim().toLowerCase();
+ if (!THEME_MODES.includes(mode)) {
+ return { error: `Unbekannter Modus (erlaubt: ${THEME_MODES.join(', ')}).` };
+ }
+ return { value: mode };
+}
diff --git a/server/test/userFields.test.mjs b/server/test/userFields.test.mjs
new file mode 100644
index 0000000..9afd762
--- /dev/null
+++ b/server/test/userFields.test.mjs
@@ -0,0 +1,67 @@
+/*
+ * Pruefungen der Benutzerfelder: laeuft mit `npm test` im Verzeichnis server/.
+ *
+ * Der eigentliche Zweck ist der letzte Block: die Liste der Farbwelten steht
+ * zweimal - hier in userFields.js (das Backend weist Unbekanntes ab) und in
+ * src/types.ts (das Frontend baut daraus die Auswahl). Laufen die beiden
+ * auseinander, faellt es sonst niemandem auf: das Auswahlfeld zeigt ein Design
+ * an, das Speichern schlaegt fehl, und die Oberflaeche bleibt beim alten
+ * Aussehen.
+ */
+import { readFileSync } from 'node:fs';
+
+const { normalizeTheme, normalizeThemeMode, normalizeColor, THEMES, THEME_MODES } = await import(
+ '../src/userFields.js'
+);
+
+let failures = 0;
+
+function check(name, condition, detail = '') {
+ if (condition) {
+ console.log(` ok ${name}${detail ? ` - ${detail}` : ''}`);
+ } else {
+ console.log(` FAIL ${name}${detail ? ` - ${detail}` : ''}`);
+ failures += 1;
+ }
+}
+
+console.log('\nFarbwelt');
+check('Bekanntes Design', normalizeTheme('pixel').value === 'pixel');
+check('Gross geschrieben', normalizeTheme('Outdoor').value === 'outdoor');
+check('Mit Leerzeichen', normalizeTheme(' perinco ').value === 'perinco');
+check('Leer wird null', normalizeTheme('').value === null);
+check('null bleibt null', normalizeTheme(null).value === null);
+check('Unbekanntes wird abgewiesen', Boolean(normalizeTheme('barbie').error));
+check(
+ 'Die Fehlermeldung nennt die erlaubten Werte',
+ (normalizeTheme('barbie').error ?? '').includes('outdoor')
+);
+
+console.log('\nModus');
+check('hell', normalizeThemeMode('light').value === 'light');
+check('dunkel', normalizeThemeMode('dark').value === 'dark');
+check('Leer heisst "folgt dem System"', normalizeThemeMode('').value === null);
+check('Unbekanntes wird abgewiesen', Boolean(normalizeThemeMode('auto').error));
+
+console.log('\nFarbe (unveraendert, als Regressionsschutz)');
+check('Hex wird angenommen', normalizeColor('#6BA04A').value === '#6ba04a');
+check('Kurzform wird abgewiesen', Boolean(normalizeColor('#fff').error));
+
+console.log('\nBackend und Frontend kennen dieselben Designs');
+const types = readFileSync(new URL('../../src/types.ts', import.meta.url), 'utf8');
+const match = /export const THEME_PALETTES = \[([^\]]*)\]/.exec(types);
+check('THEME_PALETTES in src/types.ts gefunden', Boolean(match));
+if (match) {
+ const frontend = [...match[1].matchAll(/'([^']+)'/g)].map((entry) => entry[1]);
+ check(
+ 'Beide Listen sind gleich',
+ JSON.stringify(frontend) === JSON.stringify(THEMES),
+ `Frontend ${JSON.stringify(frontend)} / Backend ${JSON.stringify(THEMES)}`
+ );
+}
+check('Es gibt genau zwei Modi', THEME_MODES.length === 2);
+
+console.log(
+ failures === 0 ? '\nAlle Pruefungen bestanden.' : `\n${failures} Pruefung(en) fehlgeschlagen.`
+);
+process.exit(failures === 0 ? 0 : 1);
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index eed0f00..0619f96 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import { useTheme } from '../lib/theme';
+import { THEME_PALETTES, THEME_PALETTE_LABELS } from '../types';
interface HeaderProps {
/** Beschriftung der Ansicht - meist ihr Name, siehe viewTitle(). */
@@ -13,7 +14,7 @@ const SUN_ICON =
const MOON_ICON = 'M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z';
export const Header: React.FC = ({ title, subtitle, onToggleSidebar }) => {
- const { theme, preference, toggle, useSystem } = useTheme();
+ const { theme, preference, toggle, useSystem, palette, setPalette } = useTheme();
const isDark = theme === 'dark';
return (
@@ -35,6 +36,29 @@ export const Header: React.FC = ({ title, subtitle, onToggleSidebar
+ {/*
+ Farbwelt. Ein Auswahlfeld und keine Knopfreihe: es kommen weitere
+ Designs dazu, und drei bis fuenf Knoepfe im Kopf wuerden den Titel der
+ Ansicht verdraengen. Jede Wahl gilt nur fuer den angemeldeten
+ Benutzer - sie steht an seinem Konto, nicht im Browser.
+ */}
+
+
+
{/* Doppelklick loest die manuelle Wahl und folgt wieder dem System. */}