feat(design): neues Theme Snow mit Schneefall und Schneedecke
Eisblau in hell und dunkel. Flocken fallen per Canvas und sammeln sich am unteren Fensterrand in einer Hoehenkarte, die per Diffusion geglaettet wird; nach rund vier Minuten ist die Decke voll (26 px). Bei prefers-reduced-motion bleibt es still. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
77b61b4a04
commit
831429d559
+25
-4
@@ -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`, `theme-cyberpunk`, `theme-rainy` | perinco ist der Standard |
|
| Farbwelt | `theme-perinco`, `theme-pixel`, `theme-outdoor`, `theme-cyberpunk`, `theme-rainy`, `theme-snow` | 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-*`,
|
||||||
@@ -173,7 +173,7 @@ er liest den Benutzer.
|
|||||||
|
|
||||||
Alle Themes außer perinco legen eine Grafik hinter die obere Leiste und die
|
Alle Themes außer perinco legen eine Grafik hinter die obere Leiste und die
|
||||||
Seitenleiste: 8-bit-Himmel und Steinmauer, blauer Himmel und Wiese,
|
Seitenleiste: 8-bit-Himmel und Steinmauer, blauer Himmel und Wiese,
|
||||||
Neonröhren und regnerische Gasse, Regenwolken und nasse Hügel. Die Dateien liegen als **SVG** in
|
Neonröhren und regnerische Gasse, Regenwolken und nasse Hügel, verschneite Berge und Waldrand. Die Dateien liegen als **SVG** in
|
||||||
`public/themes/` und sind von Hand gezeichnet - stilisierte Grafik statt Foto,
|
`public/themes/` und sind von Hand gezeichnet - stilisierte Grafik statt Foto,
|
||||||
damit sie in jeder Auflösung scharf bleibt und wenige Kilobyte wiegt.
|
damit sie in jeder Auflösung scharf bleibt und wenige Kilobyte wiegt.
|
||||||
|
|
||||||
@@ -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
|
> nur Licht - über Dunkel leuchtet das Neon, über Weiss bleiben blasse farbige
|
||||||
> Linien. Wer eine Grafik ergänzt: **keine deckenden Flächen**, nur Motiv.
|
> Linien. Wer eine Grafik ergänzt: **keine deckenden Flächen**, nur Motiv.
|
||||||
|
|
||||||
### Rainy: das Theme mit Wetter
|
### Rainy und Snow: Themes mit Wetter
|
||||||
|
|
||||||
Rainy ist das einzige Theme mit Bewegung. Farben und Grafiken funktionieren wie
|
Rainy und Snow sind die Themes mit Bewegung. Hier zuerst Rainy, Snow folgt unten. Farben und Grafiken funktionieren wie
|
||||||
bei den anderen; dazu kommt `src/components/RainyWeather.tsx`, das der
|
bei den anderen; dazu kommt `src/components/RainyWeather.tsx`, das der
|
||||||
`ThemeProvider` nur bei aktivem Rainy einhängt - auch auf der Anmeldeseite, und
|
`ThemeProvider` nur bei aktivem Rainy einhängt - auch auf der Anmeldeseite, und
|
||||||
beim Wechsel auf ein anderes Theme verschwindet es samt Animationsschleife.
|
beim Wechsel auf ein anderes Theme verschwindet es samt Animationsschleife.
|
||||||
@@ -238,6 +238,27 @@ noch Blitz. Die Canvas-Schleife läuft über `requestAnimationFrame` und ruht
|
|||||||
damit in Hintergrund-Tabs; der Zeitschritt ist auf 50 ms begrenzt, damit nach
|
damit in Hintergrund-Tabs; der Zeitschritt ist auf 50 ms begrenzt, damit nach
|
||||||
der Rückkehr nichts nachgeholt wird.
|
der Rückkehr nichts nachgeholt wird.
|
||||||
|
|
||||||
|
#### Snow
|
||||||
|
|
||||||
|
`src/components/SnowWeather.tsx`, eingehängt vom `ThemeProvider` wie der Regen:
|
||||||
|
ein `<canvas>` über dem Fenster (`z-[90]`, `pointer-events: none`), bei
|
||||||
|
`prefers-reduced-motion` still.
|
||||||
|
|
||||||
|
| Teil | Umsetzung |
|
||||||
|
| --- | --- |
|
||||||
|
| **Flocken** | Alle 70–160 ms eine neue, höchstens 70 gleichzeitig. Radius 1–3 px, große fallen schneller, alle schaukeln seitlich (Sinus). Farbe `--snow-flake`, im hellen Modus mit Kontur `--snow-flake-edge` - weiss auf fast weiss wäre unsichtbar |
|
||||||
|
| **Schneedecke** | Eine Höhenkarte in 3-px-Spalten über die Fensterbreite. Eine Flocke landet, sobald sie die Oberfläche **ihrer** Spalte erreicht, und verteilt sich auf fünf Spalten |
|
||||||
|
| **Glätten** | Diffusion: jede Spalte gleicht sich an ihre Nachbarn an, Faktor an der Zeit (`SPREAD`), nicht an der Framezahl - gleich auf 60 und 144 Hz. Mengenerhaltend, aus Einzelflocken werden flache Hügel |
|
||||||
|
| **Deckel** | `MAX_DEPTH` = 26 px, voll nach rund vier Minuten. Was danach landet, verschwindet |
|
||||||
|
|
||||||
|
Die Decke lebt nur im Speicher; beim Ändern der Fensterbreite wird sie auf die
|
||||||
|
neue Spaltenzahl umgerechnet, nach dem Neuladen ist der Boden frei.
|
||||||
|
|
||||||
|
> **Erfahrungswert Wachstum:** Das Flockenlimit bestimmt die Landerate, nicht
|
||||||
|
> das Spawn-Intervall - eine Flocke braucht gut 10 s durchs Fenster, bei 70
|
||||||
|
> gleichzeitig landen also nur etwa 5 je Sekunde. Wer die Wachstumszeit
|
||||||
|
> ändert, rechnet mit dieser Zahl.
|
||||||
|
|
||||||
### Was ein neues Theme können muss
|
### Was ein neues Theme können muss
|
||||||
|
|
||||||
Die Rollen der Skalen sind über die ganze App gleich; ein Theme, das sie
|
Die Rollen der Skalen sind über die ganze App gleich; ein Theme, das sie
|
||||||
|
|||||||
@@ -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 fünf zur Wahl, jedes in hell und
|
* Links daneben steht das **Design**. Es gibt sechs zur Wahl, jedes in hell und
|
||||||
dunkel:
|
dunkel:
|
||||||
|
|
||||||
| Design | Aussehen |
|
| Design | Aussehen |
|
||||||
@@ -76,6 +76,7 @@ die Scanner-Einstellungen. Alles andere ist für beide Rollen gleich.
|
|||||||
| **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 |
|
| **Cyberpunk** | Neon in der Nacht: scharfe Kanten, violettes Leuchten, Blaustahl |
|
||||||
| **Rainy** | Regenwetter: Schieferblau, leichter Nieselregen, Wolken mit Wetterleuchten |
|
| **Rainy** | Regenwetter: Schieferblau, leichter Nieselregen, Wolken mit Wetterleuchten |
|
||||||
|
| **Snow** | Winter: Eisblau, Schneefall, der sich am unteren Rand zu einer Schneedecke sammelt |
|
||||||
|
|
||||||
> **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
|
||||||
@@ -88,17 +89,25 @@ die Scanner-Einstellungen. Alles andere ist für beide Rollen gleich.
|
|||||||
|
|
||||||
Die Designs neben perinco haben zusätzlich eine **Grafik** hinter der
|
Die Designs neben perinco haben zusätzlich eine **Grafik** hinter der
|
||||||
oberen Leiste und der Seitenleiste: 8-bit-Himmel und Steinmauer, blauer Himmel
|
oberen Leiste und der Seitenleiste: 8-bit-Himmel und Steinmauer, blauer Himmel
|
||||||
und Wiese, Neonröhren und Regen, Regenwolken und nasse Hügel. Sie liegt hinter
|
und Wiese, Neonröhren und Regen, Regenwolken und nasse Hügel, verschneite
|
||||||
einem Schleier, damit die Schrift darauf lesbar bleibt, und ist dort am
|
Berge und Tannen. Sie liegt hinter einem Schleier, damit die Schrift darauf
|
||||||
deutlichsten, wo die Leisten frei sind - in der Mitte oben und unten links. perinco bleibt einfarbig.
|
lesbar bleibt, und ist dort am deutlichsten, wo die Leisten frei sind - in der
|
||||||
|
Mitte oben und unten links. perinco bleibt einfarbig.
|
||||||
|
|
||||||
**Rainy** ist das einzige Design, in dem sich etwas bewegt: ein paar
|
**Rainy** und **Snow** sind die Designs, in denen sich etwas bewegt. Das
|
||||||
Regentropfen fallen über den Bildschirm und zerspritzen am unteren Rand, und
|
Wetter liegt nur optisch über der Oberfläche: es fängt keine Klicks ab, und
|
||||||
die Wolken in der oberen Leiste leuchten alle paar Sekunden kurz auf. Der
|
geöffnete Dialoge liegen darüber.
|
||||||
Regen liegt nur optisch über der Oberfläche - er verdeckt keine Knöpfe und
|
|
||||||
fängt keine Klicks ab, und in geöffneten Dialogen regnet es nicht. Wer im
|
* Bei **Rainy** fallen ein paar Regentropfen über den Bildschirm und
|
||||||
Betriebssystem **„Bewegung reduzieren"** eingeschaltet hat, sieht nur die
|
zerspritzen am unteren Rand, und die Wolken in der oberen Leiste leuchten
|
||||||
Farben und die Wolken, ohne Regen und Blitz.
|
alle paar Sekunden kurz auf.
|
||||||
|
* Bei **Snow** schneit es, und der Schnee bleibt unten liegen: nach etwa vier
|
||||||
|
Minuten ist am unteren Rand eine Schneedecke gewachsen. Sie bleibt ein
|
||||||
|
schmaler Streifen und wächst nicht weiter. Neu laden, und der Boden ist
|
||||||
|
wieder frei.
|
||||||
|
|
||||||
|
Wer im Betriebssystem **„Bewegung reduzieren"** eingeschaltet hat, sieht bei
|
||||||
|
beiden nur Farben und Grafiken - ohne Regen, Blitz und Schnee.
|
||||||
|
|
||||||

|

|
||||||
* Die Menüpunkte **Akquise**, **Opportunities**, **Kontakte**, **Mails** und
|
* Die Menüpunkte **Akquise**, **Opportunities**, **Kontakte**, **Mails** und
|
||||||
|
|||||||
@@ -295,7 +295,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/Cyberpunk/Rainy, Hintergrundgrafiken in `public/themes/`; bei Rainy hängt er zusätzlich `components/RainyWeather.tsx` mit Regen und Wetterleuchten ein) 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 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 |
|
||||||
| `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 |
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -29,7 +29,8 @@
|
|||||||
if (palette === 'pixel' ||
|
if (palette === 'pixel' ||
|
||||||
palette === 'outdoor' ||
|
palette === 'outdoor' ||
|
||||||
palette === 'cyberpunk' ||
|
palette === 'cyberpunk' ||
|
||||||
palette === 'rainy') {
|
palette === 'rainy' ||
|
||||||
|
palette === 'snow') {
|
||||||
document.documentElement.classList.add('theme-' + palette);
|
document.documentElement.classList.add('theme-' + palette);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 80" width="1600" height="80" preserveAspectRatio="xMidYMid slice">
|
||||||
|
<!--
|
||||||
|
Snow, obere Leiste: verschneite Bergkette mit Tannen.
|
||||||
|
|
||||||
|
Wie bei den anderen Themes ohne deckenden Himmel - die Flaeche kommt aus dem
|
||||||
|
Theme, hier steht nur Motiv. Die Berge sind blaugrau mit weissen Kuppen,
|
||||||
|
davor eine Reihe Tannen, deren Zweige Schnee tragen. Der Schneefall selbst
|
||||||
|
ist nicht gezeichnet, er faellt lebendig (components/SnowWeather.tsx).
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Ferne Berge -->
|
||||||
|
<path d="M0 80 L0 52 L90 30 L170 46 L260 18 L360 44 L450 26 L540 48 L640 22 L740 46 L830 28 L930 50 L1030 20 L1130 44 L1220 30 L1320 48 L1420 24 L1520 44 L1600 34 L1600 80 Z" fill="#9fb3c8" opacity="0.45" />
|
||||||
|
|
||||||
|
<!-- Schneekuppen der fernen Berge -->
|
||||||
|
<g fill="#ffffff" opacity="0.9">
|
||||||
|
<path d="M260 18 L236 28 L250 27 L262 32 L274 26 L286 29 Z" />
|
||||||
|
<path d="M640 22 L614 32 L630 31 L642 36 L654 30 L668 33 Z" />
|
||||||
|
<path d="M1030 20 L1004 30 L1020 29 L1032 34 L1044 28 L1058 31 Z" />
|
||||||
|
<path d="M1420 24 L1394 34 L1410 33 L1422 38 L1434 32 L1448 35 Z" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Nahe Huegel, verschneit -->
|
||||||
|
<path d="M0 80 L0 66 C 120 56 220 62 340 58 C 480 54 560 64 700 60 C 840 56 940 64 1080 58 C 1220 54 1340 64 1460 60 C 1520 58 1570 62 1600 60 L1600 80 Z" fill="#e8f0f8" opacity="0.9" />
|
||||||
|
|
||||||
|
<!-- Tannen in drei Etagen, jede mit Schneekappe -->
|
||||||
|
<g fill="#4d6478" opacity="0.85">
|
||||||
|
<rect x="442.8" y="66.0" width="2.4" height="3.0" />
|
||||||
|
<rect x="465.1" y="68.0" width="1.7" height="2.1" />
|
||||||
|
<rect x="774.7" y="64.0" width="2.6" height="3.4" />
|
||||||
|
<rect x="799.1" y="66.0" width="1.8" height="2.2" />
|
||||||
|
<rect x="1162.8" y="64.0" width="2.4" height="3.0" />
|
||||||
|
<rect x="1185.2" y="66.0" width="1.7" height="2.1" />
|
||||||
|
<rect x="1511.0" y="66.0" width="1.9" height="2.4" />
|
||||||
|
<path d="M444 28.0 L449.0 45.1 L439.0 45.1 Z" />
|
||||||
|
<path d="M444 38.6 L452.4 55.4 L435.6 55.4 Z" />
|
||||||
|
<path d="M444 48.9 L456.0 66.0 L432.0 66.0 Z" />
|
||||||
|
<path d="M466 42.0 L469.6 53.7 L462.4 53.7 Z" />
|
||||||
|
<path d="M466 49.3 L471.9 60.7 L460.1 60.7 Z" />
|
||||||
|
<path d="M466 56.3 L474.5 68.0 L457.5 68.0 Z" />
|
||||||
|
<path d="M776 22.0 L781.5 40.9 L770.5 40.9 Z" />
|
||||||
|
<path d="M776 33.8 L785.1 52.2 L766.9 52.2 Z" />
|
||||||
|
<path d="M776 45.1 L789.0 64.0 L763.0 64.0 Z" />
|
||||||
|
<path d="M800 38.0 L803.8 50.6 L796.2 50.6 Z" />
|
||||||
|
<path d="M800 45.8 L806.3 58.2 L793.7 58.2 Z" />
|
||||||
|
<path d="M800 53.4 L809.0 66.0 L791.0 66.0 Z" />
|
||||||
|
<path d="M1164 26.0 L1169.0 43.1 L1159.0 43.1 Z" />
|
||||||
|
<path d="M1164 36.6 L1172.4 53.4 L1155.6 53.4 Z" />
|
||||||
|
<path d="M1164 46.9 L1176.0 64.0 L1152.0 64.0 Z" />
|
||||||
|
<path d="M1186 40.0 L1189.6 51.7 L1182.4 51.7 Z" />
|
||||||
|
<path d="M1186 47.3 L1192.0 58.7 L1180.0 58.7 Z" />
|
||||||
|
<path d="M1186 54.3 L1194.5 66.0 L1177.5 66.0 Z" />
|
||||||
|
<path d="M1512 36.0 L1516.0 49.5 L1508.0 49.5 Z" />
|
||||||
|
<path d="M1512 44.4 L1518.7 57.6 L1505.3 57.6 Z" />
|
||||||
|
<path d="M1512 52.5 L1521.5 66.0 L1502.5 66.0 Z" />
|
||||||
|
</g>
|
||||||
|
<g fill="#ffffff" opacity="0.95">
|
||||||
|
<path d="M444 28.0 L446.3 35.7 Q444 33.6 441.7 35.7 Z" />
|
||||||
|
<path d="M444 38.6 L447.8 46.2 Q444 44.2 440.2 46.2 Z" />
|
||||||
|
<path d="M444 48.9 L449.4 56.6 Q444 54.5 438.6 56.6 Z" />
|
||||||
|
<path d="M466 42.0 L467.6 47.3 Q466 45.9 464.4 47.3 Z" />
|
||||||
|
<path d="M466 49.3 L468.7 54.4 Q466 53.1 463.3 54.4 Z" />
|
||||||
|
<path d="M466 56.3 L469.8 61.6 Q466 60.2 462.2 61.6 Z" />
|
||||||
|
<path d="M776 22.0 L778.5 30.5 Q776 28.2 773.5 30.5 Z" />
|
||||||
|
<path d="M776 33.8 L780.1 42.1 Q776 39.9 771.9 42.1 Z" />
|
||||||
|
<path d="M776 45.1 L781.9 53.6 Q776 51.3 770.1 53.6 Z" />
|
||||||
|
<path d="M800 38.0 L801.7 43.7 Q800 42.2 798.3 43.7 Z" />
|
||||||
|
<path d="M800 45.8 L802.8 51.4 Q800 49.9 797.2 51.4 Z" />
|
||||||
|
<path d="M800 53.4 L804.0 59.1 Q800 57.6 796.0 59.1 Z" />
|
||||||
|
<path d="M1164 26.0 L1166.3 33.7 Q1164 31.6 1161.7 33.7 Z" />
|
||||||
|
<path d="M1164 36.6 L1167.8 44.2 Q1164 42.2 1160.2 44.2 Z" />
|
||||||
|
<path d="M1164 46.9 L1169.4 54.6 Q1164 52.5 1158.6 54.6 Z" />
|
||||||
|
<path d="M1186 40.0 L1187.6 45.3 Q1186 43.9 1184.4 45.3 Z" />
|
||||||
|
<path d="M1186 47.3 L1188.7 52.4 Q1186 51.1 1183.3 52.4 Z" />
|
||||||
|
<path d="M1186 54.3 L1189.8 59.6 Q1186 58.2 1182.2 59.6 Z" />
|
||||||
|
<path d="M1512 36.0 L1513.8 42.1 Q1512 40.5 1510.2 42.1 Z" />
|
||||||
|
<path d="M1512 44.4 L1515.0 50.3 Q1512 48.8 1509.0 50.3 Z" />
|
||||||
|
<path d="M1512 52.5 L1516.3 58.6 Q1512 57.0 1507.7 58.6 Z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,65 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 900" width="260" height="900" preserveAspectRatio="xMidYMax slice">
|
||||||
|
<!--
|
||||||
|
Snow, Seitenleiste: verschneiter Waldrand, von unten hereinwachsend.
|
||||||
|
|
||||||
|
Das Motiv sitzt unten, oben liegt der Schleier ueber den Menuepunkten
|
||||||
|
(index.css). Die Schneeverwehung ganz unten ist bewusst flach und hell: dort
|
||||||
|
stehen Name und Rolle des Benutzers, und darueber haeuft sich zusaetzlich
|
||||||
|
der fallende Schnee an.
|
||||||
|
-->
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="dunst" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#c9d8e8" stop-opacity="0" />
|
||||||
|
<stop offset="1" stop-color="#c9d8e8" stop-opacity="0.5" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="weich" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="12" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect y="540" width="260" height="360" fill="url(#dunst)" filter="url(#weich)" />
|
||||||
|
|
||||||
|
<!-- Tannen hinten: blass -->
|
||||||
|
<g fill="#8aa0b5" opacity="0.4">
|
||||||
|
<rect x="43.2" y="770.0" width="5.6" height="8.0" />
|
||||||
|
<rect x="180.9" y="760.0" width="6.2" height="8.8" />
|
||||||
|
<path d="M46 670.0 L57.8 715.0 L34.2 715.0 Z" />
|
||||||
|
<path d="M46 698.0 L65.6 742.0 L26.4 742.0 Z" />
|
||||||
|
<path d="M46 725.0 L74.0 770.0 L18.0 770.0 Z" />
|
||||||
|
<path d="M184 650.0 L197.0 699.5 L171.0 699.5 Z" />
|
||||||
|
<path d="M184 680.8 L205.7 729.2 L162.3 729.2 Z" />
|
||||||
|
<path d="M184 710.5 L215.0 760.0 L153.0 760.0 Z" />
|
||||||
|
</g>
|
||||||
|
<g fill="#ffffff" opacity="0.6">
|
||||||
|
<path d="M46 670.0 L51.3 690.2 Q46 684.9 40.7 690.2 Z" />
|
||||||
|
<path d="M46 698.0 L54.8 717.8 Q46 712.5 37.2 717.8 Z" />
|
||||||
|
<path d="M46 725.0 L58.6 745.2 Q46 739.9 33.4 745.2 Z" />
|
||||||
|
<path d="M184 650.0 L189.9 672.3 Q184 666.3 178.1 672.3 Z" />
|
||||||
|
<path d="M184 680.8 L193.8 702.6 Q184 696.8 174.2 702.6 Z" />
|
||||||
|
<path d="M184 710.5 L197.9 732.8 Q184 726.8 170.1 732.8 Z" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Tannen vorn, mit Schneelast -->
|
||||||
|
<g fill="#4d6478" opacity="0.65">
|
||||||
|
<rect x="100.5" y="800.0" width="7.0" height="9.6" />
|
||||||
|
<rect x="223.3" y="808.0" width="5.4" height="7.2" />
|
||||||
|
<path d="M104 680.0 L118.7 734.0 L89.3 734.0 Z" />
|
||||||
|
<path d="M104 713.6 L128.5 766.4 L79.5 766.4 Z" />
|
||||||
|
<path d="M104 746.0 L139.0 800.0 L69.0 800.0 Z" />
|
||||||
|
<path d="M226 718.0 L237.3 758.5 L214.7 758.5 Z" />
|
||||||
|
<path d="M226 743.2 L244.9 782.8 L207.1 782.8 Z" />
|
||||||
|
<path d="M226 767.5 L253.0 808.0 L199.0 808.0 Z" />
|
||||||
|
</g>
|
||||||
|
<g fill="#ffffff" opacity="0.9">
|
||||||
|
<path d="M104 680.0 L110.6 704.3 Q104 697.8 97.4 704.3 Z" />
|
||||||
|
<path d="M104 713.6 L115.0 737.4 Q104 731.0 93.0 737.4 Z" />
|
||||||
|
<path d="M104 746.0 L119.8 770.3 Q104 763.8 88.2 770.3 Z" />
|
||||||
|
<path d="M226 718.0 L231.1 736.2 Q226 731.4 220.9 736.2 Z" />
|
||||||
|
<path d="M226 743.2 L234.5 761.0 Q226 756.3 217.5 761.0 Z" />
|
||||||
|
<path d="M226 767.5 L238.2 785.7 Q226 780.9 213.8 785.7 Z" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Schneeverwehung -->
|
||||||
|
<path d="M0 800 C 50 784 110 796 160 788 C 200 782 236 792 260 786 L260 900 L0 900 Z" fill="#f2f7fc" opacity="0.8" />
|
||||||
|
<path d="M0 800 C 50 784 110 796 160 788 C 200 782 236 792 260 786" fill="none" stroke="#b9cadb" stroke-width="1.5" opacity="0.6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -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', 'cyberpunk', 'rainy'];
|
export const THEMES = ['perinco', 'pixel', 'outdoor', 'cyberpunk', 'rainy', 'snow'];
|
||||||
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. */
|
||||||
|
|||||||
@@ -0,0 +1,268 @@
|
|||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Das Wetter des Themes "Snow": Schneefall ueber dem Bildschirm, und was
|
||||||
|
* unten ankommt, bleibt liegen und haeuft sich am unteren Fensterrand an.
|
||||||
|
*
|
||||||
|
* Wie beim Regen (RainyWeather.tsx) ein Canvas ueber der ganzen Oberflaeche,
|
||||||
|
* ohne Klicks abzufangen und unter den Dialogen (z-[100]). Bei "Bewegung
|
||||||
|
* reduzieren" im Betriebssystem schneit es nicht.
|
||||||
|
*
|
||||||
|
* Die Schneedecke ist eine Hoehenkarte: der Fensterrand ist in schmale Spalten
|
||||||
|
* geteilt, jede Flocke erhoeht die Spalte, in der sie landet, ein wenig. Damit
|
||||||
|
* keine Nadeln wachsen, gleicht sich jede Spalte laufend an ihre Nachbarn an -
|
||||||
|
* so entstehen weiche Verwehungen. Die Decke waechst langsam
|
||||||
|
* (bis zur vollen Hoehe rund vier Minuten) und ist gedeckelt, damit sie nie
|
||||||
|
* mehr als einen schmalen Streifen am Rand verdeckt.
|
||||||
|
*
|
||||||
|
* Die Decke lebt nur im Speicher: neu laden, und der Boden ist wieder frei.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Flake {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
radius: number;
|
||||||
|
/** Fallgeschwindigkeit in px/s */
|
||||||
|
vy: number;
|
||||||
|
/** Phase und Staerke des seitlichen Schaukelns */
|
||||||
|
phase: number;
|
||||||
|
sway: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Breite einer Spalte der Hoehenkarte in px. */
|
||||||
|
const COLUMN = 3;
|
||||||
|
/** Hoechste Schneedecke in px - danach schmilzt, was noch landet. */
|
||||||
|
const MAX_DEPTH = 26;
|
||||||
|
/** Wie schnell sich die Decke glaettet (Diffusion, Spalten^2 je Sekunde). */
|
||||||
|
const SPREAD = 6;
|
||||||
|
|
||||||
|
const SPAWN_MIN_MS = 70;
|
||||||
|
const SPAWN_MAX_MS = 160;
|
||||||
|
const MAX_FLAKES = 70;
|
||||||
|
|
||||||
|
function random(min: number, max: number) {
|
||||||
|
return min + Math.random() * (max - min);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefersReducedMotion() {
|
||||||
|
return (
|
||||||
|
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Farben aus index.css, damit hell und dunkel passen. */
|
||||||
|
function snowColors() {
|
||||||
|
const style = getComputedStyle(document.documentElement);
|
||||||
|
const read = (name: string, fallback: string) =>
|
||||||
|
style.getPropertyValue(name).trim() || fallback;
|
||||||
|
return {
|
||||||
|
flake: read("--snow-flake", "#ffffff"),
|
||||||
|
flakeEdge: read("--snow-flake-edge", "rgb(120 145 170 / 0.35)"),
|
||||||
|
pile: read("--snow-pile", "#ffffff"),
|
||||||
|
pileShade: read("--snow-pile-shade", "#dbe6f1"),
|
||||||
|
pileEdge: read("--snow-pile-edge", "rgb(120 145 170 / 0.5)"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SnowWeather: React.FC = () => {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const ctx = canvas?.getContext("2d");
|
||||||
|
if (!canvas || !ctx || prefersReducedMotion()) return;
|
||||||
|
|
||||||
|
let width = 0;
|
||||||
|
let height = 0;
|
||||||
|
let pile = new Float32Array(0);
|
||||||
|
|
||||||
|
const resize = () => {
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
width = window.innerWidth;
|
||||||
|
height = window.innerHeight;
|
||||||
|
canvas.width = Math.round(width * dpr);
|
||||||
|
canvas.height = Math.round(height * dpr);
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
|
||||||
|
// Die Decke beim Aendern der Fensterbreite mitnehmen statt wegwerfen:
|
||||||
|
// auf die neue Spaltenzahl umrechnen.
|
||||||
|
const next = new Float32Array(Math.ceil(width / COLUMN) + 1);
|
||||||
|
if (pile.length > 0) {
|
||||||
|
for (let i = 0; i < next.length; i++) {
|
||||||
|
next[i] =
|
||||||
|
pile[
|
||||||
|
Math.min(
|
||||||
|
pile.length - 1,
|
||||||
|
Math.floor((i / next.length) * pile.length),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pile = next;
|
||||||
|
};
|
||||||
|
resize();
|
||||||
|
window.addEventListener("resize", resize);
|
||||||
|
|
||||||
|
const flakes: Flake[] = [];
|
||||||
|
let nextSpawn = 0;
|
||||||
|
let last = performance.now();
|
||||||
|
let frame = 0;
|
||||||
|
let colors = snowColors();
|
||||||
|
let colorsReadAt = 0;
|
||||||
|
|
||||||
|
const spawn = () => {
|
||||||
|
if (flakes.length >= MAX_FLAKES) return;
|
||||||
|
const radius = random(1, 3);
|
||||||
|
flakes.push({
|
||||||
|
x: random(-20, width + 20),
|
||||||
|
y: -radius - random(0, 30),
|
||||||
|
radius,
|
||||||
|
// Grosse Flocken fallen etwas schneller - das gibt Tiefe.
|
||||||
|
vy: random(28, 45) + radius * 14,
|
||||||
|
phase: random(0, Math.PI * 2),
|
||||||
|
sway: random(8, 22),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Flocke landet: Spalte erhoehen, die Nachbarn etwas mit. */
|
||||||
|
const land = (x: number, radius: number) => {
|
||||||
|
const i = Math.round(x / COLUMN);
|
||||||
|
const amount = radius * 5;
|
||||||
|
const add = (index: number, value: number) => {
|
||||||
|
if (index < 0 || index >= pile.length) return;
|
||||||
|
pile[index] = Math.min(MAX_DEPTH, pile[index] + value);
|
||||||
|
};
|
||||||
|
// Ueber fuenf Spalten verteilt statt als Spitze: sonst waechst die Decke
|
||||||
|
// als Saegezahn.
|
||||||
|
add(i, amount * 0.36);
|
||||||
|
add(i - 1, amount * 0.24);
|
||||||
|
add(i + 1, amount * 0.24);
|
||||||
|
add(i - 2, amount * 0.08);
|
||||||
|
add(i + 2, amount * 0.08);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Glaetten: jede Spalte gleicht sich ein wenig an ihre Nachbarn an
|
||||||
|
* (Diffusion). Das erhaelt die Schneemenge, laesst einzelne Flocken zu
|
||||||
|
* flachen Huegeln zerlaufen und bleibt auf 60 und 144 Hz gleich schnell,
|
||||||
|
* weil der Faktor an der Zeit haengt und nicht an der Framezahl.
|
||||||
|
*/
|
||||||
|
let smoothed = new Float32Array(0);
|
||||||
|
const settle = (dt: number) => {
|
||||||
|
if (smoothed.length !== pile.length)
|
||||||
|
smoothed = new Float32Array(pile.length);
|
||||||
|
const k = Math.min(0.25, SPREAD * dt);
|
||||||
|
const last = pile.length - 1;
|
||||||
|
for (let i = 0; i <= last; i++) {
|
||||||
|
const left = pile[i > 0 ? i - 1 : i];
|
||||||
|
const right = pile[i < last ? i + 1 : i];
|
||||||
|
smoothed[i] = pile[i] + k * (left + right - 2 * pile[i]);
|
||||||
|
}
|
||||||
|
pile.set(smoothed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawPile = () => {
|
||||||
|
let top = 0;
|
||||||
|
for (let i = 0; i < pile.length; i++) top = Math.max(top, pile[i]);
|
||||||
|
if (top < 0.3) return;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, height);
|
||||||
|
ctx.lineTo(0, height - pile[0]);
|
||||||
|
// Durch die Mittelpunkte zwischen den Spalten - weiche Kanten statt Treppe.
|
||||||
|
for (let i = 1; i < pile.length; i++) {
|
||||||
|
const x0 = (i - 1) * COLUMN;
|
||||||
|
const x1 = i * COLUMN;
|
||||||
|
ctx.quadraticCurveTo(
|
||||||
|
x0,
|
||||||
|
height - pile[i - 1],
|
||||||
|
(x0 + x1) / 2,
|
||||||
|
height - (pile[i - 1] + pile[i]) / 2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ctx.lineTo(width, height - pile[pile.length - 1]);
|
||||||
|
ctx.lineTo(width, height);
|
||||||
|
ctx.closePath();
|
||||||
|
|
||||||
|
const gradient = ctx.createLinearGradient(0, height - top, 0, height);
|
||||||
|
gradient.addColorStop(0, colors.pile);
|
||||||
|
gradient.addColorStop(1, colors.pileShade);
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Nur die Oberkante nachziehen, nicht den Rahmen unten und seitlich.
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, height - pile[0]);
|
||||||
|
for (let i = 1; i < pile.length; i++) {
|
||||||
|
const x0 = (i - 1) * COLUMN;
|
||||||
|
const x1 = i * COLUMN;
|
||||||
|
ctx.quadraticCurveTo(
|
||||||
|
x0,
|
||||||
|
height - pile[i - 1],
|
||||||
|
(x0 + x1) / 2,
|
||||||
|
height - (pile[i - 1] + pile[i]) / 2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ctx.strokeStyle = colors.pileEdge;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.stroke();
|
||||||
|
};
|
||||||
|
|
||||||
|
const tick = (now: number) => {
|
||||||
|
const dt = Math.min((now - last) / 1000, 0.05);
|
||||||
|
last = now;
|
||||||
|
|
||||||
|
if (now - colorsReadAt > 500) {
|
||||||
|
colors = snowColors();
|
||||||
|
colorsReadAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now >= nextSpawn) {
|
||||||
|
spawn();
|
||||||
|
nextSpawn = now + random(SPAWN_MIN_MS, SPAWN_MAX_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
ctx.fillStyle = colors.flake;
|
||||||
|
ctx.strokeStyle = colors.flakeEdge;
|
||||||
|
ctx.lineWidth = 0.6;
|
||||||
|
const t = now / 1000;
|
||||||
|
for (let i = flakes.length - 1; i >= 0; i--) {
|
||||||
|
const flake = flakes[i];
|
||||||
|
flake.y += flake.vy * dt;
|
||||||
|
const x = flake.x + Math.sin(t * 1.3 + flake.phase) * flake.sway;
|
||||||
|
const column = Math.round(x / COLUMN);
|
||||||
|
const ground = height - (pile[column] ?? 0);
|
||||||
|
if (flake.y + flake.radius >= ground) {
|
||||||
|
flakes.splice(i, 1);
|
||||||
|
if (x >= 0 && x <= width) land(x, flake.radius);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, flake.y, flake.radius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
settle(dt);
|
||||||
|
drawPile();
|
||||||
|
|
||||||
|
frame = requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
frame = requestAnimationFrame(tick);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(frame);
|
||||||
|
window.removeEventListener("resize", resize);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none fixed inset-0 z-[90] h-full w-full"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
+139
-2
@@ -10,8 +10,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Fuenf Farbwelten, jede in hell und dunkel. Sie haengen an einer Klasse am
|
* Sechs Farbwelten, jede in hell und dunkel. Sie haengen an einer Klasse am
|
||||||
* <html> (theme-pixel, theme-outdoor, theme-cyberpunk, theme-rainy); ohne Klasse gilt
|
* <html> (theme-pixel, theme-outdoor, theme-cyberpunk, theme-rainy, theme-snow); ohne Klasse gilt
|
||||||
* perinco. Der Modus 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
|
||||||
@@ -728,6 +728,143 @@ html.theme-rainy.dark {
|
|||||||
--shadow-lift: 0 12px 32px rgb(0 0 0 / 0.4);
|
--shadow-lift: 0 12px 32px rgb(0 0 0 / 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===========================================================================
|
||||||
|
* Snow - Winter.
|
||||||
|
*
|
||||||
|
* Zweites Theme mit Bewegung: Schnee faellt und sammelt sich am unteren
|
||||||
|
* Fensterrand (components/SnowWeather.tsx). Hier stehen die Farben, dazu die
|
||||||
|
* --snow-* Variablen fuer Flocken und Schneedecke.
|
||||||
|
*
|
||||||
|
* Eisblau auf fast weissem Grund. Weisse Flocken waeren darauf unsichtbar,
|
||||||
|
* deshalb bekommen sie im hellen Modus eine zarte blaugraue Kontur, und die
|
||||||
|
* Schneedecke eine Schattierung nach unten.
|
||||||
|
* =========================================================================== */
|
||||||
|
html.theme-snow:not(.dark) {
|
||||||
|
--theme-header-image: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgb(250 252 255 / 0.96) 0%,
|
||||||
|
rgb(250 252 255 / 0.35) 30%,
|
||||||
|
rgb(250 252 255 / 0.3) 60%,
|
||||||
|
rgb(250 252 255 / 0.96) 100%
|
||||||
|
),
|
||||||
|
url('/themes/snow-header.svg');
|
||||||
|
--theme-sidebar-image: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgb(250 252 255 / 0.97) 0%,
|
||||||
|
rgb(250 252 255 / 0.88) 45%,
|
||||||
|
rgb(250 252 255 / 0.4) 78%,
|
||||||
|
rgb(250 252 255 / 0.55) 100%
|
||||||
|
),
|
||||||
|
url('/themes/snow-sidebar.svg');
|
||||||
|
|
||||||
|
--snow-flake: #f4f9ff;
|
||||||
|
--snow-flake-edge: rgb(90 125 160 / 0.7);
|
||||||
|
--snow-pile: #ffffff;
|
||||||
|
--snow-pile-shade: #c2d3e4;
|
||||||
|
--snow-pile-edge: rgb(80 115 150 / 0.75);
|
||||||
|
|
||||||
|
--color-canvas: #edf2f7;
|
||||||
|
--color-surface: #fafcff;
|
||||||
|
|
||||||
|
--color-brand-50: #e9f3fa;
|
||||||
|
--color-brand-100: #d0e5f3;
|
||||||
|
--color-brand-200: #a3cbe6;
|
||||||
|
--color-brand-300: #6eaad4;
|
||||||
|
--color-brand-400: #3f89bd;
|
||||||
|
--color-brand-500: #1f6a9e;
|
||||||
|
--color-brand-600: #185681;
|
||||||
|
--color-brand-700: #144566;
|
||||||
|
--color-brand-800: #10354f;
|
||||||
|
--color-brand-900: #0b273b;
|
||||||
|
--color-brand-accent: #1f6a9e;
|
||||||
|
|
||||||
|
--color-ink-50: #f1f5f9;
|
||||||
|
--color-ink-100: #e2e9f0;
|
||||||
|
--color-ink-200: #cbd6e1;
|
||||||
|
--color-ink-300: #97a6b6;
|
||||||
|
--color-ink-400: #5c6b7c;
|
||||||
|
--color-ink-500: #4a5868;
|
||||||
|
--color-ink-600: #3a4655;
|
||||||
|
--color-ink-700: #2c3643;
|
||||||
|
--color-ink-800: #1f2731;
|
||||||
|
--color-ink-900: #141a21;
|
||||||
|
--color-ink-950: #0a0e13;
|
||||||
|
|
||||||
|
--radius-sm: 0.3rem;
|
||||||
|
--radius-md: 0.45rem;
|
||||||
|
--radius-lg: 0.65rem;
|
||||||
|
--radius-xl: 0.85rem;
|
||||||
|
--radius-2xl: 1.15rem;
|
||||||
|
|
||||||
|
--shadow-card: 0 1px 2px rgb(20 26 33 / 0.05), 0 6px 20px rgb(31 106 158 / 0.08);
|
||||||
|
--shadow-lift: 0 12px 30px rgb(31 106 158 / 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Snow, dunkel: Winternacht. Tiefes Nachtblau, der Schnee leuchtet darauf von
|
||||||
|
* selbst und braucht keine Kontur.
|
||||||
|
*/
|
||||||
|
html.theme-snow.dark {
|
||||||
|
--theme-header-image: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgb(18 26 38 / 0.95) 0%,
|
||||||
|
rgb(18 26 38 / 0.55) 30%,
|
||||||
|
rgb(18 26 38 / 0.5) 60%,
|
||||||
|
rgb(18 26 38 / 0.95) 100%
|
||||||
|
),
|
||||||
|
url('/themes/snow-header.svg');
|
||||||
|
--theme-sidebar-image: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgb(18 26 38 / 0.97) 0%,
|
||||||
|
rgb(18 26 38 / 0.88) 45%,
|
||||||
|
rgb(18 26 38 / 0.55) 78%,
|
||||||
|
rgb(18 26 38 / 0.65) 100%
|
||||||
|
),
|
||||||
|
url('/themes/snow-sidebar.svg');
|
||||||
|
|
||||||
|
--snow-flake: rgb(240 246 252 / 0.9);
|
||||||
|
--snow-flake-edge: transparent;
|
||||||
|
--snow-pile: #e9f1f9;
|
||||||
|
--snow-pile-shade: #a9bdd1;
|
||||||
|
--snow-pile-edge: rgb(255 255 255 / 0.6);
|
||||||
|
|
||||||
|
--color-canvas: #0b1119;
|
||||||
|
--color-surface: #121a26;
|
||||||
|
|
||||||
|
--color-brand-50: #0e1f2e;
|
||||||
|
--color-brand-100: #13283c;
|
||||||
|
--color-brand-200: #1c3d59;
|
||||||
|
--color-brand-300: #27567c;
|
||||||
|
--color-brand-400: #3571a0;
|
||||||
|
--color-brand-500: #2f73a8;
|
||||||
|
--color-brand-600: #3b80b5;
|
||||||
|
--color-brand-700: #92c2e6;
|
||||||
|
--color-brand-800: #c0dcf1;
|
||||||
|
--color-brand-900: #e3f0fa;
|
||||||
|
--color-brand-accent: #8cc6ef;
|
||||||
|
|
||||||
|
--color-ink-50: #172131;
|
||||||
|
--color-ink-100: #1f2a3b;
|
||||||
|
--color-ink-200: #2e3b4e;
|
||||||
|
--color-ink-300: #5b6a7e;
|
||||||
|
--color-ink-400: #8493a7;
|
||||||
|
--color-ink-500: #9fadbf;
|
||||||
|
--color-ink-600: #b9c5d3;
|
||||||
|
--color-ink-700: #d0d9e4;
|
||||||
|
--color-ink-800: #e4eaf1;
|
||||||
|
--color-ink-900: #f5f8fb;
|
||||||
|
--color-ink-950: #000000;
|
||||||
|
|
||||||
|
--radius-sm: 0.3rem;
|
||||||
|
--radius-md: 0.45rem;
|
||||||
|
--radius-lg: 0.65rem;
|
||||||
|
--radius-xl: 0.85rem;
|
||||||
|
--radius-2xl: 1.15rem;
|
||||||
|
|
||||||
|
--shadow-card: 0 1px 2px rgb(0 0 0 / 0.3), 0 6px 20px rgb(0 0 0 / 0.25);
|
||||||
|
--shadow-lift: 0 12px 32px rgb(0 0 0 / 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
|
|||||||
+4
-2
@@ -11,11 +11,12 @@ import { authApi } from './api';
|
|||||||
import { useAuth } from './auth';
|
import { useAuth } from './auth';
|
||||||
import { THEME_PALETTES, type ThemePalette } from '../types';
|
import { THEME_PALETTES, type ThemePalette } from '../types';
|
||||||
import { RainyWeather } from '../components/RainyWeather';
|
import { RainyWeather } from '../components/RainyWeather';
|
||||||
|
import { SnowWeather } from '../components/SnowWeather';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Darstellung der Oberflaeche. Sie hat zwei unabhaengige Achsen:
|
* Darstellung der Oberflaeche. Sie hat zwei unabhaengige Achsen:
|
||||||
*
|
*
|
||||||
* Farbwelt (palette) perinco | pixel | outdoor | cyberpunk | rainy
|
* Farbwelt (palette) perinco | pixel | outdoor | cyberpunk | rainy | snow
|
||||||
* -> Klasse theme-* am <html>
|
* -> Klasse theme-* am <html>
|
||||||
* Modus (mode) hell | dunkel -> Klasse dark am <html>
|
* Modus (mode) hell | dunkel -> Klasse dark am <html>
|
||||||
*
|
*
|
||||||
@@ -223,7 +224,7 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
|||||||
);
|
);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Rainy ist das einzige Theme mit Bewegung. Das Wetter haengt hier und nicht
|
* Rainy und Snow sind die Themes mit Bewegung. Das Wetter haengt hier und nicht
|
||||||
* im Layout, damit es auch auf der Anmeldeseite regnet - und damit es beim
|
* im Layout, damit es auch auf der Anmeldeseite regnet - und damit es beim
|
||||||
* Wechsel auf ein anderes Theme sofort samt Animationsschleife verschwindet.
|
* Wechsel auf ein anderes Theme sofort samt Animationsschleife verschwindet.
|
||||||
*/
|
*/
|
||||||
@@ -231,6 +232,7 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
|||||||
<ThemeContext.Provider value={value}>
|
<ThemeContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
{palette === 'rainy' && <RainyWeather />}
|
{palette === 'rainy' && <RainyWeather />}
|
||||||
|
{palette === 'snow' && <SnowWeather />}
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-1
@@ -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', 'cyberpunk', 'rainy'] as const;
|
export const THEME_PALETTES = ['perinco', 'pixel', 'outdoor', 'cyberpunk', 'rainy', 'snow'] as const;
|
||||||
|
|
||||||
export type ThemePalette = (typeof THEME_PALETTES)[number];
|
export type ThemePalette = (typeof THEME_PALETTES)[number];
|
||||||
|
|
||||||
@@ -102,6 +102,7 @@ export const THEME_PALETTE_LABELS: Record<ThemePalette, { name: string; hint: st
|
|||||||
hint: 'Neon in der Nacht: scharfe Kanten, violettes Leuchten, Chakra Petch',
|
hint: 'Neon in der Nacht: scharfe Kanten, violettes Leuchten, Chakra Petch',
|
||||||
},
|
},
|
||||||
rainy: { name: 'Rainy', hint: 'Regenwetter: Nieselregen, Wolken mit Wetterleuchten, Schieferblau' },
|
rainy: { name: 'Rainy', hint: 'Regenwetter: Nieselregen, Wolken mit Wetterleuchten, Schieferblau' },
|
||||||
|
snow: { name: 'Snow', hint: 'Winter: Schneefall, der sich unten sammelt, Eisblau' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AppUser {
|
export interface AppUser {
|
||||||
|
|||||||
Reference in New Issue
Block a user