Merge dev into main (v0.27.1)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 28s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 28s
This commit is contained in:
commit
e5dd797201
7 files changed
+71
-13
No files matched your search
+7
-1
@@ -6,6 +6,11 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.27.0] - 2026-10-31
|
||||||
|
|
||||||
### Changed
|
### 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.
|
- 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.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.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.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
|
[0.26.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.25.1...v0.26.0
|
||||||
|
|||||||
@@ -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.27.0",
|
"version": "0.27.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.27.0",
|
"version": "0.27.1",
|
||||||
"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.27.0",
|
"version": "0.27.1",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ArrowDown, ArrowUp, IndentDecrease, IndentIncrease } from 'lucide-react
|
|||||||
import { useKeyboardInset } from '../../lib/useKeyboardInset';
|
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
|
// 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 =
|
export const isTouchOnly =
|
||||||
typeof window !== 'undefined' &&
|
typeof window !== 'undefined' &&
|
||||||
!!window.matchMedia?.('(hover: none) and (pointer: coarse)').matches &&
|
!!window.matchMedia?.('(hover: none) and (pointer: coarse)').matches &&
|
||||||
@@ -19,7 +19,7 @@ interface MobileOutlineBarProps {
|
|||||||
|
|
||||||
export const MobileOutlineBar: React.FC<MobileOutlineBarProps> = ({ onOutdent, onIndent, onMoveUp, onMoveDown }) => {
|
export const MobileOutlineBar: React.FC<MobileOutlineBarProps> = ({ onOutdent, onIndent, onMoveUp, onMoveDown }) => {
|
||||||
// iOS keeps fixed elements behind the keyboard: lift the bar by the keyboard's height
|
// 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) => (
|
const button = (label: string, icon: React.ReactNode, action: () => void) => (
|
||||||
<button
|
<button
|
||||||
@@ -36,6 +36,9 @@ export const MobileOutlineBar: React.FC<MobileOutlineBarProps> = ({ onOutdent, o
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Keyboard closed (e.g. back button) while the bullet keeps its focus: no bar over the bottom navigation
|
||||||
|
if (!keyboardOpen) return null;
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
className="fixed left-0 right-0 z-[70] flex items-center justify-center gap-2 px-3 py-1.5 bg-panel/95 backdrop-blur-md border-t border-border"
|
className="fixed left-0 right-0 z-[70] flex items-center justify-center gap-2 px-3 py-1.5 bg-panel/95 backdrop-blur-md border-t border-border"
|
||||||
|
|||||||
@@ -26,10 +26,30 @@ describe('useKeyboardInset', () => {
|
|||||||
const vv = mockVisualViewport(844);
|
const vv = mockVisualViewport(844);
|
||||||
const { result } = renderHook(() => useKeyboardInset());
|
const { result } = renderHook(() => useKeyboardInset());
|
||||||
expect(result.current.inset).toBe(0);
|
expect(result.current.inset).toBe(0);
|
||||||
|
expect(result.current.keyboardOpen).toBe(false);
|
||||||
act(() => vv.open(336));
|
act(() => vv.open(336));
|
||||||
expect(result.current.inset).toBe(336);
|
expect(result.current.inset).toBe(336);
|
||||||
expect(result.current.visibleHeight).toBe(508);
|
expect(result.current.visibleHeight).toBe(508);
|
||||||
|
expect(result.current.keyboardOpen).toBe(true);
|
||||||
act(() => vv.open(0));
|
act(() => vv.open(0));
|
||||||
expect(result.current.inset).toBe(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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,13 +1,42 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
// Space the on-screen keyboard takes at the bottom (0 without keyboard) and the height that stays
|
// Tallest visible height seen at the current width, i.e. the height without keyboard. Tracked from app
|
||||||
// visible. Fixed bottom sheets use it to sit right above the keyboard instead of behind it (iOS keeps
|
// start, so it is known before the first field is focused; a new width (rotation) starts over.
|
||||||
// fixed elements on the layout viewport; on Android the inset is usually 0 because the page shrinks).
|
let fullHeight = 0;
|
||||||
export function useKeyboardInset(): { inset: number; visibleHeight: number } {
|
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 read = () => {
|
||||||
const vv = typeof window !== 'undefined' ? window.visualViewport : null;
|
const vv = typeof window !== 'undefined' ? window.visualViewport : null;
|
||||||
if (!vv) return { inset: 0, visibleHeight: typeof window !== 'undefined' ? window.innerHeight : 0 };
|
if (!vv) return { inset: 0, visibleHeight: typeof window !== 'undefined' ? window.innerHeight : 0, keyboardOpen: true };
|
||||||
return { inset: Math.max(0, Math.round(window.innerHeight - vv.height - vv.offsetTop)), visibleHeight: Math.round(vv.height) };
|
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);
|
const [state, setState] = useState(read);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user