From 83b3f27b3e8c3c4b89001ad875ca8e6ee7033cb8 Mon Sep 17 00:00:00 2001 From: Sandro Date: Sun, 1 Nov 2026 11:08:15 +0100 Subject: [PATCH] feat(voice): microphone in the add-task and add-item rows; phone fixes in the column dialog 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 --- CHANGELOG.md | 11 +++- Doku/ui-patterns.md | 9 +++ README.md | 2 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- frontend/src/components/VoiceInputButton.tsx | 63 +++++++++++++++++++ .../src/features/lists/SharedListsView.tsx | 16 ++++- .../features/triage/ManageColumnsModal.tsx | 58 +++++++++-------- frontend/src/features/triage/TasksView.tsx | 47 ++++++++++---- 9 files changed, 168 insertions(+), 44 deletions(-) create mode 100644 frontend/src/components/VoiceInputButton.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index babee67..725c47e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [Unreleased] +## [0.32.0] - 2026-11-01 + +### Added +- Spracheingabe auch beim Hinzufügen von Aufgaben (am PC in der Eingabezeile über der Aufgabenliste, am Handy im Fenster „Neue Aufgabe anlegen“ hinter dem +-Knopf) und von Listeneinträgen (am PC in der Zeile, am Handy im Erfassen-Fenster): ein Mikrofon direkt im Feld wie beim Quick Dump. Diktierter Text wird an das Vorhandene angehängt. + +### Fixed +- Handy: Beim Anlegen einer Kanban-Spalte waren „Erledigt“ und „Hinzufügen“ rechts abgeschnitten; sie stehen jetzt unter dem Textfeld. In der Spaltenliste zeigt „Erledigt“ am Handy nur den Haken, damit der Mülleimer nicht mehr aus dem Bild rutscht. + ## [0.31.0] - 2026-11-01 ### Added @@ -582,7 +590,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.31.0...dev +[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.32.0...dev +[0.32.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.31.0...v0.32.0 [0.31.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.30.0...v0.31.0 [0.30.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.29.0...v0.30.0 [0.29.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.28.0...v0.29.0 diff --git a/Doku/ui-patterns.md b/Doku/ui-patterns.md index 4ab835c..6eff36d 100644 --- a/Doku/ui-patterns.md +++ b/Doku/ui-patterns.md @@ -72,6 +72,15 @@ Komponente: [OriginBadge.tsx](../frontend/src/components/OriginBadge.tsx) - Details (angelegt / zuletzt geändert, mit Schlüssel und Zeit) per Mouseover und per Tippen (kleines Info-Fenster, am Handy mit vergrößerter Trefferfläche); in der Aufgaben-Detailansicht stehen sie zusätzlich unten (`originDetails`). Ein Tipp auf die Badge öffnet nie den Eintrag. - Größen: `sm` in Aufgabenzeilen, Inbox-Liste und Detailansicht, `xs` auf Kanban-Karten und in Listen, `md` auf den Triage-Karten. +## Mikrofon in Eingabezeilen + +Komponente: [VoiceInputButton.tsx](../frontend/src/components/VoiceInputButton.tsx) (nutzt `useVoiceInput` wie der Quick Dump) + +- Runder Mikrofon-Knopf direkt in der Zeile, rechts im Feld; diktierter Text wird an den Text angehängt, der beim Start im Feld stand. Am Handy 36 px, am PC 28 px. +- Während der Aufnahme `MicOff` in Akzentfarbe, mit eigenem Whisper-Server zusätzlich die Live-Wellen, danach „Wird erkannt…“ mit Spinner. +- Ausgeblendet, wenn die Spracheingabe aus ist (`STT_DISABLED`) oder der Browser sie nicht kann (z. B. Firefox ohne eigenen Whisper-Server). Fehler zeigt das Feld kurz als Platzhalter. +- Eingesetzt: Quick Dump (eigene Umsetzung), Aufgabe hinzufügen (Zeile am PC, „Neue Aufgabe anlegen“ am Handy), Listeneintrag hinzufügen. + ## Ladebildschirm & Statusleiste - `manifest.json`: `name` „MindSift“ (kurz, der Slogan steht in `description`), `background_color` und `theme_color` `#2563eb` – Android baut daraus den Splash-Screen. diff --git a/README.md b/README.md index 60a60c6..5c97672 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Self-Hosted](https://img.shields.io/badge/100%25-Self--Hosted-2563eb?style=flat-square)](#) [![Docker](https://img.shields.io/badge/Deployment-Single%20Container-2496ed?style=flat-square&logo=docker&logoColor=white)](#-schnellstart) [![SQLite](https://img.shields.io/badge/Daten-SQLite-003b57?style=flat-square&logo=sqlite&logoColor=white)](#-deine-daten-gehören-dir) -[![Version](https://img.shields.io/badge/Version-v0.31.0-16a34a?style=flat-square)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/Version-v0.32.0-16a34a?style=flat-square)](CHANGELOG.md) **MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 423961c..5580625 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.31.0", + "version": "0.32.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.31.0", + "version": "0.32.0", "license": "Unlicense", "dependencies": { "@fontsource-variable/inter": "5.3.0", diff --git a/frontend/package.json b/frontend/package.json index 3697a20..3792a8a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.31.0", + "version": "0.32.0", "license": "Unlicense", "type": "module", "scripts": { diff --git a/frontend/src/components/VoiceInputButton.tsx b/frontend/src/components/VoiceInputButton.tsx new file mode 100644 index 0000000..b119b26 --- /dev/null +++ b/frontend/src/components/VoiceInputButton.tsx @@ -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; + 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 = ({ 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 ( + + {listening && voice.mode === 'server' && } + + + ); +}; diff --git a/frontend/src/features/lists/SharedListsView.tsx b/frontend/src/features/lists/SharedListsView.tsx index a90d925..dea2f4a 100644 --- a/frontend/src/features/lists/SharedListsView.tsx +++ b/frontend/src/features/lists/SharedListsView.tsx @@ -17,6 +17,7 @@ import { UnifiedShareModal } from '../../components/UnifiedShareModal'; import { AvatarStack, AvatarPerson } from '../../components/AvatarStack'; import { CountBadge } from '../../components/CountBadge'; import { OriginBadge } from '../../components/OriginBadge'; +import { VoiceInputButton } from '../../components/VoiceInputButton'; import { FormSheet } from '../../components/FormSheet'; import { useMobileActions } from '../../context/MobileActionContext'; @@ -102,6 +103,7 @@ export const SharedListsView: React.FC = ({ onRefreshGloba const [newItemText, setNewItemText] = useState(''); const [newItemQty, setNewItemQty] = useState(''); + const [itemVoiceError, setItemVoiceError] = useState(null); const [isSubmittingItem, setIsSubmittingItem] = useState(false); const [editingQtyItemId, setEditingQtyItemId] = useState(null); const [editingQtyValue, setEditingQtyValue] = useState(''); @@ -955,9 +957,19 @@ export const SharedListsView: React.FC = ({ onRefreshGloba onBlur={() => { setTimeout(() => setShowSuggestions(false), 200); }} - placeholder={`Was soll auf „${activeList.title}“? (z. B. Hafermilch #Kühlregal)`} + placeholder={itemVoiceError || `Was soll auf „${activeList.title}“? (z. B. Hafermilch #Kühlregal)`} // 16px on phones: iOS zooms into smaller fields and the layout jumps under the keyboard - className="w-full h-11 sm:h-9 pl-9 pr-3 input-field text-base sm:text-sm" + className="w-full h-11 sm:h-9 pl-9 pr-11 sm:pr-9 input-field text-base sm:text-sm" + /> + { + setItemVoiceError(message); + window.setTimeout(() => setItemVoiceError(null), 4000); + }} + className="absolute right-1 top-1/2 -translate-y-1/2" /> {/* Hashtag Autocomplete Suggestions Dropdown */} diff --git a/frontend/src/features/triage/ManageColumnsModal.tsx b/frontend/src/features/triage/ManageColumnsModal.tsx index b3a7459..f1ce307 100644 --- a/frontend/src/features/triage/ManageColumnsModal.tsx +++ b/frontend/src/features/triage/ManageColumnsModal.tsx @@ -273,7 +273,7 @@ const ManageColumnsModalContent: React.FC = ({ type="text" value={col.name} onChange={(e) => handleRename(col.id, e.target.value)} - className="flex-1 h-7 px-2 text-xs input-field" + className="flex-1 min-w-0 h-7 px-2 text-xs input-field" placeholder="Spaltenname" /> @@ -281,15 +281,18 @@ const ManageColumnsModalContent: React.FC = ({ - +
+ + +
diff --git a/frontend/src/features/triage/TasksView.tsx b/frontend/src/features/triage/TasksView.tsx index dedfdb4..39d93cf 100644 --- a/frontend/src/features/triage/TasksView.tsx +++ b/frontend/src/features/triage/TasksView.tsx @@ -35,6 +35,7 @@ import { api, triggerHaptic } from '../../lib/api'; import { UnifiedShareModal } from '../../components/UnifiedShareModal'; import { TaskDetailModal } from './TaskDetailModal'; import { OriginBadge } from '../../components/OriginBadge'; +import { VoiceInputButton } from '../../components/VoiceInputButton'; import { ManageColumnsModal, DEFAULT_KANBAN_COLUMNS, columnDotClass } from './ManageColumnsModal'; import { AvatarStack, AvatarPerson } from '../../components/AvatarStack'; import { CountBadge } from '../../components/CountBadge'; @@ -138,6 +139,7 @@ export const TasksView: React.FC = ({ // Quick Task Add state const [quickTitle, setQuickTitle] = useState(''); + const [quickVoiceError, setQuickVoiceError] = useState(null); const [quickDueDate, setQuickDueDate] = useState(''); const quickInputRef = useRef(null); const { setGroups, setPrimaryAction } = useMobileActions(); @@ -145,6 +147,8 @@ export const TasksView: React.FC = ({ // Mobile task creation & filter modal state const [showMobileCreateModal, setShowMobileCreateModal] = useState(false); const [mobileCreateTitle, setMobileCreateTitle] = useState(''); + const [mobileVoiceError, setMobileVoiceError] = useState(null); + const mobileCreateInputRef = useRef(null); const [mobileCreateDueDate, setMobileCreateDueDate] = useState(''); const [mobileCreatePriority, setMobileCreatePriority] = useState('normal'); const [mobileCreateListId, setMobileCreateListId] = useState(''); @@ -1679,13 +1683,21 @@ export const TasksView: React.FC = ({ value={quickTitle} onChange={(e) => setQuickTitle(e.target.value)} placeholder={ - currentSelectedList - ? `Aufgabe zu "${currentSelectedList.title}" hinzufügen…` - : 'Neue Aufgabe anlegen…' + quickVoiceError || + (currentSelectedList ? `Aufgabe zu "${currentSelectedList.title}" hinzufügen…` : 'Neue Aufgabe anlegen…') } className="flex-1 bg-transparent px-2 py-1.5 sm:py-1 text-sm text-main placeholder-faint focus:outline-none min-w-0" />
+ { + setQuickVoiceError(message); + window.setTimeout(() => setQuickVoiceError(null), 4000); + }} + /> = ({
- setMobileCreateTitle(e.target.value)} - placeholder="Was ist zu tun?…" - className="w-full input-field px-3 py-2 text-sm" - /> +
+ setMobileCreateTitle(e.target.value)} + placeholder={mobileVoiceError || 'Was ist zu tun?…'} + className="w-full input-field pl-3 pr-11 py-2 text-sm" + /> + { + setMobileVoiceError(message); + window.setTimeout(() => setMobileVoiceError(null), 4000); + }} + className="absolute right-1 top-1/2 -translate-y-1/2" + /> +
{/* List Selector (if lists exist) */}