Dev #5
No files matched your search
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user