diff --git a/CHANGELOG.md b/CHANGELOG.md index 06af852..68990fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [Unreleased] +## [0.27.1] - 2026-10-31 + +### Fixed +- Notizen am Handy: Die Leiste zum Ein- und Ausrücken über der Tastatur blieb stehen, wenn man die Tastatur schloss (z. B. mit der Zurück-Taste), und verdeckte die untere Navigation. Sie erscheint jetzt nur, solange die Tastatur offen ist; ein Tipp in den Punkt holt beides zurück. + ## [0.27.0] - 2026-10-31 ### Changed @@ -539,7 +544,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.27.0...dev +[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.27.1...dev +[0.27.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.27.0...v0.27.1 [0.27.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.26.1...v0.27.0 [0.26.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.26.0...v0.26.1 [0.26.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.25.1...v0.26.0 diff --git a/README.md b/README.md index 38ca075..171401a 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.27.0-16a34a?style=flat-square)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/Version-v0.27.1-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 33a1dda..361e31c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.27.0", + "version": "0.27.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.27.0", + "version": "0.27.1", "license": "Unlicense", "dependencies": { "@fontsource-variable/inter": "5.3.0", diff --git a/frontend/package.json b/frontend/package.json index 507c144..d1720e6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.27.0", + "version": "0.27.1", "license": "Unlicense", "type": "module", "scripts": { diff --git a/frontend/src/features/workspace/MobileOutlineBar.tsx b/frontend/src/features/workspace/MobileOutlineBar.tsx index ba13ccc..7bf43b8 100644 --- a/frontend/src/features/workspace/MobileOutlineBar.tsx +++ b/frontend/src/features/workspace/MobileOutlineBar.tsx @@ -4,7 +4,7 @@ import { ArrowDown, ArrowUp, IndentDecrease, IndentIncrease } from 'lucide-react import { useKeyboardInset } from '../../lib/useKeyboardInset'; // Phones and tablets have no Tab key: while a bullet is being edited, a slim bar sits right above the -// on-screen keyboard with outdent / indent and move up / down (the bullet takes its children along). +// on-screen keyboard (only while it is open) with outdent / indent and move up / down (the bullet takes its children along). export const isTouchOnly = typeof window !== 'undefined' && !!window.matchMedia?.('(hover: none) and (pointer: coarse)').matches && @@ -19,7 +19,7 @@ interface MobileOutlineBarProps { export const MobileOutlineBar: React.FC = ({ onOutdent, onIndent, onMoveUp, onMoveDown }) => { // iOS keeps fixed elements behind the keyboard: lift the bar by the keyboard's height - const { inset } = useKeyboardInset(); + const { inset, keyboardOpen } = useKeyboardInset(); const button = (label: string, icon: React.ReactNode, action: () => void) => ( ); + // Keyboard closed (e.g. back button) while the bullet keeps its focus: no bar over the bottom navigation + if (!keyboardOpen) return null; + return createPortal(
{ const vv = mockVisualViewport(844); const { result } = renderHook(() => useKeyboardInset()); expect(result.current.inset).toBe(0); + expect(result.current.keyboardOpen).toBe(false); act(() => vv.open(336)); expect(result.current.inset).toBe(336); expect(result.current.visibleHeight).toBe(508); + expect(result.current.keyboardOpen).toBe(true); act(() => vv.open(0)); expect(result.current.inset).toBe(0); + expect(result.current.keyboardOpen).toBe(false); + }); + + it('notices the keyboard also when the page shrinks instead (no inset)', () => { + const vv = mockVisualViewport(844); + const { result } = renderHook(() => useKeyboardInset()); + act(() => { + Object.defineProperty(window, 'innerHeight', { value: 508, configurable: true }); + vv.open(336); + }); + expect(result.current.inset).toBe(0); + expect(result.current.keyboardOpen).toBe(true); + // Browser bars appearing are not a keyboard + act(() => { + Object.defineProperty(window, 'innerHeight', { value: 788, configurable: true }); + vv.open(56); + }); + expect(result.current.keyboardOpen).toBe(false); }); }); diff --git a/frontend/src/lib/useKeyboardInset.ts b/frontend/src/lib/useKeyboardInset.ts index 074f5f6..ae3eeba 100644 --- a/frontend/src/lib/useKeyboardInset.ts +++ b/frontend/src/lib/useKeyboardInset.ts @@ -1,13 +1,42 @@ import { useEffect, useState } from 'react'; -// Space the on-screen keyboard takes at the bottom (0 without keyboard) and the height that stays -// visible. Fixed bottom sheets use it to sit right above the keyboard instead of behind it (iOS keeps -// fixed elements on the layout viewport; on Android the inset is usually 0 because the page shrinks). -export function useKeyboardInset(): { inset: number; visibleHeight: number } { +// Tallest visible height seen at the current width, i.e. the height without keyboard. Tracked from app +// start, so it is known before the first field is focused; a new width (rotation) starts over. +let fullHeight = 0; +let fullWidth = 0; +function noteFullHeight() { + const vv = window.visualViewport; + if (!vv) return; + if (window.innerWidth !== fullWidth) { + fullWidth = window.innerWidth; + fullHeight = 0; + } + fullHeight = Math.max(fullHeight, Math.round(vv.height)); +} +if (typeof window !== 'undefined' && window.visualViewport) { + noteFullHeight(); + window.visualViewport.addEventListener('resize', noteFullHeight); +} + +// More than browser bars appearing or disappearing (about 60 px) +const KEYBOARD_MIN_PX = 150; + +// Space the on-screen keyboard takes at the bottom (0 without keyboard), the height that stays visible +// and whether a keyboard is open. Fixed bottom sheets use the inset to sit right above the keyboard +// instead of behind it (iOS keeps fixed elements on the layout viewport; on Android the inset is usually +// 0 because the page shrinks). keyboardOpen compares with the height without keyboard, so it works in +// both cases; without visualViewport it is unknown and reported as open. +export function useKeyboardInset(): { inset: number; visibleHeight: number; keyboardOpen: boolean } { const read = () => { const vv = typeof window !== 'undefined' ? window.visualViewport : null; - if (!vv) return { inset: 0, visibleHeight: typeof window !== 'undefined' ? window.innerHeight : 0 }; - return { inset: Math.max(0, Math.round(window.innerHeight - vv.height - vv.offsetTop)), visibleHeight: Math.round(vv.height) }; + if (!vv) return { inset: 0, visibleHeight: typeof window !== 'undefined' ? window.innerHeight : 0, keyboardOpen: true }; + noteFullHeight(); + const visibleHeight = Math.round(vv.height); + return { + inset: Math.max(0, Math.round(window.innerHeight - vv.height - vv.offsetTop)), + visibleHeight, + keyboardOpen: visibleHeight < fullHeight - KEYBOARD_MIN_PX, + }; }; const [state, setState] = useState(read);