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. ![Dieselbe Ansicht im dunklen Design](bilder/aus-mails-dunkel.png) * 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. */}