import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Layers, CheckSquare, FileText, ShoppingCart, Brain, Settings, Shield, LogOut, ChevronDown, RotateCcw, Palette, HelpCircle, Plus, X, Download, Smartphone, Share, Users, Calendar, AlertTriangle, Flame, Mic, MicOff, Loader2, Search, } from 'lucide-react'; import { AuthStatus, DashboardStats, PriorityLevel, SharedList, Thought, User, UserTheme } from './lib/types'; import { api, clearToken, getToken, triggerHaptic, connectWebSocket, disconnectWebSocket, isSharingDisabled, } from './lib/api'; import { useVoiceInput } from './lib/useVoiceInput'; import { VoiceWave } from './components/VoiceWave'; import { AVAILABLE_THEMES, customThemeOption, initThemeOnBoot, hasChosenTheme, DEMO_THEME, watchSystemScheme, isCustomThemeId, setActiveTheme as applyTheme, ThemeId, } from './lib/theme'; import { InstantCaptureBar } from './features/capture/InstantCaptureBar'; import { HoldToRecordFab } from './features/capture/HoldToRecordFab'; import { CountBadge } from './components/CountBadge'; import { TriageDeck } from './features/triage/TriageDeck'; import { TasksView } from './features/triage/TasksView'; import { WorkspaceView } from './features/workspace/WorkspaceView'; import { SharedListsView } from './features/lists/SharedListsView'; import { SetupAndAuthView } from './features/auth/SetupAndAuthView'; import { OfflineBanner } from './components/OfflineBanner'; import { useServerOnline } from './lib/useServerOnline'; import { SharedImportToast } from './components/SharedImportToast'; import { outboxCount, clearOfflineData } from './lib/offline'; import { DemoCountdown } from './components/DemoCountdown'; import { UserSettingsView } from './features/settings/UserSettingsView'; import { AdminSettingsView } from './features/settings/AdminSettingsView'; import { HelpView } from './features/help/HelpView'; import { GroupsView } from './features/teams/GroupsView'; import { useMobileActions } from './context/MobileActionContext'; 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'; type ActiveTab = | 'triage' | 'tasks' | 'workspace' | 'lists' | 'groups' | 'user_settings' | 'admin_settings' | 'help'; interface ErrorBoundaryState { hasError: boolean; errorText: string; } class ViewErrorBoundary extends React.Component< { children: React.ReactNode; onReset: () => void }, ErrorBoundaryState > { constructor(props: { children: React.ReactNode; onReset: () => void }) { super(props); this.state = { hasError: false, errorText: '' }; } static getDerivedStateFromError(err: Error): ErrorBoundaryState { return { hasError: true, errorText: err?.message || 'Unbekannter Darstellungsfehler' }; } render() { if (this.state.hasError) { return (

Ansicht konnte nicht geladen werden

{this.state.errorText}

); } return this.props.children; } } export const App: React.FC = () => { const [activeTheme, setActiveThemeState] = useState(() => initThemeOnBoot()); const [bootLoading, setBootLoading] = useState(true); const [authStatus, setAuthStatus] = useState({ needsSetup: false, allowRegistration: false, instanceName: 'MindSift', }); const [user, setUser] = useState(null); // Demo mode: when the next reset happens (local clock), and a counter that remounts all views after it const [demoDeadline, setDemoDeadline] = useState(null); const [dataEpoch, setDataEpoch] = useState(0); const [demoNotice, setDemoNotice] = useState(null); // Just shared from another app: these Quick Dump entries can still be sent on as task / list item const [sharedImportIds, setSharedImportIds] = useState(null); const closeSharedImport = useCallback(() => setSharedImportIds(null), []); const lastDemoResetRef = useRef(0); const handleDemoResetRef = useRef<() => void>(() => {}); // Link from the password reset mail (?resetToken=…); removed from the address bar right away const [resetToken, setResetToken] = useState(() => { try { return new URLSearchParams(window.location.search).get('resetToken'); } catch { return null; } }); useEffect(() => { if (resetToken) window.history.replaceState(null, '', window.location.pathname); }, [resetToken]); const [customThemes, setCustomThemes] = useState([]); const allThemes = useMemo(() => [...AVAILABLE_THEMES, ...customThemes.map(customThemeOption)], [customThemes]); const [stats, setStats] = useState({ inboxCount: 0, tasksOpenCount: 0, tasksDoneCount: 0, docsCount: 0, docsTodosOpenCount: 0, listItemsOpenCount: 0, listsCount: 0, discardedCount: 0, }); const [deck, setDeck] = useState([]); const [lists, setLists] = useState([]); const [activeTab, setActiveTab] = useState('triage'); const [previousTab, setPreviousTab] = useState('triage'); const [selectedDocId, setSelectedDocId] = useState(null); const [selectedBlockId, setSelectedBlockId] = useState(null); const [selectedTaskId, setSelectedTaskId] = useState(null); const [selectedListId, setSelectedListId] = useState(null); const [showCommandPalette, setShowCommandPalette] = useState(false); const [captureFocusCount, setCaptureFocusCount] = useState(0); const [refreshRevision, setRefreshRevision] = useState(0); // Profile Dropdown State const [showProfileMenu, setShowProfileMenu] = useState(false); const serverOnline = useServerOnline(); const [showAbout, setShowAbout] = useState(false); const dropdownRef = useRef(null); // Mobile Navigation & Quick-Action State const [showMobileCapture, setShowMobileCapture] = useState(false); const [showMobileProfile, setShowMobileProfile] = useState(false); const [mobileTriageContent, setMobileTriageContent] = useState(''); const [mobileTriagePriority, setMobileTriagePriority] = useState('normal'); const [mobileTriageDueDate, setMobileTriageDueDate] = useState(''); const [isVoiceCaptured, setIsVoiceCaptured] = useState(false); const [mobileVoiceError, setMobileVoiceError] = useState(null); const mobileBaseVoiceRef = useRef(''); // Voice input of the mobile capture sheet: own Whisper server or the browser (lib/voice.ts) const mobileVoice = useVoiceInput({ onText: (text) => { if (!text) return; const base = mobileBaseVoiceRef.current; setMobileTriageContent(base ? `${base} ${text}` : text); setIsVoiceCaptured(true); }, onError: (message) => { setMobileVoiceError(message); window.setTimeout(() => setMobileVoiceError(null), 4000); }, }); const mobileVoiceActive = mobileVoice.state === 'listening'; const toggleMobileVoice = () => { if (mobileVoice.state === 'idle') { mobileBaseVoiceRef.current = mobileTriageContent.replace(/\s*\[…\]$/, '').trim(); setMobileVoiceError(null); triggerHaptic(20); } mobileVoice.toggle(); }; const handleMobileCreateTriage = async (e: React.FormEvent) => { e.preventDefault(); const trimmed = mobileTriageContent.trim(); if (!trimmed) return; try { const thought = await api.captureThought( trimmed, mobileTriagePriority, isVoiceCaptured ? 'voice' : 'manual', mobileTriageDueDate, { target: 'inbox' } ); handleCaptured(thought, 'inbox'); setMobileTriageContent(''); setMobileTriagePriority('normal'); setMobileTriageDueDate(''); setIsVoiceCaptured(false); setShowMobileCapture(false); triggerHaptic(20); } catch (err: any) { alert(`Fehler beim Speichern: ${err.message || 'Unbekannter Fehler'}`); } }; const openMobileTriageCapture = () => { setMobileTriageContent(''); setMobileTriagePriority('normal'); setMobileTriageDueDate(''); setIsVoiceCaptured(false); if (mobileVoice.state !== 'idle') mobileVoice.cancel(); setShowMobileCapture(true); }; const { isMenuOpen, setIsMenuOpen, closeMenu, setGroups, primaryAction, setPrimaryAction } = useMobileActions(); const primaryActionRef = useRef(primaryAction); primaryActionRef.current = primaryAction; // App icon shortcuts (manifest "shortcuts", long-press on the icon): open the tab and its input right away. // ?shortcut=quickdump|task|list; the old links (?mode=capture, ?tab=triage) still lead to the Quick Dump. const [pendingShortcut, setPendingShortcut] = useState(() => { const params = new URLSearchParams(window.location.search); const target = params.get('shortcut') || (params.get('mode') === 'capture' || params.get('tab') === 'triage' ? 'quickdump' : ''); return ({ quickdump: 'triage', task: 'tasks', list: 'lists' } as Record)[target] || null; }); useEffect(() => { if (!pendingShortcut || !user) return; if (activeTab !== pendingShortcut) { setActiveTab(pendingShortcut); return; } const target = pendingShortcut; setPendingShortcut(null); const url = new URL(window.location.href); ['shortcut', 'mode', 'tab'].forEach((k) => url.searchParams.delete(k)); window.history.replaceState({}, '', url.pathname + url.search + url.hash); // The view registers its "+" action once it is mounted, so wait a moment (no cleanup: resetting // pendingShortcut above re-runs this effect and must not cancel the timer) window.setTimeout(() => { if (target === 'triage') handleTriggerAddButton(); else if (window.innerWidth < 640 || target === 'lists') primaryActionRef.current?.(); }, 400); }, [pendingShortcut, user?.id, activeTab]); useEffect(() => { closeMenu(); if (activeTab === 'triage') { setPrimaryAction(openMobileTriageCapture); setGroups([ { id: 'triage-actions', title: 'Schnellaktionen', items: [ { id: 'new-thought', label: 'Neuen Gedanken erfassen', icon: , variant: 'accent', onClick: () => openMobileTriageCapture(), }, { id: 'refresh-deck', label: 'Inbox aktualisieren', icon: , badge: stats.inboxCount, onClick: () => refreshAll(), }, ], }, { id: 'triage-nav', title: 'Bereiche wechseln', items: [ { id: 'goto-workspace', label: 'Notizen & Workspace', icon: , badge: stats.docsCount, onClick: () => setActiveTab('workspace'), }, { id: 'goto-tasks', label: 'Aufgaben & Kanban', icon: , badge: stats.tasksOpenCount, onClick: () => setActiveTab('tasks'), }, { id: 'goto-lists', label: 'Einkaufslisten', icon: , badge: stats.listsCount, onClick: () => setActiveTab('lists'), }, ...(isSharingDisabled() ? [] : [ { id: 'goto-groups', label: 'Gruppen', icon: , onClick: () => setActiveTab('groups'), }, ]), ], }, ]); } else if ( activeTab === 'groups' || activeTab === 'user_settings' || activeTab === 'admin_settings' || activeTab === 'help' ) { setGroups([ { id: 'subview-nav', title: 'Navigation', items: [ { id: 'back-triage', label: 'Zurück zur Triage (Startseite)', icon: , variant: 'accent', onClick: () => setActiveTab('triage'), }, { id: 'goto-workspace', label: 'Zum Workspace', icon: , onClick: () => setActiveTab('workspace'), }, { id: 'goto-tasks', label: 'Zu den Aufgaben', icon: , onClick: () => setActiveTab('tasks'), }, { id: 'goto-lists', label: 'Zu den Listen', icon: , onClick: () => setActiveTab('lists'), }, ], }, ]); } }, [activeTab, stats.inboxCount, stats.tasksOpenCount, stats.docsCount, stats.listsCount]); // Mobile Web-App (PWA) Installation State const [isStandaloneApp, setIsStandaloneApp] = useState(() => { if (typeof window === 'undefined') return false; return ( window.matchMedia('(display-mode: standalone)').matches || (window.navigator as any).standalone === true ); }); const [deferredPwaPrompt, setDeferredPwaPrompt] = useState(() => typeof window !== 'undefined' ? (window as any).__deferredPwaPrompt || null : null ); const [showPwaInstallModal, setShowPwaInstallModal] = useState(false); const [showManualPwaGuide, setShowManualPwaGuide] = useState(false); useEffect(() => { const syncPwaPrompt = () => { if ((window as any).__deferredPwaPrompt) { setDeferredPwaPrompt((window as any).__deferredPwaPrompt); } }; const onBeforeInstallPrompt = (e: Event) => { e.preventDefault(); (window as any).__deferredPwaPrompt = e; setDeferredPwaPrompt(e); }; const onAppInstalled = () => { (window as any).__deferredPwaPrompt = null; setDeferredPwaPrompt(null); setIsStandaloneApp(true); setShowPwaInstallModal(false); setShowManualPwaGuide(false); }; window.addEventListener('beforeinstallprompt', onBeforeInstallPrompt); window.addEventListener('mindsift:pwa-ready', syncPwaPrompt); window.addEventListener('appinstalled', onAppInstalled); return () => { window.removeEventListener('beforeinstallprompt', onBeforeInstallPrompt); window.removeEventListener('mindsift:pwa-ready', syncPwaPrompt); window.removeEventListener('appinstalled', onAppInstalled); }; }, []); // Immediately offer Web-App installation on mobile if not yet running as installed standalone app useEffect(() => { if (!user || isStandaloneApp) return; if (window.innerWidth < 640 && !sessionStorage.getItem('mindsift_pwa_dismissed')) { setShowPwaInstallModal(true); } }, [user?.id, isStandaloneApp]); const handleInstallWebApp = async () => { triggerHaptic(15); const promptEvent = deferredPwaPrompt || (window as any).__deferredPwaPrompt; if (promptEvent && typeof promptEvent.prompt === 'function') { try { await promptEvent.prompt(); const choice = await promptEvent.userChoice; if (choice && choice.outcome === 'accepted') { (window as any).__deferredPwaPrompt = null; setDeferredPwaPrompt(null); setShowPwaInstallModal(false); setShowManualPwaGuide(false); } } catch { setShowManualPwaGuide(true); } } else { setShowManualPwaGuide(true); } }; const handleDismissPwaModal = () => { triggerHaptic(10); sessionStorage.setItem('mindsift_pwa_dismissed', '1'); setShowPwaInstallModal(false); setShowManualPwaGuide(false); }; const handleOpenHelp = () => { triggerHaptic(10); setPreviousTab((prev) => (activeTab === 'help' ? prev : activeTab)); setActiveTab('help'); setShowProfileMenu(false); }; const activateTheme = (themeId: ThemeId, themes: UserTheme[]) => { const css = themes.find((t) => 'custom:' + t.id === themeId)?.css; applyTheme(themeId, css); 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); }; // Custom themes live on the server; after login refresh them and the active one (it may have // been edited or deleted on another device) const handleCustomThemesChange = (themes: UserTheme[], activate?: ThemeId) => { setCustomThemes(themes); const next = activate ?? activeTheme; if (activate || isCustomThemeId(next)) activateTheme(next, themes); }; useEffect(() => { if (!user?.id) return; api .getThemes() .then((themes) => handleCustomThemesChange(themes)) .catch(() => {}); }, [user?.id]); const refreshAll = useCallback(async () => { try { const [meRes, deckRes, listsRes] = await Promise.all([ api.getMe(), api.getTriageDeck(), api.getAllLists(), ]); setUser(meRes.user); setStats(meRes.stats); setDeck(Array.isArray(deckRes) ? deckRes : []); setLists(Array.isArray(listsRes) ? listsRes : []); } catch { // If token expired or invalid, api.ts dispatches mindsift:unauthorized } }, []); const checkBootAndAuth = async () => { setBootLoading(true); try { const st = await api.getAuthStatus(); setAuthStatus(st); if (st.needsSetup) { clearToken(); setUser(null); return; } if (getToken()) { await api.flushOfflineQueue(); await refreshAll(); } else { setUser(null); } } catch { // Server temporarily unreachable } finally { setBootLoading(false); } }; useEffect(() => { checkBootAndAuth(); const onUnauthorized = async () => { setUser(null); setShowProfileMenu(false); try { const st = await api.getAuthStatus(); setAuthStatus(st); } catch { // ignore } }; const onOnline = async () => { if (getToken()) { const synced = await api.flushOfflineQueue(); if (synced > 0) refreshAll(); } }; // Changes still waiting (server was unreachable although the device was online) are retried const retry = window.setInterval(() => { if (navigator.onLine && outboxCount() > 0) onOnline(); }, 30000); window.addEventListener('mindsift:unauthorized', onUnauthorized); window.addEventListener('online', onOnline); return () => { window.clearInterval(retry); window.removeEventListener('mindsift:unauthorized', onUnauthorized); window.removeEventListener('online', onOnline); }; }, []); // WebSocket Connection Management useEffect(() => { if (user && getToken()) { connectWebSocket(); } else { disconnectWebSocket(); } }, [user?.id]); useEffect(() => { setDemoDeadline( authStatus.demoMode && typeof authStatus.demoResetInSeconds === 'number' ? Date.now() + authStatus.demoResetInSeconds * 1000 : null ); }, [authStatus.demoMode, authStatus.demoResetInSeconds]); // Demo: first visit on this device starts with a round theme instead of the square default useEffect(() => { if (!authStatus.demoMode || hasChosenTheme()) return; applyTheme(DEMO_THEME); setActiveThemeState(DEMO_THEME); }, [authStatus.demoMode]); // Web Share Target: import shared thoughts from external apps useEffect(() => { if (!user) return; const params = new URLSearchParams(window.location.search); const hasPendingShare = params.get('shared') === 'pending'; const processShares = async () => { try { const shares = await consumePendingShares(); if (shares.length === 0) return; let importedCount = 0; const ids: string[] = []; for (const share of shares) { const content = formatSharedThought(share); if (content) { const created = await api.captureThought(content, 'normal', 'manual', '', { target: 'inbox' }); importedCount++; if (created?.id && !created.id.startsWith('th_offline_')) ids.push(created.id); } } if (importedCount > 0) { triggerHaptic(15); if (ids.length === importedCount) { setSharedImportIds(ids); } else { // Offline: kept for later, nothing to convert yet setDemoNotice('Im Quick Dump abgelegt'); setTimeout(() => setDemoNotice(null), 4000); } refreshAll(); } } catch (err) { console.error('Fehler beim Import geteilter Daten:', err); } finally { if (hasPendingShare) { const url = new URL(window.location.href); url.searchParams.delete('shared'); window.history.replaceState({}, '', url.pathname + (url.search ? url.search : '') + url.hash); } } }; processShares(); }, [user, refreshAll]); // The demo database was reset: drop cached answers of the old data, remount the views (they load // fresh data), refresh counters and the countdown. Sessions stay valid (fixed demo account IDs). const handleDemoReset = useCallback(() => { if (Date.now() - lastDemoResetRef.current < 30000) return; // event and countdown may both report it lastDemoResetRef.current = Date.now(); void clearOfflineData(); setDataEpoch((n) => n + 1); refreshAll(); api.getAuthStatus().then(setAuthStatus).catch(() => {}); setDemoNotice('Demo-Instanz wurde zurückgesetzt'); window.setTimeout(() => setDemoNotice(null), 4000); }, [refreshAll]); handleDemoResetRef.current = handleDemoReset; // Countdown reached zero but no DEMO_RESET arrived (e.g. long polling): ask the server shortly after const handleDemoCountdownElapsed = useCallback(() => { window.setTimeout(() => { api .getAuthStatus() .then((st) => { if ((st.demoResetInSeconds ?? 0) > 60) handleDemoResetRef.current(); else setAuthStatus(st); }) .catch(() => {}); }, 5000); }, []); // Refresh stats on task/doc/list real-time events (counters in the navigation). Task events also // reload the triage deck: a note sorted on another device must leave this deck too. useEffect(() => { let deckTimer: number | undefined; const reloadDeck = () => { window.clearTimeout(deckTimer); deckTimer = window.setTimeout(() => { api.getTriageDeck().then((res) => setDeck(Array.isArray(res) ? res : [])).catch(() => {}); }, 300); }; // Events missed while the tab slept (desktop in the background, connection dropped) const onVisible = () => { if (document.visibilityState === 'visible' && getToken()) refreshAll(); }; const handleWs = (e: any) => { const msg = e.detail; if (!msg) return; if (msg.event === 'DEMO_RESET') { handleDemoResetRef.current(); return; } if ( typeof msg.event === 'string' && ['TASK_', 'DOC_', 'SPACE_', 'ITEM_', 'LIST_'].some((prefix) => msg.event.startsWith(prefix)) ) { api.getMe().then((res) => { setStats(res.stats); }).catch(() => {}); if (msg.event.startsWith('TASK_')) reloadDeck(); } }; window.addEventListener('mindsift:ws', handleWs); document.addEventListener('visibilitychange', onVisible); return () => { window.clearTimeout(deckTimer); window.removeEventListener('mindsift:ws', handleWs); document.removeEventListener('visibilitychange', onVisible); }; }, [refreshAll]); // Close profile dropdown on outside click useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setShowProfileMenu(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const handleAuthenticated = async (loggedInUser: User) => { setUser(loggedInUser); setAuthStatus((prev) => ({ ...prev, needsSetup: false })); setActiveTab('triage'); await refreshAll(); }; const handleCaptured = ( thought: Thought, target: 'inbox' | 'task' | 'list_item' | 'document' = 'inbox' ) => { if (target === 'inbox' && thought.status === 'inbox') { setDeck((prev) => { if (prev.some((t) => t.id === thought.id)) return prev; return [thought, ...prev]; }); setStats((prev) => ({ ...prev, inboxCount: prev.inboxCount + 1 })); } else if (target === 'document' && thought.convertedRefId) { setSelectedDocId(thought.convertedRefId); setActiveTab('workspace'); } setRefreshRevision((r) => r + 1); refreshAll(); }; const handleOpenDocument = (docId: string, blockId?: string) => { setSelectedDocId(docId); setSelectedBlockId(blockId || null); setActiveTab('workspace'); }; const handleOpenTask = useCallback((taskId: string) => { setSelectedTaskId(taskId); setActiveTab('tasks'); }, []); const handleOpenList = (listId: string) => { setSelectedListId(listId); setActiveTab('lists'); }; // Global Command Palette shortcuts: Cmd+K / Ctrl+K, and '/' when not in input useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { const el = e.target as HTMLElement; const isEditing = el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable; if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); setShowCommandPalette((prev) => !prev); return; } if (!isEditing && e.key === '/') { e.preventDefault(); setShowCommandPalette(true); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, []); const handleTriggerAddButton = () => { triggerHaptic(20); if (window.innerWidth < 640) { openMobileTriageCapture(); } else { window.scrollTo({ top: 0, behavior: 'smooth' }); setCaptureFocusCount((c) => c + 1); } }; const handleLogout = async () => { triggerHaptic(20); disconnectWebSocket(); try { await api.logout(); } catch { clearToken(); } setUser(null); setShowProfileMenu(false); setActiveTab('triage'); try { const st = await api.getAuthStatus(); setAuthStatus(st); } catch { // ignore } }; if (bootLoading) { return (
Verbinde mit MindSift…
); } // Show Setup Wizard or Login Screen if needsSetup or not logged in if (authStatus.needsSetup || !user || resetToken) { return ( { // The new password ended every session, this one included setResetToken(null); clearToken(); setUser(null); }} onResetCancel={() => setResetToken(null)} /> ); } const isServerAdmin = user.role === 'admin'; const roleLabel = isServerAdmin ? 'Server-Admin' : 'Mitglied'; const activeTabLabelMap: Record = { triage: 'Triage', tasks: 'Aufgaben', workspace: 'Workspace', lists: 'Listen', groups: 'Gruppen', user_settings: 'Einstellungen', admin_settings: 'Administration', help: 'Hilfe', }; return (
{/* Top Header Bar: Floating on mobile, solid panel on desktop */}
{/* Mobile Header: Floating Bar with Logo Action Dropdown Left & Profile Avatar Right */}
{!isStandaloneApp && ( )}
{/* Desktop Header: 3-column Layout (Logo, InstantCaptureBar, Profile Pill) */}
{/* Logo & Headline */}
{ setActiveTab('triage'); handleTriggerAddButton(); }} className="h-9 flex items-center gap-2.5 cursor-pointer group select-none flex-shrink-0" >
{authStatus.instanceName || 'MindSift'} {demoDeadline ? ( ) : ( {APP_VERSION} )}
{/* Quick-Capture Input Bubble directly in Header (above Tabs) */}
{/* User Profile Pill & Dropdown Menu */}
{showProfileMenu && (
{/* User Info Header */}

{user.displayName}

{user.email}

{roleLabel}
{/* Theme Quick Switcher */}
Design-Theme
{/* Menu Items */}
{!isSharingDisabled() && ( )} {isServerAdmin && ( )}
{/* Logout */}
)}
{/* Desktop Navigation tabs attached to header */}
{sharedImportIds && ( { setSharedImportIds(null); setDemoNotice(message); setTimeout(() => setDemoNotice(null), 3000); refreshAll(); }} /> )} {demoNotice && (
✨ {demoNotice}
)} {/* Main Container */} {/* The workspace fills the window exactly and scrolls only inside the note, never the page */}
setActiveTab('triage')}>
{activeTab === 'triage' && ( )} {activeTab === 'tasks' && ( )} {activeTab === 'workspace' && ( setShowCommandPalette(true)} /> )} {activeTab === 'help' && ( setActiveTab(previousTab || 'triage')} /> )} {activeTab === 'lists' && ( )} {activeTab === 'groups' && ( setActiveTab(previousTab || 'triage')} /> )} {activeTab === 'user_settings' && ( { setUser(u); refreshAll(); }} onBack={() => setActiveTab('triage')} /> )} {activeTab === 'admin_settings' && isServerAdmin && ( setActiveTab('triage')} /> )}
setShowAbout(true)} /> {/* MOBILE BOTTOM NAVIGATION BAR (5 equal columns -> + FAB is dead-center) */} {/* MOBILE QUICK-ADD BOTTOM SHEET / MODAL (DESIGN-ALIGNED WITH TASKS VIEW) */} {showMobileCapture && (
setShowMobileCapture(false)} >
e.stopPropagation()} > {/* Header */}

Quick Dump erfassen

{/* Text Input with Voice Dictation */}
setMobileTriageContent(e.target.value)} placeholder={ mobileVoiceError ? mobileVoiceError : mobileVoiceActive ? 'Ich höre zu…' : mobileVoice.state === 'transcribing' ? 'Wird erkannt…' : 'Was geht dir durch den Kopf?…' } className={ mobileVoiceActive && mobileVoice.mode === 'server' ? 'w-full input-field px-3 py-2 pr-24 text-sm' : 'w-full input-field px-3 py-2 pr-10 text-sm' } /> {mobileVoice.available && (
{mobileVoiceActive && mobileVoice.mode === 'server' && ( )}
)}
{/* Priority - 4 Buttons (Identical to TasksView) */}
{(['low', 'normal', 'high', 'urgent'] as PriorityLevel[]).map((p) => { const config: Record< PriorityLevel, { label: string; icon?: React.ReactNode; activeClass: string } > = { low: { label: 'Niedrig', activeClass: 'bg-canvas border-strong text-main font-semibold shadow-xs', }, normal: { label: 'Normal', activeClass: 'bg-accent/15 border-accent text-accent font-semibold shadow-xs', }, high: { label: 'Wichtig', icon: , activeClass: 'bg-status-warn/15 border-status-warn text-status-warn font-semibold shadow-xs', }, urgent: { label: 'Dringend', icon: , activeClass: 'bg-status-err/15 border-status-err text-status-err font-semibold shadow-xs', }, }; const active = mobileTriagePriority === p; const item = config[p]; return ( ); })}
{/* Due Date (Optional) */}
setMobileTriageDueDate(e.target.value)} className="w-full input-field px-3 py-2 text-xs" />
{/* Bottom Buttons */}
)} {/* MOBILE USER PROFILE SHEET */} {showMobileProfile && (
setShowMobileProfile(false)} >
e.stopPropagation()} > {/* User Info Header */}

{user.displayName}

{user.email}

{roleLabel}
{/* Theme Quick Switcher */}
Design-Theme
{/* Menu Items */}
{!isStandaloneApp && ( )} {!isSharingDisabled() && ( )} {isServerAdmin && ( )}
{/* Logout */}
{/* Version & About Hint on Mobile */}
MindSift {APP_VERSION}
)} {/* IMMEDIATE MOBILE WEB-APP INSTALLATION OFFER SHEET */} {showPwaInstallModal && !isStandaloneApp && (
e.stopPropagation()} >

{authStatus.instanceName || 'MindSift'} als App installieren

Vollbild ohne Browser-Leiste, schnellerer Start & Direktzugriff vom Home-Bildschirm.

{!showManualPwaGuide ? (
) : (
{/* Chrome may report "installable" only after the guide opened (e.g. shortly after the old app was removed): then the real install is one tap away again */} {deferredPwaPrompt && ( )}

In 2 Sekunden zum Home-Bildschirm hinzufügen:

  1. Tippe im Browser auf{' '} Teilen {' '} (iPhone/Safari) oder das{' '} Browser-Menü (⋮).
  2. Wähle{' '} „Zum Home-Bildschirm“ bzw.{' '} „App installieren“.
)}
)} {/* Global Command Palette (Cmd+K / Ctrl+K / '/') */} setShowCommandPalette(false)} onOpenDocument={handleOpenDocument} onOpenTask={handleOpenTask} onOpenList={handleOpenList} /> {showAbout && setShowAbout(false)} />}
); }; export default App;