From 212cb32bb0f351e00a7c3b0783df7a17e8550092 Mon Sep 17 00:00:00 2001 From: Sandro Date: Sun, 25 Oct 2026 21:08:48 +0100 Subject: [PATCH] feat(mobile): add context-sensitive MindSift logo menu, slide-over file drawer, and larger touch targets --- frontend/src/App.tsx | 146 +++++++- frontend/src/components/MobileHeaderMenu.tsx | 140 ++++++++ frontend/src/context/MobileActionContext.tsx | 62 ++++ .../src/features/lists/SharedListsView.tsx | 152 +++++++- .../src/features/triage/TaskDetailModal.tsx | 15 +- frontend/src/features/triage/TasksView.tsx | 207 +++++++++-- .../src/features/workspace/WorkspaceView.tsx | 326 +++++++++++++++--- frontend/src/main.tsx | 6 +- 8 files changed, 943 insertions(+), 111 deletions(-) create mode 100644 frontend/src/components/MobileHeaderMenu.tsx create mode 100644 frontend/src/context/MobileActionContext.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9c5deac..b250bac 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -44,6 +44,8 @@ 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'; type ActiveTab = | 'triage' @@ -129,6 +131,107 @@ export const App: React.FC = () => { // Mobile Navigation & Quick-Action State const [showMobileCapture, setShowMobileCapture] = useState(false); const [showMobileProfile, setShowMobileProfile] = useState(false); + const { isMenuOpen, setIsMenuOpen, closeMenu, setGroups } = useMobileActions(); + + useEffect(() => { + closeMenu(); + if (activeTab === 'triage') { + setGroups([ + { + id: 'triage-actions', + title: 'Schnellaktionen', + items: [ + { + id: 'new-thought', + label: 'Neuen Gedanken erfassen', + icon: , + variant: 'accent', + onClick: () => setShowMobileCapture(true), + }, + { + id: 'refresh-deck', + label: 'Inbox aktualisieren', + icon: , + badge: stats.inboxCount, + onClick: () => refreshAll(), + }, + ], + }, + { + id: 'triage-nav', + title: 'Bereiche wechseln', + items: [ + { + id: 'goto-tasks', + label: 'Aufgaben & Kanban', + icon: , + badge: stats.tasksOpenCount, + onClick: () => setActiveTab('tasks'), + }, + { + id: 'goto-workspace', + label: 'Notizen & Workspace', + icon: , + badge: stats.docsCount, + onClick: () => setActiveTab('workspace'), + }, + { + id: 'goto-lists', + label: 'Einkaufslisten', + icon: , + badge: stats.listsCount, + onClick: () => setActiveTab('lists'), + }, + { + id: 'goto-groups', + label: 'Teams & 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-tasks', + label: 'Zu den Aufgaben', + icon: , + onClick: () => setActiveTab('tasks'), + }, + { + id: 'goto-workspace', + label: 'Zum Workspace', + icon: , + onClick: () => setActiveTab('workspace'), + }, + { + 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(() => { @@ -409,27 +512,50 @@ export const App: React.FC = () => { const isServerAdmin = user.role === 'admin'; const roleLabel = isServerAdmin ? 'Server-Admin' : 'Mitglied'; + const activeTabLabelMap: Record = { + triage: 'Triage', + tasks: 'Aufgaben', + workspace: 'Workspace', + lists: 'Listen', + groups: 'Teams & 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 Larger Logo Left (Triage Home) & Profile Avatar Right */} + {/* Mobile Header: Floating Bar with Logo Action Dropdown Left & Profile Avatar Right */}
@@ -715,6 +841,8 @@ export const App: React.FC = () => {
+ + {/* Main Container */}
setActiveTab('triage')}> diff --git a/frontend/src/components/MobileHeaderMenu.tsx b/frontend/src/components/MobileHeaderMenu.tsx new file mode 100644 index 0000000..c33bf98 --- /dev/null +++ b/frontend/src/components/MobileHeaderMenu.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { Check, X } from 'lucide-react'; +import { useMobileActions } from '../context/MobileActionContext'; +import { triggerHaptic } from '../lib/api'; + +interface MobileHeaderMenuProps { + activeTabLabel: string; +} + +export const MobileHeaderMenu: React.FC = ({ activeTabLabel }) => { + const { groups, isMenuOpen, closeMenu } = useMobileActions(); + + if (!isMenuOpen) return null; + + return ( +
+ {/* Backdrop */} +
{ + triggerHaptic(8); + closeMenu(); + }} + /> + + {/* Menu Sheet below Header */} +
+ {/* Menu Header */} +
+
+ + + {activeTabLabel} — Optionen & Aktionen + +
+ +
+ + {groups.length === 0 ? ( +
+ Keine spezifischen Optionen für diese Ansicht verfügbar. +
+ ) : ( +
+ {groups.map((group) => ( +
+ {group.title && ( +
+ {group.title} +
+ )} +
+ {group.items.map((item) => { + const isAccent = item.variant === 'accent'; + const isDanger = item.variant === 'danger'; + const isActive = item.active; + + let btnClass = + 'bg-canvas/70 hover:bg-canvas text-main border border-border/60'; + if (isAccent) { + btnClass = + 'bg-accent text-on-accent border border-accent font-semibold shadow-sm'; + } else if (isDanger) { + btnClass = + 'bg-status-err/10 text-status-err border border-status-err/30 font-medium'; + } else if (isActive) { + btnClass = + 'bg-accent/15 text-accent border border-accent/40 font-semibold'; + } + + return ( + + ); + })} +
+
+ ))} +
+ )} +
+
+ ); +}; diff --git a/frontend/src/context/MobileActionContext.tsx b/frontend/src/context/MobileActionContext.tsx new file mode 100644 index 0000000..ef661a4 --- /dev/null +++ b/frontend/src/context/MobileActionContext.tsx @@ -0,0 +1,62 @@ +import React, { createContext, useContext, useState, useCallback } from 'react'; + +export interface MobileActionItem { + id: string; + label: string; + icon?: React.ReactNode; + badge?: string | number; + variant?: 'default' | 'accent' | 'danger'; + active?: boolean; + onClick: () => void; +} + +export interface MobileMenuGroup { + id: string; + title?: string; + items: MobileActionItem[]; +} + +interface MobileActionContextValue { + groups: MobileMenuGroup[]; + setGroups: (groups: MobileMenuGroup[]) => void; + isMenuOpen: boolean; + setIsMenuOpen: React.Dispatch>; + closeMenu: () => void; +} + +const MobileActionContext = createContext({ + groups: [], + setGroups: () => {}, + isMenuOpen: false, + setIsMenuOpen: () => {}, + closeMenu: () => {}, +}); + +export const MobileActionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [groups, setGroupsState] = useState([]); + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const setGroups = useCallback((newGroups: MobileMenuGroup[]) => { + setGroupsState(newGroups); + }, []); + + const closeMenu = useCallback(() => { + setIsMenuOpen(false); + }, []); + + return ( + + {children} + + ); +}; + +export const useMobileActions = () => useContext(MobileActionContext); diff --git a/frontend/src/features/lists/SharedListsView.tsx b/frontend/src/features/lists/SharedListsView.tsx index d4615fe..4f3853c 100644 --- a/frontend/src/features/lists/SharedListsView.tsx +++ b/frontend/src/features/lists/SharedListsView.tsx @@ -12,6 +12,7 @@ import { import { ListItem, SharedList, ListShare, Team, User, WSMessage } from '../../lib/types'; import { api, buildWebSocketURL, triggerHaptic } from '../../lib/api'; import { UnifiedShareModal } from '../../components/UnifiedShareModal'; +import { useMobileActions } from '../../context/MobileActionContext'; interface SharedListsViewProps { onRefreshGlobal: () => void; @@ -76,6 +77,7 @@ export function deduplicateItems(items: ListItem[]): ListItem[] { } export const SharedListsView: React.FC = ({ onRefreshGlobal, currentUser }) => { + const { setGroups } = useMobileActions(); const [lists, setLists] = useState([]); const [activeListId, setActiveListId] = useState(''); @@ -710,10 +712,122 @@ export const SharedListsView: React.FC = ({ onRefreshGloba return result; }, [openItems]); + // Register context-sensitive mobile menu groups behind the MindSift logo + useEffect(() => { + setGroups([ + { + id: 'lists-actions', + title: 'Aktionen', + items: [ + { + id: 'list-new-item', + label: 'Neuen Eintrag hinzufügen', + icon: , + variant: 'accent', + onClick: () => { + setTimeout(() => { + inputRef.current?.focus(); + inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }, 60); + }, + }, + { + id: 'list-new-list', + label: 'Neue Einkaufsliste erstellen', + icon: , + onClick: () => setShowNewListModal(true), + }, + ...(activeList + ? [ + { + id: 'list-share', + label: `Liste "${activeList.title}" teilen`, + icon: , + onClick: () => openShareModal(), + }, + ] + : []), + ...(completedItems.length > 0 + ? [ + { + id: 'list-clear-completed', + label: `Erledigte aufräumen (${completedItems.length})`, + icon: , + onClick: () => handleClearCompleted(), + }, + ] + : []), + ...(activeList && lists.length > 1 + ? [ + { + id: 'list-delete', + label: `Liste "${activeList.title}" löschen`, + icon: , + variant: 'danger' as const, + onClick: () => handleDeleteList(activeList.id), + }, + ] + : []), + ], + }, + { + id: 'lists-switcher', + title: 'Einkaufslisten wechseln', + items: lists.map((lst) => { + const openCount = deduplicateItems(lst.items || []).filter((i) => !i.isCompleted).length; + return { + id: `select-list-${lst.id}`, + label: lst.title, + icon: {lst.icon}, + badge: openCount > 0 ? openCount : undefined, + active: activeList?.id === lst.id, + onClick: () => setActiveListId(lst.id), + }; + }), + }, + ]); + }, [lists, activeList, completedItems.length, setGroups]); + return (
- {/* Top Header: List Tabs & Actions */} -
+ {/* Mobile Compact Header Bar (List switching & actions live in MindSift Logo Menu) */} +
+
+ + {activeList ? `${activeList.icon} ${activeList.title}` : '🛒 Einkaufsliste'} + + {openItems.length > 0 && ( + + {openItems.length} offen + + )} +
+
+ {activeList && ( + + )} +
+ +
+
+
+ + {/* Top Header: List Tabs & Actions (Desktop) */} +
{/* List Tabs */}
{lists.map((lst) => { @@ -862,7 +976,7 @@ export const SharedListsView: React.FC = ({ onRefreshGloba {/* Quick-Add Form */}
@@ -888,7 +1002,7 @@ export const SharedListsView: React.FC = ({ onRefreshGloba setTimeout(() => setShowSuggestions(false), 200); }} placeholder={`Eintrag zu "${activeList.title}" hinzufügen (z. B. Hafermilch #Kühlregal)…`} - className="flex-1 bg-transparent py-1 text-sm text-main placeholder-faint focus:outline-none" + className="flex-1 bg-transparent py-1.5 sm:py-1 text-sm text-main placeholder-faint focus:outline-none" /> {/* Hashtag Autocomplete Suggestions Dropdown */} @@ -907,7 +1021,7 @@ export const SharedListsView: React.FC = ({ onRefreshGloba e.preventDefault(); applySuggestion(tag); }} - className={`w-full text-left px-3 py-1.5 text-xs flex items-center justify-between transition-colors ${ + className={`w-full text-left px-3 py-2 sm:py-1.5 text-xs flex items-center justify-between transition-colors ${ isSelected ? 'bg-accent/20 text-accent font-medium' : 'text-main hover:bg-panel-hover' @@ -932,12 +1046,12 @@ export const SharedListsView: React.FC = ({ onRefreshGloba value={newItemQty} onChange={(e) => setNewItemQty(e.target.value)} placeholder="Menge (z.B. 2x)" - className="w-28 h-7 px-2 input-field text-xs" + className="flex-1 sm:flex-none sm:w-28 h-9 sm:h-7 px-2.5 sm:px-2 input-field text-xs" /> @@ -1005,7 +1119,7 @@ export const SharedListsView: React.FC = ({ onRefreshGloba onDragLeave={(e) => handleItemDragLeave(e, item.id)} onDrop={(e) => handleDropOnItem(e, item, group.category)} onClick={() => handleToggleItem(item)} - className={`px-3 py-2.5 flex items-center justify-between gap-3 cursor-pointer cursor-grab active:cursor-grabbing select-none transition-colors hover:bg-panel-hover group relative ${ + className={`px-3.5 py-3.5 sm:px-3 sm:py-2.5 flex items-center justify-between gap-3 cursor-pointer cursor-grab active:cursor-grabbing select-none transition-colors hover:bg-panel-hover group relative ${ isDragging ? 'opacity-30 border border-dashed border-accent/40' : '' }`} > @@ -1017,7 +1131,7 @@ export const SharedListsView: React.FC = ({ onRefreshGloba
)} -
+
{/* Eckiges Abhaken */} @@ -1062,10 +1176,10 @@ export const SharedListsView: React.FC = ({ onRefreshGloba e.stopPropagation(); handleDeleteItem(item.id); }} - className="btn-icon btn-icon-danger opacity-0 group-hover:opacity-100 transition-opacity" + className="btn-icon btn-icon-danger p-2 sm:p-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity" title="Eintrag löschen" > - +
); @@ -1085,7 +1199,7 @@ export const SharedListsView: React.FC = ({ onRefreshGloba @@ -1099,7 +1213,7 @@ export const SharedListsView: React.FC = ({ onRefreshGloba
handleToggleItem(item)} - className="px-3 py-2 flex items-center justify-between gap-3 cursor-pointer hover:bg-panel-hover transition-colors group" + className="px-3.5 py-3 sm:px-3 sm:py-2 flex items-center justify-between gap-3 cursor-pointer hover:bg-panel-hover transition-colors group" >
{/* Eckiges Abhaken */} @@ -1109,10 +1223,10 @@ export const SharedListsView: React.FC = ({ onRefreshGloba e.stopPropagation(); handleToggleItem(item); }} - className="w-4 h-4 checkbox-square flex items-center justify-center flex-shrink-0 transition-colors border bg-status-ok border-status-ok text-on-accent" + className="w-5 h-5 sm:w-4 sm:h-4 checkbox-square flex items-center justify-center flex-shrink-0 transition-colors border bg-status-ok border-status-ok text-on-accent" title="Als offen markieren" > - + @@ -1134,10 +1248,10 @@ export const SharedListsView: React.FC = ({ onRefreshGloba e.stopPropagation(); handleDeleteItem(item.id); }} - className="btn-icon btn-icon-danger opacity-0 group-hover:opacity-100 transition-opacity" + className="btn-icon btn-icon-danger p-2 sm:p-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity" title="Löschen" > - +
diff --git a/frontend/src/features/triage/TaskDetailModal.tsx b/frontend/src/features/triage/TaskDetailModal.tsx index 4f138c6..58709b8 100644 --- a/frontend/src/features/triage/TaskDetailModal.tsx +++ b/frontend/src/features/triage/TaskDetailModal.tsx @@ -194,7 +194,7 @@ export const TaskDetailModal: React.FC = ({
-
+
diff --git a/frontend/src/features/triage/TasksView.tsx b/frontend/src/features/triage/TasksView.tsx index a723c16..df53be1 100644 --- a/frontend/src/features/triage/TasksView.tsx +++ b/frontend/src/features/triage/TasksView.tsx @@ -30,6 +30,7 @@ import { UnifiedShareModal } from '../../components/UnifiedShareModal'; import { TaskDetailModal } from './TaskDetailModal'; import { ManageColumnsModal, DEFAULT_KANBAN_COLUMNS } from './ManageColumnsModal'; import { UserAutocomplete } from '../UserAutocomplete'; +import { useMobileActions } from '../../context/MobileActionContext'; const getInitials = (name?: string): string => { if (!name) return '?'; @@ -71,6 +72,8 @@ export const TasksView: React.FC = ({ // Quick Task Add state const [quickTitle, setQuickTitle] = useState(''); const [quickDueDate, setQuickDueDate] = useState(''); + const quickInputRef = useRef(null); + const { setGroups } = useMobileActions(); // New TaskList modal state const [showNewListModal, setShowNewListModal] = useState(false); @@ -811,6 +814,141 @@ export const TasksView: React.FC = ({ } }; + useEffect(() => { + const actionItems: any[] = [ + { + id: 'new-task', + label: 'Neue Aufgabe erstellen', + icon: , + variant: 'accent', + onClick: () => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + setTimeout(() => quickInputRef.current?.focus(), 100); + }, + }, + { + id: 'new-task-list', + label: 'Neue Aufgabenliste anlegen', + icon: , + onClick: () => setShowNewListModal(true), + }, + ]; + + if (currentSelectedList) { + actionItems.push({ + id: 'share-task-list', + label: `Liste „${currentSelectedList.title}“ teilen`, + icon: , + onClick: () => handleOpenShareModal(currentSelectedList), + }); + actionItems.push({ + id: 'delete-task-list', + label: `Liste „${currentSelectedList.title}“ löschen`, + icon: , + variant: 'danger', + onClick: () => handleDeleteTaskList(currentSelectedList.id), + }); + } + + const viewItems: any[] = [ + { + id: 'filter-open', + label: 'Offene Aufgaben (Liste)', + icon: , + badge: openTasks.length, + active: viewMode === 'list' && filter === 'open', + onClick: () => { + setViewMode('list'); + setFilter('open'); + }, + }, + { + id: 'filter-done', + label: 'Erledigte Aufgaben', + icon: , + badge: doneTasks.length, + active: viewMode === 'list' && filter === 'done', + onClick: () => { + setViewMode('list'); + setFilter('done'); + }, + }, + { + id: 'filter-discarded', + label: 'Papierkorb', + icon: , + badge: safeDiscarded.length, + active: viewMode === 'list' && filter === 'discarded', + onClick: () => { + setViewMode('list'); + setFilter('discarded'); + }, + }, + { + id: 'view-kanban', + label: 'Kanban-Board Ansicht', + icon: , + badge: kanbanTasks.length, + active: viewMode === 'kanban', + onClick: () => setViewMode('kanban'), + }, + ]; + + if (viewMode === 'kanban') { + viewItems.push({ + id: 'manage-columns', + label: 'Kanban-Spalten verwalten', + icon: , + onClick: () => setShowManageColumnsModal(true), + }); + } + + const listItems: any[] = [ + { + id: 'list-all', + label: 'Alle Aufgaben', + icon: , + active: selectedListId === 'all', + onClick: () => setSelectedListId('all'), + }, + ...safeTaskLists.map((tl) => ({ + id: `list-${tl.id}`, + label: `${tl.icon || '📋'} ${tl.title}`, + badge: tl.tasksCount || undefined, + active: selectedListId === tl.id, + onClick: () => setSelectedListId(tl.id), + })), + ]; + + setGroups([ + { + id: 'tasks-actions', + title: 'Aktionen', + items: actionItems, + }, + { + id: 'tasks-views', + title: 'Ansicht & Status', + items: viewItems, + }, + { + id: 'tasks-lists', + title: 'Aufgabenlisten', + items: listItems, + }, + ]); + }, [ + viewMode, + filter, + selectedListId, + openTasks.length, + doneTasks.length, + safeDiscarded.length, + kanbanTasks.length, + safeTaskLists, + currentSelectedList, + ]); + const renderTaskProgressBar = (task: Thought) => { if (!task.subtasks || task.subtasks.length === 0) return null; const total = task.subtasks.length; @@ -868,7 +1006,7 @@ export const TasksView: React.FC = ({ setDetailTaskId(task.id); setShowDetailModal(true); }} - className={`px-3 py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-panel-hover group border-b border-border last:border-b-0 relative cursor-pointer cursor-grab active:cursor-grabbing select-none ${ + className={`px-3.5 py-3.5 sm:px-3 sm:py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-panel-hover group border-b border-border last:border-b-0 relative cursor-pointer cursor-grab active:cursor-grabbing select-none ${ isCompleted ? 'opacity-65' : '' } ${isDragging ? 'opacity-30 border border-dashed border-accent/40' : ''}`} > @@ -880,8 +1018,8 @@ export const TasksView: React.FC = ({
)} -
- {/* Checkbox */} +
+ {/* Checkbox (larger touch target on mobile) */} {/* Clickable Title & Metadata */} @@ -1006,8 +1144,8 @@ export const TasksView: React.FC = ({
- {/* Action Controls */} -
e.stopPropagation()} onClick={(e) => e.stopPropagation()}> + {/* Action Controls (Desktop only - on mobile, tapping the task opens Detail Modal) */} +
e.stopPropagation()} onClick={(e) => e.stopPropagation()}> + )} +
+ + {/* Top Header: Task Lists Tabs & View Mode Switcher (Desktop) */} +
{/* Task Lists selector tabs */}
@@ -1559,7 +1726,7 @@ export const TasksView: React.FC = ({ > {task.content}

-
e.stopPropagation()}> +
e.stopPropagation()}>
{filter === 'discarded' && safeDiscarded.length > 0 && ( - diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index b422bb0..171ab02 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -53,6 +53,7 @@ import { api, getToken, triggerHaptic } from '../../lib/api'; import { CodeBlockItem } from './CodeBlockItem'; import { UserAutocomplete } from '../UserAutocomplete'; import { UnifiedShareModal } from '../../components/UnifiedShareModal'; +import { useMobileActions } from '../../context/MobileActionContext'; interface WorkspaceViewProps { currentUser?: User | null; @@ -99,6 +100,9 @@ export const WorkspaceView: React.FC = ({ initialDocId, onStatsChange, }) => { + const { setGroups } = useMobileActions(); + const [showMobileFileDrawer, setShowMobileFileDrawer] = useState(false); + // Top Workspace Tab Mode: 'editor' vs 'todos' vs 'tags' const [workspaceMode, setWorkspaceMode] = useState<'editor' | 'todos' | 'tags'>('editor'); const [selectedTag, setSelectedTag] = useState(null); @@ -2714,6 +2718,132 @@ export const WorkspaceView: React.FC = ({ return workspaceTags.filter((t) => t.tag.toLowerCase().includes(q)); }, [workspaceTags, tagSearchQuery]); + // Register context-sensitive mobile menu groups behind the MindSift logo + useEffect(() => { + const openTodosCount = allWorkspaceTodos.filter((t) => !t.block.checked).length; + setGroups([ + { + id: 'ws-view-files', + title: 'Workspace-Ansicht & Dateien', + items: [ + { + id: 'ws-open-tree', + label: 'Dateibaum & Ordner öffnen', + icon: , + badge: docs.length, + variant: 'accent', + active: showMobileFileDrawer, + onClick: () => { + setWorkspaceMode('editor'); + setShowMobileFileDrawer(true); + }, + }, + { + id: 'ws-mode-editor', + label: 'Notizen & Editor', + icon: , + active: workspaceMode === 'editor' && !showMobileFileDrawer, + onClick: () => { + setWorkspaceMode('editor'); + setShowMobileFileDrawer(false); + }, + }, + { + id: 'ws-mode-tags', + label: 'Themen & Hashtags', + icon: , + badge: workspaceTags.length, + active: workspaceMode === 'tags', + onClick: () => { + setWorkspaceMode('tags'); + setShowMobileFileDrawer(false); + }, + }, + { + id: 'ws-mode-todos', + label: 'Globale To-Dos', + icon: , + badge: openTodosCount, + active: workspaceMode === 'todos', + onClick: () => { + setWorkspaceMode('todos'); + setShowMobileFileDrawer(false); + }, + }, + ], + }, + { + id: 'ws-doc-actions', + title: 'Erstellen & Dokument-Aktionen', + items: [ + { + id: 'ws-new-doc', + label: 'Neue Datei erstellen', + icon: , + onClick: () => { + setShowMobileFileDrawer(false); + handleCreateDoc(null); + }, + }, + { + id: 'ws-new-folder', + label: 'Neuer Ordner', + icon: , + onClick: () => { + setWorkspaceMode('editor'); + setShowMobileFileDrawer(true); + handleStartCreateFolder(null); + }, + }, + ...(selectedDoc + ? [ + { + id: 'ws-share-doc', + label: `Notiz "${selectedDoc.title || 'Unbenannt'}" teilen`, + icon: , + onClick: () => openShareModal(), + }, + { + id: 'ws-pin-doc', + label: selectedDoc.isPinned ? 'Von Favoriten lösen' : 'Oben in Favoriten anpinnen', + icon: , + active: Boolean(selectedDoc.isPinned), + onClick: () => persistDocument({ ...selectedDoc, isPinned: !selectedDoc.isPinned }, true), + }, + { + id: 'ws-export-md', + label: 'Als Markdown exportieren (.md)', + icon: , + onClick: () => handleExportMarkdown(), + }, + { + id: 'ws-delete-doc', + label: selectedDoc.isOwner !== false ? 'Notiz löschen' : 'Geteilte Notiz verlassen', + icon: , + variant: 'danger' as const, + onClick: () => { + if (selectedDoc.isOwner !== false) { + handleDeleteDoc(selectedDoc.id); + } else { + handleLeaveShare(); + } + }, + }, + ] + : []), + ], + }, + ]); + }, [ + workspaceMode, + showMobileFileDrawer, + docs.length, + workspaceTags.length, + allWorkspaceTodos, + selectedDoc, + setGroups, + ]); + const renderDocItem = (doc: Document, depth: number) => { const isSelected = selectedDoc?.id === doc.id && workspaceMode === 'editor'; const isDragging = draggingDocId === doc.id; @@ -2732,9 +2862,10 @@ export const WorkspaceView: React.FC = ({ setSelectedDoc(doc); setZoomedBlockId(null); setWorkspaceMode('editor'); + setShowMobileFileDrawer(false); }} style={{ paddingLeft: `${depth * 14 + 16}px` }} - className={`group flex items-center justify-between py-1.5 pr-2 border-l-2 transition-all cursor-pointer text-xs ${ + className={`group flex items-center justify-between py-2.5 sm:py-1.5 pr-2 border-l-2 transition-all cursor-pointer text-sm sm:text-xs ${ isDragging ? 'opacity-40 scale-[0.98]' : '' } ${ isSelected @@ -2744,15 +2875,15 @@ export const WorkspaceView: React.FC = ({ title={`${doc.title || 'Notiz'} (Ziehen zum Verschieben in Ordner)`} >
- {doc.icon || '📄'} + {doc.icon || '📄'} {doc.title || 'Unbenannt'} - {doc.isPinned && } + {doc.isPinned && } {doc.isOwner === false && ( - + )}
@@ -2816,7 +2947,7 @@ export const WorkspaceView: React.FC = ({ onDragEnd={() => setDraggingFolderPath(null)} onClick={() => toggleFolder(node.path)} style={{ paddingLeft: `${node.depth * 14 + 8}px` }} - className={`group flex items-center justify-between py-1.5 pr-2 cursor-pointer transition-colors text-xs ${ + className={`group flex items-center justify-between py-2.5 sm:py-1.5 pr-2 cursor-pointer transition-colors text-sm sm:text-xs ${ isDraggingThisFolder ? 'opacity-40 scale-[0.98]' : '' } ${ isDragOver @@ -2828,31 +2959,32 @@ export const WorkspaceView: React.FC = ({
{isExpanded ? ( - + ) : ( - + )} {isExpanded ? ( - + ) : ( - + )} - {node.name} + {node.name} ({totalItems})
-
+
@@ -2882,9 +3014,9 @@ export const WorkspaceView: React.FC = ({ {isCreatingSubfolder && (
- + = ({ if (e.key === 'Escape') setIsCreatingFolder(false); }} placeholder="Unterordner…" - className="flex-1 h-6 px-1.5 input-field text-xs" + className="flex-1 h-8 sm:h-6 px-2 sm:px-1.5 input-field text-xs" />
)} @@ -2938,8 +3070,52 @@ export const WorkspaceView: React.FC = ({ return (
- {/* Top Navigation Mode Switcher: Notizen & Outliner vs. Globales To-Do Dashboard */} -
+ {/* Mobile Compact Workspace Bar (Options & mode switching live in MindSift Logo Menu) */} +
+ + +
+ {workspaceMode === 'editor' && selectedDoc && ( + + + {saveState === 'saving' ? 'Speichert…' : 'Gespeichert'} + + )} + +
+
+ + {/* Top Navigation Mode Switcher: Notizen & Outliner vs. Globales To-Do Dashboard (Desktop) */} +
+
+ +
- + setSearchQuery(e.target.value)} placeholder="Dateien durchsuchen…" - className="w-full h-7 pl-7 pr-2 input-field text-xs" + className="w-full h-9 sm:h-7 pl-8 sm:pl-7 pr-2 input-field text-sm sm:text-xs" />
@@ -3054,7 +3263,7 @@ export const WorkspaceView: React.FC = ({ {isCreatingFolder && newFolderParent === null && (
- + = ({ if (e.key === 'Escape') setIsCreatingFolder(false); }} placeholder="Neuer Ordnername…" - className="flex-1 h-6 px-1.5 input-field text-xs" + className="flex-1 h-8 sm:h-6 px-2 sm:px-1.5 input-field text-xs" />
)} @@ -3096,8 +3305,11 @@ export const WorkspaceView: React.FC = ({
) : ( -
+
{/* Drag target for Root when dragging a note or a folder */} {(draggingDocId || draggingFolderPath) && (
= ({
); @@ -3229,7 +3442,7 @@ export const WorkspaceView: React.FC = ({ {!isTagsCollapsed && ( -
+
{workspaceTags.map((wt) => (