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
+7
-40
@@ -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 = () => {
|
||||
<Palette className="w-3 h-3 text-accent" />
|
||||
<span>Design-Theme</span>
|
||||
</span>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{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>
|
||||
<ThemeQuickPicker activeTheme={activeTheme} themes={allThemes} onChange={handleChangeTheme} />
|
||||
</div>
|
||||
|
||||
{/* Menu Items */}
|
||||
@@ -1560,26 +1546,7 @@ export const App: React.FC = () => {
|
||||
<Palette className="w-3.5 h-3.5 text-accent" />
|
||||
<span>Design-Theme</span>
|
||||
</span>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{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>
|
||||
<ThemeQuickPicker activeTheme={activeTheme} themes={allThemes} onChange={handleChangeTheme} touch />
|
||||
</div>
|
||||
|
||||
{/* 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 { 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'
|
||||
|
||||
+38
-4
@@ -15,6 +15,10 @@
|
||||
@import './themes/theme-terracotta-night.css';
|
||||
@import './themes/theme-polarized.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 components;
|
||||
@@ -413,8 +417,9 @@ body {
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
/* Prism Syntax Highlighting Tokens (VS Code Dark+ / Light+ via Theme Variables) */
|
||||
:root {
|
||||
/* Prism Syntax Highlighting Tokens (VS Code Dark+ / Light+ via Theme Variables).
|
||||
Fallbacks only (:where has no specificity): a theme's own --code-* values always win. */
|
||||
:where(:root) {
|
||||
--code-keyword: #569cd6;
|
||||
--code-string: #ce9178;
|
||||
--code-number: #b5cea8;
|
||||
@@ -426,8 +431,7 @@ body {
|
||||
--code-variable: #9cdcfe;
|
||||
}
|
||||
|
||||
:root:not(.dark):not([data-theme="terracotta"]),
|
||||
:root[data-theme="light"] {
|
||||
:where(:root:not(.dark)) {
|
||||
--code-keyword: #0000ff;
|
||||
--code-string: #a31515;
|
||||
--code-number: #098658;
|
||||
@@ -603,3 +607,33 @@ fieldset:disabled :is(button, input, select, textarea) {
|
||||
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);
|
||||
}
|
||||
@@ -1,5 +1,19 @@
|
||||
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; }`;
|
||||
|
||||
@@ -83,3 +97,62 @@ describe('built-in themes', () => {
|
||||
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
@@ -15,8 +15,28 @@ export interface ThemeOption {
|
||||
previewText: string;
|
||||
isRounded: 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';
|
||||
// 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';
|
||||
@@ -90,7 +110,176 @@ export const AVAILABLE_THEMES: ThemeOption[] = [
|
||||
previewText: '#fbf7ff',
|
||||
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 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(AVAILABLE_THEMES.find((t) => t.id === themeId)?.previewBg);
|
||||
root.classList.toggle('dark', !light);
|
||||
applyAccent(getAccentColor());
|
||||
syncThemeColorMeta();
|
||||
}
|
||||
|
||||
@@ -209,7 +399,11 @@ export function setActiveTheme(themeId: ThemeId, customCss?: string): void {
|
||||
}
|
||||
|
||||
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);
|
||||
return active;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user