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
+943
-111
No files matched your search
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user