Dev #3
+315
-6
@@ -12,6 +12,8 @@ import {
|
||||
RotateCcw,
|
||||
Palette,
|
||||
HelpCircle,
|
||||
Plus,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { AuthStatus, DashboardStats, SharedList, Thought, User } from './lib/types';
|
||||
import {
|
||||
@@ -118,6 +120,10 @@ export const App: React.FC = () => {
|
||||
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// Mobile Navigation & Quick-Action State
|
||||
const [showMobileCapture, setShowMobileCapture] = useState(false);
|
||||
const [showMobileProfile, setShowMobileProfile] = useState(false);
|
||||
|
||||
const handleOpenHelp = () => {
|
||||
triggerHaptic(10);
|
||||
setPreviousTab((prev) => (activeTab === 'help' ? prev : activeTab));
|
||||
@@ -273,8 +279,12 @@ export const App: React.FC = () => {
|
||||
|
||||
const handleTriggerAddButton = () => {
|
||||
triggerHaptic(20);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
setCaptureFocusCount((c) => c + 1);
|
||||
if (window.innerWidth < 640) {
|
||||
setShowMobileCapture(true);
|
||||
} else {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
setCaptureFocusCount((c) => c + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
@@ -322,7 +332,26 @@ export const App: React.FC = () => {
|
||||
<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 border-b border-border shadow-sm">
|
||||
<div className="max-w-6xl mx-auto px-3 sm:px-4 py-2 flex items-start justify-between gap-3 sm:gap-6">
|
||||
{/* Mobile Header: Centered Logo & Instance Title */}
|
||||
<div className="sm:hidden flex items-center justify-center relative h-11 px-4">
|
||||
<div
|
||||
onClick={() => {
|
||||
triggerHaptic(10);
|
||||
setActiveTab('triage');
|
||||
}}
|
||||
className="flex items-center gap-2 cursor-pointer select-none"
|
||||
>
|
||||
<div className="w-6 h-6 bg-accent text-on-accent rounded-btn flex items-center justify-center">
|
||||
<Brain className="w-3.5 h-3.5" />
|
||||
</div>
|
||||
<span className="font-bold text-sm text-main tracking-tight">
|
||||
{authStatus.instanceName || 'MindSift'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Header: 3-column Layout (Logo, InstantCaptureBar, Profile Pill) */}
|
||||
<div className="hidden sm:flex max-w-6xl mx-auto px-4 py-2 items-start justify-between gap-6">
|
||||
{/* Logo & Headline */}
|
||||
<div
|
||||
onClick={() => {
|
||||
@@ -471,8 +500,8 @@ export const App: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation tabs attached to header */}
|
||||
<nav className="max-w-6xl mx-auto px-1 sm:px-2 border-t border-border flex items-stretch justify-between">
|
||||
{/* Desktop Navigation tabs attached to header */}
|
||||
<nav className="hidden sm:flex max-w-6xl mx-auto px-2 border-t border-border items-stretch justify-between">
|
||||
<div className="flex items-stretch overflow-x-auto no-scrollbar">
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -559,7 +588,7 @@ export const App: React.FC = () => {
|
||||
</header>
|
||||
|
||||
{/* Main Container */}
|
||||
<main className="flex-1 max-w-6xl w-full mx-auto px-3 sm:px-4 pt-3 pb-6">
|
||||
<main className="flex-1 max-w-6xl w-full mx-auto px-3 sm:px-4 pt-3 pb-24 sm:pb-6">
|
||||
<ViewErrorBoundary onReset={() => setActiveTab('triage')}>
|
||||
<section>
|
||||
{activeTab === 'triage' && (
|
||||
@@ -618,6 +647,286 @@ export const App: React.FC = () => {
|
||||
</section>
|
||||
</ViewErrorBoundary>
|
||||
</main>
|
||||
|
||||
{/* MOBILE BOTTOM NAVIGATION BAR */}
|
||||
<nav className="sm:hidden fixed bottom-0 left-0 right-0 z-40 bg-panel/95 backdrop-blur-md border-t border-border px-3 py-1 flex items-center justify-around pb-[calc(0.4rem+env(safe-area-inset-bottom))] shadow-panel">
|
||||
{/* Tab 1: Triage */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
triggerHaptic(10);
|
||||
setActiveTab('triage');
|
||||
}}
|
||||
className={`p-2 rounded-btn flex flex-col items-center justify-center transition-colors relative cursor-pointer ${
|
||||
activeTab === 'triage' ? 'text-accent' : 'text-muted hover:text-main'
|
||||
}`}
|
||||
title="Triage"
|
||||
>
|
||||
<Layers className="w-5 h-5" />
|
||||
{stats.inboxCount > 0 && (
|
||||
<span className="absolute -top-0.5 -right-0.5 min-w-[15px] h-[15px] px-0.5 rounded-full text-[9px] font-bold leading-none bg-accent text-on-accent flex items-center justify-center shadow-xs pointer-events-none">
|
||||
{stats.inboxCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Tab 2: Aufgaben */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
triggerHaptic(10);
|
||||
setActiveTab('tasks');
|
||||
}}
|
||||
className={`p-2 rounded-btn flex flex-col items-center justify-center transition-colors relative cursor-pointer ${
|
||||
activeTab === 'tasks' ? 'text-accent' : 'text-muted hover:text-main'
|
||||
}`}
|
||||
title="Aufgaben"
|
||||
>
|
||||
<CheckSquare className="w-5 h-5" />
|
||||
{stats.tasksOpenCount > 0 && (
|
||||
<span className="absolute -top-0.5 -right-0.5 min-w-[15px] h-[15px] px-0.5 rounded-full text-[9px] font-bold leading-none bg-accent text-on-accent flex items-center justify-center shadow-xs pointer-events-none">
|
||||
{stats.tasksOpenCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* CENTER ACTION: + Quick-Add FAB */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
triggerHaptic(15);
|
||||
setShowMobileCapture(true);
|
||||
}}
|
||||
className="w-12 h-12 -mt-5 rounded-full bg-accent text-on-accent shadow-lg flex items-center justify-center active:scale-90 transition-transform border-4 border-panel cursor-pointer"
|
||||
title="Schnell erfassen (+)"
|
||||
>
|
||||
<Plus className="w-6 h-6 stroke-[2.5]" />
|
||||
</button>
|
||||
|
||||
{/* Tab 3: Workspace */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
triggerHaptic(10);
|
||||
setActiveTab('workspace');
|
||||
}}
|
||||
className={`p-2 rounded-btn flex flex-col items-center justify-center transition-colors relative cursor-pointer ${
|
||||
activeTab === 'workspace' ? 'text-accent' : 'text-muted hover:text-main'
|
||||
}`}
|
||||
title="Workspace"
|
||||
>
|
||||
<FileText className="w-5 h-5" />
|
||||
{(stats.docsTodosOpenCount ?? 0) > 0 && (
|
||||
<span className="absolute -top-0.5 -right-0.5 min-w-[15px] h-[15px] px-0.5 rounded-full text-[9px] font-bold leading-none bg-accent text-on-accent flex items-center justify-center shadow-xs pointer-events-none">
|
||||
{stats.docsTodosOpenCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Tab 4: Listen */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
triggerHaptic(10);
|
||||
setActiveTab('lists');
|
||||
}}
|
||||
className={`p-2 rounded-btn flex flex-col items-center justify-center transition-colors relative cursor-pointer ${
|
||||
activeTab === 'lists' ? 'text-accent' : 'text-muted hover:text-main'
|
||||
}`}
|
||||
title="Listen"
|
||||
>
|
||||
<ShoppingCart className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* User Profile Avatar */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
triggerHaptic(10);
|
||||
setShowMobileProfile(true);
|
||||
}}
|
||||
className={`p-1.5 rounded-btn flex items-center justify-center transition-colors cursor-pointer ${
|
||||
activeTab === 'user_settings' || activeTab === 'admin_settings' || activeTab === 'help'
|
||||
? 'ring-2 ring-accent'
|
||||
: 'opacity-85 hover:opacity-100'
|
||||
}`}
|
||||
title="Profil & Einstellungen"
|
||||
>
|
||||
<div className="w-6 h-6 rounded-full bg-accent text-on-accent text-[11px] font-bold flex items-center justify-center">
|
||||
{user.displayName ? user.displayName.slice(0, 1).toUpperCase() : 'U'}
|
||||
</div>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* MOBILE QUICK-ADD BOTTOM SHEET / MODAL */}
|
||||
{showMobileCapture && (
|
||||
<div
|
||||
className="sm:hidden fixed inset-0 z-50 flex items-end justify-center bg-black/60 backdrop-blur-xs animate-in fade-in duration-150"
|
||||
onClick={() => setShowMobileCapture(false)}
|
||||
>
|
||||
<div
|
||||
className="w-full bg-panel border-t border-border rounded-t-2xl shadow-panel p-4 pb-[calc(1.5rem+env(safe-area-inset-bottom))] animate-in slide-in-from-bottom duration-200"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between pb-3 border-b border-border mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6 h-6 rounded-btn bg-accent/15 text-accent flex items-center justify-center">
|
||||
<Plus className="w-4 h-4" />
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-main">Schnell erfassen</h3>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowMobileCapture(false)}
|
||||
className="p-1 text-faint hover:text-main rounded-btn transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* InstantCaptureBar */}
|
||||
<div className="pt-1">
|
||||
<InstantCaptureBar
|
||||
onCaptured={(thought, target) => {
|
||||
handleCaptured(thought, target);
|
||||
setShowMobileCapture(false);
|
||||
}}
|
||||
lists={lists}
|
||||
autoFocusTrigger={Date.now()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* MOBILE USER PROFILE SHEET */}
|
||||
{showMobileProfile && (
|
||||
<div
|
||||
className="sm:hidden fixed inset-0 z-50 flex items-end justify-center bg-black/60 backdrop-blur-xs animate-in fade-in duration-150"
|
||||
onClick={() => setShowMobileProfile(false)}
|
||||
>
|
||||
<div
|
||||
className="w-full max-h-[85vh] overflow-y-auto bg-panel border-t border-border rounded-t-2xl shadow-panel p-4 pb-[calc(1.5rem+env(safe-area-inset-bottom))] space-y-4 animate-in slide-in-from-bottom duration-200 divide-y divide-border"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* User Info Header */}
|
||||
<div className="flex items-center justify-between pb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-accent text-on-accent text-sm font-bold flex items-center justify-center flex-shrink-0">
|
||||
{user.displayName ? user.displayName.slice(0, 1).toUpperCase() : 'U'}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-sm text-main">{user.displayName}</p>
|
||||
<p className="text-xs text-muted">{user.email}</p>
|
||||
<span className="inline-block mt-1 px-1.5 py-0.5 bg-canvas border border-border rounded-btn text-[10px] text-accent font-semibold">
|
||||
{roleLabel}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowMobileProfile(false)}
|
||||
className="p-1 text-faint hover:text-main rounded-btn transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Theme Quick Switcher */}
|
||||
<div className="pt-3 space-y-2">
|
||||
<span className="text-[11px] font-semibold text-muted uppercase tracking-wider flex items-center gap-1.5">
|
||||
<Palette className="w-3.5 h-3.5 text-accent" />
|
||||
<span>Design-Theme</span>
|
||||
</span>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{AVAILABLE_THEMES.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>
|
||||
</div>
|
||||
|
||||
{/* Menu Items */}
|
||||
<div className="pt-3 space-y-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
triggerHaptic(10);
|
||||
setShowMobileProfile(false);
|
||||
setActiveTab('user_settings');
|
||||
}}
|
||||
className={`w-full px-3 py-2.5 rounded-btn text-left flex items-center gap-3 hover:bg-panel-elevated transition-colors text-xs cursor-pointer ${
|
||||
activeTab === 'user_settings' ? 'text-accent font-semibold bg-panel-elevated' : 'text-main'
|
||||
}`}
|
||||
>
|
||||
<Settings className="w-4 h-4 text-accent" />
|
||||
<span>Kontoeinstellungen</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowMobileProfile(false);
|
||||
handleOpenHelp();
|
||||
}}
|
||||
className={`w-full px-3 py-2.5 rounded-btn text-left flex items-center gap-3 hover:bg-panel-elevated transition-colors text-xs cursor-pointer ${
|
||||
activeTab === 'help' ? 'text-accent font-semibold bg-panel-elevated' : 'text-main'
|
||||
}`}
|
||||
>
|
||||
<HelpCircle className="w-4 h-4 text-accent" />
|
||||
<span>Hilfe & Anleitung</span>
|
||||
</button>
|
||||
|
||||
{isStaff && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
triggerHaptic(10);
|
||||
setShowMobileProfile(false);
|
||||
setActiveTab('admin_settings');
|
||||
}}
|
||||
className={`w-full px-3 py-2.5 rounded-btn text-left flex items-center gap-3 hover:bg-panel-elevated transition-colors text-xs cursor-pointer ${
|
||||
activeTab === 'admin_settings' ? 'text-accent font-semibold bg-panel-elevated' : 'text-main'
|
||||
}`}
|
||||
>
|
||||
<Shield className="w-4 h-4 text-accent" />
|
||||
<span>Administration</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Logout */}
|
||||
<div className="pt-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowMobileProfile(false);
|
||||
handleLogout();
|
||||
}}
|
||||
className="w-full px-3 py-2.5 rounded-btn text-left flex items-center gap-3 text-status-err hover:bg-status-err-bg transition-colors text-xs font-medium cursor-pointer"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
<span>Abmelden</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user