feat(voice): microphone in the add-task and add-item rows; phone fixes in the column dialog
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m38s

VoiceInputButton brings the Quick Dump microphone into other input rows (useVoiceInput, appends
to the text already there, hidden when voice input is off or unsupported): the add-task row above
the task list and the add-item field of lists (desktop row and phone sheet).

Column dialog on phones: the add-column buttons go below the field, and the Erledigt toggle of a
column shows only its tick, so the bin stays on screen. Docs and changelog; v0.32.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-01 11:17:49 +01:00
1 parent 08081e6c1d
commit 83b3f27b3e
9 files changed
+168 -44

No files matched your search

@@ -0,0 +1,63 @@
import React, { useRef } from 'react';
import { Loader2, Mic, MicOff } from 'lucide-react';
import { useVoiceInput } from '../lib/useVoiceInput';
import { triggerHaptic } from '../lib/api';
import { VoiceWave } from './VoiceWave';
interface VoiceInputButtonProps {
// The field's text; dictated words are appended to what was there when the recording started
value: string;
onChange: (text: string) => void;
onError?: (message: string) => void;
// Field to focus once the text is final
focusRef?: React.RefObject<HTMLInputElement | HTMLTextAreaElement>;
className?: string;
}
// Microphone inside an input row, like the one in Quick Dump (Doku/ui-patterns.md). Hidden when voice
// input is off or the browser cannot do it (e.g. Firefox without an own Whisper server).
export const VoiceInputButton: React.FC<VoiceInputButtonProps> = ({ value, onChange, onError, focusRef, className = '' }) => {
const baseRef = useRef('');
const voice = useVoiceInput({
onText: (text, final) => {
const base = baseRef.current;
if (text) onChange(base ? `${base} ${text}` : text);
if (final) focusRef?.current?.focus();
},
onError,
});
if (!voice.available) return null;
const listening = voice.state === 'listening';
const transcribing = voice.state === 'transcribing';
return (
<span className={`inline-flex items-center gap-1 flex-shrink-0 ${className}`}>
{listening && voice.mode === 'server' && <VoiceWave getLevel={voice.level} className="h-3.5 text-accent" />}
<button
type="button"
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
if (voice.state === 'idle') {
baseRef.current = value.trim();
triggerHaptic(20);
}
voice.toggle();
}}
disabled={transcribing}
title={listening ? 'Aufnahme beenden' : transcribing ? 'Wird erkannt…' : 'Spracheingabe'}
aria-label={listening ? 'Aufnahme beenden' : transcribing ? 'Wird erkannt…' : 'Spracheingabe'}
className={`w-9 h-9 sm:w-7 sm:h-7 rounded-full flex items-center justify-center transition-colors ${
listening || transcribing ? 'bg-accent-dim text-accent' : 'text-muted hover:text-main hover:bg-panel-elevated'
}`}
>
{transcribing ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : listening ? (
<MicOff className="w-3.5 h-3.5" />
) : (
<Mic className="w-3.5 h-3.5" />
)}
</button>
</span>
);
};