Merge dev into main (v0.32.0)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 27s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 27s
This commit is contained in:
commit
4a792ce480
9 files changed
+143
-19
No files matched your search
+10
-1
@@ -6,6 +6,14 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.31.0] - 2026-11-01
|
||||||
|
|
||||||
### Added
|
### 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.
|
- 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.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.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.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
|
[0.29.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.28.0...v0.29.0
|
||||||
|
|||||||
@@ -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.
|
- 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.
|
- 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
|
## Ladebildschirm & Statusleiste
|
||||||
|
|
||||||
- `manifest.json`: `name` „MindSift“ (kurz, der Slogan steht in `description`), `background_color` und `theme_color` `#2563eb` – Android baut daraus den Splash-Screen.
|
- `manifest.json`: `name` „MindSift“ (kurz, der Slogan steht in `description`), `background_color` und `theme_color` `#2563eb` – Android baut daraus den Splash-Screen.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
[](#)
|
[](#)
|
||||||
[](#-schnellstart)
|
[](#-schnellstart)
|
||||||
[](#-deine-daten-gehören-dir)
|
[](#-deine-daten-gehören-dir)
|
||||||
[](CHANGELOG.md)
|
[](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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.31.0",
|
"version": "0.32.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.31.0",
|
"version": "0.32.0",
|
||||||
"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.31.0",
|
"version": "0.32.0",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -17,6 +17,7 @@ import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
|||||||
import { AvatarStack, AvatarPerson } from '../../components/AvatarStack';
|
import { AvatarStack, AvatarPerson } from '../../components/AvatarStack';
|
||||||
import { CountBadge } from '../../components/CountBadge';
|
import { CountBadge } from '../../components/CountBadge';
|
||||||
import { OriginBadge } from '../../components/OriginBadge';
|
import { OriginBadge } from '../../components/OriginBadge';
|
||||||
|
import { VoiceInputButton } from '../../components/VoiceInputButton';
|
||||||
import { FormSheet } from '../../components/FormSheet';
|
import { FormSheet } from '../../components/FormSheet';
|
||||||
import { useMobileActions } from '../../context/MobileActionContext';
|
import { useMobileActions } from '../../context/MobileActionContext';
|
||||||
|
|
||||||
@@ -102,6 +103,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
|
|
||||||
const [newItemText, setNewItemText] = useState('');
|
const [newItemText, setNewItemText] = useState('');
|
||||||
const [newItemQty, setNewItemQty] = useState('');
|
const [newItemQty, setNewItemQty] = useState('');
|
||||||
|
const [itemVoiceError, setItemVoiceError] = useState<string | null>(null);
|
||||||
const [isSubmittingItem, setIsSubmittingItem] = useState(false);
|
const [isSubmittingItem, setIsSubmittingItem] = useState(false);
|
||||||
const [editingQtyItemId, setEditingQtyItemId] = useState<string | null>(null);
|
const [editingQtyItemId, setEditingQtyItemId] = useState<string | null>(null);
|
||||||
const [editingQtyValue, setEditingQtyValue] = useState('');
|
const [editingQtyValue, setEditingQtyValue] = useState('');
|
||||||
@@ -955,9 +957,19 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
setTimeout(() => setShowSuggestions(false), 200);
|
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
|
// 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"
|
||||||
|
/>
|
||||||
|
<VoiceInputButton
|
||||||
|
value={newItemText}
|
||||||
|
onChange={setNewItemText}
|
||||||
|
focusRef={inputRef}
|
||||||
|
onError={(message) => {
|
||||||
|
setItemVoiceError(message);
|
||||||
|
window.setTimeout(() => setItemVoiceError(null), 4000);
|
||||||
|
}}
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Hashtag Autocomplete Suggestions Dropdown */}
|
{/* Hashtag Autocomplete Suggestions Dropdown */}
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ const ManageColumnsModalContent: React.FC<ManageColumnsModalProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
value={col.name}
|
value={col.name}
|
||||||
onChange={(e) => handleRename(col.id, e.target.value)}
|
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"
|
placeholder="Spaltenname"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -281,15 +281,18 @@ const ManageColumnsModalContent: React.FC<ManageColumnsModalProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleToggleDone(col.id)}
|
onClick={() => handleToggleDone(col.id)}
|
||||||
className={`h-7 px-2 text-[10px] rounded-btn border transition-colors flex items-center gap-1 ${
|
className={`h-7 px-2 text-[10px] rounded-btn border transition-colors flex items-center gap-1 flex-shrink-0 ${
|
||||||
col.isDone
|
col.isDone
|
||||||
? 'bg-status-ok/15 border-status-ok text-status-ok font-semibold'
|
? 'bg-status-ok/15 border-status-ok text-status-ok font-semibold'
|
||||||
: 'bg-panel border-border text-faint hover:text-muted'
|
: 'bg-panel border-border text-faint hover:text-muted'
|
||||||
}`}
|
}`}
|
||||||
title={col.isDone ? 'Gilt als Erledigt' : 'Gilt als Offen'}
|
title={col.isDone ? 'Gilt als Erledigt' : 'Gilt als Offen'}
|
||||||
|
aria-label={col.isDone ? 'Gilt als Erledigt' : 'Gilt als Offen'}
|
||||||
|
aria-pressed={!!col.isDone}
|
||||||
>
|
>
|
||||||
<Check className={`w-2.5 h-2.5 ${col.isDone ? 'stroke-[3]' : 'opacity-40'}`} />
|
<Check className={`w-2.5 h-2.5 ${col.isDone ? 'stroke-[3]' : 'opacity-40'}`} />
|
||||||
<span>Erledigt</span>
|
{/* Phones: only the tick, so the row keeps room for the bin */}
|
||||||
|
<span className="hidden sm:inline">Erledigt</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -310,18 +313,20 @@ const ManageColumnsModalContent: React.FC<ManageColumnsModalProps> = ({
|
|||||||
{/* Add New Column Form */}
|
{/* Add New Column Form */}
|
||||||
<form onSubmit={handleAddColumn} className="space-y-2 pt-2 border-t border-border">
|
<form onSubmit={handleAddColumn} className="space-y-2 pt-2 border-t border-border">
|
||||||
<label className="block text-xs font-semibold text-main">Neue Spalte hinzufügen:</label>
|
<label className="block text-xs font-semibold text-main">Neue Spalte hinzufügen:</label>
|
||||||
<div className="flex items-center gap-1.5">
|
{/* Phones: the buttons go below the field, so nothing is cut off */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center gap-1.5">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newColName}
|
value={newColName}
|
||||||
onChange={(e) => setNewColName(e.target.value)}
|
onChange={(e) => setNewColName(e.target.value)}
|
||||||
placeholder="z.B. Wartet auf Feedback, Review…"
|
placeholder="z.B. Wartet auf Feedback, Review…"
|
||||||
className="flex-1 h-8 px-2.5 text-xs input-field"
|
className="w-full sm:w-auto sm:flex-1 min-w-0 h-11 sm:h-8 px-2.5 text-base sm:text-xs input-field"
|
||||||
/>
|
/>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setNewColIsDone((prev) => !prev)}
|
onClick={() => setNewColIsDone((prev) => !prev)}
|
||||||
className={`h-8 px-2.5 text-xs rounded-btn border transition-colors flex items-center gap-1 ${
|
className={`flex-1 sm:flex-none justify-center h-11 sm:h-8 px-2.5 text-xs rounded-btn border transition-colors flex items-center gap-1 ${
|
||||||
newColIsDone
|
newColIsDone
|
||||||
? 'bg-status-ok/15 border-status-ok text-status-ok font-semibold'
|
? 'bg-status-ok/15 border-status-ok text-status-ok font-semibold'
|
||||||
: 'bg-panel border-border text-muted hover:text-main'
|
: 'bg-panel border-border text-muted hover:text-main'
|
||||||
@@ -334,12 +339,13 @@ const ManageColumnsModalContent: React.FC<ManageColumnsModalProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!newColName.trim()}
|
disabled={!newColName.trim()}
|
||||||
className="btn-primary h-8 px-3 text-xs flex items-center gap-1 disabled:opacity-40"
|
className="flex-1 sm:flex-none justify-center btn-primary h-11 sm:h-8 px-3 text-xs flex items-center gap-1 disabled:opacity-40"
|
||||||
>
|
>
|
||||||
<Plus className="w-3.5 h-3.5" />
|
<Plus className="w-3.5 h-3.5" />
|
||||||
<span>Hinzufügen</span>
|
<span>Hinzufügen</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Reset button */}
|
{/* Reset button */}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { api, triggerHaptic } from '../../lib/api';
|
|||||||
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
||||||
import { TaskDetailModal } from './TaskDetailModal';
|
import { TaskDetailModal } from './TaskDetailModal';
|
||||||
import { OriginBadge } from '../../components/OriginBadge';
|
import { OriginBadge } from '../../components/OriginBadge';
|
||||||
|
import { VoiceInputButton } from '../../components/VoiceInputButton';
|
||||||
import { ManageColumnsModal, DEFAULT_KANBAN_COLUMNS, columnDotClass } from './ManageColumnsModal';
|
import { ManageColumnsModal, DEFAULT_KANBAN_COLUMNS, columnDotClass } from './ManageColumnsModal';
|
||||||
import { AvatarStack, AvatarPerson } from '../../components/AvatarStack';
|
import { AvatarStack, AvatarPerson } from '../../components/AvatarStack';
|
||||||
import { CountBadge } from '../../components/CountBadge';
|
import { CountBadge } from '../../components/CountBadge';
|
||||||
@@ -138,6 +139,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
|
|
||||||
// Quick Task Add state
|
// Quick Task Add state
|
||||||
const [quickTitle, setQuickTitle] = useState('');
|
const [quickTitle, setQuickTitle] = useState('');
|
||||||
|
const [quickVoiceError, setQuickVoiceError] = useState<string | null>(null);
|
||||||
const [quickDueDate, setQuickDueDate] = useState('');
|
const [quickDueDate, setQuickDueDate] = useState('');
|
||||||
const quickInputRef = useRef<HTMLInputElement | null>(null);
|
const quickInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const { setGroups, setPrimaryAction } = useMobileActions();
|
const { setGroups, setPrimaryAction } = useMobileActions();
|
||||||
@@ -145,6 +147,8 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
// Mobile task creation & filter modal state
|
// Mobile task creation & filter modal state
|
||||||
const [showMobileCreateModal, setShowMobileCreateModal] = useState(false);
|
const [showMobileCreateModal, setShowMobileCreateModal] = useState(false);
|
||||||
const [mobileCreateTitle, setMobileCreateTitle] = useState('');
|
const [mobileCreateTitle, setMobileCreateTitle] = useState('');
|
||||||
|
const [mobileVoiceError, setMobileVoiceError] = useState<string | null>(null);
|
||||||
|
const mobileCreateInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [mobileCreateDueDate, setMobileCreateDueDate] = useState('');
|
const [mobileCreateDueDate, setMobileCreateDueDate] = useState('');
|
||||||
const [mobileCreatePriority, setMobileCreatePriority] = useState<PriorityLevel>('normal');
|
const [mobileCreatePriority, setMobileCreatePriority] = useState<PriorityLevel>('normal');
|
||||||
const [mobileCreateListId, setMobileCreateListId] = useState<string>('');
|
const [mobileCreateListId, setMobileCreateListId] = useState<string>('');
|
||||||
@@ -1679,13 +1683,21 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
value={quickTitle}
|
value={quickTitle}
|
||||||
onChange={(e) => setQuickTitle(e.target.value)}
|
onChange={(e) => setQuickTitle(e.target.value)}
|
||||||
placeholder={
|
placeholder={
|
||||||
currentSelectedList
|
quickVoiceError ||
|
||||||
? `Aufgabe zu "${currentSelectedList.title}" hinzufügen…`
|
(currentSelectedList ? `Aufgabe zu "${currentSelectedList.title}" hinzufügen…` : 'Neue Aufgabe anlegen…')
|
||||||
: '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"
|
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"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
|
<VoiceInputButton
|
||||||
|
value={quickTitle}
|
||||||
|
onChange={setQuickTitle}
|
||||||
|
focusRef={quickInputRef}
|
||||||
|
onError={(message) => {
|
||||||
|
setQuickVoiceError(message);
|
||||||
|
window.setTimeout(() => setQuickVoiceError(null), 4000);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={quickDueDate}
|
value={quickDueDate}
|
||||||
@@ -1879,14 +1891,27 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
<form onSubmit={handleMobileCreateTask} className="space-y-3">
|
<form onSubmit={handleMobileCreateTask} className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[11px] font-semibold text-muted block mb-1">Aufgabentext</label>
|
<label className="text-[11px] font-semibold text-muted block mb-1">Aufgabentext</label>
|
||||||
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
|
ref={mobileCreateInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
autoFocus
|
autoFocus
|
||||||
value={mobileCreateTitle}
|
value={mobileCreateTitle}
|
||||||
onChange={(e) => setMobileCreateTitle(e.target.value)}
|
onChange={(e) => setMobileCreateTitle(e.target.value)}
|
||||||
placeholder="Was ist zu tun?…"
|
placeholder={mobileVoiceError || 'Was ist zu tun?…'}
|
||||||
className="w-full input-field px-3 py-2 text-sm"
|
className="w-full input-field pl-3 pr-11 py-2 text-sm"
|
||||||
/>
|
/>
|
||||||
|
<VoiceInputButton
|
||||||
|
value={mobileCreateTitle}
|
||||||
|
onChange={setMobileCreateTitle}
|
||||||
|
focusRef={mobileCreateInputRef}
|
||||||
|
onError={(message) => {
|
||||||
|
setMobileVoiceError(message);
|
||||||
|
window.setTimeout(() => setMobileVoiceError(null), 4000);
|
||||||
|
}}
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* List Selector (if lists exist) */}
|
{/* List Selector (if lists exist) */}
|
||||||
|
|||||||
Reference in new issue
Block a user