diff --git a/CHANGELOG.md b/CHANGELOG.md index 349a9b4..43b8bcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [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 ### 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. - 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.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 diff --git a/Doku/theming.md b/Doku/theming.md index 1d78744..8a0ba43 100644 --- a/Doku/theming.md +++ b/Doku/theming.md @@ -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) | | `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`) | +| `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) | | `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 | diff --git a/README.md b/README.md index 3780ac8..24d4bf7 100644 --- a/README.md +++ b/README.md @@ -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. * Gruppen mit Gruppen-Admins für Freigaben. * 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 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c1a6ca3..237c2f7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.13.0", + "version": "0.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.13.0", + "version": "0.14.0", "license": "Unlicense", "dependencies": { "@fontsource-variable/inter": "5.3.0", diff --git a/frontend/package.json b/frontend/package.json index fea684d..efb39f9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.13.0", + "version": "0.14.0", "license": "Unlicense", "type": "module", "scripts": { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f5b7275..0e1a8e3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -42,6 +42,7 @@ import { AVAILABLE_THEMES, customThemeOption, initThemeOnBoot, + watchSystemScheme, isCustomThemeId, setActiveTheme as applyTheme, ThemeId, @@ -66,6 +67,7 @@ import { MobileHeaderMenu } from './components/MobileHeaderMenu'; import { UserAvatar } from './components/UserAvatar'; import { CommandPalette } from './components/CommandPalette'; import { AppFooter } from './components/AppFooter'; +import { ThemeQuickPicker } from './components/ThemeQuickPicker'; import { AboutModal } from './components/AboutModal'; import { consumePendingShares, formatSharedThought } from './lib/shareTarget'; import { APP_VERSION } from './version'; @@ -442,6 +444,9 @@ export const App: React.FC = () => { 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) => { triggerHaptic(10); activateTheme(themeId, customThemes); @@ -979,26 +984,7 @@ export const App: React.FC = () => { Design-Theme -
- {allThemes.map((t) => ( - - ))} -
+ {/* Menu Items */} @@ -1560,26 +1546,7 @@ export const App: React.FC = () => { Design-Theme -
- {allThemes.map((t) => ( - - ))} -
+ {/* Menu Items */} diff --git a/frontend/src/components/ThemeQuickPicker.tsx b/frontend/src/components/ThemeQuickPicker.tsx new file mode 100644 index 0000000..9560297 --- /dev/null +++ b/frontend/src/components/ThemeQuickPicker.tsx @@ -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 = ({ 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 ( +
+
+
+ {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 ( + + ); + })} +
+
+ {(['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 ( + + ); + })} +
+
+ {customThemes.length > 0 && ( +
+ {customThemes.map((t) => ( + + ))} +
+ )} +
+ ); +}; diff --git a/frontend/src/features/settings/UserSettingsView.tsx b/frontend/src/features/settings/UserSettingsView.tsx index 7e54294..b6fb11d 100644 --- a/frontend/src/features/settings/UserSettingsView.tsx +++ b/frontend/src/features/settings/UserSettingsView.tsx @@ -23,7 +23,24 @@ import { ServerBackupsCard } from './ServerBackupsCard'; import { saveBlob } from '../../lib/download'; import { api, clearToken, triggerHaptic } from '../../lib/api'; 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 { UserAvatar, resizeImageToDataUrl } from '../../components/UserAvatar'; import { LegalLinks } from '../../components/LegalLinks'; @@ -158,6 +175,10 @@ export const UserSettingsView: React.FC = ({ const [demoMode, setDemoMode] = useState(false); const linkPreviewsOn = useLinkPreviewsEnabled(); const [journalOn, setJournalOn] = useState(isJournalEnabled); + const [corners, setCorners] = useState(getCornerStyle); + const [followSystem, setFollowSystemState] = useState(getFollowSystem); + const [buttonBorders, setButtonBordersState] = useState(getButtonBorders); + const [accent, setAccent] = useState(getAccentColor); useEffect(() => { api .getAuthStatus() @@ -920,8 +941,205 @@ export const UserSettingsView: React.FC = ({ /> + {/* Colour families: left light, right dark */} +
+
+ Theme + Hell + Dunkel +
+ {THEME_FAMILIES.map((f) => ( +
+
+

{f.name}

+

{f.subtitle}

+
+ {[f.light, f.dark].map((id) => { + const t = AVAILABLE_THEMES.find((x) => x.id === id)!; + const selected = activeTheme === id; + return ( + + ); + })} +
+ ))} +
+ + {/* Corners for every theme (per device) */} +
+
+

Ecken

+

Abgerundet oder eckig – unabhängig vom Theme. Gilt für dieses Gerät.

+
+
+ {( + [ + ['theme', 'Wie Theme'], + ['round', 'Rund'], + ['square', 'Eckig'], + ] as [CornerStyle, string][] + ).map(([value, label]) => ( + + ))} +
+
+ + {/* Light/dark: fixed, or following the system inside the family */} +
+
+

Hell oder dunkel

+

„Wie System“ wechselt innerhalb der Farbfamilie mit dem Hell-/Dunkelmodus des Geräts.

+
+
+ {( + [ + [false, 'Selbst wählen'], + [true, 'Wie System'], + ] as [boolean, string][] + ).map(([value, label]) => ( + + ))} +
+
+ + {/* Frames around buttons */} +
+
+

Rahmen an Knöpfen

+

Ohne Rahmen sind Knöpfe nur leicht hinterlegt. Gilt für dieses Gerät.

+
+
+ {( + [ + ['on', 'Mit Rahmen'], + ['off', 'Ohne Rahmen'], + ] as [ButtonBorders, string][] + ).map(([value, label]) => ( + + ))} +
+
+ + {/* Own accent colour on top of the theme */} +
+
+
+

Akzentfarbe

+

Ersetzt die Akzentfarbe des Themes (Knöpfe, Links, Markierungen). Gilt für dieses Gerät.

+
+ {accent && ( + + )} +
+
+ {['#2563eb', '#0f766e', '#16a34a', '#ca8a04', '#c2410c', '#dc2626', '#db2777', '#8e44c9', '#475569'].map((c) => ( +
+
+ + {themes.some((t) => t.isCustom) &&

Eigene Themes

}
- {themes.map((t) => { + {themes.filter((t) => t.isCustom).map((t) => { const isSelected = activeTheme === t.id; const custom = customThemes.find((c) => customThemeId(c) === t.id); return ( diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index be10b23..474ab4e 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -1362,7 +1362,7 @@ export const WorkspaceView: React.FC = ({ type="button" disabled={subView !== 'editor'} 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' ? 'opacity-35 bg-canvas border-border text-faint cursor-not-allowed pointer-events-none' : isSidebarCollapsed @@ -1394,7 +1394,7 @@ export const WorkspaceView: React.FC = ({