feat(theme): implement single-file CSS theming engine with dark, light, high-contrast, and soft themes
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 16s

This commit is contained in:
sandrodach committed 2026-10-24 23:26:31 +02:00
1 parent 0f24cbdbf4
commit b3e651b3b9
18 files changed
+1455 -804

No files matched your search

+67 -23
View File
@@ -10,9 +10,16 @@ import {
LogOut,
ChevronDown,
RotateCcw,
Palette,
} from 'lucide-react';
import { AuthStatus, DashboardStats, SharedList, Thought, User } from './lib/types';
import { api, clearToken, getToken, triggerHaptic } from './lib/api';
import {
AVAILABLE_THEMES,
initThemeOnBoot,
setActiveTheme as applyTheme,
ThemeId,
} from './lib/theme';
import { InstantCaptureBar } from './features/capture/InstantCaptureBar';
import { TriageDeck } from './features/triage/TriageDeck';
import { TasksView } from './features/triage/TasksView';
@@ -51,9 +58,9 @@ class ViewErrorBoundary extends React.Component<
render() {
if (this.state.hasError) {
return (
<div className="p-6 bg-panel border border-border text-xs space-y-3 max-w-lg mx-auto my-8">
<h3 className="text-sm font-bold text-white">Ansicht konnte nicht geladen werden</h3>
<p className="text-zinc-400">{this.state.errorText}</p>
<div className="p-6 panel-surface text-xs space-y-3 max-w-lg mx-auto my-8">
<h3 className="text-sm font-bold text-main">Ansicht konnte nicht geladen werden</h3>
<p className="text-muted">{this.state.errorText}</p>
<button
onClick={() => {
this.setState({ hasError: false, errorText: '' });
@@ -72,6 +79,7 @@ class ViewErrorBoundary extends React.Component<
}
export const App: React.FC = () => {
const [activeTheme, setActiveThemeState] = useState<ThemeId>(() => initThemeOnBoot());
const [bootLoading, setBootLoading] = useState(true);
const [authStatus, setAuthStatus] = useState<AuthStatus>({
needsSetup: false,
@@ -98,6 +106,12 @@ export const App: React.FC = () => {
const [showProfileMenu, setShowProfileMenu] = useState(false);
const dropdownRef = useRef<HTMLDivElement | null>(null);
const handleChangeTheme = (themeId: ThemeId) => {
triggerHaptic(10);
applyTheme(themeId);
setActiveThemeState(themeId);
};
const refreshAll = async () => {
try {
const [meRes, deckRes, listsRes] = await Promise.all([
@@ -228,7 +242,7 @@ export const App: React.FC = () => {
if (bootLoading) {
return (
<div className="min-h-screen bg-canvas text-zinc-400 flex items-center justify-center text-xs font-sans">
<div className="min-h-screen bg-canvas text-muted flex items-center justify-center text-xs font-sans">
Verbinde mit MindSift…
</div>
);
@@ -253,9 +267,9 @@ export const App: React.FC = () => {
: 'Mitglied';
return (
<div className="min-h-screen bg-canvas text-white flex flex-col font-sans selection:bg-accent/30">
<div className="min-h-screen bg-canvas text-main flex flex-col font-sans selection:bg-accent/30">
{/* Top Header Bar */}
<header className="sticky top-0 z-30 bg-panel/95 backdrop-blur-md border-b border-border shadow-sm">
<header className="sticky top-0 z-30 bg-panel border-b border-border shadow-sm">
<div className="max-w-6xl mx-auto px-3 sm:px-4 py-2 flex items-center justify-between gap-3 sm:gap-6">
{/* Logo & Headline */}
<div
@@ -265,14 +279,14 @@ export const App: React.FC = () => {
}}
className="flex items-center gap-2.5 cursor-pointer group select-none flex-shrink-0"
>
<div className="w-7 h-7 bg-accent text-white flex items-center justify-center">
<div className="w-7 h-7 bg-accent text-on-accent rounded-btn flex items-center justify-center">
<Brain className="w-4 h-4" />
</div>
<div className="flex flex-col">
<span className="font-bold text-sm sm:text-base text-white tracking-tight leading-none">
<span className="font-bold text-sm sm:text-base text-main tracking-tight leading-none">
{authStatus.instanceName || 'MindSift'}
</span>
<span className="text-[11px] text-zinc-400 mt-0.5 hidden sm:inline-block">
<span className="text-[11px] text-muted mt-0.5 hidden sm:inline-block">
Der Kopf frei in drei Sekunden
</span>
</div>
@@ -294,29 +308,57 @@ export const App: React.FC = () => {
triggerHaptic(10);
setShowProfileMenu((prev) => !prev);
}}
className="h-8 pl-1 pr-2.5 rounded-full bg-panel hover:bg-panel-elevated border border-border text-xs font-medium text-white flex items-center gap-2 transition-colors"
className="h-8 pl-1 pr-2.5 capture-pill bg-panel hover:bg-panel-elevated border border-border text-xs font-medium text-main flex items-center gap-2 transition-colors"
title="Benutzermenü öffnen"
>
<div className="w-6 h-6 rounded-full bg-accent text-white text-[10px] font-bold flex items-center justify-center">
<div className="w-6 h-6 capture-pill bg-accent text-on-accent text-[10px] font-bold flex items-center justify-center">
{user.displayName ? user.displayName.slice(0, 1).toUpperCase() : 'U'}
</div>
<span className="max-w-[120px] truncate hidden sm:inline">
{user.displayName}
</span>
<ChevronDown className="w-3.5 h-3.5 text-zinc-400" />
<ChevronDown className="w-3.5 h-3.5 text-muted" />
</button>
{showProfileMenu && (
<div className="absolute right-0 top-full mt-1.5 w-60 bg-panel border border-border shadow-2xl z-50 divide-y divide-border text-xs">
<div className="absolute right-0 top-full mt-1.5 w-64 panel-surface shadow-panel z-50 divide-y divide-border text-xs">
{/* User Info Header */}
<div className="p-3 bg-canvas">
<p className="font-bold text-white truncate">{user.displayName}</p>
<p className="text-[11px] text-zinc-400 truncate">{user.email}</p>
<span className="inline-block mt-1.5 px-1.5 py-0.5 bg-panel border border-border text-[10px] text-accent font-semibold">
<p className="font-bold text-main truncate">{user.displayName}</p>
<p className="text-[11px] text-muted truncate">{user.email}</p>
<span className="inline-block mt-1.5 px-1.5 py-0.5 bg-panel border border-border rounded-btn text-[10px] text-accent font-semibold">
{roleLabel}
</span>
</div>
{/* Theme Quick Switcher */}
<div className="p-2.5 space-y-1.5">
<span className="text-[10px] font-semibold text-muted uppercase tracking-wider flex items-center gap-1.5 px-1">
<Palette className="w-3 h-3 text-accent" />
<span>Design-Theme</span>
</span>
<div className="grid grid-cols-2 gap-1">
{AVAILABLE_THEMES.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>
</div>
{/* Menu Items */}
<div className="py-1">
<button
@@ -326,7 +368,7 @@ export const App: React.FC = () => {
setActiveTab('user_settings');
}}
className={`w-full px-3.5 py-2.5 text-left flex items-center gap-2.5 hover:bg-panel-elevated transition-colors ${
activeTab === 'user_settings' ? 'text-accent font-semibold' : 'text-zinc-200'
activeTab === 'user_settings' ? 'text-accent font-semibold' : 'text-main'
}`}
>
<Settings className="w-4 h-4 text-accent" />
@@ -343,7 +385,7 @@ export const App: React.FC = () => {
className={`w-full px-3.5 py-2.5 text-left flex items-center gap-2.5 hover:bg-panel-elevated transition-colors ${
activeTab === 'admin_settings'
? 'text-accent font-semibold'
: 'text-zinc-200'
: 'text-main'
}`}
>
<Shield className="w-4 h-4 text-accent" />
@@ -356,7 +398,7 @@ export const App: React.FC = () => {
<div className="py-1">
<button
onClick={handleLogout}
className="w-full px-3.5 py-2.5 text-left flex items-center gap-2.5 text-red-400 hover:bg-red-950/30 transition-colors"
className="w-full px-3.5 py-2.5 text-left flex items-center gap-2.5 text-status-err hover:bg-status-err-bg transition-colors"
>
<LogOut className="w-4 h-4" />
<span>Abmelden</span>
@@ -367,7 +409,7 @@ export const App: React.FC = () => {
</div>
</div>
{/* Monochrome square tabs attached to header */}
{/* Navigation tabs attached to header */}
<nav className="max-w-6xl mx-auto px-1 sm:px-2 overflow-x-auto no-scrollbar border-t border-border">
<div className="flex items-stretch">
<button
@@ -384,7 +426,7 @@ export const App: React.FC = () => {
className={
activeTab === 'triage'
? 'badge-accent'
: 'px-1.5 py-0.5 text-[10px] bg-canvas text-zinc-400 border border-border'
: 'badge-neutral'
}
>
{stats.inboxCount}
@@ -406,7 +448,7 @@ export const App: React.FC = () => {
className={
activeTab === 'tasks'
? 'badge-accent'
: 'px-1.5 py-0.5 text-[10px] bg-canvas text-zinc-400 border border-border'
: 'badge-neutral'
}
>
{stats.tasksOpenCount}
@@ -427,7 +469,7 @@ export const App: React.FC = () => {
className={
activeTab === 'workspace'
? 'badge-accent'
: 'px-1.5 py-0.5 text-[10px] bg-canvas text-zinc-400 border border-border'
: 'badge-neutral'
}
>
{stats.docsCount}
@@ -484,6 +526,8 @@ export const App: React.FC = () => {
{activeTab === 'user_settings' && (
<UserSettingsView
currentUser={user}
activeTheme={activeTheme}
onThemeChange={handleChangeTheme}
onUserUpdated={(u) => {
setUser(u);
refreshAll();