feat(ui): update instant capture, count badges, mobile triage and folder modal

This commit is contained in:
sandrodach committed 2026-10-30 19:27:15 +01:00
1 parent fd72500d17
commit 1753208cba
6 files changed
+119 -15

No files matched your search

@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState, useMemo } from 'react';
import {
Mic,
MicOff,
ArrowUp,
ArrowDown,
Sparkles,
Calendar,
Check,
@@ -73,6 +73,7 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
});
const isListening = voice.state === 'listening';
const isTranscribing = voice.state === 'transcribing';
const [isOverMaxHeight, setIsOverMaxHeight] = useState(false);
const adjustHeight = (el?: HTMLTextAreaElement | null) => {
const targetEl = el || inputRef.current;
@@ -82,8 +83,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
const scrollH = targetEl.scrollHeight;
targetEl.style.height = `${Math.min(scrollH, 140)}px`;
setIsMultiline(targetEl.value.includes('\n') || scrollH > 28);
setIsOverMaxHeight(scrollH > 140);
} else {
setIsMultiline(false);
setIsOverMaxHeight(false);
}
};
@@ -267,16 +270,18 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
: isTranscribing
? 'Wird erkannt…'
: hideTargetPicker || fixedTarget === 'inbox'
? 'Quick Dump erfassen… (Enter zum Speichern)'
? 'Quick Dump erfassen…'
: target === 'task'
? 'Neue Aufgabe direkt anlegen… (Enter / Shift+Enter für Mehrzeiler)'
? 'Aufgabe erfassen…'
: target === 'list_item'
? `Eintrag direkt zu "${selectedList?.title || 'Liste'}" hinzufügen…`
? `Zu „${selectedList?.title || 'Liste'}“ hinzufügen…`
: target === 'document'
? 'Neue Workspace-Notiz direkt anlegen… (Enter / Shift+Enter für Mehrzeiler)'
: 'Quick Dump: Gedanke reinrotzen… (C / Enter, Shift+Enter für Mehrzeiler)'
? 'Notiz erfassen…'
: 'Quick Dump…'
}
className="w-full bg-transparent text-main placeholder-faint text-xs sm:text-sm focus:outline-none resize-none leading-5 py-0.5 overflow-y-auto"
className={`w-full bg-transparent text-main placeholder-faint text-xs sm:text-sm focus:outline-none resize-none leading-5 py-0.5 ${
isOverMaxHeight ? 'overflow-y-auto' : 'overflow-hidden'
}`}
style={{ minHeight: '22px', maxHeight: '140px' }}
autoComplete="off"
/>
@@ -360,7 +365,7 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
</>
) : (
<>
<ArrowUp className="w-3 h-3" />
<ArrowDown className="w-3 h-3" />
<span className="inline">
{hideTargetPicker || (fixedTarget || target) === 'inbox'
? 'Quick Dump'
+67 -1
View File
@@ -725,7 +725,73 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
</div>
)}
{/* Primary Triage Decision Bar: Hidden on mobile (handled via Swipe + Long-Press), shown on Desktop */}
{/* 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>
<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>
<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={() => 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>
</div>
{/* 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">
<button
onClick={() => handleAction('discard')}
@@ -160,4 +160,26 @@ describe('TriageDeck single-list shortcut', () => {
expect(api.triageAction).not.toHaveBeenCalled();
expect(onDeckChange).not.toHaveBeenCalled();
});
it('renders mobile quick action buttons and triggers discard', async () => {
const onDeckChange = vi.fn();
render(
<TriageDeck
deck={sampleDeck}
lists={[ownList]}
onDeckChange={onDeckChange}
onOpenDocument={vi.fn()}
onFocusCapture={vi.fn()}
/>
);
const [mobileDiscard] = screen.getAllByRole('button', { name: /^Wegwerfen$/i });
fireEvent.click(mobileDiscard);
await waitFor(() => {
expect(api.triageAction).toHaveBeenCalledWith('th_1', 'discard', expect.objectContaining({}));
expect(onDeckChange).toHaveBeenCalledTimes(1);
});
});
});