feat(triage): bigger phone actions like the long-press sheet, folded queue
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m22s

chore(release): v0.11.1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-30 22:42:01 +01:00
1 parent 9fb33b23ec
commit 3b9b1a5f8e
4 files changed
+93 -40

No files matched your search

+7 -1
View File
@@ -6,6 +6,11 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
## [Unreleased] ## [Unreleased]
## [0.11.1] - 2026-10-30
### Changed
- Triage am Handy: unter der Karte zwei große Knöpfe „← Wegwerfen“ und „Aufgabe →“ (wie Wischen), darunter ein Viererblock wie im Gedrückthalten-Menü: Auf Liste, Mit Termin, Als Notiz, Später. Die Warteschlange ist am Handy zugeklappt; „Warteschlange anzeigen“ klappt sie auf und scrollt hin.
## [0.11.0] - 2026-10-30 ## [0.11.0] - 2026-10-30
### Added ### Added
@@ -285,7 +290,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. - 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. - 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.11.0...dev [Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.11.1...dev
[0.11.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.11.0...v0.11.1
[0.11.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.10.0...v0.11.0 [0.11.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.10.0...v0.11.0
[0.10.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.9.0...v0.10.0 [0.10.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.9.0...v0.10.0
[0.9.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.8.0...v0.9.0 [0.9.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.8.0...v0.9.0
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.11.0", "version": "0.11.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.11.0", "version": "0.11.1",
"license": "Unlicense", "license": "Unlicense",
"dependencies": { "dependencies": {
"@fontsource-variable/inter": "5.3.0", "@fontsource-variable/inter": "5.3.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"private": true, "private": true,
"version": "0.11.0", "version": "0.11.1",
"license": "Unlicense", "license": "Unlicense",
"type": "module", "type": "module",
"scripts": { "scripts": {
+83 -36
View File
@@ -18,6 +18,9 @@ import {
Edit3, Edit3,
Layers, Layers,
MoreHorizontal, MoreHorizontal,
ArrowLeft,
ArrowRight,
ChevronDown,
} from 'lucide-react'; } from 'lucide-react';
import { SharedList, Thought } from '../../lib/types'; import { SharedList, Thought } from '../../lib/types';
import { addDays, formatDue } from '../../lib/due'; import { addDays, formatDue } from '../../lib/due';
@@ -227,6 +230,9 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
const [cards, setCards] = useState<Thought[]>(deck); const [cards, setCards] = useState<Thought[]>(deck);
const [lastTriaged, setLastTriaged] = useState<Thought | null>(null); const [lastTriaged, setLastTriaged] = useState<Thought | null>(null);
const [showDateModal, setShowDateModal] = useState(false); const [showDateModal, setShowDateModal] = useState(false);
// Phones: the waiting queue is folded until asked for
const [showQueueMobile, setShowQueueMobile] = useState(false);
const queueRef = useRef<HTMLDivElement | null>(null);
const [taskDueDate, setTaskDueDate] = useState(() => addDays(1)); const [taskDueDate, setTaskDueDate] = useState(() => addDays(1));
const [showListSelector, setShowListSelector] = useState(false); const [showListSelector, setShowListSelector] = useState(false);
@@ -725,71 +731,97 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
</div> </div>
)} )}
{/* Mobile Quick Action Buttons under the card (Touch targets >= 44px) */} {/* Phones: the swipe actions on top (← discard, → task), below a 2×2 block like the long-press sheet */}
<div className="grid grid-cols-4 gap-1.5 sm:hidden"> <div className="sm:hidden space-y-2">
<div className="grid grid-cols-2 gap-2">
<button <button
type="button" type="button"
onClick={() => handleAction('discard')} onClick={() => handleAction('discard')}
title="Wegwerfen" className="min-h-[52px] px-3 py-2 rounded-xl bg-status-err-bg border border-status-err/30 text-status-err text-sm font-semibold flex items-center justify-center gap-2 touch-manipulation cursor-pointer active:scale-98"
className="btn-action h-11 px-1 flex flex-col items-center justify-center gap-0.5 text-[11px] whitespace-nowrap touch-manipulation cursor-pointer"
> >
<X className="w-4 h-4 text-muted flex-shrink-0" /> <ArrowLeft className="w-4 h-4" />
<span>Wegwerfen</span> <span>Wegwerfen</span>
</button> </button>
<button
type="button"
onClick={() => handleAction('keep', { convertTo: 'task' })}
className="min-h-[52px] px-3 py-2 rounded-xl bg-accent text-on-accent text-sm font-semibold flex items-center justify-center gap-2 touch-manipulation cursor-pointer active:scale-98"
>
<span>Aufgabe</span>
<ArrowRight className="w-4 h-4" />
</button>
</div>
<div className="grid grid-cols-2 gap-2">
<button <button
type="button" type="button"
disabled={writableLists.length === 0} disabled={writableLists.length === 0}
onClick={() => { onClick={() => {
if (singleWritableList) { if (singleWritableList) {
handleAction('keep', { handleAction('keep', { convertTo: 'list_item', targetListId: singleWritableList.id });
convertTo: 'list_item',
targetListId: singleWritableList.id,
});
return; return;
} }
setShowDateModal(false); setShowDateModal(false);
setShowListSelector((prev) => !prev); setShowListSelector((prev) => !prev);
}} }}
title={ className={`min-h-[52px] px-3.5 py-2.5 rounded-xl bg-canvas hover:bg-panel-elevated border border-border text-left flex items-center gap-3 transition-colors cursor-pointer active:scale-98 touch-manipulation ${
writableLists.length === 0 writableLists.length === 0 ? 'opacity-40 cursor-not-allowed' : showListSelector ? 'border-accent bg-accent-dim' : ''
? 'Keine Liste vorhanden'
: singleWritableList
? `Auf ${singleWritableList.title}`
: 'Auf eine geteilte Liste setzen'
}
className={`btn-action h-11 px-1 flex flex-col items-center justify-center gap-0.5 text-[11px] whitespace-nowrap touch-manipulation cursor-pointer ${
writableLists.length === 0
? 'opacity-40 cursor-not-allowed'
: showListSelector
? 'border-accent text-accent bg-accent-dim'
: ''
}`} }`}
> >
<ShoppingCart className="w-4 h-4 text-accent flex-shrink-0" /> <ShoppingCart className="w-5 h-5 text-accent flex-shrink-0" />
<span className="truncate max-w-full">{singleWritableList ? singleWritableList.title : 'Liste'}</span> <div className="min-w-0">
<div className="text-sm font-semibold text-main">Auf Liste</div>
<div className="text-[11px] text-muted truncate">
{writableLists.length === 0
? 'Keine Liste vorhanden'
: singleWritableList
? singleWritableList.title
: 'Einkauf / Geteilt'}
</div>
</div>
</button>
<button
type="button"
onClick={() => {
setShowListSelector(false);
setShowDateModal((prev) => !prev);
}}
className={`min-h-[52px] px-3.5 py-2.5 rounded-xl bg-canvas hover:bg-panel-elevated border border-border text-left flex items-center gap-3 transition-colors cursor-pointer active:scale-98 touch-manipulation ${showDateModal ? 'border-accent bg-accent-dim' : ''}`}
>
<Calendar className="w-5 h-5 text-accent flex-shrink-0" />
<div>
<div className="text-sm font-semibold text-main">Mit Termin</div>
<div className="text-[11px] text-muted">Datum wählen</div>
</div>
</button> </button>
<button <button
type="button" type="button"
onClick={() => handleAction('keep', { convertTo: 'document' })} onClick={() => handleAction('keep', { convertTo: 'document' })}
title="In Notiz umwandeln" className="min-h-[52px] px-3.5 py-2.5 rounded-xl bg-canvas hover:bg-panel-elevated border border-border text-left flex items-center gap-3 transition-colors cursor-pointer active:scale-98 touch-manipulation"
className="btn-action h-11 px-1 flex flex-col items-center justify-center gap-0.5 text-[11px] whitespace-nowrap touch-manipulation cursor-pointer"
> >
<FileText className="w-4 h-4 text-accent flex-shrink-0" /> <FileText className="w-5 h-5 text-accent flex-shrink-0" />
<span>Notiz</span> <div>
<div className="text-sm font-semibold text-main">Als Notiz</div>
<div className="text-[11px] text-muted">Im Workspace</div>
</div>
</button> </button>
<button <button
type="button" type="button"
onClick={() => handleAction('keep', { convertTo: 'task' })} disabled={cards.length <= 1}
title="Als Aufgabe übernehmen" onClick={() => handleSkipCard()}
className="btn-primary h-11 px-1 flex flex-col items-center justify-center gap-0.5 text-[11px] font-semibold whitespace-nowrap touch-manipulation cursor-pointer" className={`min-h-[52px] px-3.5 py-2.5 rounded-xl bg-canvas hover:bg-panel-elevated border border-border text-left flex items-center gap-3 transition-colors cursor-pointer active:scale-98 touch-manipulation ${cards.length <= 1 ? 'opacity-40 cursor-not-allowed' : ''}`}
> >
<CheckSquare className="w-4 h-4 flex-shrink-0" /> <SkipForward className="w-5 h-5 text-muted flex-shrink-0" />
<span>Aufgabe</span> <div>
<div className="text-sm font-semibold text-main">Später</div>
<div className="text-[11px] text-muted">Ans Ende schieben</div>
</div>
</button> </button>
</div> </div>
</div>
{/* Primary Triage Decision Bar: Hidden on mobile (handled via Mobile Quick Actions / Swipe), shown on Desktop */} {/* Primary Triage Decision Bar: Hidden on mobile (handled via Mobile Quick Actions / Swipe), shown on Desktop */}
<div className="hidden sm:flex items-center gap-1.5"> <div className="hidden sm:flex items-center gap-1.5">
@@ -881,8 +913,23 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
{/* Waiting Quick Dump Queue List (Touch-sized & Swipeable Left/Right) */} {/* Waiting Quick Dump Queue List (Touch-sized & Swipeable Left/Right) */}
{cards.length > 1 && ( {cards.length > 1 && (
<div className="pt-2"> <div className="pt-2" ref={queueRef}>
<div className="flex items-center justify-between mb-2 px-1"> {/* Phones: the queue stays folded so the card and its buttons fit on the screen */}
{!showQueueMobile && (
<button
type="button"
onClick={() => {
setShowQueueMobile(true);
window.setTimeout(() => queueRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
}}
className="sm:hidden w-full min-h-[48px] px-3 rounded-xl border border-border bg-panel text-sm font-medium text-main flex items-center justify-center gap-2 touch-manipulation cursor-pointer"
>
<Layers className="w-4 h-4 text-accent" />
<span>Warteschlange anzeigen ({cards.length - 1})</span>
<ChevronDown className="w-4 h-4 text-muted" />
</button>
)}
<div className={`${showQueueMobile ? 'flex' : 'hidden sm:flex'} items-center justify-between mb-2 px-1`}>
<span className="text-xs font-semibold uppercase tracking-wider text-muted flex items-center gap-1.5"> <span className="text-xs font-semibold uppercase tracking-wider text-muted flex items-center gap-1.5">
<Layers className="w-3.5 h-3.5 text-accent" /> <Layers className="w-3.5 h-3.5 text-accent" />
Warteschlange ({cards.length - 1}) Warteschlange ({cards.length - 1})
@@ -892,7 +939,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
</span> </span>
</div> </div>
<div className="panel-surface divide-y divide-border max-h-80 overflow-y-auto overflow-x-hidden"> <div className={`${showQueueMobile ? 'block' : 'hidden sm:block'} panel-surface divide-y divide-border max-h-80 overflow-y-auto overflow-x-hidden`}>
{cards.slice(1).map((item) => ( {cards.slice(1).map((item) => (
<SwipeableQueueItem <SwipeableQueueItem
key={item.id} key={item.id}