From c43c99dc0cd0aba98ef20ff5991832e314a6eb98 Mon Sep 17 00:00:00 2001 From: Sandro Date: Sat, 31 Oct 2026 15:41:44 +0100 Subject: [PATCH] feat(mobile): list items and new notes via a sheet opened by "+" Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 9 +- README.md | 2 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- .../src/features/lists/SharedListsView.tsx | 217 +++++++++++------- .../src/features/workspace/WorkspaceView.tsx | 66 +++++- 6 files changed, 201 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5c3cbd..01699fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [Unreleased] +## [0.25.0] - 2026-10-31 + +### Changed +- Einkaufslisten am Handy: Das Eingabefeld steht nicht mehr dauerhaft über der Liste. „+“ öffnet es als Tafel von unten (wie bei Aufgaben und Triage); nach „Hinzufügen“ bleibt sie offen und leer für den nächsten Eintrag. Am Desktop bleibt das Feld wie bisher. +- Notizen am Handy: „+“ (und „Neue Notiz“ im Menü) fragt zuerst Name und Emoji ab; erst „Erstellen“ legt die Notiz an. Ein versehentlicher Tipp hinterlässt so keine leere Notiz mehr. Am Desktop entsteht die Notiz wie bisher sofort. + ## [0.24.2] - 2026-10-31 ### Fixed @@ -508,7 +514,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V - Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken. - Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung. -[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.24.2...dev +[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.25.0...dev +[0.25.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.24.2...v0.25.0 [0.24.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.24.1...v0.24.2 [0.24.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.24.0...v0.24.1 [0.24.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.23.0...v0.24.0 diff --git a/README.md b/README.md index 5452472..445c768 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Self-Hosted](https://img.shields.io/badge/100%25-Self--Hosted-2563eb?style=flat-square)](#) [![Docker](https://img.shields.io/badge/Deployment-Single%20Container-2496ed?style=flat-square&logo=docker&logoColor=white)](#-schnellstart) [![SQLite](https://img.shields.io/badge/Daten-SQLite-003b57?style=flat-square&logo=sqlite&logoColor=white)](#-deine-daten-gehören-dir) -[![Version](https://img.shields.io/badge/Version-v0.24.2-16a34a?style=flat-square)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/Version-v0.25.0-16a34a?style=flat-square)](CHANGELOG.md) **MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 110360c..6184624 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.24.2", + "version": "0.25.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.24.2", + "version": "0.25.0", "license": "Unlicense", "dependencies": { "@fontsource-variable/inter": "5.3.0", diff --git a/frontend/package.json b/frontend/package.json index 47ba384..0dbf2b0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.24.2", + "version": "0.25.0", "license": "Unlicense", "type": "module", "scripts": { diff --git a/frontend/src/features/lists/SharedListsView.tsx b/frontend/src/features/lists/SharedListsView.tsx index 1952a39..607817b 100644 --- a/frontend/src/features/lists/SharedListsView.tsx +++ b/frontend/src/features/lists/SharedListsView.tsx @@ -121,6 +121,15 @@ export const SharedListsView: React.FC = ({ onRefreshGloba const [workspaceTags, setWorkspaceTags] = useState([]); const inputRef = useRef(null); + // Phones: the add form lives in a sheet opened by "+" instead of taking room above the list + const [isPhone, setIsPhone] = useState(() => typeof window !== 'undefined' && window.innerWidth < 640); + const [showAddSheet, setShowAddSheet] = useState(false); + useEffect(() => { + const onResize = () => setIsPhone(window.innerWidth < 640); + window.addEventListener('resize', onResize); + return () => window.removeEventListener('resize', onResize); + }, []); + const [draggingItemId, setDraggingItemId] = useState(null); const [dragOverItemTarget, setDragOverItemTarget] = useState<{ itemId: string; position: 'before' | 'after' } | null>(null); const [dragOverCategoryTarget, setDragOverCategoryTarget] = useState(null); @@ -312,6 +321,8 @@ export const SharedListsView: React.FC = ({ onRefreshGloba const handleAddItem = async (e: React.FormEvent) => { e.preventDefault(); + // Keep the field (and the phone keyboard) for the next item: refocus inside the tap + inputRef.current?.focus({ preventScroll: true }); if (!activeList || !newItemText.trim() || isSubmittingItem) return; setIsSubmittingItem(true); triggerHaptic(15); @@ -827,6 +838,10 @@ export const SharedListsView: React.FC = ({ onRefreshGloba }, [openItems]); const focusNewItemInput = () => { + if (window.innerWidth < 640) { + setShowAddSheet(true); + return; + } inputRef.current?.focus({ preventScroll: true }); inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); }; @@ -909,6 +924,99 @@ export const SharedListsView: React.FC = ({ onRefreshGloba ]); }, [lists, activeList, completedItems.length, setGroups, setPrimaryAction]); + // The add form's fields: inline on the desktop, on phones in a sheet that "+" opens (no fixed field + // taking room above the list); the sheet brings its own buttons + const renderAddItemFields = (withButton: boolean) => + activeList ? ( + <> + {/* The item is the main field: a real input like the quantity next to it, with the + inside */} +
+ + { + setNewItemText(e.target.value); + setCursorPosition(e.target.selectionStart); + setShowSuggestions(true); + setSuggestionIndex(0); + }} + onKeyDown={handleInputKeyDown} + onSelect={(e) => { + setCursorPosition(e.currentTarget.selectionStart); + }} + onFocus={(e) => { + setCursorPosition(e.target.selectionStart); + setShowSuggestions(true); + }} + onBlur={() => { + setTimeout(() => setShowSuggestions(false), 200); + }} + placeholder={`Was soll auf „${activeList.title}“? (z. B. Hafermilch #Kühlregal)`} + // 16px on phones: iOS zooms into smaller fields and the layout jumps under the keyboard + className="w-full h-11 sm:h-9 pl-9 pr-3 input-field text-base sm:text-sm" + /> + + {/* Hashtag Autocomplete Suggestions Dropdown */} + {showSuggestions && activeHashtagQuery && matchingSuggestions.length > 0 && ( +
+
+ Hashtags +
+ {matchingSuggestions.map((tag, idx) => { + const isSelected = idx === suggestionIndex; + return ( + + ); + })} +
+ )} +
+
+ setNewItemQty(e.target.value.replace(/[^0-9.,xX]/g, ''))} + placeholder="Menge" + aria-label="Menge (optional)" + className="w-24 sm:w-20 h-11 sm:h-9 px-2.5 input-field text-base sm:text-sm" + /> + {withButton && ( + + )} +
+ + ) : null; + // Connection pill in the same look as the notes' header const livePill = ( = ({ onRefreshGloba {/* Active Shared List View */} {activeList ? (
- {/* Quick-Add Form */} -
- {/* The item is the main field: a real input like the quantity next to it, with the + inside */} -
- - { - setNewItemText(e.target.value); - setCursorPosition(e.target.selectionStart); - setShowSuggestions(true); - setSuggestionIndex(0); - }} - onKeyDown={handleInputKeyDown} - onSelect={(e) => { - setCursorPosition(e.currentTarget.selectionStart); - }} - onFocus={(e) => { - setCursorPosition(e.target.selectionStart); - setShowSuggestions(true); - }} - onBlur={() => { - setTimeout(() => setShowSuggestions(false), 200); - }} - placeholder={`Was soll auf „${activeList.title}“? (z. B. Hafermilch #Kühlregal)`} - // 16px on phones: iOS zooms into smaller fields and the layout jumps under the keyboard - className="w-full h-11 sm:h-9 pl-9 pr-3 input-field text-base sm:text-sm" - /> + {/* Phones: "+" opens the add form as a sheet; it stays open for the next item */} + {isPhone && showAddSheet && ( + } + onClose={() => setShowAddSheet(false)} + onSubmit={handleAddItem} + submitLabel="Hinzufügen" + busy={isSubmittingItem} + > +
{renderAddItemFields(false)}
+
+ )} - {/* Hashtag Autocomplete Suggestions Dropdown */} - {showSuggestions && activeHashtagQuery && matchingSuggestions.length > 0 && ( -
-
- Hashtags -
- {matchingSuggestions.map((tag, idx) => { - const isSelected = idx === suggestionIndex; - return ( - - ); - })} -
- )} -
-
- setNewItemQty(e.target.value.replace(/[^0-9.,xX]/g, ''))} - placeholder="Menge" - aria-label="Menge (optional)" - className="w-24 sm:w-20 h-11 sm:h-9 px-2.5 input-field text-base sm:text-sm" - /> - -
-
+ {/* Quick-Add Form (desktop; on phones it is the sheet above) */} + {!isPhone && ( +
+ {renderAddItemFields(true)} +
+ )} {/* Open Items Grouped by Hashtags / Categories */} {openItems.length === 0 ? ( diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index 84ee67b..8eabc4a 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -562,14 +562,14 @@ export const WorkspaceView: React.FC = ({ }, [currentUser, reloadTrash]); const handleCreateDocument = useCallback( - async (folderPath = '') => { + async (folderPath = '', title = 'Neue Notiz', icon = '📝') => { if (isCreatingRef.current) return; isCreatingRef.current = true; triggerHaptic(10); try { const created = await api.createDocument({ - title: 'Neue Notiz', - icon: '📝', + title: title.trim() || 'Neue Notiz', + icon: icon.trim() || '📝', folderPath, blocks: [ { @@ -602,6 +602,24 @@ export const WorkspaceView: React.FC = ({ [onStatsChange] ); + // Phones: "+" asks for name and emoji first, so a stray tap does not leave an empty note behind. + // The desktop keeps creating at once. + const [newNoteSheet, setNewNoteSheet] = useState<{ folderPath: string } | null>(null); + const [newNoteTitle, setNewNoteTitle] = useState(''); + const [newNoteIcon, setNewNoteIcon] = useState('📝'); + const openNewNote = useCallback( + (folderPath = '') => { + if (window.innerWidth >= 640) { + void handleCreateDocument(folderPath); + return; + } + setNewNoteTitle(''); + setNewNoteIcon('📝'); + setNewNoteSheet({ folderPath }); + }, + [handleCreateDocument] + ); + const handleJumpToDocBlock = useCallback((docId: string, blockId?: string) => { setSelectedDocId(docId); setTargetBlockId(blockId || null); @@ -1142,7 +1160,7 @@ export const WorkspaceView: React.FC = ({ // Register mobile FAB actions and the menu at the top; on phones the menu also switches between // notes, to-dos and hashtags (like the lists in Tasks and Shopping lists), the tab row is hidden there useEffect(() => { - setPrimaryAction(() => handleCreateDocument('')); + setPrimaryAction(() => openNewNote('')); setGroups([ { id: 'workspace-views', @@ -1191,7 +1209,7 @@ export const WorkspaceView: React.FC = ({ label: 'Neue Notiz', icon: , variant: 'accent', - onClick: () => handleCreateDocument(''), + onClick: () => openNewNote(''), }, { id: 'new-folder', @@ -1213,7 +1231,7 @@ export const WorkspaceView: React.FC = ({ setGroups([]); setPrimaryAction(undefined); }; - }, [handleCreateDocument, setGroups, setPrimaryAction, trash.length, openTrash, subView, totalOpenTodos, tagsSummary.length]); + }, [handleCreateDocument, openNewNote, setGroups, setPrimaryAction, trash.length, openTrash, subView, totalOpenTodos, tagsSummary.length]); // Real-time matched blocks and all nested children for the selected hashtag const activeTagGroups = useMemo(() => { @@ -2735,7 +2753,7 @@ export const WorkspaceView: React.FC = ({ )}