Merge dev into main (v0.60.0)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 29s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 29s
This commit is contained in:
commit
87411c42f7
8 files changed
+63
-85
No files matched your search
+11
-1
@@ -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
|
||||||
|
|||||||
@@ -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.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,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
@@ -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}
|
||||||
|
|||||||
@@ -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));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in new issue
Block a user