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
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user