diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f830ea..60e78bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [Unreleased] +## [0.63.0] - 2026-11-02 + +### Added +- Kanban am Handy: Jede Spalte füllt den Bildschirm zwischen Kopfleiste und unterer Navigation, ein Wisch bringt genau eine Spalte weiter und rastet ein. Beim Ziehen einer Karte springt die Seite am oberen bzw. unteren Rand zur vorigen bzw. nächsten Spalte, sodass sich Karten in jede Spalte legen lassen. +- Desktop: Triage, Workspace, Aufgaben und Listen behalten beim Wechsel zwischen den Seiten ihre Scrollposition. + +### Changed +- Kanban am Desktop mit mehreren Reihen: Beim ersten Scrollen verschwindet nur die Leiste über dem Board, die erste Reihe wächst dabei mit, bis sie den ganzen Bildschirm füllt. + ## [0.62.0] - 2026-11-02 ### Changed @@ -893,7 +902,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.62.0...dev +[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.63.0...dev +[0.63.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.62.0...v0.63.0 [0.62.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.61.0...v0.62.0 [0.61.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.60.0...v0.61.0 [0.60.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.59.3...v0.60.0 diff --git a/README.md b/README.md index e1cf19f..f549a3e 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.62.0-16a34a?style=flat-square)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/Version-v0.63.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 eac15bc..2cafe65 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.62.0", + "version": "0.63.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.62.0", + "version": "0.63.0", "license": "AGPL-3.0-only", "dependencies": { "@fontsource-variable/inter": "5.3.0", diff --git a/frontend/package.json b/frontend/package.json index d65e2ec..1e9367d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.62.0", + "version": "0.63.0", "license": "AGPL-3.0-only", "type": "module", "scripts": { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 071770e..aa0e614 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -208,10 +208,47 @@ export const App: React.FC = () => { // Desktop: the window does not scroll; this area under the header does (phones keep the window scroll) const scrollAreaRef = useRef(null); - // A new page starts at its top (the desktop scroll area would otherwise keep the old page's position) + // Each page keeps its scroll position when switching pages. The new page is often still loading and + // shorter than before: the position comes back as soon as the content is high enough (or the user + // scrolls first, or after 3 s). Scroll events meanwhile do not count as the page's position. + const scrollPositionsRef = useRef>>({}); useEffect(() => { - scrollAreaRef.current?.scrollTo?.({ top: 0 }); - }, [activeTab]); + const area = scrollAreaRef.current; + if (!area) return; + const target = scrollPositionsRef.current[activeTab] ?? 0; + let restoring = true; + let observer: ResizeObserver | null = null; + const finish = () => { + if (!restoring) return; + restoring = false; + observer?.disconnect(); + window.clearTimeout(timeout); + area.removeEventListener('wheel', finish); + area.removeEventListener('pointerdown', finish); + area.removeEventListener('keydown', finish); + }; + const tryRestore = () => { + area.scrollTop = target; + if (Math.abs(area.scrollTop - target) < 2) finish(); + }; + const timeout = window.setTimeout(finish, 3000); + area.addEventListener('wheel', finish, { passive: true }); + area.addEventListener('pointerdown', finish); + area.addEventListener('keydown', finish); + tryRestore(); + if (restoring && typeof ResizeObserver !== 'undefined' && area.firstElementChild) { + observer = new ResizeObserver(tryRestore); + observer.observe(area.firstElementChild); + } + const onScroll = () => { + if (!restoring) scrollPositionsRef.current[activeTab] = area.scrollTop; + }; + area.addEventListener('scroll', onScroll, { passive: true }); + return () => { + finish(); + area.removeEventListener('scroll', onScroll); + }; + }, [activeTab, user?.id]); const [showAbout, setShowAbout] = useState(false); const [showTriageTrash, setShowTriageTrash] = useState(false); const { update: availableUpdate, dismiss: dismissUpdate } = useUpdateCheck(); @@ -1413,7 +1450,7 @@ export const App: React.FC = () => { /> {/* MOBILE BOTTOM NAVIGATION BAR (5 equal columns -> + FAB is dead-center) */} -