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) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-09-17 18:49:45 +02:00
co-authored by Claude Opus 5
parent 5cdc42002a
commit 3c56cf8361
15 changed files with 497 additions and 15 deletions
+26 -3
View File
@@ -138,7 +138,7 @@ Die Darstellung hat **zwei unabhängige Achsen**, beide als Klasse am `<html>`:
| 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
+9 -4
View File
@@ -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
+1 -1
View File
@@ -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 |
+2 -1
View File
@@ -30,7 +30,8 @@
palette === 'outdoor' ||
palette === 'cyberpunk' ||
palette === 'rainy' ||
palette === 'snow') {
palette === 'snow' ||
palette === 'aura') {
document.documentElement.classList.add('theme-' + palette);
}
} catch (e) {
+19
View File
@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 80" width="800" height="80">
<!--
Aura, hintere Wolken. Eine Kachel, die sich nahtlos wiederholt: keine Wolke
beruehrt den linken oder rechten Rand. index.css schiebt die Kachel langsam
nach rechts, genau um ihre Breite je Umlauf - deshalb kein Ruck.
-->
<defs>
<filter id="weich" x="-20%" y="-50%" width="140%" height="200%">
<feGaussianBlur stdDeviation="3" />
</filter>
</defs>
<g fill="#ffffff" opacity="0.55" filter="url(#weich)">
<ellipse cx="110" cy="24" rx="70" ry="12" />
<ellipse cx="140" cy="18" rx="40" ry="11" />
<ellipse cx="430" cy="46" rx="80" ry="12" />
<ellipse cx="460" cy="38" rx="42" ry="12" />
<ellipse cx="650" cy="20" rx="60" ry="10" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 788 B

+18
View File
@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 80" width="800" height="80">
<!--
Aura, vordere Wolken: groesser, deutlicher, flache Unterkante. Nahtlose
Kachel wie aura-clouds-far.svg, zieht aber doppelt so schnell - das gibt
Tiefe.
-->
<g fill="#ffffff" opacity="0.92">
<path d="M60 58 C 56 46 70 38 84 42 C 90 28 116 24 128 38 C 138 30 160 34 162 48 C 176 48 184 54 180 60 Z" />
<path d="M330 34 C 328 24 340 18 350 22 C 356 10 378 8 388 20 C 396 14 414 18 414 30 C 426 30 432 36 428 40 Z" />
<path d="M560 62 C 556 50 572 42 586 46 C 594 30 624 26 638 42 C 650 34 676 38 678 54 C 694 54 704 60 698 66 Z" />
</g>
<!-- Schattenkante unten, damit die Wolken auf hellem Himmel Form haben -->
<g fill="#c9dcef" opacity="0.6">
<path d="M70 58 C 100 54 150 54 180 60 Z" />
<path d="M340 38 C 370 35 400 35 428 40 Z" />
<path d="M572 62 C 610 58 660 58 698 66 Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 933 B

+25
View File
@@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
<!--
Aura, dunkel ("Mond"): der Vollmond an derselben Stelle wie die Sonne im
hellen Modus - weicher Hof, blasse Scheibe, ein paar Krater.
-->
<defs>
<radialGradient id="hof" cx="50" cy="50" r="50" gradientUnits="userSpaceOnUse">
<stop offset="0.3" stop-color="#dfe4ff" stop-opacity="0.45" />
<stop offset="0.65" stop-color="#b9c2ff" stop-opacity="0.12" />
<stop offset="1" stop-color="#b9c2ff" stop-opacity="0" />
</radialGradient>
<radialGradient id="scheibe" cx="44" cy="44" r="22" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fbfbff" />
<stop offset="1" stop-color="#d7dcef" />
</radialGradient>
</defs>
<circle cx="50" cy="50" r="50" fill="url(#hof)" />
<circle cx="50" cy="50" r="18" fill="url(#scheibe)" />
<g fill="#b8bfd8" opacity="0.55">
<circle cx="44" cy="46" r="3.5" />
<circle cx="55" cy="55" r="2.5" />
<circle cx="52" cy="41" r="1.8" />
<circle cx="43" cy="57" r="1.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+20
View File
@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 900" width="260" height="900" preserveAspectRatio="xMidYMax slice">
<!--
Aura, Seitenleiste: sanfte Huegel in Pastell, von unten hereinwachsend.
Eine Datei fuer beide Modi - der Schleier des Mond-Modus toent sie zur
Nacht ab (index.css).
-->
<defs>
<linearGradient id="dunst" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffd9a0" stop-opacity="0" />
<stop offset="1" stop-color="#ffd9a0" stop-opacity="0.45" />
</linearGradient>
<filter id="weich" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="14" />
</filter>
</defs>
<rect y="520" width="260" height="380" fill="url(#dunst)" filter="url(#weich)" />
<path d="M0 730 C 60 690 120 700 170 720 C 210 736 240 716 260 706 L260 900 L0 900 Z" fill="#c7b8e8" opacity="0.5" />
<path d="M0 780 C 50 760 110 772 160 764 C 200 758 236 770 260 762 L260 900 L0 900 Z" fill="#f2b98a" opacity="0.55" />
<path d="M0 830 C 70 812 130 826 190 818 C 220 814 244 820 260 816 L260 900 L0 900 Z" fill="#9fcfa8" opacity="0.55" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 80" width="1600" height="80" preserveAspectRatio="xMidYMid slice">
<!-- Aura, dunkel: Sterne hinter den Wolken. Nur im Mond-Modus eingesetzt. -->
<g fill="#ffffff">
<circle cx="60" cy="14" r="1" opacity="0.7" /><circle cx="140" cy="52" r="0.8" opacity="0.5" />
<circle cx="230" cy="24" r="1.2" opacity="0.8" /><circle cx="310" cy="62" r="0.8" opacity="0.5" />
<circle cx="390" cy="10" r="1" opacity="0.6" /><circle cx="470" cy="40" r="1.3" opacity="0.9" />
<circle cx="560" cy="18" r="0.8" opacity="0.5" /><circle cx="640" cy="58" r="1" opacity="0.7" />
<circle cx="720" cy="30" r="1.2" opacity="0.8" /><circle cx="810" cy="12" r="0.8" opacity="0.5" />
<circle cx="890" cy="48" r="1" opacity="0.7" /><circle cx="970" cy="22" r="1.3" opacity="0.9" />
<circle cx="1050" cy="64" r="0.8" opacity="0.5" /><circle cx="1130" cy="16" r="1" opacity="0.6" />
<circle cx="1210" cy="42" r="1.2" opacity="0.8" /><circle cx="1290" cy="8" r="0.8" opacity="0.5" />
<circle cx="1370" cy="54" r="1" opacity="0.7" /><circle cx="1450" cy="26" r="1.2" opacity="0.8" />
<circle cx="1530" cy="60" r="0.8" opacity="0.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+22
View File
@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
<!--
Aura, hell ("Sunshine"): die Sonne. Sie sitzt in der Ecke oben rechts der
Leiste und ist nur zur Haelfte zu sehen (Position in index.css). Von ihrem
Mittelpunkt gehen die Sonnenstrahlen aus (components/AuraWeather.tsx) -
wer die Groesse oder Lage aendert, muss dort SUN_* nachziehen.
-->
<defs>
<radialGradient id="schein" cx="50" cy="50" r="50" gradientUnits="userSpaceOnUse">
<stop offset="0.3" stop-color="#ffd76a" stop-opacity="0.9" />
<stop offset="0.6" stop-color="#ffc94a" stop-opacity="0.35" />
<stop offset="1" stop-color="#ffc94a" stop-opacity="0" />
</radialGradient>
<radialGradient id="kern" cx="45" cy="45" r="22" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fff6c9" />
<stop offset="0.7" stop-color="#ffd24d" />
<stop offset="1" stop-color="#ffb81f" />
</radialGradient>
</defs>
<circle cx="50" cy="50" r="50" fill="url(#schein)" />
<circle cx="50" cy="50" r="20" fill="url(#kern)" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1 -1
View File
@@ -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. */
+120
View File
@@ -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<HTMLDivElement>(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<HTMLElement>('.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 (
<div
ref={layerRef}
aria-hidden="true"
className="pointer-events-none fixed inset-0 z-[90] overflow-hidden"
/>
);
};
+210 -2
View File
@@ -10,8 +10,8 @@
*/
/*
* Sechs Farbwelten, jede in hell und dunkel. Sie haengen an einer Klasse am
* <html> (theme-pixel, theme-outdoor, theme-cyberpunk, theme-rainy, theme-snow); ohne Klasse gilt
* Sieben Farbwelten, jede in hell und dunkel. Sie haengen an einer Klasse am
* <html> (theme-pixel, theme-outdoor, theme-cyberpunk, theme-rainy, theme-snow, theme-aura); ohne Klasse gilt
* perinco. Der Modus bleibt die vorhandene Klasse "dark".
*
* Warum das ohne Aenderung an den Komponenten geht: im Markup steht nirgends
@@ -865,6 +865,148 @@ html.theme-snow.dark {
--shadow-lift: 0 12px 32px rgb(0 0 0 / 0.4);
}
/* ===========================================================================
* Aura - hell "Sunshine", dunkel "Mond".
*
* Ein Theme mit zwei Gesichtern: bei Tag Sonne in der Ecke oben rechts,
* Himmelblau in der Leiste und warmes Bernstein als Akzent; bei Nacht der
* Mond an derselben Stelle, Sterne und Lavendel. In beiden ziehen Wolken
* langsam von links nach rechts durch die Leiste (reines CSS, siehe
* .app-header::before weiter unten), bei Tag fallen dazu ab und zu
* Sonnenstrahlen quer ueber den Bildschirm (components/AuraWeather.tsx).
*
* Die Leiste hat hier vier Ebenen statt zwei, und jede braucht eigene
* Position und Groesse - deshalb stehen die in --aura-header-position und
* --aura-header-size neben dem Bild.
* =========================================================================== */
html.theme-aura:not(.dark) {
/* Sonne ueber dem Schleier: sie soll leuchten, nicht verblassen. */
--theme-header-image: url('/themes/aura-sun.svg'),
linear-gradient(
90deg,
rgb(255 253 247 / 0.95) 0%,
rgb(255 253 247 / 0.25) 30%,
rgb(255 253 247 / 0.2) 70%,
rgb(255 253 247 / 0.7) 100%
),
linear-gradient(transparent, transparent),
linear-gradient(180deg, #a9d6ff 0%, #d6ecff 70%, #fff3d6 100%);
--aura-header-position: right -45px top -55px, center, center, center;
--aura-header-size: 130px 130px, cover, cover, cover;
--aura-cloud-opacity: 1;
--theme-sidebar-image: linear-gradient(
180deg,
rgb(255 253 247 / 0.97) 0%,
rgb(255 253 247 / 0.88) 45%,
rgb(255 253 247 / 0.35) 78%,
rgb(255 253 247 / 0.5) 100%
),
url('/themes/aura-sidebar.svg');
--color-canvas: #fbf6ea;
--color-surface: #fffdf7;
/* Bernstein. 500/600 dunkel genug fuer weisse Schrift. */
--color-brand-50: #fff7e6;
--color-brand-100: #ffeac2;
--color-brand-200: #fdd28a;
--color-brand-300: #f9b54f;
--color-brand-400: #e48a17;
--color-brand-500: #b85f08;
--color-brand-600: #9a4d07;
--color-brand-700: #7d3f0a;
--color-brand-800: #66340c;
--color-brand-900: #522b0d;
--color-brand-accent: #e8930c;
--color-ink-50: #f8f4ec;
--color-ink-100: #efe8dc;
--color-ink-200: #e0d6c5;
--color-ink-300: #aca08e;
--color-ink-400: #6f6454;
--color-ink-500: #5b5145;
--color-ink-600: #4a4137;
--color-ink-700: #3a332b;
--color-ink-800: #2a251f;
--color-ink-900: #1c1814;
--color-ink-950: #0f0d0a;
--radius-sm: 0.35rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--radius-2xl: 1.35rem;
--shadow-card: 0 1px 2px rgb(28 24 20 / 0.04), 0 8px 24px rgb(232 147 12 / 0.1);
--shadow-lift: 0 14px 34px rgb(232 147 12 / 0.18);
}
html.theme-aura.dark {
/* Der Mond liegt wie die Sonne ueber dem Schleier - darunter verschwand er
fast. Er sitzt so weit in der Ecke, dass er den Umschalter kaum streift. */
--theme-header-image: url('/themes/aura-moon.svg'),
linear-gradient(
90deg,
rgb(19 22 42 / 0.95) 0%,
rgb(19 22 42 / 0.3) 30%,
rgb(19 22 42 / 0.25) 70%,
rgb(19 22 42 / 0.45) 100%
),
url('/themes/aura-stars.svg'),
linear-gradient(180deg, #0e1230 0%, #1b1f4a 100%);
--aura-header-position: right -45px top -55px, center, center, center;
--aura-header-size: 130px 130px, cover, cover, cover;
/* Wolken bei Nacht: blass und vom Mond angeleuchtet statt weiss. */
--aura-cloud-opacity: 0.28;
--theme-sidebar-image: linear-gradient(
180deg,
rgb(19 22 42 / 0.97) 0%,
rgb(19 22 42 / 0.9) 45%,
rgb(19 22 42 / 0.6) 78%,
rgb(19 22 42 / 0.7) 100%
),
url('/themes/aura-sidebar.svg');
--color-canvas: #0c0e1c;
--color-surface: #13162a;
/* Mondsilber und Lavendel */
--color-brand-50: #16183a;
--color-brand-100: #1d2150;
--color-brand-200: #2b3170;
--color-brand-300: #3a4290;
--color-brand-400: #4b55ad;
--color-brand-500: #4d57b8;
--color-brand-600: #5a64c6;
--color-brand-700: #aab2f0;
--color-brand-800: #cfd4fa;
--color-brand-900: #eceefe;
--color-brand-accent: #c9cff7;
--color-ink-50: #171a30;
--color-ink-100: #1f233c;
--color-ink-200: #2d3250;
--color-ink-300: #5b6182;
--color-ink-400: #868cab;
--color-ink-500: #a1a7c3;
--color-ink-600: #babfd6;
--color-ink-700: #d1d5e6;
--color-ink-800: #e5e8f2;
--color-ink-900: #f6f7fb;
--color-ink-950: #000000;
--radius-sm: 0.35rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--radius-2xl: 1.35rem;
--shadow-card: 0 0 0 1px rgb(170 178 240 / 0.06), 0 8px 26px rgb(0 0 0 / 0.35);
--shadow-lift: 0 0 0 1px rgb(170 178 240 / 0.12), 0 14px 36px rgb(0 0 0 / 0.45);
}
@layer base {
html,
body {
@@ -937,6 +1079,62 @@ html.theme-snow.dark {
background-size: cover;
}
/*
* Aura: vier Hintergrundebenen in der Leiste, jede mit eigener Lage.
*
* Dazu ziehende Wolken als ::before. Zwei nahtlose 800-px-Kacheln, die per
* background-position genau um ihre Breite (bzw. das Doppelte) wandern -
* beim Neustart der Animation steht dasselbe Bild, deshalb kein Ruck. Die
* vordere Lage ist doppelt so schnell, das gibt Tiefe.
*
* isolation: isolate macht die Leiste zu einem eigenen Stapelkontext. Nur
* so kann z-index: -1 die Wolken hinter Titel und Knoepfe legen, ohne dass
* sie hinter dem Hintergrund der Leiste verschwinden. overflow: hidden
* kommt bewusst nicht an die Leiste - sie enthaelt Aufklappmenues.
*/
html.theme-aura .app-header {
isolation: isolate;
background-position: var(--aura-header-position);
background-size: var(--aura-header-size);
}
html.theme-aura .app-header::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background-image: url('/themes/aura-clouds-near.svg'), url('/themes/aura-clouds-far.svg');
background-repeat: repeat-x;
background-size: 800px 100%;
opacity: var(--aura-cloud-opacity, 1);
/* Links unter dem Titel ausblenden, rechts vor den Knoepfen duenner. */
mask-image: linear-gradient(90deg, transparent 0, #000 22%, #000 75%, rgb(0 0 0 / 0.4) 100%);
animation: aura-clouds 150s linear infinite;
}
/* Sonnenstrahl (components/AuraWeather.tsx): verjuengt zur Sonne hin. */
.aura-ray {
position: absolute;
transform-origin: 0 50%;
opacity: 0;
background: linear-gradient(
90deg,
rgb(255 214 110 / 0.9) 0%,
rgb(255 224 150 / 0.45) 35%,
rgb(255 236 190 / 0.18) 75%,
rgb(255 236 190 / 0) 100%
);
clip-path: polygon(0 46%, 100% 0, 100% 100%, 0 54%);
filter: blur(10px);
}
@media (prefers-reduced-motion: reduce) {
html.theme-aura .app-header::before {
animation: none;
}
}
/*
* Wetterleuchten im Theme Rainy (components/RainyWeather.tsx legt das
* Element exakt ueber die obere Leiste und blendet es kurz ein).
@@ -1045,3 +1243,13 @@ html.theme-snow.dark {
overflow: hidden;
}
}
/* Ziehende Wolken im Theme Aura: vordere Kachel 2 x 800 px, hintere 1 x 800 px. */
@keyframes aura-clouds {
from {
background-position: 0 0, 0 0;
}
to {
background-position: 1600px 0, 800px 0;
}
}
+4 -2
View File
@@ -12,11 +12,12 @@ import { useAuth } from './auth';
import { THEME_PALETTES, type ThemePalette } from '../types';
import { RainyWeather } from '../components/RainyWeather';
import { SnowWeather } from '../components/SnowWeather';
import { AuraWeather } from '../components/AuraWeather';
/*
* Darstellung der Oberflaeche. Sie hat zwei unabhaengige Achsen:
*
* Farbwelt (palette) perinco | pixel | outdoor | cyberpunk | rainy | snow
* Farbwelt (palette) perinco | pixel | outdoor | cyberpunk | rainy | snow | aura
* -> Klasse theme-* am <html>
* Modus (mode) hell | dunkel -> Klasse dark am <html>
*
@@ -224,7 +225,7 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
);
/*
* Rainy und Snow sind die Themes mit Bewegung. Das Wetter haengt hier und nicht
* Rainy, Snow und Aura sind die Themes mit Bewegung. Das Wetter haengt hier und nicht
* im Layout, damit es auch auf der Anmeldeseite regnet - und damit es beim
* Wechsel auf ein anderes Theme sofort samt Animationsschleife verschwindet.
*/
@@ -233,6 +234,7 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
{children}
{palette === 'rainy' && <RainyWeather />}
{palette === 'snow' && <SnowWeather />}
{palette === 'aura' && <AuraWeather />}
</ThemeContext.Provider>
);
};
+5 -1
View File
@@ -88,7 +88,7 @@ export type Role = 'admin' | 'user';
* Tippfehler hier nicht still zum Standarddesign fuehrt.
* server/test/userFields.test.mjs prueft, dass beide Listen gleich bleiben.
*/
export const THEME_PALETTES = ['perinco', 'pixel', 'outdoor', 'cyberpunk', 'rainy', 'snow'] as const;
export const THEME_PALETTES = ['perinco', 'pixel', 'outdoor', 'cyberpunk', 'rainy', 'snow', 'aura'] as const;
export type ThemePalette = (typeof THEME_PALETTES)[number];
@@ -103,6 +103,10 @@ export const THEME_PALETTE_LABELS: Record<ThemePalette, { name: string; hint: st
},
rainy: { name: 'Rainy', hint: 'Regenwetter: Nieselregen, Wolken mit Wetterleuchten, Schieferblau' },
snow: { name: 'Snow', hint: 'Winter: Schneefall, der sich unten sammelt, Eisblau' },
aura: {
name: 'Aura',
hint: 'Hell Sunshine mit Sonnenstrahlen, dunkel Mond mit Sternen - ziehende Wolken',
},
};
export interface AppUser {