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
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 16s
This commit is contained in:
1 parent
0f24cbdbf4
commit
b3e651b3b9
18 files changed
+1455
-804
No files matched your search
+67
-23
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user