feat(mobile): declutter mobile views with contextual FAB, filter sheet, and clean outliner
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 58s

This commit is contained in:
sandrodach committed 2026-10-25 22:03:28 +01:00
1 parent 1e47f3aeb7
commit f2ead7513a
5 files changed
+429 -59

No files matched your search

+6 -1
View File
@@ -132,11 +132,12 @@ export const App: React.FC = () => {
// Mobile Navigation & Quick-Action State // Mobile Navigation & Quick-Action State
const [showMobileCapture, setShowMobileCapture] = useState(false); const [showMobileCapture, setShowMobileCapture] = useState(false);
const [showMobileProfile, setShowMobileProfile] = useState(false); const [showMobileProfile, setShowMobileProfile] = useState(false);
const { isMenuOpen, setIsMenuOpen, closeMenu, setGroups } = useMobileActions(); const { isMenuOpen, setIsMenuOpen, closeMenu, setGroups, primaryAction, setPrimaryAction } = useMobileActions();
useEffect(() => { useEffect(() => {
closeMenu(); closeMenu();
if (activeTab === 'triage') { if (activeTab === 'triage') {
setPrimaryAction(() => setShowMobileCapture(true));
setGroups([ setGroups([
{ {
id: 'triage-actions', id: 'triage-actions',
@@ -966,7 +967,11 @@ export const App: React.FC = () => {
type="button" type="button"
onClick={() => { onClick={() => {
triggerHaptic(15); triggerHaptic(15);
if (primaryAction) {
primaryAction();
} else {
setShowMobileCapture(true); setShowMobileCapture(true);
}
}} }}
className="w-14 h-14 -mt-6 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" className="w-14 h-14 -mt-6 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 (+)" title="Schnell erfassen (+)"
@@ -22,6 +22,8 @@ interface MobileActionContextValue {
isMenuOpen: boolean; isMenuOpen: boolean;
setIsMenuOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsMenuOpen: React.Dispatch<React.SetStateAction<boolean>>;
closeMenu: () => void; closeMenu: () => void;
primaryAction?: () => void;
setPrimaryAction: (fn?: () => void) => void;
} }
const MobileActionContext = createContext<MobileActionContextValue>({ const MobileActionContext = createContext<MobileActionContextValue>({
@@ -30,11 +32,14 @@ const MobileActionContext = createContext<MobileActionContextValue>({
isMenuOpen: false, isMenuOpen: false,
setIsMenuOpen: () => {}, setIsMenuOpen: () => {},
closeMenu: () => {}, closeMenu: () => {},
primaryAction: undefined,
setPrimaryAction: () => {},
}); });
export const MobileActionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const MobileActionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [groups, setGroupsState] = useState<MobileMenuGroup[]>([]); const [groups, setGroupsState] = useState<MobileMenuGroup[]>([]);
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [primaryAction, setPrimaryActionState] = useState<(() => void) | undefined>(undefined);
const setGroups = useCallback((newGroups: MobileMenuGroup[]) => { const setGroups = useCallback((newGroups: MobileMenuGroup[]) => {
setGroupsState(newGroups); setGroupsState(newGroups);
@@ -44,6 +49,10 @@ export const MobileActionProvider: React.FC<{ children: React.ReactNode }> = ({
setIsMenuOpen(false); setIsMenuOpen(false);
}, []); }, []);
const setPrimaryAction = useCallback((fn?: () => void) => {
setPrimaryActionState(() => fn);
}, []);
return ( return (
<MobileActionContext.Provider <MobileActionContext.Provider
value={{ value={{
@@ -52,6 +61,8 @@ export const MobileActionProvider: React.FC<{ children: React.ReactNode }> = ({
isMenuOpen, isMenuOpen,
setIsMenuOpen, setIsMenuOpen,
closeMenu, closeMenu,
primaryAction,
setPrimaryAction,
}} }}
> >
{children} {children}
@@ -77,7 +77,7 @@ export function deduplicateItems(items: ListItem[]): ListItem[] {
} }
export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGlobal, currentUser }) => { export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGlobal, currentUser }) => {
const { setGroups } = useMobileActions(); const { setGroups, setPrimaryAction } = useMobileActions();
const [lists, setLists] = useState<SharedList[]>([]); const [lists, setLists] = useState<SharedList[]>([]);
const [activeListId, setActiveListId] = useState<string>(''); const [activeListId, setActiveListId] = useState<string>('');
@@ -714,6 +714,13 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
// Register context-sensitive mobile menu groups behind the MindSift logo // Register context-sensitive mobile menu groups behind the MindSift logo
useEffect(() => { useEffect(() => {
setPrimaryAction(() => {
setTimeout(() => {
inputRef.current?.focus();
inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 60);
});
setGroups([ setGroups([
{ {
id: 'lists-actions', id: 'lists-actions',
@@ -786,7 +793,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
}), }),
}, },
]); ]);
}, [lists, activeList, completedItems.length, setGroups]); }, [lists, activeList, completedItems.length, setGroups, setPrimaryAction]);
return ( return (
<div className="w-full space-y-3"> <div className="w-full space-y-3">
+373 -10
View File
@@ -23,8 +23,9 @@ import {
Settings, Settings,
ChevronDown, ChevronDown,
Folder, Folder,
Filter,
} from 'lucide-react'; } from 'lucide-react';
import { TaskList, Thought, SubTask, Team, User, TaskListShare, KanbanColumn } from '../../lib/types'; import { TaskList, Thought, SubTask, Team, User, TaskListShare, KanbanColumn, PriorityLevel } from '../../lib/types';
import { api, triggerHaptic } from '../../lib/api'; import { api, triggerHaptic } from '../../lib/api';
import { UnifiedShareModal } from '../../components/UnifiedShareModal'; import { UnifiedShareModal } from '../../components/UnifiedShareModal';
import { TaskDetailModal } from './TaskDetailModal'; import { TaskDetailModal } from './TaskDetailModal';
@@ -73,7 +74,15 @@ export const TasksView: React.FC<TasksViewProps> = ({
const [quickTitle, setQuickTitle] = useState(''); const [quickTitle, setQuickTitle] = useState('');
const [quickDueDate, setQuickDueDate] = useState(''); const [quickDueDate, setQuickDueDate] = useState('');
const quickInputRef = useRef<HTMLInputElement | null>(null); const quickInputRef = useRef<HTMLInputElement | null>(null);
const { setGroups } = useMobileActions(); const { setGroups, setPrimaryAction } = useMobileActions();
// Mobile task creation & filter modal state
const [showMobileCreateModal, setShowMobileCreateModal] = useState(false);
const [mobileCreateTitle, setMobileCreateTitle] = useState('');
const [mobileCreateDueDate, setMobileCreateDueDate] = useState('');
const [mobileCreatePriority, setMobileCreatePriority] = useState<PriorityLevel>('normal');
const [mobileCreateListId, setMobileCreateListId] = useState<string>('');
const [showFilterModal, setShowFilterModal] = useState(false);
// New TaskList modal state // New TaskList modal state
const [showNewListModal, setShowNewListModal] = useState(false); const [showNewListModal, setShowNewListModal] = useState(false);
@@ -530,6 +539,31 @@ export const TasksView: React.FC<TasksViewProps> = ({
} }
}; };
const handleMobileCreateTask = async (e: React.FormEvent) => {
e.preventDefault();
const title = mobileCreateTitle.trim();
if (!title) return;
triggerHaptic(12);
try {
const created = await api.captureThought(title, mobileCreatePriority, 'manual', mobileCreateDueDate || undefined, {
target: 'task',
taskListId: mobileCreateListId || (selectedListId === 'all' ? '' : selectedListId),
});
setTasks((prev) => {
if (prev.some((t) => t.id === created.id)) return prev;
return [created, ...prev];
});
setMobileCreateTitle('');
setMobileCreateDueDate('');
setMobileCreatePriority('normal');
setShowMobileCreateModal(false);
onRefreshStats();
} catch {
loadData();
}
};
const handleQuickAddTaskToColumn = async (columnId: string) => { const handleQuickAddTaskToColumn = async (columnId: string) => {
const title = prompt('Neue Aufgabe für diese Spalte:'); const title = prompt('Neue Aufgabe für diese Spalte:');
if (!title || !title.trim()) return; if (!title || !title.trim()) return;
@@ -815,6 +849,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
}; };
useEffect(() => { useEffect(() => {
setPrimaryAction(() => {
setMobileCreateListId(selectedListId === 'all' ? '' : selectedListId);
setShowMobileCreateModal(true);
});
const primaryItems: any[] = [ const primaryItems: any[] = [
{ {
id: 'new-task', id: 'new-task',
@@ -822,12 +861,26 @@ export const TasksView: React.FC<TasksViewProps> = ({
icon: <Plus className="w-4 h-4" />, icon: <Plus className="w-4 h-4" />,
variant: 'accent', variant: 'accent',
onClick: () => { onClick: () => {
window.scrollTo({ top: 0, behavior: 'smooth' }); setMobileCreateListId(selectedListId === 'all' ? '' : selectedListId);
setTimeout(() => quickInputRef.current?.focus(), 100); setShowMobileCreateModal(true);
}, },
}, },
]; ];
const filterSortActiveCount =
(selectedTag ? 1 : 0) + (groupBy !== 'none' ? 1 : 0) + (sortBy !== 'custom' ? 1 : 0);
const filterItems: any[] = [
{
id: 'tasks-filter-modal',
label: 'Filter, Hashtags & Sortierung',
icon: <Filter className="w-4 h-4 text-accent" />,
badge: filterSortActiveCount > 0 ? `${filterSortActiveCount} aktiv` : undefined,
active: filterSortActiveCount > 0,
onClick: () => setShowFilterModal(true),
},
];
const listManagementItems: any[] = [ const listManagementItems: any[] = [
{ {
id: 'new-task-list', id: 'new-task-list',
@@ -928,6 +981,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
id: 'tasks-primary', id: 'tasks-primary',
items: primaryItems, items: primaryItems,
}, },
{
id: 'tasks-filters',
title: 'Filter & Sortierung',
items: filterItems,
},
{ {
id: 'tasks-list-management', id: 'tasks-list-management',
title: 'Listen-Verwaltung', title: 'Listen-Verwaltung',
@@ -948,12 +1006,17 @@ export const TasksView: React.FC<TasksViewProps> = ({
viewMode, viewMode,
filter, filter,
selectedListId, selectedListId,
selectedTag,
groupBy,
sortBy,
openTasks.length, openTasks.length,
doneTasks.length, doneTasks.length,
safeDiscarded.length, safeDiscarded.length,
kanbanTasks.length, kanbanTasks.length,
safeTaskLists, safeTaskLists,
currentSelectedList, currentSelectedList,
setPrimaryAction,
setGroups,
]); ]);
const renderTaskProgressBar = (task: Thought) => { const renderTaskProgressBar = (task: Thought) => {
@@ -1071,11 +1134,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
className="flex flex-wrap items-center gap-1.5 pt-0.5 empty:hidden" className="flex flex-wrap items-center gap-1.5 pt-0.5 empty:hidden"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{/* Kanban status selector pill */} {/* Kanban status selector pill (Desktop only - on mobile done via TaskDetailModal) */}
<select <select
value={task.kanbanStatus || (isCompleted ? 'done' : 'todo')} value={task.kanbanStatus || (isCompleted ? 'done' : 'todo')}
onChange={(e) => handleSetKanbanStatus(task, e.target.value)} onChange={(e) => handleSetKanbanStatus(task, e.target.value)}
className="h-5 px-1.5 text-[10px] rounded-btn border border-border bg-panel text-muted hover:text-main cursor-pointer" className="hidden sm:inline-block h-5 px-1.5 text-[10px] rounded-btn border border-border bg-panel text-muted hover:text-main cursor-pointer"
title="Status / Zustand ändern" title="Status / Zustand ändern"
> >
{activeColumns.map((c) => ( {activeColumns.map((c) => (
@@ -1374,10 +1437,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div> </div>
</div> </div>
{/* Quick Add Task Input */} {/* Quick Add Task Input (Desktop only - on mobile, tapping the bottom '+' FAB opens mobile create sheet) */}
<form <form
onSubmit={handleQuickAddTask} onSubmit={handleQuickAddTask}
className="flex items-center gap-1.5 panel-surface p-2 sm:p-1.5" className="hidden sm:flex items-center gap-1.5 panel-surface p-2 sm:p-1.5"
> >
<Plus className="w-4 h-4 text-accent ml-1 flex-shrink-0" /> <Plus className="w-4 h-4 text-accent ml-1 flex-shrink-0" />
<input <input
@@ -1410,8 +1473,50 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div> </div>
</form> </form>
{/* Hashtag Filter & Grouping / Sorting Toolbar */} {/* Mobile Active Filter Badge Banner (minimal and clean) */}
<div className="flex flex-wrap items-center justify-between gap-2 pt-1 pb-0.5"> {(selectedTag || groupBy !== 'none' || sortBy !== 'custom') && (
<div className="flex sm:hidden items-center justify-between gap-2 p-2 rounded-btn bg-accent/10 border border-accent/20 text-xs">
<div className="flex items-center gap-1.5 flex-wrap min-w-0">
<Filter className="w-3.5 h-3.5 text-accent flex-shrink-0" />
{selectedTag && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-accent text-on-accent text-[11px] font-semibold">
#{selectedTag}
<button
type="button"
onClick={() => setSelectedTag(null)}
className="hover:opacity-75 cursor-pointer ml-0.5"
>
<X className="w-3 h-3" />
</button>
</span>
)}
{groupBy !== 'none' && (
<span className="text-muted text-[11px]">
Gruppe: <strong className="text-main">{groupBy}</strong>
</span>
)}
{sortBy !== 'custom' && (
<span className="text-muted text-[11px]">
Sortiert: <strong className="text-main">{sortBy}</strong>
</span>
)}
</div>
<button
type="button"
onClick={() => {
setSelectedTag(null);
setGroupBy('none');
setSortBy('custom');
}}
className="text-accent text-[11px] font-semibold flex-shrink-0 hover:underline cursor-pointer"
>
Zurücksetzen
</button>
</div>
)}
{/* Hashtag Filter & Grouping / Sorting Toolbar (Desktop only - on mobile accessible via Logo Menu Filter & Sort) */}
<div className="hidden sm:flex flex-wrap items-center justify-between gap-2 pt-1 pb-0.5">
{/* Hashtag filter chips */} {/* Hashtag filter chips */}
<div className="flex items-center gap-1.5 overflow-x-auto max-w-full pb-1"> <div className="flex items-center gap-1.5 overflow-x-auto max-w-full pb-1">
<span className="text-[11px] font-semibold text-muted flex items-center gap-1 flex-shrink-0"> <span className="text-[11px] font-semibold text-muted flex items-center gap-1 flex-shrink-0">
@@ -1497,6 +1602,264 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div> </div>
</div> </div>
{/* MOBILE CREATE TASK BOTTOM SHEET MODAL */}
{showMobileCreateModal && (
<div
className="sm:hidden fixed inset-0 !m-0 z-50 flex items-end justify-center bg-black/60 backdrop-blur-xs animate-in fade-in duration-150"
onClick={() => setShowMobileCreateModal(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))] animate-in slide-in-from-bottom duration-200"
onClick={(e) => e.stopPropagation()}
>
<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-7 h-7 rounded-btn bg-accent/15 text-accent flex items-center justify-center">
<CheckSquare className="w-4 h-4" />
</div>
<h3 className="text-sm font-semibold text-main">Neue Aufgabe anlegen</h3>
</div>
<button
type="button"
onClick={() => setShowMobileCreateModal(false)}
className="p-1.5 text-faint hover:text-main rounded-btn transition-colors cursor-pointer"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleMobileCreateTask} className="space-y-3">
<div>
<label className="text-[11px] font-semibold text-muted block mb-1">Aufgabentext</label>
<input
type="text"
autoFocus
value={mobileCreateTitle}
onChange={(e) => setMobileCreateTitle(e.target.value)}
placeholder="Was ist zu tun?…"
className="w-full input-field px-3 py-2 text-sm"
/>
</div>
{/* List Selector (if lists exist) */}
{safeTaskLists.length > 0 && (
<div>
<label className="text-[11px] font-semibold text-muted block mb-1">Aufgabenliste</label>
<select
value={mobileCreateListId}
onChange={(e) => setMobileCreateListId(e.target.value)}
className="w-full input-field px-2.5 py-2 text-xs bg-panel"
>
<option value="">Standard (Alle Aufgaben)</option>
{safeTaskLists.map((tl) => (
<option key={tl.id} value={tl.id}>
{tl.icon || '📋'} {tl.title}
</option>
))}
</select>
</div>
)}
{/* Priority & Due Date */}
<div className="grid grid-cols-2 gap-2 pt-1">
<div>
<label className="text-[11px] font-semibold text-muted block mb-1">Priorität</label>
<select
value={mobileCreatePriority}
onChange={(e) => setMobileCreatePriority(e.target.value as PriorityLevel)}
className="w-full input-field px-2 py-2 text-xs bg-panel"
>
<option value="normal">Normal</option>
<option value="high">⚠️ Wichtig</option>
<option value="urgent">🔥 Dringend</option>
<option value="low">Niedrig</option>
</select>
</div>
<div>
<label className="text-[11px] font-semibold text-muted block mb-1">Fällig am</label>
<input
type="date"
value={mobileCreateDueDate}
onChange={(e) => setMobileCreateDueDate(e.target.value)}
className="w-full input-field px-2 py-2 text-xs"
/>
</div>
</div>
<div className="pt-2 flex justify-end gap-2">
<button
type="button"
onClick={() => setShowMobileCreateModal(false)}
className="btn-ghost h-10 px-4 text-xs cursor-pointer"
>
Abbrechen
</button>
<button
type="submit"
disabled={!mobileCreateTitle.trim()}
className="btn-primary h-10 px-5 text-xs font-semibold disabled:opacity-40 cursor-pointer"
>
Aufgabe erstellen
</button>
</div>
</form>
</div>
</div>
)}
{/* MOBILE FILTER & SORT MODAL */}
{showFilterModal && (
<div
className="sm:hidden fixed inset-0 !m-0 z-50 flex items-end justify-center bg-black/60 backdrop-blur-xs animate-in fade-in duration-150"
onClick={() => setShowFilterModal(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))] animate-in slide-in-from-bottom duration-200 space-y-4"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between pb-3 border-b border-border">
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-btn bg-accent/15 text-accent flex items-center justify-center">
<Filter className="w-4 h-4" />
</div>
<h3 className="text-sm font-semibold text-main">Filter & Sortierung</h3>
</div>
<button
type="button"
onClick={() => setShowFilterModal(false)}
className="p-1.5 text-faint hover:text-main rounded-btn transition-colors cursor-pointer"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Hashtag Filters */}
<div className="space-y-2">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted flex items-center gap-1">
<Hash className="w-3 h-3 text-accent" />
<span>Nach Hashtags filtern</span>
</span>
<div className="flex flex-wrap gap-1.5">
<button
type="button"
onClick={() => setSelectedTag(null)}
className={`min-h-[32px] px-3 text-xs rounded-full border transition-colors cursor-pointer ${
selectedTag === null
? 'bg-accent text-on-accent border-accent font-semibold'
: 'bg-canvas border-border text-muted'
}`}
>
Alle ({safeTasks.length})
</button>
{allAvailableTags.map((tag) => {
const count = safeTasks.filter((t) => t.tags?.includes(tag)).length;
const isSelected = selectedTag === tag;
return (
<button
key={tag}
type="button"
onClick={() => setSelectedTag(isSelected ? null : tag)}
className={`min-h-[32px] px-3 text-xs rounded-full border transition-colors flex items-center gap-1 cursor-pointer ${
isSelected
? 'bg-accent text-on-accent border-accent font-semibold'
: 'bg-canvas border-border text-muted'
}`}
>
<span>#{tag}</span>
<span className="text-[10px] opacity-75">({count})</span>
</button>
);
})}
</div>
</div>
{/* Grouping */}
{viewMode !== 'kanban' && (
<div className="space-y-2 pt-2 border-t border-border">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted block">
Gruppierung
</span>
<div className="grid grid-cols-2 gap-1.5">
{[
{ id: 'none', label: 'Keine' },
{ id: 'tag', label: 'Nach Hashtag' },
{ id: 'status', label: 'Nach Status' },
{ id: 'priority', label: 'Nach Priorität' },
{ id: 'due', label: 'Nach Fälligkeit' },
].map((g) => (
<button
key={g.id}
type="button"
onClick={() => setGroupBy(g.id as any)}
className={`min-h-[36px] px-3 text-xs rounded-btn border text-left transition-colors cursor-pointer ${
groupBy === g.id
? 'bg-accent-dim border-accent text-accent font-semibold'
: 'bg-canvas border-border text-muted hover:text-main'
}`}
>
{g.label}
</button>
))}
</div>
</div>
)}
{/* Sorting */}
{viewMode !== 'kanban' && (
<div className="space-y-2 pt-2 border-t border-border">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted block">
Sortierung
</span>
<div className="grid grid-cols-2 gap-1.5">
{[
{ id: 'custom', label: 'Manuell (Drag & Drop)' },
{ id: 'created', label: 'Erstelldatum' },
{ id: 'due', label: 'Fälligkeitsdatum' },
{ id: 'priority', label: 'Priorität' },
{ id: 'alpha', label: 'Alphabetisch' },
].map((s) => (
<button
key={s.id}
type="button"
onClick={() => setSortBy(s.id as any)}
className={`min-h-[36px] px-3 text-xs rounded-btn border text-left transition-colors cursor-pointer ${
sortBy === s.id
? 'bg-accent-dim border-accent text-accent font-semibold'
: 'bg-canvas border-border text-muted hover:text-main'
}`}
>
{s.label}
</button>
))}
</div>
</div>
)}
<div className="pt-3 border-t border-border flex items-center justify-between gap-2">
<button
type="button"
onClick={() => {
setSelectedTag(null);
setGroupBy('none');
setSortBy('custom');
}}
className="btn-ghost h-9 px-3 text-xs text-muted hover:text-main cursor-pointer"
>
Filter zurücksetzen
</button>
<button
type="button"
onClick={() => setShowFilterModal(false)}
className="btn-primary h-9 px-5 text-xs font-semibold cursor-pointer"
>
Fertig
</button>
</div>
</div>
</div>
)}
{/* NEW LIST MODAL */} {/* NEW LIST MODAL */}
{showNewListModal && ( {showNewListModal && (
<div className="fixed inset-0 !m-0 modal-overlay z-50 flex items-center justify-center p-4"> <div className="fixed inset-0 !m-0 modal-overlay z-50 flex items-center justify-center p-4">
@@ -100,7 +100,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
initialDocId, initialDocId,
onStatsChange, onStatsChange,
}) => { }) => {
const { setGroups } = useMobileActions(); const { setGroups, setPrimaryAction } = useMobileActions();
const [showMobileFileDrawer, setShowMobileFileDrawer] = useState(false); const [showMobileFileDrawer, setShowMobileFileDrawer] = useState(false);
// Top Workspace Tab Mode: 'editor' vs 'todos' vs 'tags' // Top Workspace Tab Mode: 'editor' vs 'todos' vs 'tags'
@@ -2836,6 +2836,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
// Register context-sensitive mobile menu groups behind the MindSift logo // Register context-sensitive mobile menu groups behind the MindSift logo
useEffect(() => { useEffect(() => {
setPrimaryAction(() => {
handleCreateDoc(null);
});
const openTodosCount = allWorkspaceTodos.filter((t) => !t.block.checked).length; const openTodosCount = allWorkspaceTodos.filter((t) => !t.block.checked).length;
setGroups([ setGroups([
{ {
@@ -2958,6 +2962,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
allWorkspaceTodos, allWorkspaceTodos,
selectedDoc, selectedDoc,
setGroups, setGroups,
setPrimaryAction,
]); ]);
const renderDocItem = (doc: Document, depth: number) => { const renderDocItem = (doc: Document, depth: number) => {
@@ -3186,49 +3191,6 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
return ( return (
<div className="w-full space-y-2"> <div className="w-full space-y-2">
{/* Mobile Compact Workspace Bar (Options & mode switching live in MindSift Logo Menu) */}
<div className="flex sm:hidden items-center justify-between gap-2 px-3 py-2 rounded-btn bg-panel border border-border">
<button
type="button"
onClick={() => {
setWorkspaceMode('editor');
setShowMobileFileDrawer(true);
}}
className="flex items-center gap-2 min-w-0 text-left py-0.5"
title="Dateibaum & Ordner öffnen"
>
<FolderTree className="w-4 h-4 text-accent flex-shrink-0" />
<span className="text-sm font-semibold text-main truncate">
{workspaceMode === 'tags'
? 'Themen & Hashtags'
: workspaceMode === 'todos'
? 'Globale To-Dos'
: selectedDoc
? `${selectedDoc.icon || '📄'} ${selectedDoc.title || 'Unbenannt'}`
: 'Dateibaum öffnen'}
</span>
</button>
<div className="flex items-center gap-1.5 flex-shrink-0">
{workspaceMode === 'editor' && selectedDoc && (
<span className="badge-accent text-[10px]">
<Check className="w-2.5 h-2.5" />
{saveState === 'saving' ? 'Speichert…' : 'Gespeichert'}
</span>
)}
<button
type="button"
onClick={() => {
setWorkspaceMode('editor');
setShowMobileFileDrawer(true);
}}
className="btn-action min-h-[36px] px-2.5 text-xs flex items-center gap-1"
>
<Folder className="w-3.5 h-3.5 text-accent" />
<span>Dateien</span>
</button>
</div>
</div>
{/* Top Navigation Mode Switcher: Notizen & Outliner vs. Globales To-Do Dashboard (Desktop) */} {/* Top Navigation Mode Switcher: Notizen & Outliner vs. Globales To-Do Dashboard (Desktop) */}
<div className="hidden sm:flex flex-wrap items-center justify-between gap-2 border-b border-border pb-2"> <div className="hidden sm:flex flex-wrap items-center justify-between gap-2 border-b border-border pb-2">
@@ -4342,8 +4304,30 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</div> </div>
</div> </div>
) : ( ) : (
<div className="p-8 panel-surface text-center text-muted text-xs"> <div className="p-8 panel-surface text-center space-y-3">
Wähle links eine Datei im Datei-Explorer aus oder erstelle eine neue Datei. <FolderTree className="w-8 h-8 text-accent/60 mx-auto" />
<p className="text-xs text-muted">Keine Notiz geöffnet.</p>
<div className="flex flex-wrap items-center justify-center gap-2 pt-1">
<button
type="button"
onClick={() => {
setWorkspaceMode('editor');
setShowMobileFileDrawer(true);
}}
className="btn-action h-8 px-3 text-xs flex items-center gap-1.5 cursor-pointer"
>
<FolderTree className="w-3.5 h-3.5 text-accent" />
<span>Dateibaum öffnen</span>
</button>
<button
type="button"
onClick={() => handleCreateDoc(null)}
className="btn-primary h-8 px-3 text-xs flex items-center gap-1.5 cursor-pointer"
>
<FilePlus className="w-3.5 h-3.5" />
<span>Neue Notiz anlegen</span>
</button>
</div>
</div> </div>
)} )}
</div> </div>