Merge dev into main (v0.60.0)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 29s

This commit is contained in:
sandrodach committed 2026-11-02 19:23:07 +01:00
commit 87411c42f7
8 files changed
+63 -85

No files matched your search

+11 -1
View File
@@ -6,6 +6,15 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
## [Unreleased] ## [Unreleased]
## [0.60.0] - 2026-11-02
### Changed
- Desktop: Das Fenster scrollt nicht mehr als Ganzes. Kopfleiste und Fußzeile stehen fest, nur der Bereich dazwischen scrollt; der Scrollbalken beginnt unter der Kopfleiste. Damit verschiebt sich beim Seitenwechsel nichts mehr, und der Rand neben der Kopfleiste braucht keine Sonderbehandlung.
- Triage am Desktop: Kachel und Knöpfe bleiben stehen, die Warteschlange füllt den restlichen Platz und scrollt in sich.
### Fixed
- Chrome: Die Kopfleiste scrollte seit 0.59.3 beim Scrollen mit aus dem Bild.
## [0.59.3] - 2026-11-02 ## [0.59.3] - 2026-11-02
### Fixed ### Fixed
@@ -869,7 +878,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.59.3...dev [Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.60.0...dev
[0.60.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.59.3...v0.60.0
[0.59.3]: https://git.dsit.me/sandrodach/MindSift/compare/v0.59.2...v0.59.3 [0.59.3]: https://git.dsit.me/sandrodach/MindSift/compare/v0.59.2...v0.59.3
[0.59.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.59.1...v0.59.2 [0.59.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.59.1...v0.59.2
[0.59.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.59.0...v0.59.1 [0.59.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.59.0...v0.59.1
+1 -1
View File
@@ -6,7 +6,7 @@
[![Self-Hosted](https://img.shields.io/badge/100%25-Self--Hosted-2563eb?style=flat-square)](#) [![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) [![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) [![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.59.3-16a34a?style=flat-square)](CHANGELOG.md) [![Version](https://img.shields.io/badge/Version-v0.60.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. **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.
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.59.3", "version": "0.60.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.59.3", "version": "0.60.0",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@fontsource-variable/inter": "5.3.0", "@fontsource-variable/inter": "5.3.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"private": true, "private": true,
"version": "0.59.3", "version": "0.60.0",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"type": "module", "type": "module",
"scripts": { "scripts": {
+33 -20
View File
@@ -206,21 +206,12 @@ export const App: React.FC = () => {
const [showProfileMenu, setShowProfileMenu] = useState(false); const [showProfileMenu, setShowProfileMenu] = useState(false);
const connection = useConnectionState(); const connection = useConnectionState();
// Height of the app header as --app-header-h: the page background paints the header colour up to there, // Desktop: the window does not scroll; this area under the header does (phones keep the window scroll)
// so the scrollbar gutter on the right continues the header instead of showing the page colour const scrollAreaRef = useRef<HTMLDivElement>(null);
const [headerEl, setHeaderEl] = useState<HTMLElement | null>(null); // A new page starts at its top (the desktop scroll area would otherwise keep the old page's position)
useEffect(() => { useEffect(() => {
if (!headerEl || typeof ResizeObserver === 'undefined') return; scrollAreaRef.current?.scrollTo?.({ top: 0 });
const root = document.documentElement; }, [activeTab]);
const update = () => root.style.setProperty('--app-header-h', `${headerEl.offsetHeight}px`);
update();
const observer = new ResizeObserver(update);
observer.observe(headerEl);
return () => {
observer.disconnect();
root.style.removeProperty('--app-header-h');
};
}, [headerEl]);
const [showAbout, setShowAbout] = useState(false); const [showAbout, setShowAbout] = useState(false);
const [showTriageTrash, setShowTriageTrash] = useState(false); const [showTriageTrash, setShowTriageTrash] = useState(false);
const { update: availableUpdate, dismiss: dismissUpdate } = useUpdateCheck(); const { update: availableUpdate, dismiss: dismissUpdate } = useUpdateCheck();
@@ -848,7 +839,7 @@ export const App: React.FC = () => {
if (isPhoneWidth()) { if (isPhoneWidth()) {
openMobileTriageCapture(); openMobileTriageCapture();
} else { } else {
window.scrollTo({ top: 0, behavior: 'smooth' }); scrollAreaRef.current?.scrollTo?.({ top: 0, behavior: 'smooth' });
setCaptureFocusCount((c) => c + 1); setCaptureFocusCount((c) => c + 1);
} }
}; };
@@ -920,13 +911,12 @@ export const App: React.FC = () => {
return ( return (
<div <div
className={`bg-canvas text-main flex flex-col font-sans selection:bg-accent/30 ${ className={`bg-canvas text-main flex flex-col font-sans selection:bg-accent/30 sm:h-[100dvh] sm:overflow-hidden ${
activeTab === 'workspace' ? 'h-[100dvh] overflow-hidden' : 'min-h-[100dvh]' activeTab === 'workspace' ? 'h-[100dvh] overflow-hidden' : 'min-h-[100dvh]'
}`} }`}
> >
{/* Top Header Bar: Floating on mobile, solid panel on desktop */} {/* Top Header Bar: Floating on mobile, solid panel on desktop */}
<header <header
ref={setHeaderEl}
className="sticky top-0 z-30 bg-canvas/85 backdrop-blur-md sm:bg-panel sm:border-b sm:border-border sm:shadow-sm" className="sticky top-0 z-30 bg-canvas/85 backdrop-blur-md sm:bg-panel sm:border-b sm:border-border sm:shadow-sm"
> >
{/* Mobile Header: Floating Bar with Logo Action Dropdown Left & Profile Avatar Right */} {/* Mobile Header: Floating Bar with Logo Action Dropdown Left & Profile Avatar Right */}
@@ -1306,14 +1296,36 @@ export const App: React.FC = () => {
)} )}
{/* Main Container */} {/* Main Container */}
{/* The workspace fills the window exactly and scrolls only inside the note, never the page */} {/* Desktop: the window never scrolls, only this area under the header (room for the scrollbar kept on
both sides, so the content stays centred under the header and nothing shifts between pages). The
workspace fills it exactly and scrolls only inside the note. Phones scroll the window as before. */}
<div
ref={scrollAreaRef}
data-app-scroll
className={`flex-1 flex flex-col min-h-0 sm:[scrollbar-gutter:stable_both-edges] ${
activeTab === 'workspace' ? 'sm:overflow-hidden' : 'sm:overflow-y-auto'
}`}
>
<main <main
className={`flex-1 max-w-6xl w-full mx-auto px-3 sm:px-4 pt-3 pb-24 ${ className={`flex-1 max-w-6xl w-full mx-auto px-3 sm:px-4 pt-3 pb-24 ${
activeTab === 'workspace' ? 'min-h-0 flex flex-col sm:pb-0' : 'sm:pb-6' activeTab === 'workspace'
? 'min-h-0 flex flex-col sm:pb-0'
: activeTab === 'triage'
? 'sm:min-h-0 sm:flex sm:flex-col sm:pb-6'
: 'sm:pb-6'
}`} }`}
> >
<ViewErrorBoundary onReset={() => setActiveTab('triage')}> <ViewErrorBoundary onReset={() => setActiveTab('triage')}>
<section key={dataEpoch} className={activeTab === 'workspace' ? 'flex-1 min-h-0 flex flex-col' : undefined}> <section
key={dataEpoch}
className={
activeTab === 'workspace'
? 'flex-1 min-h-0 flex flex-col'
: activeTab === 'triage'
? 'sm:flex-1 sm:min-h-0 sm:flex sm:flex-col' // the queue fills the rest of the height (TriageDeck)
: undefined
}
>
{activeTab === 'triage' && ( {activeTab === 'triage' && (
<TriageDeck <TriageDeck
deck={deck} deck={deck}
@@ -1393,6 +1405,7 @@ export const App: React.FC = () => {
</section> </section>
</ViewErrorBoundary> </ViewErrorBoundary>
</main> </main>
</div>
<AppFooter <AppFooter
legal={authStatus.legal} legal={authStatus.legal}
+8 -4
View File
@@ -554,11 +554,15 @@ export const TasksView: React.FC<TasksViewProps> = ({
setKanbanColHeight(null); setKanbanColHeight(null);
return; return;
} }
// From the board's top to the window's bottom; then take back what still overflows (footer, paddings) // From the board's top to the bottom of the app's scroll area (the desktop window itself does not
const top = el.getBoundingClientRect().top + window.scrollY; // scroll); then take back what still overflows (paddings)
setKanbanColHeight(Math.max(320, window.innerHeight - top - 16)); const area = el.closest<HTMLElement>('[data-app-scroll]');
const areaTop = area ? area.getBoundingClientRect().top - area.scrollTop : -window.scrollY;
const areaHeight = area ? area.clientHeight : window.innerHeight;
const top = el.getBoundingClientRect().top - areaTop;
setKanbanColHeight(Math.max(320, areaHeight - top - 16));
requestAnimationFrame(() => { requestAnimationFrame(() => {
const over = document.documentElement.scrollHeight - window.innerHeight; const over = area ? area.scrollHeight - area.clientHeight : document.documentElement.scrollHeight - window.innerHeight;
if (over > 0) setKanbanColHeight((h) => (h ? Math.max(320, h - over) : h)); if (over > 0) setKanbanColHeight((h) => (h ? Math.max(320, h - over) : h));
}); });
}; };
+7 -5
View File
@@ -488,7 +488,9 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
} }
return ( return (
<div className="max-w-2xl mx-auto select-none space-y-4"> // Desktop: a column filling the height under the header; card and buttons keep their size, the queue
// takes the rest and scrolls inside (the page only scrolls when the window is too low even for that)
<div className="w-full max-w-2xl mx-auto select-none space-y-4 sm:flex-1 sm:min-h-0 sm:flex sm:flex-col">
{/* Top Deck Status & Undo Bar */} {/* Top Deck Status & Undo Bar */}
<div className="flex items-center justify-between px-1"> <div className="flex items-center justify-between px-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -931,7 +933,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
{/* Waiting Quick Dump Queue List (Touch-sized & Swipeable Left/Right) */} {/* Waiting Quick Dump Queue List (Touch-sized & Swipeable Left/Right) */}
{cards.length > 1 && ( {cards.length > 1 && (
<div className="pt-2" ref={queueRef}> <div className="pt-2 sm:flex-1 sm:min-h-[9rem] sm:flex sm:flex-col" ref={queueRef}>
{/* Phones: the queue stays folded so the card and its buttons fit on the screen */} {/* Phones: the queue stays folded so the card and its buttons fit on the screen */}
<button <button
type="button" type="button"
@@ -955,11 +957,11 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
</button> </button>
{/* Folds open/closed on phones; always open from sm upwards */} {/* Folds open/closed on phones; always open from sm upwards */}
<div <div
className={`grid transition-[grid-template-rows,opacity,visibility] duration-300 ease-out sm:grid-rows-[1fr] sm:opacity-100 sm:visible ${ className={`grid transition-[grid-template-rows,opacity,visibility] duration-300 ease-out sm:flex-1 sm:min-h-0 sm:grid-rows-[1fr] sm:opacity-100 sm:visible ${
showQueueMobile ? 'grid-rows-[1fr] opacity-100 visible' : 'grid-rows-[0fr] opacity-0 invisible' showQueueMobile ? 'grid-rows-[1fr] opacity-100 visible' : 'grid-rows-[0fr] opacity-0 invisible'
}`} }`}
> >
<div className="overflow-hidden min-h-0"> <div className="overflow-hidden min-h-0 sm:flex sm:flex-col">
<div className="flex items-center justify-between mb-2 px-1 pt-2 sm:pt-0"> <div className="flex items-center justify-between mb-2 px-1 pt-2 sm:pt-0">
<span className="text-xs font-semibold uppercase tracking-wider text-muted flex items-center gap-1.5"> <span className="text-xs font-semibold uppercase tracking-wider text-muted flex items-center gap-1.5">
<Layers className="w-3.5 h-3.5 text-accent" /> <Layers className="w-3.5 h-3.5 text-accent" />
@@ -970,7 +972,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
</span> </span>
</div> </div>
<div className="panel-surface divide-y divide-border max-h-80 overflow-y-auto overflow-x-hidden"> <div className="panel-surface divide-y divide-border max-h-80 sm:max-h-none sm:flex-1 sm:min-h-0 overflow-y-auto overflow-x-hidden">
{cards.slice(1).map((item) => ( {cards.slice(1).map((item) => (
<SwipeableQueueItem <SwipeableQueueItem
key={item.id} key={item.id}
-51
View File
@@ -29,45 +29,6 @@
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
/* Room for the scrollbar is always kept: pages that scroll (e.g. a wide Kanban board) and pages that do
not keep the same width, so nothing shifts when switching tabs. Overlay scrollbars (phones) are not
affected. */
html {
scrollbar-gutter: stable;
}
/* Chrome paints an empty gutter (page without scrollbar) plain white, ignoring the page background. There
the scrollbar is always shown instead: without anything to scroll it is an empty track, coloured like
the header and page by body::-webkit-scrollbar below. Firefox keeps the gutter, which it paints with the
html background. */
@supports selector(::-webkit-scrollbar) {
html {
overflow-y: scroll;
}
}
/* Header colour down to the header's height (--app-header-h, set by App), then the page colour. The header's
bottom line is its own layer on top, since the theme gives it as a translucent colour. Defined on body:
Chrome resolves variables for the page scrollbar from there (html sees it through inheritance too). */
:root,
body {
--header-gutter-bg: linear-gradient(
to bottom,
transparent calc(var(--app-header-h, 0px) - 1px),
var(--border-color) calc(var(--app-header-h, 0px) - 1px),
var(--border-color) var(--app-header-h, 0px),
transparent var(--app-header-h, 0px)
),
linear-gradient(to bottom, var(--bg-panel) var(--app-header-h, 0px), var(--bg-app) var(--app-header-h, 0px));
}
/* The gutter shows the page background; on the desktop it carries the header colour (and its bottom
line) down to the header's height (--app-header-h, set by App), fixed like the sticky header, so the
header seems to run to the window edge */
@media (min-width: 768px) {
html {
background: var(--header-gutter-bg);
background-attachment: fixed;
}
}
body { body {
background-color: var(--bg-app); background-color: var(--bg-app);
color: var(--text-main); color: var(--text-main);
@@ -86,18 +47,6 @@ body {
background: transparent; background: transparent;
} }
/* Chrome takes the page scrollbar's style from body (html:: does not apply) and paints white behind a
transparent track, where Firefox shows the html background: the page scrollbar gets the same
background as html above, on the desktop with the header colour on top */
body::-webkit-scrollbar {
background: var(--bg-app);
}
@media (min-width: 768px) {
body::-webkit-scrollbar {
background: var(--header-gutter-bg);
}
}
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb); background: var(--scrollbar-thumb);
border-radius: var(--radius-btn); border-radius: var(--radius-btn);