feat(themes): colour families with light/dark, follow system, corners, frames, accent
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m30s

- Fjord (new), Polarized Hell and Suri Hell complete the families
  Klassik, Fjord, Terrakotta, Polarized, Suri
- settings: family table, follow-system, corners, button frames and an own
  accent colour (per device); compact quick picker in the user menus
- code colours in index.css are fallbacks only

chore(release): v0.14.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-30 23:29:52 +01:00
1 parent 2c9b4762fc
commit 3f0e596356
16 files changed
+949 -58

No files matched your search

+12 -1
View File
@@ -6,6 +6,16 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
## [Unreleased] ## [Unreleased]
## [0.14.0] - 2026-10-30
### Added
- Themes als Farbfamilien mit je einer hellen und dunklen Variante: Klassik, **Fjord** (neu, Gletscherblau / Nachtblau), Terrakotta, Polarized (neu: **Polarized Hell**), Suri (neu: **Suri Hell**).
- Darstellung in den Kontoeinstellungen: Tabelle Familie × Hell/Dunkel, „Hell oder dunkel: Wie System“ (wechselt innerhalb der Familie mit dem Gerät), Ecken (Wie Theme / Rund / Eckig), Rahmen an Knöpfen (mit / ohne) und eine eigene Akzentfarbe (Vorschläge oder frei wählbar). Alles gilt pro Gerät.
- Benutzermenü und Handy-Profil: kompakte Schnellwahl mit einem Farbpunkt pro Familie und Hell / Dunkel / Wie System; eigene Themes darunter.
### Changed
- Code-Farben aus `index.css` sind nur noch Rückfallwerte; die Code-Farben eines Themes gewinnen immer.
## [0.13.0] - 2026-10-30 ## [0.13.0] - 2026-10-30
### Added ### Added
@@ -324,7 +334,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
- Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken. - Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken.
- Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung. - Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung.
[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.13.0...dev [Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.0...dev
[0.14.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.13.0...v0.14.0
[0.13.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.12.2...v0.13.0 [0.13.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.12.2...v0.13.0
[0.12.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.12.1...v0.12.2 [0.12.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.12.1...v0.12.2
[0.12.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.12.0...v0.12.1 [0.12.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.12.0...v0.12.1
+5
View File
@@ -82,6 +82,11 @@ Jede Theme-Datei gliedert sich in sechs exakt definierte Bereiche:
| `light` | [`theme-light.css`](../frontend/src/themes/theme-light.css) | Daylight Clean White / Minimalistisch | Modern dezent (`4px` Cards, `4px` Buttons) | | `light` | [`theme-light.css`](../frontend/src/themes/theme-light.css) | Daylight Clean White / Minimalistisch | Modern dezent (`4px` Cards, `4px` Buttons) |
| `terracotta` | [`theme-terracotta.css`](../frontend/src/themes/theme-terracotta.css) | Warmes Cremepapier, Tinte, Terrakotta-Akzent, eigene Code-Farben in Erdtönen | Weich (`12px`) | | `terracotta` | [`theme-terracotta.css`](../frontend/src/themes/theme-terracotta.css) | Warmes Cremepapier, Tinte, Terrakotta-Akzent, eigene Code-Farben in Erdtönen | Weich (`12px`) |
| `terracotta-night` | [`theme-terracotta-night.css`](../frontend/src/themes/theme-terracotta-night.css) | Espresso-Braun, Creme-Schrift, glühender Terrakotta-Akzent | Weich (`12px`) | | `terracotta-night` | [`theme-terracotta-night.css`](../frontend/src/themes/theme-terracotta-night.css) | Espresso-Braun, Creme-Schrift, glühender Terrakotta-Akzent | Weich (`12px`) |
| `fjord` / `fjord-night` | [`theme-fjord.css`](../frontend/src/themes/theme-fjord.css), [`theme-fjord-night.css`](../frontend/src/themes/theme-fjord-night.css) | Kühles Papier und Gletscherblau / Nachtblau | Weich (`12px`) |
| `polarized-light` | [`theme-polarized-light.css`](../frontend/src/themes/theme-polarized-light.css) | Helles Gegenstück zu Polarized: Mintweiß, Petrol | Eckig (`0px`) |
| `suri-light` | [`theme-suri-light.css`](../frontend/src/themes/theme-suri-light.css) | Helles Gegenstück zu Suri: Lavendel, Pflaume | Weich (`10px`) |
Die eingebauten Themes sind in Farbfamilien mit je einer hellen und dunklen Variante gruppiert (`THEME_FAMILIES` in [`theme.ts`](../frontend/src/lib/theme.ts)). Pro Gerät lassen sich unter Darstellung zusätzlich einstellen: Hell/Dunkel „Wie System“ (wechselt innerhalb der Familie), Ecken (`data-corners="round|square"` überschreibt die `--radius-*`-Werte), Rahmen an Knöpfen (`data-button-borders="off"`) und eine eigene Akzentfarbe (setzt `--accent-*` direkt am Wurzelelement). Code-Farben in `index.css` sind nur Rückfallwerte (`:where(...)`), die `--code-*`-Werte eines Themes gewinnen immer.
| `polarized` | [`theme-polarized.css`](../frontend/src/themes/theme-polarized.css) | Tiefdunkles Obsidian mit elektrischem Grün/Türkis | Streng eckig (`0px` Cards, `2px` Buttons) | | `polarized` | [`theme-polarized.css`](../frontend/src/themes/theme-polarized.css) | Tiefdunkles Obsidian mit elektrischem Grün/Türkis | Streng eckig (`0px` Cards, `2px` Buttons) |
| `suri` | [`theme-suri.css`](../frontend/src/themes/theme-suri.css) | Warmes Mitternachts-Pflaume mit samtiger Orchidee | Weich gerundet (`10px` Cards, `6px` Buttons) | | `suri` | [`theme-suri.css`](../frontend/src/themes/theme-suri.css) | Warmes Mitternachts-Pflaume mit samtiger Orchidee | Weich gerundet (`10px` Cards, `6px` Buttons) |
| *Template* | [`theme-template.css`](../frontend/src/themes/theme-template.css) | Vollständig dokumentierte Kopiervorlage | Frei anpassbar | | *Template* | [`theme-template.css`](../frontend/src/themes/theme-template.css) | Vollständig dokumentierte Kopiervorlage | Frei anpassbar |
+1 -1
View File
@@ -100,7 +100,7 @@ Technik dahinter: [Doku/modules/collaboration.md](Doku/modules/collaboration.md)
* Geführte Ersteinrichtung, Rollen *Admin* und *Mitglied*, Einladungscodes, optional offene Registrierung. * Geführte Ersteinrichtung, Rollen *Admin* und *Mitglied*, Einladungscodes, optional offene Registrierung.
* Gruppen mit Gruppen-Admins für Freigaben. * Gruppen mit Gruppen-Admins für Freigaben.
* Admin-Bereich: Benutzer, Einladungen, Instanzname, SMTP, Freigabe-Modus. * Admin-Bereich: Benutzer, Einladungen, Instanzname, SMTP, Freigabe-Modus.
* Sechs Themes (Dark, Light, Terrakotta, Terrakotta Nacht, Polarized, Suri) plus Theme-Manager für eigene CSS-Themes, installierbar als PWA. Die App startet auch ohne Netz mit dem zuletzt geladenen Stand; offline erfasste Gedanken und abgehakte Aufgaben oder Listeneinträge werden nachgereicht, sobald die Verbindung wieder da ist. * Fünf Farbfamilien mit je einer hellen und dunklen Variante (Klassik, Fjord, Terrakotta, Polarized, Suri), Hell/Dunkel auf Wunsch wie das System, eigene Akzentfarbe, Ecken rund oder eckig, Knöpfe mit oder ohne Rahmen plus Theme-Manager für eigene CSS-Themes, installierbar als PWA. Die App startet auch ohne Netz mit dem zuletzt geladenen Stand; offline erfasste Gedanken und abgehakte Aufgaben oder Listeneinträge werden nachgereicht, sobald die Verbindung wieder da ist.
### 🔌 Integrationen ### 🔌 Integrationen
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.13.0", "version": "0.14.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.13.0", "version": "0.14.0",
"license": "Unlicense", "license": "Unlicense",
"dependencies": { "dependencies": {
"@fontsource-variable/inter": "5.3.0", "@fontsource-variable/inter": "5.3.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"private": true, "private": true,
"version": "0.13.0", "version": "0.14.0",
"license": "Unlicense", "license": "Unlicense",
"type": "module", "type": "module",
"scripts": { "scripts": {
+7 -40
View File
@@ -42,6 +42,7 @@ import {
AVAILABLE_THEMES, AVAILABLE_THEMES,
customThemeOption, customThemeOption,
initThemeOnBoot, initThemeOnBoot,
watchSystemScheme,
isCustomThemeId, isCustomThemeId,
setActiveTheme as applyTheme, setActiveTheme as applyTheme,
ThemeId, ThemeId,
@@ -66,6 +67,7 @@ import { MobileHeaderMenu } from './components/MobileHeaderMenu';
import { UserAvatar } from './components/UserAvatar'; import { UserAvatar } from './components/UserAvatar';
import { CommandPalette } from './components/CommandPalette'; import { CommandPalette } from './components/CommandPalette';
import { AppFooter } from './components/AppFooter'; import { AppFooter } from './components/AppFooter';
import { ThemeQuickPicker } from './components/ThemeQuickPicker';
import { AboutModal } from './components/AboutModal'; import { AboutModal } from './components/AboutModal';
import { consumePendingShares, formatSharedThought } from './lib/shareTarget'; import { consumePendingShares, formatSharedThought } from './lib/shareTarget';
import { APP_VERSION } from './version'; import { APP_VERSION } from './version';
@@ -442,6 +444,9 @@ export const App: React.FC = () => {
setActiveThemeState(isCustomThemeId(themeId) && css === undefined ? 'dark' : themeId); setActiveThemeState(isCustomThemeId(themeId) && css === undefined ? 'dark' : themeId);
}; };
// "Wie System": the theme follows the system's light/dark switch while the app is open
useEffect(() => watchSystemScheme(setActiveThemeState), []);
const handleChangeTheme = (themeId: ThemeId) => { const handleChangeTheme = (themeId: ThemeId) => {
triggerHaptic(10); triggerHaptic(10);
activateTheme(themeId, customThemes); activateTheme(themeId, customThemes);
@@ -979,26 +984,7 @@ export const App: React.FC = () => {
<Palette className="w-3 h-3 text-accent" /> <Palette className="w-3 h-3 text-accent" />
<span>Design-Theme</span> <span>Design-Theme</span>
</span> </span>
<div className="grid grid-cols-2 gap-1"> <ThemeQuickPicker activeTheme={activeTheme} themes={allThemes} onChange={handleChangeTheme} />
{allThemes.map((t) => (
<button
key={t.id}
type="button"
onClick={() => handleChangeTheme(t.id)}
className={`h-7 px-2 text-[11px] font-medium border rounded-btn flex items-center gap-1.5 transition-colors ${
activeTheme === t.id
? 'bg-panel-elevated border-accent text-main font-semibold'
: 'bg-canvas border-border text-muted hover:text-main'
}`}
>
<span
className="w-2.5 h-2.5 border border-strong flex-shrink-0"
style={{ backgroundColor: t.previewBg }}
/>
<span className="truncate">{t.name.replace('MindSift ', '').split(' ')[0]}</span>
</button>
))}
</div>
</div> </div>
{/* Menu Items */} {/* Menu Items */}
@@ -1560,26 +1546,7 @@ export const App: React.FC = () => {
<Palette className="w-3.5 h-3.5 text-accent" /> <Palette className="w-3.5 h-3.5 text-accent" />
<span>Design-Theme</span> <span>Design-Theme</span>
</span> </span>
<div className="grid grid-cols-2 gap-2"> <ThemeQuickPicker activeTheme={activeTheme} themes={allThemes} onChange={handleChangeTheme} touch />
{allThemes.map((t) => (
<button
key={t.id}
type="button"
onClick={() => handleChangeTheme(t.id)}
className={`h-9 px-2.5 text-xs font-medium border rounded-btn flex items-center gap-2 transition-colors cursor-pointer ${
activeTheme === t.id
? 'bg-panel-elevated border-accent text-main font-semibold'
: 'bg-canvas border-border text-muted hover:text-main'
}`}
>
<span
className="w-3 h-3 border border-strong flex-shrink-0 rounded-full"
style={{ backgroundColor: t.previewBg }}
/>
<span className="truncate">{t.name.replace('MindSift ', '')}</span>
</button>
))}
</div>
</div> </div>
{/* Menu Items */} {/* Menu Items */}
@@ -0,0 +1,113 @@
import React from 'react';
import { Monitor, Moon, Sun } from 'lucide-react';
import {
AVAILABLE_THEMES,
getFollowSystem,
resolveTheme,
setFollowSystem,
THEME_FAMILIES,
ThemeId,
ThemeOption,
} from '../lib/theme';
interface ThemeQuickPickerProps {
activeTheme: ThemeId;
/** All themes including the user's own (shown as small chips) */
themes: ThemeOption[];
onChange: (id: ThemeId) => void;
/** Bigger touch targets in the phone sheet */
touch?: boolean;
}
// Compact switcher for the user menu: one dot per colour family plus light/dark; own themes below
export const ThemeQuickPicker: React.FC<ThemeQuickPickerProps> = ({ activeTheme, themes, onChange, touch = false }) => {
const active = AVAILABLE_THEMES.find((t) => t.id === activeTheme);
const mode = active?.mode ?? 'dark';
const customThemes = themes.filter((t) => t.isCustom);
const dot = touch ? 'w-9 h-9' : 'w-7 h-7';
const followSystem = getFollowSystem();
const pick = (id: ThemeId) => onChange(resolveTheme(id));
return (
<div className="space-y-2">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-1.5" role="radiogroup" aria-label="Farbfamilie">
{THEME_FAMILIES.map((f) => {
const preview = AVAILABLE_THEMES.find((t) => t.id === (mode === 'light' ? f.light : f.dark));
const selected = active?.family === f.id;
return (
<button
key={f.id}
type="button"
role="radio"
aria-checked={selected}
onClick={() => pick(mode === 'light' ? f.light : f.dark)}
title={f.name}
className={`${dot} rounded-full flex items-center justify-center transition-transform active:scale-95 cursor-pointer ${
selected ? 'ring-2 ring-accent ring-offset-2 ring-offset-panel' : 'hover:scale-105'
}`}
style={{ backgroundColor: preview?.previewBg }}
>
<span className="w-3 h-3 rounded-full" style={{ backgroundColor: preview?.previewAccent }} />
</button>
);
})}
</div>
<div className="flex items-center rounded-full bg-panel-elevated p-0.5" role="radiogroup" aria-label="Hell oder dunkel">
{(['light', 'dark', 'auto'] as const).map((m) => {
const family = active?.family ? THEME_FAMILIES.find((f) => f.id === active.family) : undefined;
const selected = m === 'auto' ? followSystem : !followSystem && !!active && mode === m;
const disabled = !family;
return (
<button
key={m}
type="button"
role="radio"
aria-checked={selected}
disabled={disabled}
onClick={() => {
if (!family) return;
setFollowSystem(m === 'auto');
onChange(m === 'auto' ? resolveTheme(family.dark) : family[m]);
}}
title={m === 'light' ? 'Hell' : m === 'dark' ? 'Dunkel' : 'Wie System'}
aria-label={m === 'light' ? 'Hell' : m === 'dark' ? 'Dunkel' : 'Wie System'}
className={`${touch ? 'h-9 w-9' : 'h-7 w-7'} rounded-full flex items-center justify-center transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed ${
selected ? 'bg-panel text-accent shadow-sm' : 'text-muted hover:text-main'
}`}
>
{m === 'light' ? (
<Sun className="w-3.5 h-3.5" />
) : m === 'dark' ? (
<Moon className="w-3.5 h-3.5" />
) : (
<Monitor className="w-3.5 h-3.5" />
)}
</button>
);
})}
</div>
</div>
{customThemes.length > 0 && (
<div className="flex flex-wrap gap-1">
{customThemes.map((t) => (
<button
key={t.id}
type="button"
onClick={() => {
setFollowSystem(false);
onChange(t.id);
}}
className={`h-7 px-2 rounded-full text-[11px] flex items-center gap-1.5 border transition-colors cursor-pointer ${
activeTheme === t.id ? 'border-accent text-main' : 'border-border text-muted hover:text-main'
}`}
>
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: t.previewAccent }} />
<span className="truncate max-w-[8rem]">{t.name}</span>
</button>
))}
</div>
)}
</div>
);
};
@@ -23,7 +23,24 @@ import { ServerBackupsCard } from './ServerBackupsCard';
import { saveBlob } from '../../lib/download'; import { saveBlob } from '../../lib/download';
import { api, clearToken, triggerHaptic } from '../../lib/api'; import { api, clearToken, triggerHaptic } from '../../lib/api';
import { passwordProblem } from '../../lib/password'; import { passwordProblem } from '../../lib/password';
import { customThemeId, ThemeId, ThemeOption } from '../../lib/theme'; import {
AVAILABLE_THEMES,
CornerStyle,
customThemeId,
getCornerStyle,
setCornerStyle,
getFollowSystem,
setFollowSystem,
resolveTheme,
getButtonBorders,
setButtonBorders,
ButtonBorders,
getAccentColor,
setAccentColor,
THEME_FAMILIES,
ThemeId,
ThemeOption,
} from '../../lib/theme';
import themeTemplateCss from '../../themes/theme-template.css?raw'; import themeTemplateCss from '../../themes/theme-template.css?raw';
import { UserAvatar, resizeImageToDataUrl } from '../../components/UserAvatar'; import { UserAvatar, resizeImageToDataUrl } from '../../components/UserAvatar';
import { LegalLinks } from '../../components/LegalLinks'; import { LegalLinks } from '../../components/LegalLinks';
@@ -158,6 +175,10 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
const [demoMode, setDemoMode] = useState(false); const [demoMode, setDemoMode] = useState(false);
const linkPreviewsOn = useLinkPreviewsEnabled(); const linkPreviewsOn = useLinkPreviewsEnabled();
const [journalOn, setJournalOn] = useState(isJournalEnabled); const [journalOn, setJournalOn] = useState(isJournalEnabled);
const [corners, setCorners] = useState<CornerStyle>(getCornerStyle);
const [followSystem, setFollowSystemState] = useState(getFollowSystem);
const [buttonBorders, setButtonBordersState] = useState<ButtonBorders>(getButtonBorders);
const [accent, setAccent] = useState<string | null>(getAccentColor);
useEffect(() => { useEffect(() => {
api api
.getAuthStatus() .getAuthStatus()
@@ -920,8 +941,205 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
/> />
</div> </div>
{/* Colour families: left light, right dark */}
<div className="space-y-2">
<div className="grid grid-cols-[1fr_minmax(0,9rem)_minmax(0,9rem)] sm:grid-cols-[1fr_12rem_12rem] items-center gap-2 px-1 text-[11px] font-semibold uppercase tracking-wider text-faint">
<span>Theme</span>
<span className="text-center">Hell</span>
<span className="text-center">Dunkel</span>
</div>
{THEME_FAMILIES.map((f) => (
<div
key={f.id}
className="grid grid-cols-[1fr_minmax(0,9rem)_minmax(0,9rem)] sm:grid-cols-[1fr_12rem_12rem] items-center gap-2 p-2 rounded-card bg-canvas"
>
<div className="min-w-0 px-1">
<p className="font-semibold text-main text-sm truncate">{f.name}</p>
<p className="text-[11px] text-muted truncate">{f.subtitle}</p>
</div>
{[f.light, f.dark].map((id) => {
const t = AVAILABLE_THEMES.find((x) => x.id === id)!;
const selected = activeTheme === id;
return (
<button
key={id}
type="button"
onClick={() => onThemeChange(resolveTheme(id))}
aria-pressed={selected}
title={t.name}
className={`h-14 rounded-card p-2 flex flex-col justify-between text-left transition-all cursor-pointer ${
selected ? 'ring-2 ring-accent' : 'hover:scale-[1.02]'
}`}
style={{ backgroundColor: t.previewBg, border: '1px solid rgba(127,127,127,0.25)' }}
>
<span className="flex items-center gap-1">
<span className="w-6 h-2.5 rounded-full" style={{ backgroundColor: t.previewPanel }} />
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: t.previewAccent }} />
</span>
<span className="text-[11px] font-semibold truncate" style={{ color: t.previewText }}>
{selected ? '✓ Aktiv' : t.mode === 'light' ? 'Hell' : 'Dunkel'}
</span>
</button>
);
})}
</div>
))}
</div>
{/* Corners for every theme (per device) */}
<div className="p-3.5 rounded-card border border-border bg-canvas flex flex-wrap items-center justify-between gap-3">
<div className="text-xs">
<p className="font-semibold text-main">Ecken</p>
<p className="text-[11px] text-faint">Abgerundet oder eckig – unabhängig vom Theme. Gilt für dieses Gerät.</p>
</div>
<div className="flex items-center bg-panel-elevated rounded-btn p-0.5 text-xs" role="radiogroup" aria-label="Ecken">
{(
[
['theme', 'Wie Theme'],
['round', 'Rund'],
['square', 'Eckig'],
] as [CornerStyle, string][]
).map(([value, label]) => (
<button
key={value}
type="button"
role="radio"
aria-checked={corners === value}
onClick={() => {
setCornerStyle(value);
setCorners(value);
}}
className={`h-9 sm:h-7 px-3 rounded-btn transition-colors cursor-pointer ${
corners === value ? 'bg-panel text-main font-semibold shadow-sm' : 'text-muted hover:text-main'
}`}
>
{label}
</button>
))}
</div>
</div>
{/* Light/dark: fixed, or following the system inside the family */}
<div className="p-3.5 rounded-card border border-border bg-canvas flex flex-wrap items-center justify-between gap-3">
<div className="text-xs">
<p className="font-semibold text-main">Hell oder dunkel</p>
<p className="text-[11px] text-faint">„Wie System“ wechselt innerhalb der Farbfamilie mit dem Hell-/Dunkelmodus des Geräts.</p>
</div>
<div className="flex items-center bg-panel-elevated rounded-btn p-0.5 text-xs" role="radiogroup" aria-label="Hell oder dunkel">
{(
[
[false, 'Selbst wählen'],
[true, 'Wie System'],
] as [boolean, string][]
).map(([value, label]) => (
<button
key={label}
type="button"
role="radio"
aria-checked={followSystem === value}
onClick={() => {
setFollowSystem(value);
setFollowSystemState(value);
if (value) onThemeChange(resolveTheme(activeTheme));
}}
className={`h-9 sm:h-7 px-3 rounded-btn transition-colors cursor-pointer ${
followSystem === value ? 'bg-panel text-main font-semibold shadow-sm' : 'text-muted hover:text-main'
}`}
>
{label}
</button>
))}
</div>
</div>
{/* Frames around buttons */}
<div className="p-3.5 rounded-card border border-border bg-canvas flex flex-wrap items-center justify-between gap-3">
<div className="text-xs">
<p className="font-semibold text-main">Rahmen an Knöpfen</p>
<p className="text-[11px] text-faint">Ohne Rahmen sind Knöpfe nur leicht hinterlegt. Gilt für dieses Gerät.</p>
</div>
<div className="flex items-center bg-panel-elevated rounded-btn p-0.5 text-xs" role="radiogroup" aria-label="Rahmen an Knöpfen">
{(
[
['on', 'Mit Rahmen'],
['off', 'Ohne Rahmen'],
] as [ButtonBorders, string][]
).map(([value, label]) => (
<button
key={value}
type="button"
role="radio"
aria-checked={buttonBorders === value}
onClick={() => {
setButtonBorders(value);
setButtonBordersState(value);
}}
className={`h-9 sm:h-7 px-3 rounded-btn transition-colors cursor-pointer ${
buttonBorders === value ? 'bg-panel text-main font-semibold shadow-sm' : 'text-muted hover:text-main'
}`}
>
{label}
</button>
))}
</div>
</div>
{/* Own accent colour on top of the theme */}
<div className="p-3.5 rounded-card border border-border bg-canvas space-y-2.5">
<div className="flex flex-wrap items-center justify-between gap-2 text-xs">
<div>
<p className="font-semibold text-main">Akzentfarbe</p>
<p className="text-[11px] text-faint">Ersetzt die Akzentfarbe des Themes (Knöpfe, Links, Markierungen). Gilt für dieses Gerät.</p>
</div>
{accent && (
<button
type="button"
onClick={() => {
setAccentColor(null);
setAccent(null);
}}
className="btn-action h-9 sm:h-7 px-2.5 text-xs"
>
Wie Theme
</button>
)}
</div>
<div className="flex flex-wrap items-center gap-2">
{['#2563eb', '#0f766e', '#16a34a', '#ca8a04', '#c2410c', '#dc2626', '#db2777', '#8e44c9', '#475569'].map((c) => (
<button
key={c}
type="button"
onClick={() => {
setAccentColor(c);
setAccent(c);
}}
title={c}
aria-label={`Akzentfarbe ${c}`}
className={`w-9 h-9 sm:w-7 sm:h-7 rounded-full transition-transform hover:scale-110 cursor-pointer ${
accent === c ? 'ring-2 ring-offset-2 ring-offset-canvas ring-main' : ''
}`}
style={{ backgroundColor: c }}
/>
))}
<label className="flex items-center gap-1.5 text-[11px] text-muted cursor-pointer">
<input
type="color"
value={accent || '#2563eb'}
onChange={(e) => {
setAccentColor(e.target.value);
setAccent(e.target.value);
}}
className="w-9 h-9 sm:w-7 sm:h-7 rounded-full border border-border bg-transparent p-0 cursor-pointer"
aria-label="Eigene Akzentfarbe wählen"
/>
Eigene
</label>
</div>
</div>
{themes.some((t) => t.isCustom) && <p className="font-semibold text-main text-sm pt-1">Eigene Themes</p>}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{themes.map((t) => { {themes.filter((t) => t.isCustom).map((t) => {
const isSelected = activeTheme === t.id; const isSelected = activeTheme === t.id;
const custom = customThemes.find((c) => customThemeId(c) === t.id); const custom = customThemes.find((c) => customThemeId(c) === t.id);
return ( return (
@@ -1362,7 +1362,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
type="button" type="button"
disabled={subView !== 'editor'} disabled={subView !== 'editor'}
onClick={subView === 'editor' ? toggleSidebar : undefined} onClick={subView === 'editor' ? toggleSidebar : undefined}
className={`hidden sm:inline-flex h-8 w-8 items-center justify-center rounded-btn border transition-colors ${ className={`chip-btn hidden sm:inline-flex h-8 w-8 items-center justify-center rounded-btn border transition-colors ${
subView !== 'editor' subView !== 'editor'
? 'opacity-35 bg-canvas border-border text-faint cursor-not-allowed pointer-events-none' ? 'opacity-35 bg-canvas border-border text-faint cursor-not-allowed pointer-events-none'
: isSidebarCollapsed : isSidebarCollapsed
@@ -1394,7 +1394,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button <button
type="button" type="button"
onClick={() => setSubView('editor')} onClick={() => setSubView('editor')}
className={`h-8 px-2 sm:px-3 text-xs font-medium border rounded-btn transition-colors flex items-center justify-center gap-1.5 cursor-pointer min-w-0 ${ className={`chip-btn h-8 px-2 sm:px-3 text-xs font-medium border rounded-btn transition-colors flex items-center justify-center gap-1.5 cursor-pointer min-w-0 ${
subView === 'editor' subView === 'editor'
? 'bg-panel border-accent text-main' ? 'bg-panel border-accent text-main'
: 'bg-canvas border-border text-muted hover:text-main' : 'bg-canvas border-border text-muted hover:text-main'
@@ -1410,7 +1410,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
setSubView('todos'); setSubView('todos');
setMobileShowDetail(true); setMobileShowDetail(true);
}} }}
className={`h-8 px-2 sm:px-3 text-xs font-medium border rounded-btn transition-colors flex items-center justify-center gap-1.5 cursor-pointer min-w-0 ${ className={`chip-btn h-8 px-2 sm:px-3 text-xs font-medium border rounded-btn transition-colors flex items-center justify-center gap-1.5 cursor-pointer min-w-0 ${
subView === 'todos' subView === 'todos'
? 'bg-panel border-accent text-main' ? 'bg-panel border-accent text-main'
: 'bg-canvas border-border text-muted hover:text-main' : 'bg-canvas border-border text-muted hover:text-main'
@@ -1429,7 +1429,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
setSubView('tags'); setSubView('tags');
setMobileShowDetail(true); setMobileShowDetail(true);
}} }}
className={`h-8 px-2 sm:px-3 text-xs font-medium border rounded-btn transition-colors flex items-center justify-center gap-1.5 cursor-pointer min-w-0 ${ className={`chip-btn h-8 px-2 sm:px-3 text-xs font-medium border rounded-btn transition-colors flex items-center justify-center gap-1.5 cursor-pointer min-w-0 ${
subView === 'tags' || subView === 'tag_detail' subView === 'tags' || subView === 'tag_detail'
? 'bg-panel border-accent text-main' ? 'bg-panel border-accent text-main'
: 'bg-canvas border-border text-muted hover:text-main' : 'bg-canvas border-border text-muted hover:text-main'
+38 -4
View File
@@ -15,6 +15,10 @@
@import './themes/theme-terracotta-night.css'; @import './themes/theme-terracotta-night.css';
@import './themes/theme-polarized.css'; @import './themes/theme-polarized.css';
@import './themes/theme-suri.css'; @import './themes/theme-suri.css';
@import './themes/theme-fjord.css';
@import './themes/theme-fjord-night.css';
@import './themes/theme-polarized-light.css';
@import './themes/theme-suri-light.css';
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@@ -413,8 +417,9 @@ body {
color: var(--text-main); color: var(--text-main);
} }
/* Prism Syntax Highlighting Tokens (VS Code Dark+ / Light+ via Theme Variables) */ /* Prism Syntax Highlighting Tokens (VS Code Dark+ / Light+ via Theme Variables).
:root { Fallbacks only (:where has no specificity): a theme's own --code-* values always win. */
:where(:root) {
--code-keyword: #569cd6; --code-keyword: #569cd6;
--code-string: #ce9178; --code-string: #ce9178;
--code-number: #b5cea8; --code-number: #b5cea8;
@@ -426,8 +431,7 @@ body {
--code-variable: #9cdcfe; --code-variable: #9cdcfe;
} }
:root:not(.dark):not([data-theme="terracotta"]), :where(:root:not(.dark)) {
:root[data-theme="light"] {
--code-keyword: #0000ff; --code-keyword: #0000ff;
--code-string: #a31515; --code-string: #a31515;
--code-number: #098658; --code-number: #098658;
@@ -603,3 +607,33 @@ fieldset:disabled :is(button, input, select, textarea) {
animation: none !important; animation: none !important;
} }
} }
/* Corner switch (Darstellung): overrides the theme's radii on every theme */
:root[data-corners='square'] {
--radius-card: 0px !important;
--radius-btn: 2px !important;
--radius-input: 2px !important;
--radius-checkbox: 0px !important;
}
:root[data-corners='round'] {
--radius-card: 12px !important;
--radius-btn: 8px !important;
--radius-input: 8px !important;
--radius-checkbox: 4px !important;
}
/* Button frames switch (Darstellung): without frames, regular buttons are a soft tint instead */
:root[data-button-borders='off'] :is(.btn-action, .btn-icon, .btn-secondary, .tab-btn, .chip-btn) {
border-color: transparent !important;
}
:root[data-button-borders='off'] :is(.btn-action, .btn-icon, .btn-secondary, .chip-btn):not(:disabled) {
background-color: color-mix(in srgb, var(--text-main) 6%, transparent);
}
:root[data-button-borders='off'] :is(.btn-action, .btn-icon, .btn-secondary, .chip-btn):not(:disabled):hover {
background-color: color-mix(in srgb, var(--text-main) 11%, transparent);
}
/* … the selected tab keeps a visible accent tint */
:root[data-button-borders='off'] .chip-btn.border-accent {
background-color: var(--accent-dim) !important;
color: var(--accent-primary);
}
+74 -1
View File
@@ -1,5 +1,19 @@
import { describe, it, expect, beforeEach } from 'vitest'; import { describe, it, expect, beforeEach } from 'vitest';
import { setActiveTheme, initThemeOnBoot, customThemeOption, getActiveTheme } from '../theme'; import {
setActiveTheme,
initThemeOnBoot,
customThemeOption,
getActiveTheme,
themeCounterpart,
setCornerStyle,
getCornerStyle,
THEME_FAMILIES,
AVAILABLE_THEMES,
setFollowSystem,
resolveTheme,
setAccentColor,
setButtonBorders,
} from '../theme';
const lightCss = `:root[data-theme="papier"] { --bg-app: #fafafa; --accent-primary: #15803d; --radius-card: 8px; }`; const lightCss = `:root[data-theme="papier"] { --bg-app: #fafafa; --accent-primary: #15803d; --radius-card: 8px; }`;
@@ -83,3 +97,62 @@ describe('built-in themes', () => {
expect(document.documentElement.classList.contains('dark')).toBe(true); expect(document.documentElement.classList.contains('dark')).toBe(true);
}); });
}); });
describe('theme families and corners', () => {
beforeEach(() => localStorage.clear());
it('pairs every family with an existing light and dark theme', () => {
for (const f of THEME_FAMILIES) {
expect(AVAILABLE_THEMES.find((t) => t.id === f.light)?.mode).toBe('light');
expect(AVAILABLE_THEMES.find((t) => t.id === f.dark)?.mode).toBe('dark');
}
expect(themeCounterpart('fjord')).toBe('fjord-night');
expect(themeCounterpart('polarized')).toBe('polarized-light');
expect(themeCounterpart('custom:x')).toBeNull();
});
it('forces round or square corners and keeps the choice', () => {
setCornerStyle('square');
expect(document.documentElement.getAttribute('data-corners')).toBe('square');
expect(getCornerStyle()).toBe('square');
setCornerStyle('theme');
expect(document.documentElement.hasAttribute('data-corners')).toBe(false);
expect(getCornerStyle()).toBe('theme');
});
});
describe('follow system, accent and button frames', () => {
beforeEach(() => localStorage.clear());
it('picks the family variant that matches the system when following it', () => {
const original = window.matchMedia;
window.matchMedia = ((q: string) => ({ matches: q.includes('dark'), media: q, addEventListener() {}, removeEventListener() {} })) as unknown as typeof window.matchMedia;
try {
expect(resolveTheme('fjord')).toBe('fjord');
setFollowSystem(true);
expect(resolveTheme('fjord')).toBe('fjord-night');
expect(resolveTheme('suri-light')).toBe('suri');
expect(resolveTheme('custom:x')).toBe('custom:x');
} finally {
window.matchMedia = original;
}
});
it('sets the accent variables and removes them again', () => {
setActiveTheme('dark');
setAccentColor('#c2410c');
const style = document.documentElement.style;
expect(style.getPropertyValue('--accent-primary')).toBe('#c2410c');
expect(style.getPropertyValue('--text-on-accent')).toBe('#ffffff');
expect(style.getPropertyValue('--accent-dim')).toBe('rgba(194, 65, 12, 0.16)');
setAccentColor(null);
expect(style.getPropertyValue('--accent-primary')).toBe('');
});
it('switches button frames off and on', () => {
setButtonBorders('off');
expect(document.documentElement.getAttribute('data-button-borders')).toBe('off');
setButtonBorders('on');
expect(document.documentElement.hasAttribute('data-button-borders')).toBe(false);
});
});
+196 -2
View File
@@ -15,8 +15,28 @@ export interface ThemeOption {
previewText: string; previewText: string;
isRounded: boolean; isRounded: boolean;
isCustom?: boolean; isCustom?: boolean;
/** Built-in themes come in families with a light and a dark variant */
family?: string;
mode?: 'light' | 'dark';
} }
export interface ThemeFamily {
id: string;
name: string;
subtitle: string;
light: ThemeId;
dark: ThemeId;
}
// Left light, right dark – the settings and the quick switcher show them in this order
export const THEME_FAMILIES: ThemeFamily[] = [
{ id: 'classic', name: 'Klassik', subtitle: 'Neutral mit Blau, eckig', light: 'light', dark: 'dark' },
{ id: 'fjord', name: 'Fjord', subtitle: 'Kühles Papier, Gletscherblau', light: 'fjord', dark: 'fjord-night' },
{ id: 'terracotta', name: 'Terrakotta', subtitle: 'Warmes Papier, gebrannte Erde', light: 'terracotta', dark: 'terracotta-night' },
{ id: 'polarized', name: 'Polarized', subtitle: 'Petrol und Türkis, eckig', light: 'polarized-light', dark: 'polarized' },
{ id: 'suri', name: 'Suri', subtitle: 'Lavendel, Pflaume, Orchidee', light: 'suri-light', dark: 'suri' },
];
export const THEME_STORAGE_KEY = 'mindsift_active_theme'; export const THEME_STORAGE_KEY = 'mindsift_active_theme';
// CSS of the active custom theme, so it applies on boot before the API answers (no flash) // CSS of the active custom theme, so it applies on boot before the API answers (no flash)
const CUSTOM_CSS_CACHE_KEY = 'mindsift_custom_theme_css'; const CUSTOM_CSS_CACHE_KEY = 'mindsift_custom_theme_css';
@@ -90,7 +110,176 @@ export const AVAILABLE_THEMES: ThemeOption[] = [
previewText: '#fbf7ff', previewText: '#fbf7ff',
isRounded: true, isRounded: true,
}, },
]; {
id: 'fjord',
name: 'Fjord (Gletscherblau)',
subtitle: 'Kühles Papier, Tintenblau & Fjordblau (12px)',
file: 'src/themes/theme-fjord.css',
previewBg: '#e9eef5',
previewPanel: '#f7f9fc',
previewAccent: '#2b6cb0',
previewText: '#14202e',
isRounded: true,
},
{
id: 'fjord-night',
name: 'Fjord Nacht (Tiefsee)',
subtitle: 'Nachtblau, Mondlicht & leuchtendes Gletscherblau (12px)',
file: 'src/themes/theme-fjord-night.css',
previewBg: '#0e1621',
previewPanel: '#152131',
previewAccent: '#5b9bdf',
previewText: '#e6eef8',
isRounded: true,
},
{
id: 'polarized-light',
name: 'Polarized Hell (Lagune)',
subtitle: 'Kreidiges Mintweiß mit tiefem Petrol (0px)',
file: 'src/themes/theme-polarized-light.css',
previewBg: '#e6f0ef',
previewPanel: '#f6fbfa',
previewAccent: '#0f766e',
previewText: '#0f2422',
isRounded: false,
},
{
id: 'suri-light',
name: 'Suri Hell (Lavendel)',
subtitle: 'Lavendelpapier, Pflaume & Orchidee (10px)',
file: 'src/themes/theme-suri-light.css',
previewBg: '#efe9f4',
previewPanel: '#fbf8fd',
previewAccent: '#8e44c9',
previewText: '#23182e',
isRounded: true,
},
].map((t) => {
const family = THEME_FAMILIES.find((f) => f.light === t.id || f.dark === t.id);
return family ? { ...t, family: family.id, mode: family.light === t.id ? ('light' as const) : ('dark' as const) } : t;
});
/** The same family in the other mode (light ↔ dark); custom themes have none */
export function themeCounterpart(id: ThemeId): ThemeId | null {
const f = THEME_FAMILIES.find((x) => x.light === id || x.dark === id);
if (!f) return null;
return f.light === id ? f.dark : f.light;
}
// Light/dark follows the system inside the chosen family (per device)
const FOLLOW_SYSTEM_KEY = 'mindsift_theme_follow_system';
export function getFollowSystem(): boolean {
return load(FOLLOW_SYSTEM_KEY) === '1';
}
export function setFollowSystem(on: boolean): void {
store(FOLLOW_SYSTEM_KEY, on ? '1' : null);
}
function systemPrefersDark(): boolean {
return typeof window !== 'undefined' && !!window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}
/** With "follow system": the family's variant that matches the system; otherwise the theme itself */
export function resolveTheme(id: ThemeId): ThemeId {
if (!getFollowSystem()) return id;
const f = THEME_FAMILIES.find((x) => x.light === id || x.dark === id);
if (!f) return id;
return systemPrefersDark() ? f.dark : f.light;
}
/** Switches the theme when the system changes between light and dark; returns the unsubscribe */
export function watchSystemScheme(onChange: (id: ThemeId) => void): () => void {
if (typeof window === 'undefined' || !window.matchMedia) return () => {};
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const handler = () => {
if (!getFollowSystem()) return;
const next = resolveTheme(getActiveTheme());
if (next !== getActiveTheme()) {
setActiveTheme(next);
onChange(next);
}
};
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}
// Buttons with or without frames (per device): "off" removes the border of the regular buttons
export type ButtonBorders = 'on' | 'off';
const BUTTON_BORDERS_KEY = 'mindsift_button_borders';
export function getButtonBorders(): ButtonBorders {
return load(BUTTON_BORDERS_KEY) === 'off' ? 'off' : 'on';
}
export function setButtonBorders(value: ButtonBorders): void {
store(BUTTON_BORDERS_KEY, value === 'off' ? 'off' : null);
applyButtonBorders(value);
}
function applyButtonBorders(value: ButtonBorders): void {
if (typeof document === 'undefined') return;
if (value === 'off') document.documentElement.setAttribute('data-button-borders', 'off');
else document.documentElement.removeAttribute('data-button-borders');
}
// Own accent colour on top of any theme (per device); null = the theme's accent
const ACCENT_KEY = 'mindsift_accent';
const ACCENT_VARS = ['--accent-primary', '--accent-hover', '--accent-dim', '--accent-border', '--text-on-accent'];
export function getAccentColor(): string | null {
const v = load(ACCENT_KEY);
return v && /^#[0-9a-f]{6}$/i.test(v) ? v : null;
}
export function setAccentColor(hex: string | null): void {
store(ACCENT_KEY, hex);
applyAccent(hex);
}
function hexToRgb(hex: string): [number, number, number] {
return [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)) as [number, number, number];
}
function applyAccent(hex: string | null): void {
if (typeof document === 'undefined') return;
const style = document.documentElement.style;
if (!hex) {
ACCENT_VARS.forEach((v) => style.removeProperty(v));
return;
}
const [r, g, b] = hexToRgb(hex);
// Hover goes towards the text colour: darker on light themes, lighter on dark ones
const dark = document.documentElement.classList.contains('dark');
const mix = (c: number) => Math.round(dark ? c + (255 - c) * 0.2 : c * 0.82);
const toHex = (c: number) => c.toString(16).padStart(2, '0');
style.setProperty('--accent-primary', hex);
style.setProperty('--accent-hover', `#${toHex(mix(r))}${toHex(mix(g))}${toHex(mix(b))}`);
style.setProperty('--accent-dim', `rgba(${r}, ${g}, ${b}, ${dark ? 0.16 : 0.1})`);
style.setProperty('--accent-border', `rgba(${r}, ${g}, ${b}, 0.38)`);
style.setProperty('--text-on-accent', 0.2126 * r + 0.7152 * g + 0.0722 * b > 160 ? '#111111' : '#ffffff');
}
// Corners: follow the theme, or force round / square for every theme (per device)
export type CornerStyle = 'theme' | 'round' | 'square';
const CORNERS_STORAGE_KEY = 'mindsift_corners';
export function getCornerStyle(): CornerStyle {
const v = load(CORNERS_STORAGE_KEY);
return v === 'round' || v === 'square' ? v : 'theme';
}
export function setCornerStyle(style: CornerStyle): void {
store(CORNERS_STORAGE_KEY, style === 'theme' ? null : style);
applyCorners(style);
}
function applyCorners(style: CornerStyle): void {
if (typeof document === 'undefined') return;
if (style === 'theme') document.documentElement.removeAttribute('data-corners');
else document.documentElement.setAttribute('data-corners', style);
}
export const isCustomThemeId = (id: ThemeId) => id.startsWith(CUSTOM_THEME_PREFIX); export const isCustomThemeId = (id: ThemeId) => id.startsWith(CUSTOM_THEME_PREFIX);
export const customThemeId = (t: UserTheme): ThemeId => CUSTOM_THEME_PREFIX + t.id; export const customThemeId = (t: UserTheme): ThemeId => CUSTOM_THEME_PREFIX + t.id;
@@ -157,6 +346,7 @@ function applyToDocument(themeId: ThemeId, customCss: string | null): void {
? isLightColor(readCssVar(customCss, 'bg-app')) ? isLightColor(readCssVar(customCss, 'bg-app'))
: isLightColor(AVAILABLE_THEMES.find((t) => t.id === themeId)?.previewBg); : isLightColor(AVAILABLE_THEMES.find((t) => t.id === themeId)?.previewBg);
root.classList.toggle('dark', !light); root.classList.toggle('dark', !light);
applyAccent(getAccentColor());
syncThemeColorMeta(); syncThemeColorMeta();
} }
@@ -209,7 +399,11 @@ export function setActiveTheme(themeId: ThemeId, customCss?: string): void {
} }
export function initThemeOnBoot(): ThemeId { export function initThemeOnBoot(): ThemeId {
const active = getActiveTheme(); applyCorners(getCornerStyle());
applyButtonBorders(getButtonBorders());
const saved = getActiveTheme();
const active = resolveTheme(saved);
if (active !== saved) store(THEME_STORAGE_KEY, active);
applyToDocument(active, isCustomThemeId(active) ? load(CUSTOM_CSS_CACHE_KEY) : null); applyToDocument(active, isCustomThemeId(active) ? load(CUSTOM_CSS_CACHE_KEY) : null);
return active; return active;
} }
+69
View File
@@ -0,0 +1,69 @@
/* ==========================================================================
MINDSIFT THEME: FJORD NACHT (Tiefsee & Mondlicht)
Das dunkle Gegenstück zu Fjord: tiefes Nachtblau statt Schwarz,
mondhelle Schrift, leuchtendes Gletscherblau, weiche Ecken.
========================================================================== */
:root[data-theme="fjord-night"] {
/* --- 1. AKZENTFARBE (Leuchtendes Gletscherblau) --- */
--accent-primary: #5b9bdf;
--accent-hover: #7db2ea;
--accent-dim: rgba(91, 155, 223, 0.16);
--accent-border: rgba(91, 155, 223, 0.40);
--text-on-accent: #0b1622;
/* --- 2. OBERFLÄCHEN & HINTERGRÜNDE (Tiefsee) --- */
--bg-app: #0e1621;
--bg-panel: #152131;
--bg-panel-elevated: #1c2b3e;
--bg-panel-hover: #24364c;
--bg-input: #101a26;
--bg-overlay: rgba(5, 10, 16, 0.72);
--bg-code: #0b121b;
--bg-editor: #121d2a;
--bg-sidebar: #0e1621;
/* --- 3. TEXT & KONTRASTE (Mondlicht) --- */
--text-main: #e6eef8;
--text-muted: #9fb1c6;
--text-faint: #6b7d93;
/* --- 4. RAHMEN, LINIEN & SCHATTEN --- */
--border-color: rgba(180, 205, 235, 0.12);
--border-subtle: rgba(180, 205, 235, 0.06);
--border-strong: rgba(180, 205, 235, 0.24);
--shadow-panel: 0 12px 32px rgba(2, 6, 12, 0.55);
--scrollbar-thumb: rgba(91, 155, 223, 0.22);
--scrollbar-thumb-hover: rgba(91, 155, 223, 0.38);
/* --- 5. STATUS- & SIGNALFARBEN --- */
--color-success: #6fcf97;
--bg-success: rgba(111, 207, 151, 0.14);
--border-success: rgba(111, 207, 151, 0.36);
--color-danger: #f28b82;
--bg-danger: rgba(242, 139, 130, 0.14);
--border-danger: rgba(242, 139, 130, 0.36);
--color-warning: #f6c177;
--bg-warning: rgba(246, 193, 119, 0.14);
--border-warning: rgba(246, 193, 119, 0.36);
/* --- 6. RUNDUNGEN (Weich) --- */
--radius-card: 12px;
--radius-btn: 8px;
--radius-input: 8px;
--radius-checkbox: 4px;
--radius-pill: 9999px;
/* --- 7. CODE-SYNTAX-HIGHLIGHTING --- */
--code-keyword: #7db2ea;
--code-string: #9fd4a3;
--code-number: #f6c177;
--code-comment: #6b7d93;
--code-property: #c3d8f0;
--code-function: #c4a7f5;
--code-type: #6fd3c8;
--code-punctuation: #9fb1c6;
--code-variable: #c3d8f0;
}
+69
View File
@@ -0,0 +1,69 @@
/* ==========================================================================
MINDSIFT THEME: FJORD (Kühles Papier & Gletscherblau)
Bläulich-weißes Papier, nebelgraue Seitenleiste, tintenblaue Schrift,
klares Fjordblau als Akzent und weiche Ecken.
========================================================================== */
:root[data-theme="fjord"] {
/* --- 1. AKZENTFARBE (Fjordblau) --- */
--accent-primary: #2b6cb0;
--accent-hover: #1f5590;
--accent-dim: rgba(43, 108, 176, 0.10);
--accent-border: rgba(43, 108, 176, 0.34);
--text-on-accent: #ffffff;
/* --- 2. OBERFLÄCHEN & HINTERGRÜNDE (Papier, Nebel, Eis) --- */
--bg-app: #e9eef5;
--bg-panel: #f7f9fc;
--bg-panel-elevated: #dde5ef;
--bg-panel-hover: #cfd9e6;
--bg-input: #fbfcfe;
--bg-overlay: rgba(14, 24, 38, 0.45);
--bg-code: #eef2f8;
--bg-editor: #fafcff;
--bg-sidebar: #edf2f8;
/* --- 3. TEXT & KONTRASTE (Tintenblau) --- */
--text-main: #14202e;
--text-muted: #45556a;
--text-faint: #7a8798;
/* --- 4. RAHMEN, LINIEN & SCHATTEN --- */
--border-color: rgba(30, 55, 90, 0.16);
--border-subtle: rgba(30, 55, 90, 0.08);
--border-strong: rgba(30, 55, 90, 0.30);
--shadow-panel: 0 10px 28px rgba(20, 40, 70, 0.10);
--scrollbar-thumb: rgba(30, 55, 90, 0.22);
--scrollbar-thumb-hover: rgba(30, 55, 90, 0.38);
/* --- 5. STATUS- & SIGNALFARBEN --- */
--color-success: #2f855a;
--bg-success: rgba(47, 133, 90, 0.11);
--border-success: rgba(47, 133, 90, 0.34);
--color-danger: #c53030;
--bg-danger: rgba(197, 48, 48, 0.09);
--border-danger: rgba(197, 48, 48, 0.34);
--color-warning: #b7791f;
--bg-warning: rgba(183, 121, 31, 0.11);
--border-warning: rgba(183, 121, 31, 0.34);
/* --- 6. RUNDUNGEN (Weich) --- */
--radius-card: 12px;
--radius-btn: 8px;
--radius-input: 8px;
--radius-checkbox: 4px;
--radius-pill: 9999px;
/* --- 7. CODE-SYNTAX-HIGHLIGHTING (kühle Töne auf Papier) --- */
--code-keyword: #2b6cb0;
--code-string: #2f855a;
--code-number: #b7791f;
--code-comment: #7a8798;
--code-property: #1a4e7a;
--code-function: #6b46c1;
--code-type: #0e7490;
--code-punctuation: #45556a;
--code-variable: #1a4e7a;
}
@@ -0,0 +1,69 @@
/* ==========================================================================
MINDSIFT THEME: POLARIZED HELL (Lagune & Kreide)
Das helle Gegenstück zu Polarized: kreidiges Mintweiß, tiefes Petrol als
Akzent, dunkle Schiefer-Schrift, klar und eckig wie das Original.
========================================================================== */
:root[data-theme="polarized-light"] {
/* --- 1. AKZENTFARBE (Tiefes Petrol) --- */
--accent-primary: #0f766e;
--accent-hover: #115e59;
--accent-dim: rgba(15, 118, 110, 0.10);
--accent-border: rgba(15, 118, 110, 0.34);
--text-on-accent: #ffffff;
/* --- 2. OBERFLÄCHEN & HINTERGRÜNDE (Kreide, Lagune) --- */
--bg-app: #e6f0ef;
--bg-panel: #f6fbfa;
--bg-panel-elevated: #d8e8e6;
--bg-panel-hover: #c7dcd9;
--bg-input: #fbfefd;
--bg-overlay: rgba(8, 30, 28, 0.45);
--bg-code: #eaf3f2;
--bg-editor: #f9fdfc;
--bg-sidebar: #ebf4f3;
/* --- 3. TEXT & KONTRASTE --- */
--text-main: #0f2422;
--text-muted: #3f5c59;
--text-faint: #728a87;
/* --- 4. RAHMEN, LINIEN & SCHATTEN --- */
--border-color: rgba(15, 80, 75, 0.17);
--border-subtle: rgba(15, 80, 75, 0.08);
--border-strong: rgba(15, 80, 75, 0.32);
--shadow-panel: 0 8px 24px rgba(10, 45, 42, 0.09);
--scrollbar-thumb: rgba(15, 80, 75, 0.22);
--scrollbar-thumb-hover: rgba(15, 80, 75, 0.38);
/* --- 5. STATUS- & SIGNALFARBEN --- */
--color-success: #15803d;
--bg-success: rgba(21, 128, 61, 0.10);
--border-success: rgba(21, 128, 61, 0.34);
--color-danger: #be123c;
--bg-danger: rgba(190, 18, 60, 0.09);
--border-danger: rgba(190, 18, 60, 0.34);
--color-warning: #b45309;
--bg-warning: rgba(180, 83, 9, 0.10);
--border-warning: rgba(180, 83, 9, 0.34);
/* --- 6. RUNDUNGEN (Klar & eckig wie Polarized) --- */
--radius-card: 0px;
--radius-btn: 2px;
--radius-input: 2px;
--radius-checkbox: 0px;
--radius-pill: 9999px;
/* --- 7. CODE-SYNTAX-HIGHLIGHTING --- */
--code-keyword: #0f766e;
--code-string: #9a3412;
--code-number: #b45309;
--code-comment: #728a87;
--code-property: #155e75;
--code-function: #7e22ce;
--code-type: #0e7490;
--code-punctuation: #3f5c59;
--code-variable: #155e75;
}
+69
View File
@@ -0,0 +1,69 @@
/* ==========================================================================
MINDSIFT THEME: SURI HELL (Lavendel & Pflaume)
Das helle Gegenstück zu Suri: lavendelhelles Papier, pflaumendunkle
Schrift, samtiger Orchideen-Akzent und weich gerundete Ecken.
========================================================================== */
:root[data-theme="suri-light"] {
/* --- 1. AKZENTFARBE (Orchidee) --- */
--accent-primary: #8e44c9;
--accent-hover: #7431a8;
--accent-dim: rgba(142, 68, 201, 0.10);
--accent-border: rgba(142, 68, 201, 0.34);
--text-on-accent: #ffffff;
/* --- 2. OBERFLÄCHEN & HINTERGRÜNDE (Lavendel) --- */
--bg-app: #efe9f4;
--bg-panel: #fbf8fd;
--bg-panel-elevated: #e6dcef;
--bg-panel-hover: #d9cce6;
--bg-input: #fdfbfe;
--bg-overlay: rgba(30, 18, 40, 0.45);
--bg-code: #f2ecf7;
--bg-editor: #fdfaff;
--bg-sidebar: #f2ecf7;
/* --- 3. TEXT & KONTRASTE (Pflaume) --- */
--text-main: #23182e;
--text-muted: #574a68;
--text-faint: #8a7c9c;
/* --- 4. RAHMEN, LINIEN & SCHATTEN --- */
--border-color: rgba(70, 40, 100, 0.16);
--border-subtle: rgba(70, 40, 100, 0.08);
--border-strong: rgba(70, 40, 100, 0.30);
--shadow-panel: 0 10px 28px rgba(50, 25, 75, 0.10);
--scrollbar-thumb: rgba(70, 40, 100, 0.22);
--scrollbar-thumb-hover: rgba(70, 40, 100, 0.38);
/* --- 5. STATUS- & SIGNALFARBEN --- */
--color-success: #2f855a;
--bg-success: rgba(47, 133, 90, 0.11);
--border-success: rgba(47, 133, 90, 0.34);
--color-danger: #c2185b;
--bg-danger: rgba(194, 24, 91, 0.09);
--border-danger: rgba(194, 24, 91, 0.34);
--color-warning: #b7791f;
--bg-warning: rgba(183, 121, 31, 0.11);
--border-warning: rgba(183, 121, 31, 0.34);
/* --- 6. RUNDUNGEN (Weich wie Suri) --- */
--radius-card: 10px;
--radius-btn: 6px;
--radius-input: 6px;
--radius-checkbox: 4px;
--radius-pill: 9999px;
/* --- 7. CODE-SYNTAX-HIGHLIGHTING --- */
--code-keyword: #7431a8;
--code-string: #2f855a;
--code-number: #b7791f;
--code-comment: #8a7c9c;
--code-property: #5b2a86;
--code-function: #b4235f;
--code-type: #0e7490;
--code-punctuation: #574a68;
--code-variable: #5b2a86;
}