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:
co-authored by
Claude Opus 5
parent
5cdc42002a
commit
3c56cf8361
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user