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) */}