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

@@ -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<UserSettingsViewProps> = ({
const [demoMode, setDemoMode] = useState(false);
const linkPreviewsOn = useLinkPreviewsEnabled();
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(() => {
api
.getAuthStatus()
@@ -920,8 +941,205 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
/>
</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">
{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 (
@@ -1362,7 +1362,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
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<WorkspaceViewProps> = ({
<button
type="button"
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'
? 'bg-panel border-accent text-main'
: 'bg-canvas border-border text-muted hover:text-main'
@@ -1410,7 +1410,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
setSubView('todos');
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'
? 'bg-panel border-accent text-main'
: 'bg-canvas border-border text-muted hover:text-main'
@@ -1429,7 +1429,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
setSubView('tags');
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'
? 'bg-panel border-accent text-main'
: 'bg-canvas border-border text-muted hover:text-main'