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
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:
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'
|
||||
|
||||
Reference in new issue
Block a user