feat(ui): vierte Farbwelt "Cyberpunk" in hell und dunkel

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>
This commit is contained in:
Sascha Wolf
2026-09-09 13:47:08 +02:00
co-authored by Claude Opus 5
parent 87759a8a24
commit 8403f1c39d
8 changed files with 162 additions and 19 deletions
+1 -1
View File
@@ -138,7 +138,7 @@ Die Darstellung hat **zwei unabhängige Achsen**, beide als Klasse am `<html>`:
| Achse | Klasse | Werte | | Achse | Klasse | Werte |
| --- | --- | --- | | --- | --- | --- |
| Farbwelt | `theme-perinco`, `theme-pixel`, `theme-outdoor` | perinco ist der Standard | | Farbwelt | `theme-perinco`, `theme-pixel`, `theme-outdoor`, `theme-cyberpunk` | perinco ist der Standard |
| Modus | `dark` bzw. keine | hell, dunkel, oder der Systemeinstellung folgen | | Modus | `dark` bzw. keine | hell, dunkel, oder der Systemeinstellung folgen |
`src/index.css` tauscht darin nur Variablenwerte: `canvas`, `surface`, `ink-*`, `src/index.css` tauscht darin nur Variablenwerte: `canvas`, `surface`, `ink-*`,
+2 -1
View File
@@ -66,7 +66,7 @@ die Scanner-Einstellungen. Alles andere ist für beide Rollen gleich.
zusammen - hilfreich auf kleinen Bildschirmen. zusammen - hilfreich auf kleinen Bildschirmen.
* Rechts oben stehen zwei Schalter. Der rechte wechselt zwischen **hell und * Rechts oben stehen zwei Schalter. Der rechte wechselt zwischen **hell und
dunkel**; ein **Doppelklick** darauf folgt wieder der Systemeinstellung. dunkel**; ein **Doppelklick** darauf folgt wieder der Systemeinstellung.
* Links daneben steht das **Design**. Es gibt drei zur Wahl, jedes in hell und * Links daneben steht das **Design**. Es gibt vier zur Wahl, jedes in hell und
dunkel: dunkel:
| Design | Aussehen | | Design | Aussehen |
@@ -74,6 +74,7 @@ die Scanner-Einstellungen. Alles andere ist für beide Rollen gleich.
| **perinco** | Das Design des Hauses: Blattgrün auf neutralem Grau | | **perinco** | Das Design des Hauses: Blattgrün auf neutralem Grau |
| **Pixel Art** | 8-bit: eckige Kanten, harte Schatten, Pixelschrift in den Überschriften | | **Pixel Art** | 8-bit: eckige Kanten, harte Schatten, Pixelschrift in den Überschriften |
| **Outdoor** | Wanderkarte: Sandpapier, Bergsee-Türkis, weiche Ecken | | **Outdoor** | Wanderkarte: Sandpapier, Bergsee-Türkis, weiche Ecken |
| **Cyberpunk** | Neon in der Nacht: scharfe Kanten, violettes Leuchten, Blaustahl |
> **Die Wahl gehört dir, nicht dem Rechner.** Sie wird an deinem Benutzerkonto > **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 > gespeichert. Meldest du dich an einem anderen Rechner an, ist dein Design
+2 -2
View File
@@ -222,8 +222,8 @@ Browser-Download.
* **Kommentare erklären das Warum.** Was der Code tut, steht im Code. * **Kommentare erklären das Warum.** Was der Code tut, steht im Code.
* **Kein `dark:` und keine Theme-Klasse im Markup.** Nur die Skalen `ink-*`, * **Kein `dark:` und keine Theme-Klasse im Markup.** Nur die Skalen `ink-*`,
`brand-*`, `canvas`, `surface` benutzen, den Rest macht `index.css`. Das ist `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 nicht Kosmetik: daran hängen vier 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 Farbe im Markup bricht sieben davon. Die Rollen der Skalen und die Regeln für ein
neues Theme stehen in [05 Frontend](05-frontend.md#themes-ohne-dark-varianten) - neues Theme stehen in [05 Frontend](05-frontend.md#themes-ohne-dark-varianten) -
wichtigste davon: auf `brand-500`/`brand-600` steht weisse Schrift. wichtigste davon: auf `brand-500`/`brand-600` steht weisse Schrift.
* **Gemeinsame Klassen** statt kopierter Utility-Ketten: `inputClass`, * **Gemeinsame Klassen** statt kopierter Utility-Ketten: `inputClass`,
+1 -1
View File
@@ -247,7 +247,7 @@ Uhrzeit, dieses Jahr Tag und Monat, sonst mit Jahr.
| Datei | Hält | Eigenheit | | Datei | Hält | Eigenheit |
| --- | --- | --- | | --- | --- | --- |
| `auth.tsx` | Benutzer, `isAdmin`, `login`, `logout`, `updateUser` | prüft beim Start das gespeicherte Token gegen `/auth/me` | | `auth.tsx` | Benutzer, `isAdmin`, `login`, `logout`, `updateUser` | prüft beim Start das gespeicherte Token gegen `/auth/me` |
| `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 | | `theme.tsx` | Farbwelt (perinco/Pixel Art/Outdoor/Cyberpunk) 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 | | `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 | | `whisper.tsx` | Modellstatus, Worker, Fortschritt, `transcribe` | ein Worker für die ganze App |
+5 -5
View File
@@ -26,7 +26,7 @@
var palette = localStorage.getItem('perinco_crm_palette'); var palette = localStorage.getItem('perinco_crm_palette');
// Whitelist, damit ein manipulierter Eintrag keine Klasse setzt. // Whitelist, damit ein manipulierter Eintrag keine Klasse setzt.
if (palette === 'pixel' || palette === 'outdoor') { if (palette === 'pixel' || palette === 'outdoor' || palette === 'cyberpunk') {
document.documentElement.classList.add('theme-' + palette); document.documentElement.classList.add('theme-' + palette);
} }
} catch (e) { } catch (e) {
@@ -38,12 +38,12 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- <!--
Nunito Sans traegt perinco und Pixel Art (Fliesstext), Cabin das Outdoor- Nunito Sans traegt perinco und Pixel Art (Fliesstext), Cabin das Outdoor-
Design, Silkscreen die Ueberschriften von Pixel Art. Alle drei in einer Design, Silkscreen die Ueberschriften von Pixel Art, Chakra Petch das
Anfrage: ein Nachladen beim Umschalten wuerde die Schrift sichtbar Cyberpunk-Design. Alle in einer Anfrage: ein Nachladen beim Umschalten
springen lassen, und Silkscreen wiegt nur wenige Kilobyte. wuerde die Schrift sichtbar springen lassen.
--> -->
<link <link
href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,300;0,6..12,400;0,6..12,600;0,6..12,700;0,6..12,800&family=Cabin:wght@400;500;600;700&family=Silkscreen:wght@400;700&display=swap" href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,300;0,6..12,400;0,6..12,600;0,6..12,700;0,6..12,800&family=Cabin:wght@400;500;600;700&family=Silkscreen:wght@400;700&family=Chakra+Petch:wght@400;500;600;700&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
</head> </head>
+1 -1
View File
@@ -48,7 +48,7 @@ export function normalizeAvatar(value) {
* Standard gesetzt: sonst faellt ein Tippfehler im Frontend nie auf, die * Standard gesetzt: sonst faellt ein Tippfehler im Frontend nie auf, die
* Oberflaeche zeigt weiter das alte Design und niemand weiss, warum. * Oberflaeche zeigt weiter das alte Design und niemand weiss, warum.
*/ */
export const THEMES = ['perinco', 'pixel', 'outdoor']; export const THEMES = ['perinco', 'pixel', 'outdoor', 'cyberpunk'];
export const THEME_MODES = ['light', 'dark']; export const THEME_MODES = ['light', 'dark'];
/** Farbwelt; null bzw. '' bedeutet "perinco", der Standard. */ /** Farbwelt; null bzw. '' bedeutet "perinco", der Standard. */
+145 -7
View File
@@ -10,9 +10,9 @@
*/ */
/* /*
* Drei Farbwelten, jede in hell und dunkel. Sie haengen an einer Klasse am * Vier Farbwelten, jede in hell und dunkel. Sie haengen an einer Klasse am
* <html> (theme-pixel, theme-outdoor); ohne Klasse gilt perinco. Der Modus * <html> (theme-pixel, theme-outdoor, theme-cyberpunk); ohne Klasse gilt
* bleibt die vorhandene Klasse "dark". * perinco. Der Modus bleibt die vorhandene Klasse "dark".
* *
* Warum das ohne Aenderung an den Komponenten geht: im Markup steht nirgends * Warum das ohne Aenderung an den Komponenten geht: im Markup steht nirgends
* eine feste Farbe, sondern durchgaengig ink-*, brand-*, canvas und surface - * eine feste Farbe, sondern durchgaengig ink-*, brand-*, canvas und surface -
@@ -36,10 +36,10 @@
--font-sans: 'Avenir Next', 'Avenir', 'Nunito Sans', ui-sans-serif, system-ui, sans-serif; --font-sans: 'Avenir Next', 'Avenir', 'Nunito Sans', ui-sans-serif, system-ui, sans-serif;
/* /*
* Zweite Schrift fuer Ueberschriften und Feldbeschriftungen. Bei perinco und * Zweite Schrift fuer Ueberschriften und Feldbeschriftungen. Meist ist sie mit
* Outdoor ist sie mit der Grundschrift identisch, nur "Pixel Art" setzt hier * der Grundschrift identisch; nur "Pixel Art" setzt hier eine andere ein - die
* eine Pixelschrift ein - ohne dass ein Theme dafuer eigene Selektoren * Pixelschrift bleibt damit aus dem Fliesstext heraus, ohne dass es dafuer
* braucht. * eigene Selektoren braucht.
*/ */
--font-display: 'Avenir Next', 'Avenir', 'Nunito Sans', ui-sans-serif, system-ui, sans-serif; --font-display: 'Avenir Next', 'Avenir', 'Nunito Sans', ui-sans-serif, system-ui, sans-serif;
@@ -358,6 +358,127 @@ html.theme-pixel.dark {
--font-display: 'Silkscreen', 'Courier New', ui-monospace, monospace; --font-display: 'Silkscreen', 'Courier New', ui-monospace, monospace;
} }
/* ===========================================================================
* Cyberpunk
*
* Neon in der Nacht: fast schwarze Flaechen mit blauem Stich, violettes
* Leuchten, Kanten so scharf, dass die Karten wie HUD-Elemente wirken (2 px
* statt 12 px Radius - nicht ganz eckig, das ist schon Pixel Art).
*
* 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 drauf steht.
*
* Schatten sind hier kein Schatten, sondern ein Schein: eine farbige Kontur
* plus weicher Halo in der Markenfarbe.
* =========================================================================== */
html.theme-cyberpunk.dark {
--color-canvas: #07080f;
--color-surface: #0f1120;
--color-brand-50: #1a0e2e;
--color-brand-100: #24123f;
--color-brand-200: #4a1f7a;
--color-brand-300: #5f27a0;
--color-brand-400: #7a30cc;
--color-brand-500: #8b31f0;
--color-brand-600: #a24cff;
--color-brand-700: #c79bff;
--color-brand-800: #ddc4ff;
--color-brand-900: #efe3ff;
--color-brand-accent: #00e5ff;
/* Blaustahl statt Neutralgrau - Grau waere hier zu brav. */
--color-ink-50: #141728;
--color-ink-100: #1c2135;
--color-ink-200: #2b4560;
--color-ink-300: #6b7899;
--color-ink-400: #8e9bbd;
--color-ink-500: #a9b5d4;
--color-ink-600: #c1cbe4;
--color-ink-700: #d6ddf0;
--color-ink-800: #e9eefa;
--color-ink-900: #f7f9ff;
--color-ink-950: #000000;
--radius-sm: 0.125rem;
--radius-md: 0.125rem;
--radius-lg: 0.1875rem;
--radius-xl: 0.25rem;
--radius-2xl: 0.375rem;
--shadow-card: 0 0 0 1px rgb(139 49 240 / 0.16), 0 0 22px rgb(139 49 240 / 0.2);
--shadow-lift: 0 0 0 1px rgb(139 49 240 / 0.3), 0 0 40px rgb(139 49 240 / 0.35);
--font-sans: 'Chakra Petch', 'Avenir Next', 'Nunito Sans', ui-sans-serif, system-ui, sans-serif;
--font-display: 'Chakra Petch', 'Avenir Next', 'Nunito Sans', ui-sans-serif, system-ui, sans-serif;
/* Die getoenten Pillen aus .dark ins Neon gezogen. */
--color-sky-50: #04202e;
--color-sky-200: #0b4a63;
--color-sky-700: #22d3ee;
--color-amber-50: #2b1f04;
--color-amber-200: #57400a;
--color-amber-700: #ffcc4d;
--color-violet-50: #1d1140;
--color-violet-200: #382068;
--color-violet-700: #d0a2ff;
--color-red-50: #2c0e1c;
--color-red-200: #5c1c34;
--color-red-700: #ff6b8a;
}
/*
* Cyberpunk, hell: Konzern bei Tageslicht. Kuehles Blaugrau statt warmem
* Weiss, dasselbe Violett - nur dunkler, damit weisse Schrift darauf haelt.
* Die scharfen Kanten bleiben, das Leuchten wird zu einem knappen kalten
* Schatten: ein Halo auf hellem Grund sieht nach Fehler aus, nicht nach Neon.
*/
html.theme-cyberpunk:not(.dark) {
--color-canvas: #eceff7;
--color-surface: #ffffff;
--color-brand-50: #f3ecff;
--color-brand-100: #e6d9ff;
--color-brand-200: #c9adff;
--color-brand-300: #a97bff;
--color-brand-400: #8d4df0;
--color-brand-500: #7326d6;
--color-brand-600: #5d1cb0;
--color-brand-700: #4a1590;
--color-brand-800: #380f6e;
--color-brand-900: #290a52;
--color-brand-accent: #7326d6;
--color-ink-50: #f2f4fb;
--color-ink-100: #e4e8f4;
--color-ink-200: #c3cbe0;
--color-ink-300: #98a2bd;
--color-ink-400: #5d6786;
--color-ink-500: #4a5473;
--color-ink-600: #3a4260;
--color-ink-700: #2c334d;
--color-ink-800: #1e2438;
--color-ink-900: #121728;
--color-ink-950: #0a0d18;
--radius-sm: 0.125rem;
--radius-md: 0.125rem;
--radius-lg: 0.1875rem;
--radius-xl: 0.25rem;
--radius-2xl: 0.375rem;
--shadow-card: 0 1px 2px rgb(18 23 40 / 0.06), 0 6px 18px rgb(115 38 214 / 0.1);
--shadow-lift: 0 12px 30px rgb(115 38 214 / 0.18);
--font-sans: 'Chakra Petch', 'Avenir Next', 'Nunito Sans', ui-sans-serif, system-ui, sans-serif;
--font-display: 'Chakra Petch', 'Avenir Next', 'Nunito Sans', ui-sans-serif, system-ui, sans-serif;
}
@layer base { @layer base {
html, html,
body { body {
@@ -414,6 +535,23 @@ html.theme-pixel.dark {
font-smooth: never; font-smooth: never;
} }
/*
* Cyberpunk laesst die Ueberschriften atmen: die negative Laufweite zieht
* Chakra Petch unangenehm zusammen, gesperrt liest es sich nach Anzeigetafel.
*/
html.theme-cyberpunk h1,
html.theme-cyberpunk h2,
html.theme-cyberpunk h3,
html.theme-cyberpunk h4,
html.theme-cyberpunk h5,
html.theme-cyberpunk h6 {
letter-spacing: 0.015em;
}
html.theme-cyberpunk .field-label {
letter-spacing: 0.12em;
}
/* Fokus-Ring durchgaengig in CI-Gruen */ /* Fokus-Ring durchgaengig in CI-Gruen */
:focus-visible { :focus-visible {
outline: 2px solid var(--color-brand-500); outline: 2px solid var(--color-brand-500);
+5 -1
View File
@@ -88,7 +88,7 @@ export type Role = 'admin' | 'user';
* Tippfehler hier nicht still zum Standarddesign fuehrt. * Tippfehler hier nicht still zum Standarddesign fuehrt.
* server/test/userFields.test.mjs prueft, dass beide Listen gleich bleiben. * server/test/userFields.test.mjs prueft, dass beide Listen gleich bleiben.
*/ */
export const THEME_PALETTES = ['perinco', 'pixel', 'outdoor'] as const; export const THEME_PALETTES = ['perinco', 'pixel', 'outdoor', 'cyberpunk'] as const;
export type ThemePalette = (typeof THEME_PALETTES)[number]; export type ThemePalette = (typeof THEME_PALETTES)[number];
@@ -97,6 +97,10 @@ export const THEME_PALETTE_LABELS: Record<ThemePalette, { name: string; hint: st
perinco: { name: 'perinco', hint: 'Das Design des Hauses - Blattgrün auf neutralem Grau' }, perinco: { name: 'perinco', hint: 'Das Design des Hauses - Blattgrün auf neutralem Grau' },
pixel: { name: 'Pixel Art', hint: '8-bit: eckige Kanten, harte Schatten, Pixelschrift' }, pixel: { name: 'Pixel Art', hint: '8-bit: eckige Kanten, harte Schatten, Pixelschrift' },
outdoor: { name: 'Outdoor', hint: 'Wanderkarte: Sandpapier, Bergsee-Türkis, weiche Ecken' }, outdoor: { name: 'Outdoor', hint: 'Wanderkarte: Sandpapier, Bergsee-Türkis, weiche Ecken' },
cyberpunk: {
name: 'Cyberpunk',
hint: 'Neon in der Nacht: scharfe Kanten, violettes Leuchten, Chakra Petch',
},
}; };
export interface AppUser { export interface AppUser {