From 3c56cf83612de5aa1ed966144cf37353337fab9d Mon Sep 17 00:00:00 2001 From: Sascha Wolf Date: Thu, 17 Sep 2026 18:49:45 +0200 Subject: [PATCH] feat(design): neues Theme Aura (hell Sunshine, dunkel Mond) Sonne bzw. Mond in der Ecke oben rechts der Leiste, dahinter ziehen Wolken in zwei Lagen langsam von links nach rechts (reines CSS, nahtlose Kacheln). Im hellen Modus fallen alle 16-24 s zwei bis drei Sonnenstrahlen diagonal ueber den Bildschirm. Bernstein bei Tag, Lavendel und Sterne bei Nacht. Bei prefers-reduced-motion still. Co-Authored-By: Claude Opus 5 (1M context) --- docs/05-frontend.md | 29 +++- docs/07-anleitung-benutzer.md | 13 +- docs/09-quellcode.md | 2 +- index.html | 3 +- public/themes/aura-clouds-far.svg | 19 +++ public/themes/aura-clouds-near.svg | 18 +++ public/themes/aura-moon.svg | 25 ++++ public/themes/aura-sidebar.svg | 20 +++ public/themes/aura-stars.svg | 15 ++ public/themes/aura-sun.svg | 22 +++ server/src/userFields.js | 2 +- src/components/AuraWeather.tsx | 120 ++++++++++++++++ src/index.css | 212 ++++++++++++++++++++++++++++- src/lib/theme.tsx | 6 +- src/types.ts | 6 +- 15 files changed, 497 insertions(+), 15 deletions(-) create mode 100644 public/themes/aura-clouds-far.svg create mode 100644 public/themes/aura-clouds-near.svg create mode 100644 public/themes/aura-moon.svg create mode 100644 public/themes/aura-sidebar.svg create mode 100644 public/themes/aura-stars.svg create mode 100644 public/themes/aura-sun.svg create mode 100644 src/components/AuraWeather.tsx diff --git a/docs/05-frontend.md b/docs/05-frontend.md index 6312d03..dcb959d 100644 --- a/docs/05-frontend.md +++ b/docs/05-frontend.md @@ -138,7 +138,7 @@ Die Darstellung hat **zwei unabhängige Achsen**, beide als Klasse am ``: | Achse | Klasse | Werte | | --- | --- | --- | -| Farbwelt | `theme-perinco`, `theme-pixel`, `theme-outdoor`, `theme-cyberpunk`, `theme-rainy`, `theme-snow` | perinco ist der Standard | +| Farbwelt | `theme-perinco`, `theme-pixel`, `theme-outdoor`, `theme-cyberpunk`, `theme-rainy`, `theme-snow`, `theme-aura` | perinco ist der Standard | | Modus | `dark` bzw. keine | hell, dunkel, oder der Systemeinstellung folgen | `src/index.css` tauscht darin nur Variablenwerte: `canvas`, `surface`, `ink-*`, @@ -212,9 +212,9 @@ Das ist kein Umweg, sondern erledigt zwei Dinge zugleich: > nur Licht - über Dunkel leuchtet das Neon, über Weiss bleiben blasse farbige > Linien. Wer eine Grafik ergänzt: **keine deckenden Flächen**, nur Motiv. -### Rainy und Snow: Themes mit Wetter +### Rainy, Snow und Aura: Themes mit Wetter -Rainy und Snow sind die Themes mit Bewegung. Hier zuerst Rainy, Snow folgt unten. Farben und Grafiken funktionieren wie +Rainy, Snow und Aura sind die Themes mit Bewegung. Hier zuerst Rainy, Snow und Aura folgen unten. Farben und Grafiken funktionieren wie bei den anderen; dazu kommt `src/components/RainyWeather.tsx`, das der `ThemeProvider` nur bei aktivem Rainy einhängt - auch auf der Anmeldeseite, und beim Wechsel auf ein anderes Theme verschwindet es samt Animationsschleife. @@ -262,6 +262,29 @@ neue Spaltenzahl umgerechnet, nach dem Neuladen ist der Boden frei. > gleichzeitig landen also nur etwa 5 je Sekunde. Wer die Wachstumszeit > ändert, rechnet mit dieser Zahl. +#### Aura + +Ein Theme mit zwei Namen: hell heißt es **Sunshine**, dunkel **Mond**. Beides +ist `theme-aura`, der Unterschied kommt wie immer aus `.dark`. + +| Teil | Umsetzung | +| --- | --- | +| **Leiste** | Vier Hintergrundebenen statt zwei: Sonne bzw. Mond (`aura-sun.svg`, `aura-moon.svg`, 130 px in der Ecke oben rechts, halb angeschnitten), Schleier, Sterne (`aura-stars.svg`, nur nachts) und ein Himmelsverlauf. Weil jede Ebene eigene Lage und Größe braucht, stehen die in `--aura-header-position` und `--aura-header-size` | +| **Ziehende Wolken** | Reines CSS: `.app-header::before` mit zwei nahtlosen 800-px-Kacheln (`aura-clouds-near.svg`, `aura-clouds-far.svg`). `@keyframes aura-clouds` schiebt sie in 150 s um genau 1600 bzw. 800 px - Neustart ohne Ruck, vordere Lage doppelt so schnell. Nachts auf `--aura-cloud-opacity` 0,28 abgedunkelt | +| **Sonnenstrahlen** | `src/components/AuraWeather.tsx`, nur hell. Alle 16–24 s zwei oder drei `.aura-ray`-Elemente: vom Sonnenmittelpunkt in Richtung Ecke unten links, leicht aufgefächert, per `clip-path` zur Sonne hin verjüngt und weichgezeichnet. Web Animations API: 4 s anschwellen, leicht schwenken, verblassen, danach entfernt | + +Zwei Stolperstellen: + +1. **Wolken hinter die Schrift.** `::before` mit `z-index: -1` verschwände + hinter dem Hintergrund der Leiste. `isolation: isolate` an `.app-header` + macht sie zum eigenen Stapelkontext - dann liegen die Wolken über dem + Himmel, aber unter Titel und Knöpfen. `overflow: hidden` an der Leiste wäre + der naheliegende Weg und ist falsch: sie enthält Aufklappmenüs. +2. **Sonne und Strahlen gehören zusammen.** `SUN_FROM_RIGHT`/`SUN_FROM_TOP` in + `AuraWeather.tsx` sind aus Position und Größe der Sonne in `index.css` + gerechnet. Wer die Sonne verschiebt, muss beide mitziehen, sonst kommen die + Strahlen aus dem Nichts. + ### Was ein neues Theme können muss Die Rollen der Skalen sind über die ganze App gleich; ein Theme, das sie diff --git a/docs/07-anleitung-benutzer.md b/docs/07-anleitung-benutzer.md index 3e666fa..4b07877 100644 --- a/docs/07-anleitung-benutzer.md +++ b/docs/07-anleitung-benutzer.md @@ -66,7 +66,7 @@ die Scanner-Einstellungen. Alles andere ist für beide Rollen gleich. zusammen - hilfreich auf kleinen Bildschirmen. * 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 sechs zur Wahl, jedes in hell und +* Links daneben steht das **Design**. Es gibt sieben zur Wahl, jedes in hell und dunkel: | Design | Aussehen | @@ -77,6 +77,7 @@ die Scanner-Einstellungen. Alles andere ist für beide Rollen gleich. | **Cyberpunk** | Neon in der Nacht: scharfe Kanten, violettes Leuchten, Blaustahl | | **Rainy** | Regenwetter: Schieferblau, leichter Nieselregen, Wolken mit Wetterleuchten | | **Snow** | Winter: Eisblau, Schneefall, der sich am unteren Rand zu einer Schneedecke sammelt | + | **Aura** | Hell „Sunshine“: Sonne, Himmelblau, warmes Bernstein; dunkel „Mond“: Mond, Sterne, Lavendel | > **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 @@ -90,11 +91,11 @@ die Scanner-Einstellungen. Alles andere ist für beide Rollen gleich. Die Designs neben perinco haben zusätzlich eine **Grafik** hinter der oberen Leiste und der Seitenleiste: 8-bit-Himmel und Steinmauer, blauer Himmel und Wiese, Neonröhren und Regen, Regenwolken und nasse Hügel, verschneite - Berge und Tannen. Sie liegt hinter einem Schleier, damit die Schrift darauf + Berge und Tannen, Sonne bzw. Mond mit Himmel. Sie liegt hinter einem Schleier, damit die Schrift darauf lesbar bleibt, und ist dort am deutlichsten, wo die Leisten frei sind - in der Mitte oben und unten links. perinco bleibt einfarbig. - **Rainy** und **Snow** sind die Designs, in denen sich etwas bewegt. Das + **Rainy**, **Snow** und **Aura** sind die Designs, in denen sich etwas bewegt. Das Wetter liegt nur optisch über der Oberfläche: es fängt keine Klicks ab, und geöffnete Dialoge liegen darüber. @@ -107,9 +108,13 @@ die Scanner-Einstellungen. Alles andere ist für beide Rollen gleich. kleines **Räumfahrzeug** in perinco-Grün mit Rundumleuchte vorbei und schiebt den Schnee vom Bildschirm - abwechselnd von links nach rechts und zurück. Die erste Fahrt startet gleich nach dem Laden der Seite. + * Bei **Aura** ziehen in der oberen Leiste langsam Wolken von links nach + rechts. Im hellen Modus („Sunshine“) fallen etwa alle 20 Sekunden zwei bis + drei Sonnenstrahlen von der Sonne oben rechts quer über den Bildschirm nach + unten links; im dunklen Modus („Mond“) steht dort der Mond, ohne Strahlen. Wer im Betriebssystem **„Bewegung reduzieren"** eingeschaltet hat, sieht bei - beiden nur Farben und Grafiken - ohne Regen, Blitz und Schnee. + allen nur Farben und Grafiken - ohne Regen, Blitz, Schnee, Wolkenzug und Sonnenstrahlen. ![Dieselbe Ansicht im dunklen Design](bilder/aus-mails-dunkel.png) * Die Menüpunkte **Akquise**, **Opportunities**, **Kontakte**, **Mails** und diff --git a/docs/09-quellcode.md b/docs/09-quellcode.md index 3df1680..a8aced6 100644 --- a/docs/09-quellcode.md +++ b/docs/09-quellcode.md @@ -295,7 +295,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` | Farbwelt (perinco/Pixel Art/Outdoor/Cyberpunk/Rainy/Snow, Hintergrundgrafiken in `public/themes/`; bei Rainy hängt er zusätzlich `components/RainyWeather.tsx` mit Regen und Wetterleuchten ein, bei Snow `components/SnowWeather.tsx` mit Schneefall und Schneedecke) 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/Rainy/Snow, Hintergrundgrafiken in `public/themes/`; bei Rainy hängt er zusätzlich `components/RainyWeather.tsx` mit Regen und Wetterleuchten ein, bei Snow `components/SnowWeather.tsx` mit Schneefall, Schneedecke und Räumfahrzeug, bei Aura `components/AuraWeather.tsx` mit Sonnenstrahlen) 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 d0b0659..ccd696e 100644 --- a/index.html +++ b/index.html @@ -30,7 +30,8 @@ palette === 'outdoor' || palette === 'cyberpunk' || palette === 'rainy' || - palette === 'snow') { + palette === 'snow' || + palette === 'aura') { document.documentElement.classList.add('theme-' + palette); } } catch (e) { diff --git a/public/themes/aura-clouds-far.svg b/public/themes/aura-clouds-far.svg new file mode 100644 index 0000000..6cfd4fe --- /dev/null +++ b/public/themes/aura-clouds-far.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/public/themes/aura-clouds-near.svg b/public/themes/aura-clouds-near.svg new file mode 100644 index 0000000..8ca9cb3 --- /dev/null +++ b/public/themes/aura-clouds-near.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/public/themes/aura-moon.svg b/public/themes/aura-moon.svg new file mode 100644 index 0000000..30c1c21 --- /dev/null +++ b/public/themes/aura-moon.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/themes/aura-sidebar.svg b/public/themes/aura-sidebar.svg new file mode 100644 index 0000000..163766f --- /dev/null +++ b/public/themes/aura-sidebar.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/public/themes/aura-stars.svg b/public/themes/aura-stars.svg new file mode 100644 index 0000000..f7868e0 --- /dev/null +++ b/public/themes/aura-stars.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/themes/aura-sun.svg b/public/themes/aura-sun.svg new file mode 100644 index 0000000..ad2b2d7 --- /dev/null +++ b/public/themes/aura-sun.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/server/src/userFields.js b/server/src/userFields.js index d1f8546..c6701d0 100644 --- a/server/src/userFields.js +++ b/server/src/userFields.js @@ -48,7 +48,7 @@ export function normalizeAvatar(value) { * 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', 'cyberpunk', 'rainy', 'snow']; +export const THEMES = ['perinco', 'pixel', 'outdoor', 'cyberpunk', 'rainy', 'snow', 'aura']; export const THEME_MODES = ['light', 'dark']; /** Farbwelt; null bzw. '' bedeutet "perinco", der Standard. */ diff --git a/src/components/AuraWeather.tsx b/src/components/AuraWeather.tsx new file mode 100644 index 0000000..185f908 --- /dev/null +++ b/src/components/AuraWeather.tsx @@ -0,0 +1,120 @@ +import React, { useEffect, useRef } from 'react'; + +/* + * Sonnenstrahlen im Theme "Aura", hell ("Sunshine"). + * + * Etwa alle 20 Sekunden fallen zwei bis drei Strahlen von der Sonne oben + * rechts quer ueber den Bildschirm in die Ecke unten links, schwellen an und + * verblassen wieder. Im dunklen Modus ("Mond") passiert nichts - dort steht + * der Mond, und Mondstrahlen waeren Kitsch. + * + * Die ziehenden Wolken macht nicht diese Komponente, sondern reines CSS in + * index.css (.app-header::before). Hier steht nur, was Zufall braucht. + * + * Wie beim Regen und Schnee: pointer-events: none, unter den Dialogen, und bei + * "Bewegung reduzieren" still. + */ + +/** Pause zwischen zwei Strahlenbuendeln in ms - "etwa alle 20 Sekunden". */ +const RAYS_MIN_MS = 16000; +const RAYS_MAX_MS = 24000; +const RAYS_DURATION_MS = 4200; + +/* + * Mittelpunkt der Sonne relativ zur rechten oberen Ecke der Leiste. Muss zu + * background-position und -size der Sonne in index.css passen + * (right -45px top -55px, 130px): Mitte = 65 - 45 von rechts, 65 - 55 von oben. + */ +const SUN_FROM_RIGHT = 20; +const SUN_FROM_TOP = 10; + +function random(min: number, max: number) { + return min + Math.random() * (max - min); +} + +function prefersReducedMotion() { + return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false; +} + +export const AuraWeather: React.FC = () => { + const layerRef = useRef(null); + + useEffect(() => { + const layer = layerRef.current; + if (!layer || prefersReducedMotion() || typeof layer.animate !== 'function') return; + + let timer = 0; + let cancelled = false; + const schedule = (delay = random(RAYS_MIN_MS, RAYS_MAX_MS)) => { + if (cancelled) return; + timer = window.setTimeout(shine, delay); + }; + + const shine = () => { + const header = document.querySelector('.app-header'); + // Nur bei Tag und nur, wenn jemand hinsieht. + if (!header || document.hidden || document.documentElement.classList.contains('dark')) { + schedule(); + return; + } + + const rect = header.getBoundingClientRect(); + const sunX = rect.right - SUN_FROM_RIGHT; + const sunY = rect.top + SUN_FROM_TOP; + + // Richtung: in die Ecke unten links. Die Laenge reicht etwas darueber + // hinaus, damit der Strahl nicht mitten im Bild abbricht. + const dx = 0 - sunX; + const dy = window.innerHeight - sunY; + const baseAngle = (Math.atan2(dy, dx) * 180) / Math.PI; + const length = Math.hypot(dx, dy) * 1.15; + + const count = Math.random() < 0.5 ? 2 : 3; + for (let i = 0; i < count; i++) { + const beam = document.createElement('div'); + const thickness = random(70, 150); + // Gefaechert um die Diagonale; jeder Strahl etwas versetzt im Takt. + const angle = baseAngle + (i - (count - 1) / 2) * random(5, 8); + beam.className = 'aura-ray'; + beam.style.left = `${sunX}px`; + beam.style.top = `${sunY - thickness / 2}px`; + beam.style.width = `${length}px`; + beam.style.height = `${thickness}px`; + layer.appendChild(beam); + + const animation = beam.animate( + [ + { opacity: 0, transform: `rotate(${angle - 1.5}deg)` }, + { opacity: random(0.55, 0.85), offset: 0.4 }, + { opacity: 0, transform: `rotate(${angle + 1.5}deg)` }, + ], + { + duration: RAYS_DURATION_MS + random(-600, 600), + delay: i * random(250, 500), + easing: 'ease-in-out', + fill: 'both', + } + ); + animation.onfinish = () => beam.remove(); + } + + schedule(); + }; + + // Der erste Strahl kommt frueher, damit man das Theme beim Umschalten erlebt. + schedule(random(2500, 4000)); + return () => { + cancelled = true; + window.clearTimeout(timer); + layer.replaceChildren(); + }; + }, []); + + return ( +