feat(mobile): add context-sensitive MindSift logo menu, slide-over file drawer, and larger touch targets
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 58s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 58s
This commit is contained in:
1 parent
cb0999325f
commit
212cb32bb0
8 files changed
+939
-107
No files matched your search
+134
-6
@@ -44,6 +44,8 @@ import { UserSettingsView } from './features/settings/UserSettingsView';
|
|||||||
import { AdminSettingsView } from './features/settings/AdminSettingsView';
|
import { AdminSettingsView } from './features/settings/AdminSettingsView';
|
||||||
import { HelpView } from './features/help/HelpView';
|
import { HelpView } from './features/help/HelpView';
|
||||||
import { GroupsView } from './features/teams/GroupsView';
|
import { GroupsView } from './features/teams/GroupsView';
|
||||||
|
import { useMobileActions } from './context/MobileActionContext';
|
||||||
|
import { MobileHeaderMenu } from './components/MobileHeaderMenu';
|
||||||
|
|
||||||
type ActiveTab =
|
type ActiveTab =
|
||||||
| 'triage'
|
| 'triage'
|
||||||
@@ -129,6 +131,107 @@ 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();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
closeMenu();
|
||||||
|
if (activeTab === 'triage') {
|
||||||
|
setGroups([
|
||||||
|
{
|
||||||
|
id: 'triage-actions',
|
||||||
|
title: 'Schnellaktionen',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'new-thought',
|
||||||
|
label: 'Neuen Gedanken erfassen',
|
||||||
|
icon: <Plus className="w-4 h-4" />,
|
||||||
|
variant: 'accent',
|
||||||
|
onClick: () => setShowMobileCapture(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'refresh-deck',
|
||||||
|
label: 'Inbox aktualisieren',
|
||||||
|
icon: <RotateCcw className="w-4 h-4" />,
|
||||||
|
badge: stats.inboxCount,
|
||||||
|
onClick: () => refreshAll(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'triage-nav',
|
||||||
|
title: 'Bereiche wechseln',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'goto-tasks',
|
||||||
|
label: 'Aufgaben & Kanban',
|
||||||
|
icon: <CheckSquare className="w-4 h-4" />,
|
||||||
|
badge: stats.tasksOpenCount,
|
||||||
|
onClick: () => setActiveTab('tasks'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'goto-workspace',
|
||||||
|
label: 'Notizen & Workspace',
|
||||||
|
icon: <FileText className="w-4 h-4" />,
|
||||||
|
badge: stats.docsCount,
|
||||||
|
onClick: () => setActiveTab('workspace'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'goto-lists',
|
||||||
|
label: 'Einkaufslisten',
|
||||||
|
icon: <ShoppingCart className="w-4 h-4" />,
|
||||||
|
badge: stats.listsCount,
|
||||||
|
onClick: () => setActiveTab('lists'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'goto-groups',
|
||||||
|
label: 'Teams & Gruppen',
|
||||||
|
icon: <Users className="w-4 h-4" />,
|
||||||
|
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: <Layers className="w-4 h-4" />,
|
||||||
|
variant: 'accent',
|
||||||
|
onClick: () => setActiveTab('triage'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'goto-tasks',
|
||||||
|
label: 'Zu den Aufgaben',
|
||||||
|
icon: <CheckSquare className="w-4 h-4" />,
|
||||||
|
onClick: () => setActiveTab('tasks'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'goto-workspace',
|
||||||
|
label: 'Zum Workspace',
|
||||||
|
icon: <FileText className="w-4 h-4" />,
|
||||||
|
onClick: () => setActiveTab('workspace'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'goto-lists',
|
||||||
|
label: 'Zu den Listen',
|
||||||
|
icon: <ShoppingCart className="w-4 h-4" />,
|
||||||
|
onClick: () => setActiveTab('lists'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}, [activeTab, stats.inboxCount, stats.tasksOpenCount, stats.docsCount, stats.listsCount]);
|
||||||
|
|
||||||
// Mobile Web-App (PWA) Installation State
|
// Mobile Web-App (PWA) Installation State
|
||||||
const [isStandaloneApp, setIsStandaloneApp] = useState<boolean>(() => {
|
const [isStandaloneApp, setIsStandaloneApp] = useState<boolean>(() => {
|
||||||
@@ -409,27 +512,50 @@ export const App: React.FC = () => {
|
|||||||
const isServerAdmin = user.role === 'admin';
|
const isServerAdmin = user.role === 'admin';
|
||||||
const roleLabel = isServerAdmin ? 'Server-Admin' : 'Mitglied';
|
const roleLabel = isServerAdmin ? 'Server-Admin' : 'Mitglied';
|
||||||
|
|
||||||
|
const activeTabLabelMap: Record<ActiveTab, string> = {
|
||||||
|
triage: 'Triage',
|
||||||
|
tasks: 'Aufgaben',
|
||||||
|
workspace: 'Workspace',
|
||||||
|
lists: 'Listen',
|
||||||
|
groups: 'Teams & Gruppen',
|
||||||
|
user_settings: 'Einstellungen',
|
||||||
|
admin_settings: 'Administration',
|
||||||
|
help: 'Hilfe',
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-canvas text-main flex flex-col font-sans selection:bg-accent/30">
|
<div className="min-h-screen bg-canvas text-main flex flex-col font-sans selection:bg-accent/30">
|
||||||
{/* Top Header Bar: Floating on mobile, solid panel on desktop */}
|
{/* Top Header Bar: Floating on mobile, solid panel on desktop */}
|
||||||
<header className="sticky top-0 z-30 bg-canvas/85 backdrop-blur-md sm:bg-panel sm:border-b sm:border-border sm:shadow-sm">
|
<header className="sticky top-0 z-30 bg-canvas/85 backdrop-blur-md sm:bg-panel sm:border-b sm:border-border sm:shadow-sm">
|
||||||
{/* 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 */}
|
||||||
<div className="sm:hidden flex items-center justify-between h-14 px-4 pt-1">
|
<div className="sm:hidden flex items-center justify-between h-14 px-4 pt-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
triggerHaptic(10);
|
triggerHaptic(12);
|
||||||
setActiveTab('triage');
|
setIsMenuOpen((prev) => !prev);
|
||||||
}}
|
}}
|
||||||
className="h-11 px-2 -ml-2 rounded-xl flex items-center gap-2.5 cursor-pointer select-none active:scale-95 transition-transform"
|
className={`min-h-[44px] px-2.5 -ml-2 rounded-xl flex items-center gap-2.5 cursor-pointer select-none active:scale-95 transition-all ${
|
||||||
title="Zur Startseite (Triage)"
|
isMenuOpen ? 'bg-panel border border-accent/40 shadow-sm' : 'hover:bg-panel/60'
|
||||||
|
}`}
|
||||||
|
title="Menü & Ansichts-Optionen öffnen"
|
||||||
>
|
>
|
||||||
<div className="w-8 h-8 bg-accent text-on-accent rounded-xl shadow-sm flex items-center justify-center">
|
<div className="w-8 h-8 bg-accent text-on-accent rounded-xl shadow-sm flex items-center justify-center flex-shrink-0">
|
||||||
<Brain className="w-4 h-4" />
|
<Brain className="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="font-bold text-base text-main tracking-tight">
|
<span className="font-bold text-base text-main tracking-tight">
|
||||||
{authStatus.instanceName || 'MindSift'}
|
{authStatus.instanceName || 'MindSift'}
|
||||||
</span>
|
</span>
|
||||||
|
<span className="text-[11px] font-semibold text-accent bg-accent/15 px-1.5 py-0.5 rounded-md">
|
||||||
|
{activeTabLabelMap[activeTab]}
|
||||||
|
</span>
|
||||||
|
<ChevronDown
|
||||||
|
className={`w-4 h-4 text-muted transition-transform duration-150 ${
|
||||||
|
isMenuOpen ? 'rotate-180 text-accent' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
@@ -715,6 +841,8 @@ export const App: React.FC = () => {
|
|||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<MobileHeaderMenu activeTabLabel={activeTabLabelMap[activeTab]} />
|
||||||
|
|
||||||
{/* Main Container */}
|
{/* Main Container */}
|
||||||
<main className="flex-1 max-w-6xl w-full mx-auto px-3 sm:px-4 pt-3 pb-24 sm:pb-6">
|
<main className="flex-1 max-w-6xl w-full mx-auto px-3 sm:px-4 pt-3 pb-24 sm:pb-6">
|
||||||
<ViewErrorBoundary onReset={() => setActiveTab('triage')}>
|
<ViewErrorBoundary onReset={() => setActiveTab('triage')}>
|
||||||
|
|||||||
@@ -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<MobileHeaderMenuProps> = ({ activeTabLabel }) => {
|
||||||
|
const { groups, isMenuOpen, closeMenu } = useMobileActions();
|
||||||
|
|
||||||
|
if (!isMenuOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sm:hidden fixed inset-0 z-50">
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/60 backdrop-blur-xs animate-in fade-in duration-150"
|
||||||
|
onClick={() => {
|
||||||
|
triggerHaptic(8);
|
||||||
|
closeMenu();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Menu Sheet below Header */}
|
||||||
|
<div className="fixed top-14 left-2.5 right-2.5 max-h-[80vh] overflow-y-auto bg-panel border border-border rounded-2xl shadow-2xl p-3 space-y-3.5 z-50 animate-in fade-in slide-in-from-top-2 duration-150">
|
||||||
|
{/* Menu Header */}
|
||||||
|
<div className="flex items-center justify-between px-2 pb-2 border-b border-border">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-accent"></span>
|
||||||
|
<span className="text-xs font-bold uppercase tracking-wider text-muted">
|
||||||
|
{activeTabLabel} — Optionen & Aktionen
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
triggerHaptic(8);
|
||||||
|
closeMenu();
|
||||||
|
}}
|
||||||
|
className="w-9 h-9 rounded-xl flex items-center justify-center text-muted hover:text-main hover:bg-canvas active:scale-95 transition-all"
|
||||||
|
title="Menü schließen"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{groups.length === 0 ? (
|
||||||
|
<div className="py-6 text-center text-xs text-muted">
|
||||||
|
Keine spezifischen Optionen für diese Ansicht verfügbar.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{groups.map((group) => (
|
||||||
|
<div key={group.id} className="space-y-1.5">
|
||||||
|
{group.title && (
|
||||||
|
<div className="px-2 text-[11px] font-bold uppercase tracking-wider text-faint">
|
||||||
|
{group.title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-1">
|
||||||
|
{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 (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
triggerHaptic(12);
|
||||||
|
closeMenu();
|
||||||
|
item.onClick();
|
||||||
|
}}
|
||||||
|
className={`w-full min-h-[46px] px-3.5 py-2.5 rounded-xl flex items-center justify-between gap-3 text-sm transition-all active:scale-[0.98] cursor-pointer ${btnClass}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 min-w-0 flex-1 text-left">
|
||||||
|
{item.icon && (
|
||||||
|
<span
|
||||||
|
className={`flex-shrink-0 flex items-center justify-center w-5 h-5 ${
|
||||||
|
isAccent
|
||||||
|
? 'text-on-accent'
|
||||||
|
: isDanger
|
||||||
|
? 'text-status-err'
|
||||||
|
: isActive
|
||||||
|
? 'text-accent'
|
||||||
|
: 'text-muted'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="truncate">{item.label}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
|
{item.badge !== undefined && item.badge !== '' && (
|
||||||
|
<span
|
||||||
|
className={`px-2 py-0.5 rounded-full text-xs font-mono font-semibold ${
|
||||||
|
isAccent
|
||||||
|
? 'bg-black/20 text-on-accent'
|
||||||
|
: isActive
|
||||||
|
? 'bg-accent/20 text-accent'
|
||||||
|
: 'bg-panel text-muted border border-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isActive && !isAccent && (
|
||||||
|
<Check className="w-4 h-4 text-accent flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<React.SetStateAction<boolean>>;
|
||||||
|
closeMenu: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MobileActionContext = createContext<MobileActionContextValue>({
|
||||||
|
groups: [],
|
||||||
|
setGroups: () => {},
|
||||||
|
isMenuOpen: false,
|
||||||
|
setIsMenuOpen: () => {},
|
||||||
|
closeMenu: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MobileActionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [groups, setGroupsState] = useState<MobileMenuGroup[]>([]);
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const setGroups = useCallback((newGroups: MobileMenuGroup[]) => {
|
||||||
|
setGroupsState(newGroups);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeMenu = useCallback(() => {
|
||||||
|
setIsMenuOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MobileActionContext.Provider
|
||||||
|
value={{
|
||||||
|
groups,
|
||||||
|
setGroups,
|
||||||
|
isMenuOpen,
|
||||||
|
setIsMenuOpen,
|
||||||
|
closeMenu,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MobileActionContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMobileActions = () => useContext(MobileActionContext);
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { ListItem, SharedList, ListShare, Team, User, WSMessage } from '../../lib/types';
|
import { ListItem, SharedList, ListShare, Team, User, WSMessage } from '../../lib/types';
|
||||||
import { api, buildWebSocketURL, triggerHaptic } from '../../lib/api';
|
import { api, buildWebSocketURL, triggerHaptic } from '../../lib/api';
|
||||||
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
||||||
|
import { useMobileActions } from '../../context/MobileActionContext';
|
||||||
|
|
||||||
interface SharedListsViewProps {
|
interface SharedListsViewProps {
|
||||||
onRefreshGlobal: () => void;
|
onRefreshGlobal: () => void;
|
||||||
@@ -76,6 +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 [lists, setLists] = useState<SharedList[]>([]);
|
const [lists, setLists] = useState<SharedList[]>([]);
|
||||||
const [activeListId, setActiveListId] = useState<string>('');
|
const [activeListId, setActiveListId] = useState<string>('');
|
||||||
|
|
||||||
@@ -710,10 +712,122 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
return result;
|
return result;
|
||||||
}, [openItems]);
|
}, [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: <Plus className="w-4 h-4 text-accent" />,
|
||||||
|
variant: 'accent',
|
||||||
|
onClick: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}, 60);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'list-new-list',
|
||||||
|
label: 'Neue Einkaufsliste erstellen',
|
||||||
|
icon: <Plus className="w-4 h-4" />,
|
||||||
|
onClick: () => setShowNewListModal(true),
|
||||||
|
},
|
||||||
|
...(activeList
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: 'list-share',
|
||||||
|
label: `Liste "${activeList.title}" teilen`,
|
||||||
|
icon: <Users className="w-4 h-4" />,
|
||||||
|
onClick: () => openShareModal(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(completedItems.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: 'list-clear-completed',
|
||||||
|
label: `Erledigte aufräumen (${completedItems.length})`,
|
||||||
|
icon: <Sparkles className="w-4 h-4" />,
|
||||||
|
onClick: () => handleClearCompleted(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(activeList && lists.length > 1
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: 'list-delete',
|
||||||
|
label: `Liste "${activeList.title}" löschen`,
|
||||||
|
icon: <Trash2 className="w-4 h-4" />,
|
||||||
|
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: <span className="text-base">{lst.icon}</span>,
|
||||||
|
badge: openCount > 0 ? openCount : undefined,
|
||||||
|
active: activeList?.id === lst.id,
|
||||||
|
onClick: () => setActiveListId(lst.id),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}, [lists, activeList, completedItems.length, setGroups]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full space-y-3">
|
<div className="w-full space-y-3">
|
||||||
{/* Top Header: List Tabs & Actions */}
|
{/* Mobile Compact Header Bar (List switching & actions live in MindSift Logo Menu) */}
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-border pb-2">
|
<div className="flex sm:hidden items-center justify-between gap-2 px-3 py-2 rounded-btn bg-panel border border-border">
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<span className="text-sm font-semibold text-main truncate">
|
||||||
|
{activeList ? `${activeList.icon} ${activeList.title}` : '🛒 Einkaufsliste'}
|
||||||
|
</span>
|
||||||
|
{openItems.length > 0 && (
|
||||||
|
<span className="badge-neutral text-[11px] flex-shrink-0">
|
||||||
|
{openItems.length} offen
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||||
|
{activeList && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={openShareModal}
|
||||||
|
className="btn-icon min-h-[36px] min-w-[36px] flex items-center justify-center"
|
||||||
|
title="Liste teilen"
|
||||||
|
>
|
||||||
|
<Users className="w-4 h-4 text-accent" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`h-9 w-9 flex items-center justify-center rounded-btn border ${
|
||||||
|
wsConnected
|
||||||
|
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-500'
|
||||||
|
: 'bg-canvas border-border text-muted'
|
||||||
|
}`}
|
||||||
|
title={wsConnected ? 'Live synchronisiert (Online)' : 'Offline'}
|
||||||
|
>
|
||||||
|
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : ''}`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top Header: List Tabs & Actions (Desktop) */}
|
||||||
|
<div className="hidden sm:flex flex-wrap items-center justify-between gap-2 border-b border-border pb-2">
|
||||||
{/* List Tabs */}
|
{/* List Tabs */}
|
||||||
<div className="flex flex-wrap items-center gap-1">
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
{lists.map((lst) => {
|
{lists.map((lst) => {
|
||||||
@@ -862,7 +976,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
{/* Quick-Add Form */}
|
{/* Quick-Add Form */}
|
||||||
<form
|
<form
|
||||||
onSubmit={handleAddItem}
|
onSubmit={handleAddItem}
|
||||||
className="flex flex-col sm:flex-row items-center gap-2 panel-surface p-1.5 relative"
|
className="flex flex-col sm:flex-row items-center gap-2 panel-surface p-2 sm:p-1.5 relative"
|
||||||
>
|
>
|
||||||
<div className="relative flex items-center gap-2 flex-1 w-full px-1.5">
|
<div className="relative flex items-center gap-2 flex-1 w-full px-1.5">
|
||||||
<Plus className="w-4 h-4 text-accent flex-shrink-0" />
|
<Plus className="w-4 h-4 text-accent flex-shrink-0" />
|
||||||
@@ -888,7 +1002,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
setTimeout(() => setShowSuggestions(false), 200);
|
setTimeout(() => setShowSuggestions(false), 200);
|
||||||
}}
|
}}
|
||||||
placeholder={`Eintrag zu "${activeList.title}" hinzufügen (z. B. Hafermilch #Kühlregal)…`}
|
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 */}
|
{/* Hashtag Autocomplete Suggestions Dropdown */}
|
||||||
@@ -907,7 +1021,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
applySuggestion(tag);
|
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
|
isSelected
|
||||||
? 'bg-accent/20 text-accent font-medium'
|
? 'bg-accent/20 text-accent font-medium'
|
||||||
: 'text-main hover:bg-panel-hover'
|
: 'text-main hover:bg-panel-hover'
|
||||||
@@ -932,12 +1046,12 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
value={newItemQty}
|
value={newItemQty}
|
||||||
onChange={(e) => setNewItemQty(e.target.value)}
|
onChange={(e) => setNewItemQty(e.target.value)}
|
||||||
placeholder="Menge (z.B. 2x)"
|
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"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmittingItem || !newItemText.trim()}
|
disabled={isSubmittingItem || !newItemText.trim()}
|
||||||
className="btn-primary h-7 px-3 text-xs disabled:opacity-40 disabled:cursor-not-allowed flex items-center gap-1 flex-shrink-0"
|
className="btn-primary h-9 sm:h-7 px-3.5 sm:px-3 text-xs disabled:opacity-40 disabled:cursor-not-allowed flex items-center gap-1 flex-shrink-0"
|
||||||
>
|
>
|
||||||
{isSubmittingItem ? 'Wird hinzugefügt…' : 'Hinzufügen'}
|
{isSubmittingItem ? 'Wird hinzugefügt…' : 'Hinzufügen'}
|
||||||
</button>
|
</button>
|
||||||
@@ -1005,7 +1119,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
onDragLeave={(e) => handleItemDragLeave(e, item.id)}
|
onDragLeave={(e) => handleItemDragLeave(e, item.id)}
|
||||||
onDrop={(e) => handleDropOnItem(e, item, group.category)}
|
onDrop={(e) => handleDropOnItem(e, item, group.category)}
|
||||||
onClick={() => handleToggleItem(item)}
|
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' : ''
|
isDragging ? 'opacity-30 border border-dashed border-accent/40' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -1017,7 +1131,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent z-20 pointer-events-none rounded-full shadow-[0_0_8px_rgba(var(--accent-rgb),0.6)]" />
|
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent z-20 pointer-events-none rounded-full shadow-[0_0_8px_rgba(var(--accent-rgb),0.6)]" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
<div className="flex items-center gap-3 sm:gap-2.5 min-w-0 flex-1">
|
||||||
{/* Eckiges Abhaken */}
|
{/* Eckiges Abhaken */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1027,14 +1141,14 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleToggleItem(item);
|
handleToggleItem(item);
|
||||||
}}
|
}}
|
||||||
className={`w-4 h-4 checkbox-square flex items-center justify-center flex-shrink-0 transition-colors border ${
|
className={`w-5 h-5 sm:w-4 sm:h-4 checkbox-square flex items-center justify-center flex-shrink-0 transition-colors border ${
|
||||||
item.isCompleted
|
item.isCompleted
|
||||||
? 'bg-status-ok border-status-ok text-on-accent'
|
? 'bg-status-ok border-status-ok text-on-accent'
|
||||||
: 'border-strong hover:border-accent text-transparent'
|
: 'border-strong hover:border-accent text-transparent'
|
||||||
}`}
|
}`}
|
||||||
title={item.isCompleted ? 'Als offen markieren' : 'Als erledigt abhaken'}
|
title={item.isCompleted ? 'Als offen markieren' : 'Als erledigt abhaken'}
|
||||||
>
|
>
|
||||||
<Check className="w-3 h-3 stroke-[3]" />
|
<Check className="w-3.5 h-3.5 sm:w-3 sm:h-3 stroke-[3]" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span className="text-sm font-medium text-main truncate">
|
<span className="text-sm font-medium text-main truncate">
|
||||||
@@ -1062,10 +1176,10 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDeleteItem(item.id);
|
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"
|
title="Eintrag löschen"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
<Trash2 className="w-4 h-4 sm:w-3.5 sm:h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1085,7 +1199,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={handleClearCompleted}
|
onClick={handleClearCompleted}
|
||||||
className="btn-action text-xs"
|
className="btn-action min-h-[36px] sm:min-h-0 text-xs"
|
||||||
>
|
>
|
||||||
Erledigte aufräumen
|
Erledigte aufräumen
|
||||||
</button>
|
</button>
|
||||||
@@ -1099,7 +1213,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => handleToggleItem(item)}
|
onClick={() => 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"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||||
{/* Eckiges Abhaken */}
|
{/* Eckiges Abhaken */}
|
||||||
@@ -1109,10 +1223,10 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleToggleItem(item);
|
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"
|
title="Als offen markieren"
|
||||||
>
|
>
|
||||||
<Check className="w-3 h-3 stroke-[3]" />
|
<Check className="w-3.5 h-3.5 sm:w-3 sm:h-3 stroke-[3]" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span className="text-sm text-faint line-through truncate">
|
<span className="text-sm text-faint line-through truncate">
|
||||||
@@ -1134,10 +1248,10 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDeleteItem(item.id);
|
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"
|
title="Löschen"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
<Trash2 className="w-4 h-4 sm:w-3.5 sm:h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -202,9 +202,9 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
|
|||||||
onConvertToDoc(task);
|
onConvertToDoc(task);
|
||||||
}}
|
}}
|
||||||
title="Als Dokument / Notiz vertiefen"
|
title="Als Dokument / Notiz vertiefen"
|
||||||
className="btn-action h-7 px-2 text-xs flex items-center gap-1"
|
className="btn-action min-h-[38px] sm:min-h-0 h-9 sm:h-7 px-2.5 sm:px-2 text-xs flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
<FileText className="w-3.5 h-3.5 text-accent" />
|
<FileText className="w-4 h-4 sm:w-3.5 sm:h-3.5 text-accent" />
|
||||||
<span className="hidden sm:inline">Als Notiz vertiefen</span>
|
<span className="hidden sm:inline">Als Notiz vertiefen</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -216,17 +216,18 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title="Aufgabe löschen"
|
title="Aufgabe löschen"
|
||||||
className="text-muted hover:text-status-err p-1.5 transition-colors"
|
className="btn-action min-h-[38px] sm:min-h-0 h-9 sm:h-7 px-2.5 sm:px-2 text-xs text-status-err border-status-err/30 hover:bg-status-err/10 flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4 sm:w-3.5 sm:h-3.5" />
|
||||||
|
<span className="sm:hidden font-medium">Löschen</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-muted hover:text-main p-1.5 transition-colors"
|
className="text-muted hover:text-main p-2 sm:p-1.5 min-h-[38px] min-w-[38px] sm:min-h-0 sm:min-w-0 flex items-center justify-center transition-colors"
|
||||||
title="Schließen"
|
title="Schließen"
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-5 h-5 sm:w-4 sm:h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
|||||||
import { TaskDetailModal } from './TaskDetailModal';
|
import { TaskDetailModal } from './TaskDetailModal';
|
||||||
import { ManageColumnsModal, DEFAULT_KANBAN_COLUMNS } from './ManageColumnsModal';
|
import { ManageColumnsModal, DEFAULT_KANBAN_COLUMNS } from './ManageColumnsModal';
|
||||||
import { UserAutocomplete } from '../UserAutocomplete';
|
import { UserAutocomplete } from '../UserAutocomplete';
|
||||||
|
import { useMobileActions } from '../../context/MobileActionContext';
|
||||||
|
|
||||||
const getInitials = (name?: string): string => {
|
const getInitials = (name?: string): string => {
|
||||||
if (!name) return '?';
|
if (!name) return '?';
|
||||||
@@ -71,6 +72,8 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
// Quick Task Add state
|
// Quick Task Add state
|
||||||
const [quickTitle, setQuickTitle] = useState('');
|
const [quickTitle, setQuickTitle] = useState('');
|
||||||
const [quickDueDate, setQuickDueDate] = useState('');
|
const [quickDueDate, setQuickDueDate] = useState('');
|
||||||
|
const quickInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const { setGroups } = useMobileActions();
|
||||||
|
|
||||||
// New TaskList modal state
|
// New TaskList modal state
|
||||||
const [showNewListModal, setShowNewListModal] = useState(false);
|
const [showNewListModal, setShowNewListModal] = useState(false);
|
||||||
@@ -811,6 +814,141 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const actionItems: any[] = [
|
||||||
|
{
|
||||||
|
id: 'new-task',
|
||||||
|
label: 'Neue Aufgabe erstellen',
|
||||||
|
icon: <Plus className="w-4 h-4" />,
|
||||||
|
variant: 'accent',
|
||||||
|
onClick: () => {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
setTimeout(() => quickInputRef.current?.focus(), 100);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'new-task-list',
|
||||||
|
label: 'Neue Aufgabenliste anlegen',
|
||||||
|
icon: <Folder className="w-4 h-4" />,
|
||||||
|
onClick: () => setShowNewListModal(true),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (currentSelectedList) {
|
||||||
|
actionItems.push({
|
||||||
|
id: 'share-task-list',
|
||||||
|
label: `Liste „${currentSelectedList.title}“ teilen`,
|
||||||
|
icon: <Users className="w-4 h-4" />,
|
||||||
|
onClick: () => handleOpenShareModal(currentSelectedList),
|
||||||
|
});
|
||||||
|
actionItems.push({
|
||||||
|
id: 'delete-task-list',
|
||||||
|
label: `Liste „${currentSelectedList.title}“ löschen`,
|
||||||
|
icon: <Trash2 className="w-4 h-4" />,
|
||||||
|
variant: 'danger',
|
||||||
|
onClick: () => handleDeleteTaskList(currentSelectedList.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewItems: any[] = [
|
||||||
|
{
|
||||||
|
id: 'filter-open',
|
||||||
|
label: 'Offene Aufgaben (Liste)',
|
||||||
|
icon: <LayoutList className="w-4 h-4" />,
|
||||||
|
badge: openTasks.length,
|
||||||
|
active: viewMode === 'list' && filter === 'open',
|
||||||
|
onClick: () => {
|
||||||
|
setViewMode('list');
|
||||||
|
setFilter('open');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'filter-done',
|
||||||
|
label: 'Erledigte Aufgaben',
|
||||||
|
icon: <Check className="w-4 h-4" />,
|
||||||
|
badge: doneTasks.length,
|
||||||
|
active: viewMode === 'list' && filter === 'done',
|
||||||
|
onClick: () => {
|
||||||
|
setViewMode('list');
|
||||||
|
setFilter('done');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'filter-discarded',
|
||||||
|
label: 'Papierkorb',
|
||||||
|
icon: <Trash2 className="w-4 h-4" />,
|
||||||
|
badge: safeDiscarded.length,
|
||||||
|
active: viewMode === 'list' && filter === 'discarded',
|
||||||
|
onClick: () => {
|
||||||
|
setViewMode('list');
|
||||||
|
setFilter('discarded');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'view-kanban',
|
||||||
|
label: 'Kanban-Board Ansicht',
|
||||||
|
icon: <Columns className="w-4 h-4" />,
|
||||||
|
badge: kanbanTasks.length,
|
||||||
|
active: viewMode === 'kanban',
|
||||||
|
onClick: () => setViewMode('kanban'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (viewMode === 'kanban') {
|
||||||
|
viewItems.push({
|
||||||
|
id: 'manage-columns',
|
||||||
|
label: 'Kanban-Spalten verwalten',
|
||||||
|
icon: <Settings className="w-4 h-4" />,
|
||||||
|
onClick: () => setShowManageColumnsModal(true),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const listItems: any[] = [
|
||||||
|
{
|
||||||
|
id: 'list-all',
|
||||||
|
label: 'Alle Aufgaben',
|
||||||
|
icon: <Layers className="w-4 h-4" />,
|
||||||
|
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) => {
|
const renderTaskProgressBar = (task: Thought) => {
|
||||||
if (!task.subtasks || task.subtasks.length === 0) return null;
|
if (!task.subtasks || task.subtasks.length === 0) return null;
|
||||||
const total = task.subtasks.length;
|
const total = task.subtasks.length;
|
||||||
@@ -868,7 +1006,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
setDetailTaskId(task.id);
|
setDetailTaskId(task.id);
|
||||||
setShowDetailModal(true);
|
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' : ''
|
isCompleted ? 'opacity-65' : ''
|
||||||
} ${isDragging ? 'opacity-30 border border-dashed border-accent/40' : ''}`}
|
} ${isDragging ? 'opacity-30 border border-dashed border-accent/40' : ''}`}
|
||||||
>
|
>
|
||||||
@@ -880,8 +1018,8 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent z-20 pointer-events-none rounded-full shadow-[0_0_8px_rgba(var(--accent-rgb),0.6)]" />
|
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent z-20 pointer-events-none rounded-full shadow-[0_0_8px_rgba(var(--accent-rgb),0.6)]" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
<div className="flex items-center gap-3 sm:gap-2.5 min-w-0 flex-1">
|
||||||
{/* Checkbox */}
|
{/* Checkbox (larger touch target on mobile) */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
@@ -890,14 +1028,14 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleToggleDone(task);
|
handleToggleDone(task);
|
||||||
}}
|
}}
|
||||||
className={`w-4 h-4 checkbox-square flex items-center justify-center flex-shrink-0 transition-colors border ${
|
className={`w-5 h-5 sm:w-4 sm:h-4 checkbox-square flex items-center justify-center flex-shrink-0 transition-colors border ${
|
||||||
isCompleted
|
isCompleted
|
||||||
? 'bg-status-ok border-status-ok text-on-accent'
|
? 'bg-status-ok border-status-ok text-on-accent'
|
||||||
: 'border-strong hover:border-accent text-transparent'
|
: 'border-strong hover:border-accent text-transparent'
|
||||||
}`}
|
}`}
|
||||||
title={isCompleted ? 'Als offen markieren' : 'Als erledigt abhaken'}
|
title={isCompleted ? 'Als offen markieren' : 'Als erledigt abhaken'}
|
||||||
>
|
>
|
||||||
<Check className="w-3 h-3 stroke-[3]" />
|
<Check className="w-3.5 h-3.5 sm:w-3 sm:h-3 stroke-[3]" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Clickable Title & Metadata */}
|
{/* Clickable Title & Metadata */}
|
||||||
@@ -1006,8 +1144,8 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Controls */}
|
{/* Action Controls (Desktop only - on mobile, tapping the task opens Detail Modal) */}
|
||||||
<div className="flex items-center gap-1 flex-shrink-0" onMouseDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
|
<div className="hidden sm:flex items-center gap-1 flex-shrink-0" onMouseDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
@@ -1045,8 +1183,36 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full space-y-3">
|
<div className="w-full space-y-3">
|
||||||
{/* Top Header: Task Lists Tabs & View Mode Switcher */}
|
{/* Mobile Compact Context Bar (replaces cluttered button rows; options live in MindSift Logo Menu) */}
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-border pb-2">
|
<div className="flex sm:hidden items-center justify-between gap-2 px-3 py-2 rounded-btn bg-panel border border-border">
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<span className="text-sm font-semibold text-main truncate">
|
||||||
|
{currentSelectedList ? `${currentSelectedList.icon} ${currentSelectedList.title}` : '📋 Alle Aufgaben'}
|
||||||
|
</span>
|
||||||
|
<span className="badge-neutral text-[11px] flex-shrink-0">
|
||||||
|
{viewMode === 'kanban'
|
||||||
|
? 'Kanban'
|
||||||
|
: filter === 'open'
|
||||||
|
? `Offen (${openTasks.length})`
|
||||||
|
: filter === 'done'
|
||||||
|
? `Erledigt (${doneTasks.length})`
|
||||||
|
: `Papierkorb (${safeDiscarded.length})`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{currentSelectedList && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleOpenShareModal(currentSelectedList)}
|
||||||
|
className="btn-icon min-h-[36px] min-w-[36px] flex items-center justify-center flex-shrink-0"
|
||||||
|
title="Aufgabenliste teilen"
|
||||||
|
>
|
||||||
|
<Users className="w-4 h-4 text-accent" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top Header: Task Lists Tabs & View Mode Switcher (Desktop) */}
|
||||||
|
<div className="hidden sm:flex flex-wrap items-center justify-between gap-2 border-b border-border pb-2">
|
||||||
{/* Task Lists selector tabs */}
|
{/* Task Lists selector tabs */}
|
||||||
<div className="flex flex-wrap items-center gap-1">
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
<button
|
<button
|
||||||
@@ -1200,32 +1366,33 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
{/* Quick Add Task Input */}
|
{/* Quick Add Task Input */}
|
||||||
<form
|
<form
|
||||||
onSubmit={handleQuickAddTask}
|
onSubmit={handleQuickAddTask}
|
||||||
className="flex items-center gap-1.5 panel-surface p-1.5"
|
className="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
|
||||||
|
ref={quickInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
value={quickTitle}
|
value={quickTitle}
|
||||||
onChange={(e) => setQuickTitle(e.target.value)}
|
onChange={(e) => setQuickTitle(e.target.value)}
|
||||||
placeholder={
|
placeholder={
|
||||||
currentSelectedList
|
currentSelectedList
|
||||||
? `Aufgabe zu "${currentSelectedList.title}" hinzufügen… (Enter)`
|
? `Aufgabe zu "${currentSelectedList.title}" hinzufügen…`
|
||||||
: 'Neue Aufgabe anlegen… (Enter)'
|
: 'Neue Aufgabe anlegen…'
|
||||||
}
|
}
|
||||||
className="flex-1 bg-transparent px-2 py-1 text-sm text-main placeholder-faint focus:outline-none"
|
className="flex-1 bg-transparent px-2 py-1.5 sm:py-1 text-sm text-main placeholder-faint focus:outline-none min-w-0"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={quickDueDate}
|
value={quickDueDate}
|
||||||
onChange={(e) => setQuickDueDate(e.target.value)}
|
onChange={(e) => setQuickDueDate(e.target.value)}
|
||||||
className="h-7 px-2 input-field text-xs"
|
className="hidden sm:block h-7 px-2 input-field text-xs"
|
||||||
title="Fälligkeitsdatum"
|
title="Fälligkeitsdatum"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!quickTitle.trim()}
|
disabled={!quickTitle.trim()}
|
||||||
className="btn-primary h-7 px-3 text-xs disabled:opacity-40"
|
className="btn-primary h-9 sm:h-7 px-3.5 sm:px-3 text-xs disabled:opacity-40"
|
||||||
>
|
>
|
||||||
Hinzufügen
|
Hinzufügen
|
||||||
</button>
|
</button>
|
||||||
@@ -1559,7 +1726,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
>
|
>
|
||||||
{task.content}
|
{task.content}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-1 flex-shrink-0" onClick={(e) => e.stopPropagation()}>
|
<div className="hidden sm:flex items-center gap-1 flex-shrink-0" onClick={(e) => e.stopPropagation()}>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleConvertToDoc(task)}
|
onClick={() => handleConvertToDoc(task)}
|
||||||
title="Als Notiz vertiefen"
|
title="Als Notiz vertiefen"
|
||||||
@@ -1689,9 +1856,9 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
/* STANDARD LIST VIEW (CLEAN & UNCLUTTERED) */
|
/* STANDARD LIST VIEW (CLEAN & UNCLUTTERED) */
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/* Subfilter tabs for List View */}
|
{/* Subfilter tabs for List View (Desktop; on mobile controlled via MindSift Logo Menu) */}
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="flex items-center border border-border rounded-btn bg-canvas overflow-hidden">
|
<div className="hidden sm:flex items-center border border-border rounded-btn bg-canvas overflow-hidden">
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilter('open')}
|
onClick={() => setFilter('open')}
|
||||||
className={`h-8 px-3.5 text-xs font-medium border-r border-border transition-colors ${
|
className={`h-8 px-3.5 text-xs font-medium border-r border-border transition-colors ${
|
||||||
@@ -1721,7 +1888,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filter === 'discarded' && safeDiscarded.length > 0 && (
|
{filter === 'discarded' && safeDiscarded.length > 0 && (
|
||||||
<button onClick={handlePurgeAllDiscarded} className="btn-action">
|
<button onClick={handlePurgeAllDiscarded} className="btn-action min-h-[38px] sm:min-h-0">
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
Papierkorb leeren
|
Papierkorb leeren
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import { api, getToken, triggerHaptic } from '../../lib/api';
|
|||||||
import { CodeBlockItem } from './CodeBlockItem';
|
import { CodeBlockItem } from './CodeBlockItem';
|
||||||
import { UserAutocomplete } from '../UserAutocomplete';
|
import { UserAutocomplete } from '../UserAutocomplete';
|
||||||
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
||||||
|
import { useMobileActions } from '../../context/MobileActionContext';
|
||||||
|
|
||||||
interface WorkspaceViewProps {
|
interface WorkspaceViewProps {
|
||||||
currentUser?: User | null;
|
currentUser?: User | null;
|
||||||
@@ -99,6 +100,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
initialDocId,
|
initialDocId,
|
||||||
onStatsChange,
|
onStatsChange,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { setGroups } = useMobileActions();
|
||||||
|
const [showMobileFileDrawer, setShowMobileFileDrawer] = useState(false);
|
||||||
|
|
||||||
// Top Workspace Tab Mode: 'editor' vs 'todos' vs 'tags'
|
// Top Workspace Tab Mode: 'editor' vs 'todos' vs 'tags'
|
||||||
const [workspaceMode, setWorkspaceMode] = useState<'editor' | 'todos' | 'tags'>('editor');
|
const [workspaceMode, setWorkspaceMode] = useState<'editor' | 'todos' | 'tags'>('editor');
|
||||||
const [selectedTag, setSelectedTag] = useState<string | null>(null);
|
const [selectedTag, setSelectedTag] = useState<string | null>(null);
|
||||||
@@ -2714,6 +2718,132 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
return workspaceTags.filter((t) => t.tag.toLowerCase().includes(q));
|
return workspaceTags.filter((t) => t.tag.toLowerCase().includes(q));
|
||||||
}, [workspaceTags, tagSearchQuery]);
|
}, [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: <FolderTree className="w-4 h-4 text-accent" />,
|
||||||
|
badge: docs.length,
|
||||||
|
variant: 'accent',
|
||||||
|
active: showMobileFileDrawer,
|
||||||
|
onClick: () => {
|
||||||
|
setWorkspaceMode('editor');
|
||||||
|
setShowMobileFileDrawer(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ws-mode-editor',
|
||||||
|
label: 'Notizen & Editor',
|
||||||
|
icon: <Layers className="w-4 h-4" />,
|
||||||
|
active: workspaceMode === 'editor' && !showMobileFileDrawer,
|
||||||
|
onClick: () => {
|
||||||
|
setWorkspaceMode('editor');
|
||||||
|
setShowMobileFileDrawer(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ws-mode-tags',
|
||||||
|
label: 'Themen & Hashtags',
|
||||||
|
icon: <Hash className="w-4 h-4" />,
|
||||||
|
badge: workspaceTags.length,
|
||||||
|
active: workspaceMode === 'tags',
|
||||||
|
onClick: () => {
|
||||||
|
setWorkspaceMode('tags');
|
||||||
|
setShowMobileFileDrawer(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ws-mode-todos',
|
||||||
|
label: 'Globale To-Dos',
|
||||||
|
icon: <CheckSquare className="w-4 h-4" />,
|
||||||
|
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: <FilePlus className="w-4 h-4 text-accent" />,
|
||||||
|
onClick: () => {
|
||||||
|
setShowMobileFileDrawer(false);
|
||||||
|
handleCreateDoc(null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ws-new-folder',
|
||||||
|
label: 'Neuer Ordner',
|
||||||
|
icon: <FolderPlus className="w-4 h-4" />,
|
||||||
|
onClick: () => {
|
||||||
|
setWorkspaceMode('editor');
|
||||||
|
setShowMobileFileDrawer(true);
|
||||||
|
handleStartCreateFolder(null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...(selectedDoc
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: 'ws-share-doc',
|
||||||
|
label: `Notiz "${selectedDoc.title || 'Unbenannt'}" teilen`,
|
||||||
|
icon: <Users className="w-4 h-4" />,
|
||||||
|
onClick: () => openShareModal(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ws-pin-doc',
|
||||||
|
label: selectedDoc.isPinned ? 'Von Favoriten lösen' : 'Oben in Favoriten anpinnen',
|
||||||
|
icon: <Pin className="w-4 h-4" />,
|
||||||
|
active: Boolean(selectedDoc.isPinned),
|
||||||
|
onClick: () => persistDocument({ ...selectedDoc, isPinned: !selectedDoc.isPinned }, true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ws-export-md',
|
||||||
|
label: 'Als Markdown exportieren (.md)',
|
||||||
|
icon: <Download className="w-4 h-4" />,
|
||||||
|
onClick: () => handleExportMarkdown(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ws-delete-doc',
|
||||||
|
label: selectedDoc.isOwner !== false ? 'Notiz löschen' : 'Geteilte Notiz verlassen',
|
||||||
|
icon: <Trash2 className="w-4 h-4" />,
|
||||||
|
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 renderDocItem = (doc: Document, depth: number) => {
|
||||||
const isSelected = selectedDoc?.id === doc.id && workspaceMode === 'editor';
|
const isSelected = selectedDoc?.id === doc.id && workspaceMode === 'editor';
|
||||||
const isDragging = draggingDocId === doc.id;
|
const isDragging = draggingDocId === doc.id;
|
||||||
@@ -2732,9 +2862,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
setSelectedDoc(doc);
|
setSelectedDoc(doc);
|
||||||
setZoomedBlockId(null);
|
setZoomedBlockId(null);
|
||||||
setWorkspaceMode('editor');
|
setWorkspaceMode('editor');
|
||||||
|
setShowMobileFileDrawer(false);
|
||||||
}}
|
}}
|
||||||
style={{ paddingLeft: `${depth * 14 + 16}px` }}
|
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]' : ''
|
isDragging ? 'opacity-40 scale-[0.98]' : ''
|
||||||
} ${
|
} ${
|
||||||
isSelected
|
isSelected
|
||||||
@@ -2744,15 +2875,15 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
title={`${doc.title || 'Notiz'} (Ziehen zum Verschieben in Ordner)`}
|
title={`${doc.title || 'Notiz'} (Ziehen zum Verschieben in Ordner)`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||||
<span className="text-sm flex-shrink-0">{doc.icon || '📄'}</span>
|
<span className="text-base sm:text-sm flex-shrink-0">{doc.icon || '📄'}</span>
|
||||||
<span className="truncate">{doc.title || 'Unbenannt'}</span>
|
<span className="truncate">{doc.title || 'Unbenannt'}</span>
|
||||||
{doc.isPinned && <Pin className="w-2.5 h-2.5 text-accent flex-shrink-0" />}
|
{doc.isPinned && <Pin className="w-3 h-3 sm:w-2.5 sm:h-2.5 text-accent flex-shrink-0" />}
|
||||||
{doc.isOwner === false && (
|
{doc.isOwner === false && (
|
||||||
<span
|
<span
|
||||||
title={`Geteilt von ${doc.ownerName || 'anderem Nutzer'} (${doc.permission === 'read' ? 'Nur Lesen' : 'Schreibrecht'})`}
|
title={`Geteilt von ${doc.ownerName || 'anderem Nutzer'} (${doc.permission === 'read' ? 'Nur Lesen' : 'Schreibrecht'})`}
|
||||||
className="text-accent flex-shrink-0"
|
className="text-accent flex-shrink-0"
|
||||||
>
|
>
|
||||||
<Users className="w-3 h-3" />
|
<Users className="w-3.5 h-3.5 sm:w-3 sm:h-3" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -2816,7 +2947,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
onDragEnd={() => setDraggingFolderPath(null)}
|
onDragEnd={() => setDraggingFolderPath(null)}
|
||||||
onClick={() => toggleFolder(node.path)}
|
onClick={() => toggleFolder(node.path)}
|
||||||
style={{ paddingLeft: `${node.depth * 14 + 8}px` }}
|
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]' : ''
|
isDraggingThisFolder ? 'opacity-40 scale-[0.98]' : ''
|
||||||
} ${
|
} ${
|
||||||
isDragOver
|
isDragOver
|
||||||
@@ -2828,31 +2959,32 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
<div className="flex items-center gap-1.5 min-w-0">
|
<div className="flex items-center gap-1.5 min-w-0">
|
||||||
<span className="text-faint hover:text-muted">
|
<span className="text-faint hover:text-muted">
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<ChevronDown className="w-3 h-3 flex-shrink-0" />
|
<ChevronDown className="w-3.5 h-3.5 sm:w-3 sm:h-3 flex-shrink-0" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="w-3 h-3 flex-shrink-0" />
|
<ChevronRight className="w-3.5 h-3.5 sm:w-3 sm:h-3 flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<FolderOpen className="w-3.5 h-3.5 text-accent flex-shrink-0" />
|
<FolderOpen className="w-4 h-4 sm:w-3.5 sm:h-3.5 text-accent flex-shrink-0" />
|
||||||
) : (
|
) : (
|
||||||
<Folder className="w-3.5 h-3.5 text-muted flex-shrink-0" />
|
<Folder className="w-4 h-4 sm:w-3.5 sm:h-3.5 text-muted flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
<span className="font-medium text-xs text-main truncate">{node.name}</span>
|
<span className="font-medium text-sm sm:text-xs text-main truncate">{node.name}</span>
|
||||||
<span className="text-[10px] text-faint font-mono">({totalItems})</span>
|
<span className="text-[10px] text-faint font-mono">({totalItems})</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="opacity-0 group-hover:opacity-100 flex items-center gap-1 transition-opacity">
|
<div className="opacity-100 sm:opacity-0 sm:group-hover:opacity-100 flex items-center gap-1 transition-opacity">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
setShowMobileFileDrawer(false);
|
||||||
handleCreateDoc(node.path);
|
handleCreateDoc(node.path);
|
||||||
}}
|
}}
|
||||||
className="p-1 hover:bg-panel-elevated text-muted hover:text-main rounded-btn"
|
className="p-1.5 sm:p-1 hover:bg-panel-elevated text-muted hover:text-main rounded-btn"
|
||||||
title="Neue Datei in diesem Ordner"
|
title="Neue Datei in diesem Ordner"
|
||||||
>
|
>
|
||||||
<FilePlus className="w-3 h-3" />
|
<FilePlus className="w-3.5 h-3.5 sm:w-3 sm:h-3" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -2860,10 +2992,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleStartCreateFolder(node.path);
|
handleStartCreateFolder(node.path);
|
||||||
}}
|
}}
|
||||||
className="p-1 hover:bg-panel-elevated text-muted hover:text-main rounded-btn"
|
className="p-1.5 sm:p-1 hover:bg-panel-elevated text-muted hover:text-main rounded-btn"
|
||||||
title="Neuer Unterordner"
|
title="Neuer Unterordner"
|
||||||
>
|
>
|
||||||
<FolderPlus className="w-3 h-3" />
|
<FolderPlus className="w-3.5 h-3.5 sm:w-3 sm:h-3" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -2871,10 +3003,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDeleteFolder(node.path, node.name);
|
handleDeleteFolder(node.path, node.name);
|
||||||
}}
|
}}
|
||||||
className="p-1 hover:bg-red-500/10 text-muted hover:text-red-400 rounded-btn transition-colors"
|
className="p-1.5 sm:p-1 hover:bg-red-500/10 text-muted hover:text-red-400 rounded-btn transition-colors"
|
||||||
title="Ordner löschen"
|
title="Ordner löschen"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3 h-3" />
|
<Trash2 className="w-3.5 h-3.5 sm:w-3 sm:h-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2882,9 +3014,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
{isCreatingSubfolder && (
|
{isCreatingSubfolder && (
|
||||||
<div
|
<div
|
||||||
style={{ paddingLeft: `${(node.depth + 1) * 14 + 8}px` }}
|
style={{ paddingLeft: `${(node.depth + 1) * 14 + 8}px` }}
|
||||||
className="py-1 pr-2 flex items-center gap-1 bg-canvas"
|
className="py-1.5 sm:py-1 pr-2 flex items-center gap-1 bg-canvas"
|
||||||
>
|
>
|
||||||
<Folder className="w-3 h-3 text-muted flex-shrink-0" />
|
<Folder className="w-3.5 h-3.5 sm:w-3 sm:h-3 text-muted flex-shrink-0" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
autoFocus
|
autoFocus
|
||||||
@@ -2895,23 +3027,23 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
if (e.key === 'Escape') setIsCreatingFolder(false);
|
if (e.key === 'Escape') setIsCreatingFolder(false);
|
||||||
}}
|
}}
|
||||||
placeholder="Unterordner…"
|
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"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleConfirmCreateFolder}
|
onClick={handleConfirmCreateFolder}
|
||||||
className="p-1 text-accent hover:text-main rounded-btn"
|
className="p-1.5 sm:p-1 text-accent hover:text-main rounded-btn"
|
||||||
title="Bestätigen"
|
title="Bestätigen"
|
||||||
>
|
>
|
||||||
<Check className="w-3.5 h-3.5" />
|
<Check className="w-4 h-4 sm:w-3.5 sm:h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsCreatingFolder(false)}
|
onClick={() => setIsCreatingFolder(false)}
|
||||||
className="p-1 text-muted hover:text-main rounded-btn"
|
className="p-1.5 sm:p-1 text-muted hover:text-main rounded-btn"
|
||||||
title="Abbrechen"
|
title="Abbrechen"
|
||||||
>
|
>
|
||||||
<X className="w-3.5 h-3.5" />
|
<X className="w-4 h-4 sm:w-3.5 sm:h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -2938,8 +3070,52 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full space-y-2">
|
<div className="w-full space-y-2">
|
||||||
{/* Top Navigation Mode Switcher: Notizen & Outliner vs. Globales To-Do Dashboard */}
|
{/* Mobile Compact Workspace Bar (Options & mode switching live in MindSift Logo Menu) */}
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-border pb-2">
|
<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">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -3015,38 +3191,71 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
{/* ===================================================================== */}
|
{/* ===================================================================== */}
|
||||||
{workspaceMode === 'editor' && (
|
{workspaceMode === 'editor' && (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-2">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-2">
|
||||||
{/* Left Document & Folder Explorer Tree */}
|
{/* Mobile Backdrop for File Tree Slide-Over Drawer */}
|
||||||
<div className="lg:col-span-4 panel-surface flex flex-col self-start">
|
{showMobileFileDrawer && (
|
||||||
<div className="p-2 panel-header space-y-2">
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 backdrop-blur-xs z-40 lg:hidden animate-in fade-in duration-150"
|
||||||
|
onClick={() => setShowMobileFileDrawer(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Left Document & Folder Explorer Tree (Desktop Sidebar + Mobile Slide-Over Drawer) */}
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
showMobileFileDrawer
|
||||||
|
? 'fixed inset-y-0 left-0 z-50 w-[86vw] max-w-sm bg-panel border-r border-border shadow-2xl flex'
|
||||||
|
: 'hidden lg:flex'
|
||||||
|
} lg:col-span-4 panel-surface flex-col self-start max-h-screen lg:max-h-none`}
|
||||||
|
>
|
||||||
|
{/* Mobile Drawer Top Header */}
|
||||||
|
<div className="flex lg:hidden items-center justify-between px-3.5 py-3 border-b border-border bg-panel">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FolderTree className="w-4 h-4 text-accent" />
|
||||||
|
<span className="text-sm font-bold text-main">Dateibaum & Ordner</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowMobileFileDrawer(false)}
|
||||||
|
className="p-1.5 text-muted hover:text-main rounded-btn"
|
||||||
|
title="Schließen"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-2.5 sm:p-2 panel-header space-y-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="w-3.5 h-3.5 text-muted absolute left-2.5 top-1/2 -translate-y-1/2" />
|
<Search className="w-4 h-4 sm:w-3.5 sm:h-3.5 text-muted absolute left-2.5 top-1/2 -translate-y-1/2" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
placeholder="Dateien durchsuchen…"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleCreateDoc(null)}
|
onClick={() => {
|
||||||
className="flex-1 btn-action"
|
setShowMobileFileDrawer(false);
|
||||||
|
handleCreateDoc(null);
|
||||||
|
}}
|
||||||
|
className="flex-1 btn-action min-h-[38px] sm:min-h-0 justify-center"
|
||||||
title="Neue Datei erstellen"
|
title="Neue Datei erstellen"
|
||||||
>
|
>
|
||||||
<FilePlus className="w-3.5 h-3.5 text-muted" />
|
<FilePlus className="w-4 h-4 sm:w-3.5 sm:h-3.5 text-muted" />
|
||||||
<span>Neue Datei</span>
|
<span>Neue Datei</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleStartCreateFolder(null)}
|
onClick={() => handleStartCreateFolder(null)}
|
||||||
className="flex-1 btn-action"
|
className="flex-1 btn-action min-h-[38px] sm:min-h-0 justify-center"
|
||||||
title="Neuen Ordner erstellen"
|
title="Neuen Ordner erstellen"
|
||||||
>
|
>
|
||||||
<FolderPlus className="w-3.5 h-3.5 text-muted" />
|
<FolderPlus className="w-4 h-4 sm:w-3.5 sm:h-3.5 text-muted" />
|
||||||
<span>Neuer Ordner</span>
|
<span>Neuer Ordner</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -3054,7 +3263,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
|
|
||||||
{isCreatingFolder && newFolderParent === null && (
|
{isCreatingFolder && newFolderParent === null && (
|
||||||
<div className="p-2 panel-header bg-canvas flex items-center gap-1.5">
|
<div className="p-2 panel-header bg-canvas flex items-center gap-1.5">
|
||||||
<Folder className="w-3.5 h-3.5 text-muted flex-shrink-0" />
|
<Folder className="w-4 h-4 sm:w-3.5 sm:h-3.5 text-muted flex-shrink-0" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
autoFocus
|
autoFocus
|
||||||
@@ -3065,23 +3274,23 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
if (e.key === 'Escape') setIsCreatingFolder(false);
|
if (e.key === 'Escape') setIsCreatingFolder(false);
|
||||||
}}
|
}}
|
||||||
placeholder="Neuer Ordnername…"
|
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"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleConfirmCreateFolder}
|
onClick={handleConfirmCreateFolder}
|
||||||
className="p-1 text-accent hover:text-main rounded-btn"
|
className="p-1.5 sm:p-1 text-accent hover:text-main rounded-btn"
|
||||||
title="Bestätigen"
|
title="Bestätigen"
|
||||||
>
|
>
|
||||||
<Check className="w-3.5 h-3.5" />
|
<Check className="w-4 h-4 sm:w-3.5 sm:h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsCreatingFolder(false)}
|
onClick={() => setIsCreatingFolder(false)}
|
||||||
className="p-1 text-muted hover:text-main rounded-btn"
|
className="p-1.5 sm:p-1 text-muted hover:text-main rounded-btn"
|
||||||
title="Abbrechen"
|
title="Abbrechen"
|
||||||
>
|
>
|
||||||
<X className="w-3.5 h-3.5" />
|
<X className="w-4 h-4 sm:w-3.5 sm:h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -3096,8 +3305,11 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleCreateDoc(null)}
|
onClick={() => {
|
||||||
className="btn-action"
|
setShowMobileFileDrawer(false);
|
||||||
|
handleCreateDoc(null);
|
||||||
|
}}
|
||||||
|
className="btn-action min-h-[38px] sm:min-h-0"
|
||||||
>
|
>
|
||||||
<FilePlus className="w-3.5 h-3.5 text-muted" />
|
<FilePlus className="w-3.5 h-3.5 text-muted" />
|
||||||
<span>Neue Datei</span>
|
<span>Neue Datei</span>
|
||||||
@@ -3105,7 +3317,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleStartCreateFolder(null)}
|
onClick={() => handleStartCreateFolder(null)}
|
||||||
className="btn-action"
|
className="btn-action min-h-[38px] sm:min-h-0"
|
||||||
>
|
>
|
||||||
<FolderPlus className="w-3.5 h-3.5 text-muted" />
|
<FolderPlus className="w-3.5 h-3.5 text-muted" />
|
||||||
<span>Neuer Ordner</span>
|
<span>Neuer Ordner</span>
|
||||||
@@ -3113,7 +3325,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="py-1 max-h-[calc(100vh-240px)] overflow-y-auto">
|
<div className="py-1 flex-1 lg:max-h-[calc(100vh-240px)] overflow-y-auto">
|
||||||
{/* Drag target for Root when dragging a note or a folder */}
|
{/* Drag target for Root when dragging a note or a folder */}
|
||||||
{(draggingDocId || draggingFolderPath) && (
|
{(draggingDocId || draggingFolderPath) && (
|
||||||
<div
|
<div
|
||||||
@@ -3160,7 +3372,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsPinnedCollapsed((prev) => !prev)}
|
onClick={() => setIsPinnedCollapsed((prev) => !prev)}
|
||||||
className="w-full px-2 py-1 text-[10px] font-bold text-accent uppercase tracking-wider flex items-center justify-between hover:bg-canvas/40 rounded transition-colors"
|
className="w-full px-2 py-1.5 sm:py-1 text-[10px] font-bold text-accent uppercase tracking-wider flex items-center justify-between hover:bg-canvas/40 rounded transition-colors"
|
||||||
title={isPinnedCollapsed ? 'Angepinnte Notizen aufklappen' : 'Angepinnte Notizen einklappen'}
|
title={isPinnedCollapsed ? 'Angepinnte Notizen aufklappen' : 'Angepinnte Notizen einklappen'}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
@@ -3192,8 +3404,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
setSelectedDoc(doc);
|
setSelectedDoc(doc);
|
||||||
setZoomedBlockId(null);
|
setZoomedBlockId(null);
|
||||||
setWorkspaceMode('editor');
|
setWorkspaceMode('editor');
|
||||||
|
setShowMobileFileDrawer(false);
|
||||||
}}
|
}}
|
||||||
className={`group flex items-center justify-between px-2 py-1.5 rounded-btn transition-colors cursor-pointer text-xs ${
|
className={`group flex items-center justify-between px-2 py-2.5 sm:py-1.5 rounded-btn transition-colors cursor-pointer text-sm sm:text-xs ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-panel-elevated border-l-2 border-accent text-main font-medium'
|
? 'bg-panel-elevated border-l-2 border-accent text-main font-medium'
|
||||||
: 'text-muted hover:text-main hover:bg-canvas/50'
|
: 'text-muted hover:text-main hover:bg-canvas/50'
|
||||||
@@ -3201,7 +3414,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
title="Angepinnte Notiz öffnen"
|
title="Angepinnte Notiz öffnen"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||||
<span className="text-sm flex-shrink-0">{doc.icon || '📄'}</span>
|
<span className="text-base sm:text-sm flex-shrink-0">{doc.icon || '📄'}</span>
|
||||||
<span className="truncate">{doc.title || 'Unbenannt'}</span>
|
<span className="truncate">{doc.title || 'Unbenannt'}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -3210,10 +3423,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
persistDocument({ ...doc, isPinned: false }, true);
|
persistDocument({ ...doc, isPinned: false }, true);
|
||||||
}}
|
}}
|
||||||
className="opacity-0 group-hover:opacity-100 p-0.5 text-muted hover:text-accent rounded transition-opacity"
|
className="opacity-100 sm:opacity-0 sm:group-hover:opacity-100 p-1 sm:p-0.5 text-muted hover:text-accent rounded transition-opacity"
|
||||||
title="Von Angepinnt entfernen"
|
title="Von Angepinnt entfernen"
|
||||||
>
|
>
|
||||||
<Pin className="w-3 h-3 text-accent" />
|
<Pin className="w-3.5 h-3.5 sm:w-3 sm:h-3 text-accent" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -3229,7 +3442,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsTagsCollapsed((prev) => !prev)}
|
onClick={() => setIsTagsCollapsed((prev) => !prev)}
|
||||||
className="w-full px-2 py-1 text-[10px] font-bold text-muted uppercase tracking-wider flex items-center justify-between hover:bg-canvas/40 rounded transition-colors"
|
className="w-full px-2 py-1.5 sm:py-1 text-[10px] font-bold text-muted uppercase tracking-wider flex items-center justify-between hover:bg-canvas/40 rounded transition-colors"
|
||||||
title={isTagsCollapsed ? 'Themen & Hashtags aufklappen' : 'Themen & Hashtags einklappen'}
|
title={isTagsCollapsed ? 'Themen & Hashtags aufklappen' : 'Themen & Hashtags einklappen'}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1.5 text-accent">
|
<div className="flex items-center gap-1.5 text-accent">
|
||||||
@@ -3243,13 +3456,16 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{!isTagsCollapsed && (
|
{!isTagsCollapsed && (
|
||||||
<div className="flex flex-wrap gap-1 px-1.5 py-1 mt-1 max-h-36 overflow-y-auto">
|
<div className="flex flex-wrap gap-1.5 sm:gap-1 px-1.5 py-1 mt-1 max-h-36 overflow-y-auto">
|
||||||
{workspaceTags.map((wt) => (
|
{workspaceTags.map((wt) => (
|
||||||
<button
|
<button
|
||||||
key={wt.tag}
|
key={wt.tag}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openTagPage(wt.tag)}
|
onClick={() => {
|
||||||
className={`px-2 py-0.5 rounded-pill text-[11px] font-medium border flex items-center gap-1 transition-colors ${
|
setShowMobileFileDrawer(false);
|
||||||
|
openTagPage(wt.tag);
|
||||||
|
}}
|
||||||
|
className={`px-2.5 py-1 sm:px-2 sm:py-0.5 rounded-pill text-xs sm:text-[11px] font-medium border flex items-center gap-1 transition-colors ${
|
||||||
selectedTag === wt.tag
|
selectedTag === wt.tag
|
||||||
? 'bg-accent/20 border-accent text-accent font-semibold'
|
? 'bg-accent/20 border-accent text-accent font-semibold'
|
||||||
: 'bg-canvas border-border text-muted hover:border-accent/40 hover:text-main'
|
: 'bg-canvas border-border text-muted hover:border-accent/40 hover:text-main'
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import { MobileActionProvider } from './context/MobileActionContext';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<MobileActionProvider>
|
||||||
<App />
|
<App />
|
||||||
|
</MobileActionProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
Reference in new issue
Block a user