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 ( +