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

This commit is contained in:
sandrodach committed 2026-10-25 21:08:48 +01:00
1 parent cb0999325f
commit 212cb32bb0
8 files changed
+943 -111

No files matched your search

+133 -19
View File
@@ -12,6 +12,7 @@ import {
import { ListItem, SharedList, ListShare, Team, User, WSMessage } from '../../lib/types';
import { api, buildWebSocketURL, triggerHaptic } from '../../lib/api';
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
import { useMobileActions } from '../../context/MobileActionContext';
interface SharedListsViewProps {
onRefreshGlobal: () => void;
@@ -76,6 +77,7 @@ export function deduplicateItems(items: ListItem[]): ListItem[] {
}
export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGlobal, currentUser }) => {
const { setGroups } = useMobileActions();
const [lists, setLists] = useState<SharedList[]>([]);
const [activeListId, setActiveListId] = useState<string>('');
@@ -710,10 +712,122 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
return result;
}, [openItems]);
// Register context-sensitive mobile menu groups behind the MindSift logo
useEffect(() => {
setGroups([
{
id: 'lists-actions',
title: 'Aktionen',
items: [
{
id: 'list-new-item',
label: 'Neuen Eintrag hinzufügen',
icon: <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 (
<div className="w-full space-y-3">
{/* Top Header: List Tabs & Actions */}
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-border pb-2">
{/* Mobile Compact Header Bar (List switching & actions 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">
<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 */}
<div className="flex flex-wrap items-center gap-1">
{lists.map((lst) => {
@@ -862,7 +976,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
{/* Quick-Add Form */}
<form
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">
<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);
}}
placeholder={`Eintrag zu "${activeList.title}" hinzufügen (z. B. Hafermilch #Kühlregal)…`}
className="flex-1 bg-transparent py-1 text-sm text-main placeholder-faint focus:outline-none"
className="flex-1 bg-transparent py-1.5 sm:py-1 text-sm text-main placeholder-faint focus:outline-none"
/>
{/* Hashtag Autocomplete Suggestions Dropdown */}
@@ -907,7 +1021,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
e.preventDefault();
applySuggestion(tag);
}}
className={`w-full text-left px-3 py-1.5 text-xs flex items-center justify-between transition-colors ${
className={`w-full text-left px-3 py-2 sm:py-1.5 text-xs flex items-center justify-between transition-colors ${
isSelected
? 'bg-accent/20 text-accent font-medium'
: 'text-main hover:bg-panel-hover'
@@ -932,12 +1046,12 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
value={newItemQty}
onChange={(e) => setNewItemQty(e.target.value)}
placeholder="Menge (z.B. 2x)"
className="w-28 h-7 px-2 input-field text-xs"
className="flex-1 sm:flex-none sm:w-28 h-9 sm:h-7 px-2.5 sm:px-2 input-field text-xs"
/>
<button
type="submit"
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'}
</button>
@@ -1005,7 +1119,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
onDragLeave={(e) => handleItemDragLeave(e, item.id)}
onDrop={(e) => handleDropOnItem(e, item, group.category)}
onClick={() => handleToggleItem(item)}
className={`px-3 py-2.5 flex items-center justify-between gap-3 cursor-pointer cursor-grab active:cursor-grabbing select-none transition-colors hover:bg-panel-hover group relative ${
className={`px-3.5 py-3.5 sm:px-3 sm:py-2.5 flex items-center justify-between gap-3 cursor-pointer cursor-grab active:cursor-grabbing select-none transition-colors hover:bg-panel-hover group relative ${
isDragging ? 'opacity-30 border border-dashed border-accent/40' : ''
}`}
>
@@ -1017,7 +1131,7 @@ export const SharedListsView: React.FC<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="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 */}
<button
type="button"
@@ -1027,14 +1141,14 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
e.stopPropagation();
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
? 'bg-status-ok border-status-ok text-on-accent'
: 'border-strong hover:border-accent text-transparent'
}`}
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>
<span className="text-sm font-medium text-main truncate">
@@ -1062,10 +1176,10 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
e.stopPropagation();
handleDeleteItem(item.id);
}}
className="btn-icon btn-icon-danger opacity-0 group-hover:opacity-100 transition-opacity"
className="btn-icon btn-icon-danger p-2 sm:p-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity"
title="Eintrag löschen"
>
<Trash2 className="w-3.5 h-3.5" />
<Trash2 className="w-4 h-4 sm:w-3.5 sm:h-3.5" />
</button>
</div>
);
@@ -1085,7 +1199,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
</span>
<button
onClick={handleClearCompleted}
className="btn-action text-xs"
className="btn-action min-h-[36px] sm:min-h-0 text-xs"
>
Erledigte aufräumen
</button>
@@ -1099,7 +1213,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
<div
key={item.id}
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">
{/* Eckiges Abhaken */}
@@ -1109,10 +1223,10 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
e.stopPropagation();
handleToggleItem(item);
}}
className="w-4 h-4 checkbox-square flex items-center justify-center flex-shrink-0 transition-colors border bg-status-ok border-status-ok text-on-accent"
className="w-5 h-5 sm:w-4 sm:h-4 checkbox-square flex items-center justify-center flex-shrink-0 transition-colors border bg-status-ok border-status-ok text-on-accent"
title="Als offen markieren"
>
<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>
<span className="text-sm text-faint line-through truncate">
@@ -1134,10 +1248,10 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
e.stopPropagation();
handleDeleteItem(item.id);
}}
className="btn-icon btn-icon-danger opacity-0 group-hover:opacity-100 transition-opacity"
className="btn-icon btn-icon-danger p-2 sm:p-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity"
title="Löschen"
>
<Trash2 className="w-3.5 h-3.5" />
<Trash2 className="w-4 h-4 sm:w-3.5 sm:h-3.5" />
</button>
</div>
</div>