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
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:
1 parent
9fb33b23ec
commit
3b9b1a5f8e
4 files changed
+121
-68
No files matched your search
+7
-1
@@ -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
|
||||||
|
|||||||
Generated
+2
-2
@@ -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,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": {
|
||||||
|
|||||||
@@ -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,70 +731,96 @@ 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">
|
||||||
<button
|
<div className="grid grid-cols-2 gap-2">
|
||||||
type="button"
|
<button
|
||||||
onClick={() => handleAction('discard')}
|
type="button"
|
||||||
title="Wegwerfen"
|
onClick={() => handleAction('discard')}
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
<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>
|
||||||
|
|
||||||
<button
|
<div className="grid grid-cols-2 gap-2">
|
||||||
type="button"
|
<button
|
||||||
disabled={writableLists.length === 0}
|
type="button"
|
||||||
onClick={() => {
|
disabled={writableLists.length === 0}
|
||||||
if (singleWritableList) {
|
onClick={() => {
|
||||||
handleAction('keep', {
|
if (singleWritableList) {
|
||||||
convertTo: 'list_item',
|
handleAction('keep', { convertTo: 'list_item', targetListId: singleWritableList.id });
|
||||||
targetListId: singleWritableList.id,
|
return;
|
||||||
});
|
}
|
||||||
return;
|
setShowDateModal(false);
|
||||||
}
|
setShowListSelector((prev) => !prev);
|
||||||
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' : ''
|
||||||
title={
|
}`}
|
||||||
writableLists.length === 0
|
>
|
||||||
? 'Keine Liste vorhanden'
|
<ShoppingCart className="w-5 h-5 text-accent flex-shrink-0" />
|
||||||
: singleWritableList
|
<div className="min-w-0">
|
||||||
? `Auf ${singleWritableList.title}`
|
<div className="text-sm font-semibold text-main">Auf Liste</div>
|
||||||
: 'Auf eine geteilte Liste setzen'
|
<div className="text-[11px] text-muted truncate">
|
||||||
}
|
{writableLists.length === 0
|
||||||
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 ${
|
? 'Keine Liste vorhanden'
|
||||||
writableLists.length === 0
|
: singleWritableList
|
||||||
? 'opacity-40 cursor-not-allowed'
|
? singleWritableList.title
|
||||||
: showListSelector
|
: 'Einkauf / Geteilt'}
|
||||||
? 'border-accent text-accent bg-accent-dim'
|
</div>
|
||||||
: ''
|
</div>
|
||||||
}`}
|
</button>
|
||||||
>
|
|
||||||
<ShoppingCart className="w-4 h-4 text-accent flex-shrink-0" />
|
|
||||||
<span className="truncate max-w-full">{singleWritableList ? singleWritableList.title : 'Liste'}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleAction('keep', { convertTo: 'document' })}
|
onClick={() => {
|
||||||
title="In Notiz umwandeln"
|
setShowListSelector(false);
|
||||||
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"
|
setShowDateModal((prev) => !prev);
|
||||||
>
|
}}
|
||||||
<FileText className="w-4 h-4 text-accent flex-shrink-0" />
|
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' : ''}`}
|
||||||
<span>Notiz</span>
|
>
|
||||||
</button>
|
<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
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleAction('keep', { convertTo: 'task' })}
|
onClick={() => handleAction('keep', { convertTo: 'document' })}
|
||||||
title="Als Aufgabe übernehmen"
|
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-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"
|
>
|
||||||
>
|
<FileText className="w-5 h-5 text-accent flex-shrink-0" />
|
||||||
<CheckSquare className="w-4 h-4 flex-shrink-0" />
|
<div>
|
||||||
<span>Aufgabe</span>
|
<div className="text-sm font-semibold text-main">Als Notiz</div>
|
||||||
</button>
|
<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>
|
</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 */}
|
||||||
@@ -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}
|
||||||
|
|||||||
Reference in new issue
Block a user