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
+121 -68

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]
## [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
### 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.
- 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.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
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mindsift-frontend",
"version": "0.11.0",
"version": "0.11.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mindsift-frontend",
"version": "0.11.0",
"version": "0.11.1",
"license": "Unlicense",
"dependencies": {
"@fontsource-variable/inter": "5.3.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "mindsift-frontend",
"private": true,
"version": "0.11.0",
"version": "0.11.1",
"license": "Unlicense",
"type": "module",
"scripts": {
+111 -64
View File
@@ -18,6 +18,9 @@ import {
Edit3,
Layers,
MoreHorizontal,
ArrowLeft,
ArrowRight,
ChevronDown,
} from 'lucide-react';
import { SharedList, Thought } from '../../lib/types';
import { addDays, formatDue } from '../../lib/due';
@@ -227,6 +230,9 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
const [cards, setCards] = useState<Thought[]>(deck);
const [lastTriaged, setLastTriaged] = useState<Thought | null>(null);
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 [showListSelector, setShowListSelector] = useState(false);
@@ -725,70 +731,96 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
</div>
)}
{/* Mobile Quick Action Buttons under the card (Touch targets >= 44px) */}
<div className="grid grid-cols-4 gap-1.5 sm:hidden">
<button
type="button"
onClick={() => handleAction('discard')}
title="Wegwerfen"
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" />
<span>Wegwerfen</span>
</button>
{/* Phones: the swipe actions on top (← discard, → task), below a 2×2 block like the long-press sheet */}
<div className="sm:hidden space-y-2">
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => handleAction('discard')}
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"
>
<ArrowLeft className="w-4 h-4" />
<span>Wegwerfen</span>
</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>
<button
type="button"
disabled={writableLists.length === 0}
onClick={() => {
if (singleWritableList) {
handleAction('keep', {
convertTo: 'list_item',
targetListId: singleWritableList.id,
});
return;
}
setShowDateModal(false);
setShowListSelector((prev) => !prev);
}}
title={
writableLists.length === 0
? '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" />
<span className="truncate max-w-full">{singleWritableList ? singleWritableList.title : 'Liste'}</span>
</button>
<div className="grid grid-cols-2 gap-2">
<button
type="button"
disabled={writableLists.length === 0}
onClick={() => {
if (singleWritableList) {
handleAction('keep', { convertTo: 'list_item', targetListId: singleWritableList.id });
return;
}
setShowDateModal(false);
setShowListSelector((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 ${
writableLists.length === 0 ? 'opacity-40 cursor-not-allowed' : showListSelector ? 'border-accent bg-accent-dim' : ''
}`}
>
<ShoppingCart className="w-5 h-5 text-accent flex-shrink-0" />
<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={() => handleAction('keep', { convertTo: 'document' })}
title="In Notiz umwandeln"
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" />
<span>Notiz</span>
</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
type="button"
onClick={() => handleAction('keep', { convertTo: 'task' })}
title="Als Aufgabe übernehmen"
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"
>
<CheckSquare className="w-4 h-4 flex-shrink-0" />
<span>Aufgabe</span>
</button>
<button
type="button"
onClick={() => handleAction('keep', { convertTo: 'document' })}
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"
>
<FileText className="w-5 h-5 text-accent flex-shrink-0" />
<div>
<div className="text-sm font-semibold text-main">Als Notiz</div>
<div className="text-[11px] text-muted">Im Workspace</div>
</div>
</button>
<button
type="button"
disabled={cards.length <= 1}
onClick={() => handleSkipCard()}
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' : ''}`}
>
<SkipForward className="w-5 h-5 text-muted flex-shrink-0" />
<div>
<div className="text-sm font-semibold text-main">Später</div>
<div className="text-[11px] text-muted">Ans Ende schieben</div>
</div>
</button>
</div>
</div>
{/* Primary Triage Decision Bar: Hidden on mobile (handled via Mobile Quick Actions / Swipe), shown on Desktop */}
@@ -881,8 +913,23 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
{/* Waiting Quick Dump Queue List (Touch-sized & Swipeable Left/Right) */}
{cards.length > 1 && (
<div className="pt-2">
<div className="flex items-center justify-between mb-2 px-1">
<div className="pt-2" ref={queueRef}>
{/* 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">
<Layers className="w-3.5 h-3.5 text-accent" />
Warteschlange ({cards.length - 1})
@@ -892,7 +939,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
</span>
</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) => (
<SwipeableQueueItem
key={item.id}