Dev #5
No files matched your search
@@ -132,11 +132,12 @@ 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();
|
||||
const { isMenuOpen, setIsMenuOpen, closeMenu, setGroups, primaryAction, setPrimaryAction } = useMobileActions();
|
||||
|
||||
useEffect(() => {
|
||||
closeMenu();
|
||||
if (activeTab === 'triage') {
|
||||
setPrimaryAction(() => setShowMobileCapture(true));
|
||||
setGroups([
|
||||
{
|
||||
id: 'triage-actions',
|
||||
@@ -966,7 +967,11 @@ export const App: React.FC = () => {
|
||||
type="button"
|
||||
onClick={() => {
|
||||
triggerHaptic(15);
|
||||
setShowMobileCapture(true);
|
||||
if (primaryAction) {
|
||||
primaryAction();
|
||||
} else {
|
||||
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"
|
||||
title="Schnell erfassen (+)"
|
||||
|
||||
@@ -22,6 +22,8 @@ interface MobileActionContextValue {
|
||||
isMenuOpen: boolean;
|
||||
setIsMenuOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
closeMenu: () => void;
|
||||
primaryAction?: () => void;
|
||||
setPrimaryAction: (fn?: () => void) => void;
|
||||
}
|
||||
|
||||
const MobileActionContext = createContext<MobileActionContextValue>({
|
||||
@@ -30,11 +32,14 @@ const MobileActionContext = createContext<MobileActionContextValue>({
|
||||
isMenuOpen: false,
|
||||
setIsMenuOpen: () => {},
|
||||
closeMenu: () => {},
|
||||
primaryAction: undefined,
|
||||
setPrimaryAction: () => {},
|
||||
});
|
||||
|
||||
export const MobileActionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [groups, setGroupsState] = useState<MobileMenuGroup[]>([]);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [primaryAction, setPrimaryActionState] = useState<(() => void) | undefined>(undefined);
|
||||
|
||||
const setGroups = useCallback((newGroups: MobileMenuGroup[]) => {
|
||||
setGroupsState(newGroups);
|
||||
@@ -44,6 +49,10 @@ export const MobileActionProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
setIsMenuOpen(false);
|
||||
}, []);
|
||||
|
||||
const setPrimaryAction = useCallback((fn?: () => void) => {
|
||||
setPrimaryActionState(() => fn);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MobileActionContext.Provider
|
||||
value={{
|
||||
@@ -52,6 +61,8 @@ export const MobileActionProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
isMenuOpen,
|
||||
setIsMenuOpen,
|
||||
closeMenu,
|
||||
primaryAction,
|
||||
setPrimaryAction,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -77,7 +77,7 @@ export function deduplicateItems(items: ListItem[]): ListItem[] {
|
||||
}
|
||||
|
||||
export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGlobal, currentUser }) => {
|
||||
const { setGroups } = useMobileActions();
|
||||
const { setGroups, setPrimaryAction } = useMobileActions();
|
||||
const [lists, setLists] = useState<SharedList[]>([]);
|
||||
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
|
||||
useEffect(() => {
|
||||
setPrimaryAction(() => {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}, 60);
|
||||
});
|
||||
|
||||
setGroups([
|
||||
{
|
||||
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 (
|
||||
<div className="w-full space-y-3">
|
||||
|
||||
@@ -23,8 +23,9 @@ import {
|
||||
Settings,
|
||||
ChevronDown,
|
||||
Folder,
|
||||
Filter,
|
||||
} 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 { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
||||
import { TaskDetailModal } from './TaskDetailModal';
|
||||
@@ -73,7 +74,15 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
const [quickTitle, setQuickTitle] = useState('');
|
||||
const [quickDueDate, setQuickDueDate] = useState('');
|
||||
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
|
||||
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 title = prompt('Neue Aufgabe für diese Spalte:');
|
||||
if (!title || !title.trim()) return;
|
||||
@@ -815,6 +849,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPrimaryAction(() => {
|
||||
setMobileCreateListId(selectedListId === 'all' ? '' : selectedListId);
|
||||
setShowMobileCreateModal(true);
|
||||
});
|
||||
|
||||
const primaryItems: any[] = [
|
||||
{
|
||||
id: 'new-task',
|
||||
@@ -822,12 +861,26 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
icon: <Plus className="w-4 h-4" />,
|
||||
variant: 'accent',
|
||||
onClick: () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
setTimeout(() => quickInputRef.current?.focus(), 100);
|
||||
setMobileCreateListId(selectedListId === 'all' ? '' : selectedListId);
|
||||
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[] = [
|
||||
{
|
||||
id: 'new-task-list',
|
||||
@@ -928,6 +981,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
id: 'tasks-primary',
|
||||
items: primaryItems,
|
||||
},
|
||||
{
|
||||
id: 'tasks-filters',
|
||||
title: 'Filter & Sortierung',
|
||||
items: filterItems,
|
||||
},
|
||||
{
|
||||
id: 'tasks-list-management',
|
||||
title: 'Listen-Verwaltung',
|
||||
@@ -948,12 +1006,17 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
viewMode,
|
||||
filter,
|
||||
selectedListId,
|
||||
selectedTag,
|
||||
groupBy,
|
||||
sortBy,
|
||||
openTasks.length,
|
||||
doneTasks.length,
|
||||
safeDiscarded.length,
|
||||
kanbanTasks.length,
|
||||
safeTaskLists,
|
||||
currentSelectedList,
|
||||
setPrimaryAction,
|
||||
setGroups,
|
||||
]);
|
||||
|
||||
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"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Kanban status selector pill */}
|
||||
{/* Kanban status selector pill (Desktop only - on mobile done via TaskDetailModal) */}
|
||||
<select
|
||||
value={task.kanbanStatus || (isCompleted ? 'done' : 'todo')}
|
||||
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"
|
||||
>
|
||||
{activeColumns.map((c) => (
|
||||
@@ -1374,10 +1437,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Add Task Input */}
|
||||
{/* Quick Add Task Input (Desktop only - on mobile, tapping the bottom '+' FAB opens mobile create sheet) */}
|
||||
<form
|
||||
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" />
|
||||
<input
|
||||
@@ -1410,8 +1473,50 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Hashtag Filter & Grouping / Sorting Toolbar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 pt-1 pb-0.5">
|
||||
{/* Mobile Active Filter Badge Banner (minimal and clean) */}
|
||||
{(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 */}
|
||||
<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">
|
||||
@@ -1497,6 +1602,264 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
</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 */}
|
||||
{showNewListModal && (
|
||||
<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,
|
||||
onStatsChange,
|
||||
}) => {
|
||||
const { setGroups } = useMobileActions();
|
||||
const { setGroups, setPrimaryAction } = useMobileActions();
|
||||
const [showMobileFileDrawer, setShowMobileFileDrawer] = useState(false);
|
||||
|
||||
// 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
|
||||
useEffect(() => {
|
||||
setPrimaryAction(() => {
|
||||
handleCreateDoc(null);
|
||||
});
|
||||
|
||||
const openTodosCount = allWorkspaceTodos.filter((t) => !t.block.checked).length;
|
||||
setGroups([
|
||||
{
|
||||
@@ -2958,6 +2962,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
allWorkspaceTodos,
|
||||
selectedDoc,
|
||||
setGroups,
|
||||
setPrimaryAction,
|
||||
]);
|
||||
|
||||
const renderDocItem = (doc: Document, depth: number) => {
|
||||
@@ -3186,49 +3191,6 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
|
||||
return (
|
||||
<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) */}
|
||||
<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 className="p-8 panel-surface text-center text-muted text-xs">
|
||||
Wähle links eine Datei im Datei-Explorer aus oder erstelle eine neue Datei.
|
||||
<div className="p-8 panel-surface text-center space-y-3">
|
||||
<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>
|
||||
|
||||
Reference in new issue
Block a user