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

This commit is contained in:
sandrodach committed 2026-11-03 06:50:13 +01:00
commit cdc4e6db62
6 files changed
+156 -27

No files matched your search

+13 -1
View File
@@ -6,6 +6,17 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
## [Unreleased] ## [Unreleased]
## [0.67.0] - 2026-11-03
### Added
- Notizen (Desktop): Scrollen im Hintergrund (über der Leiste mit Notizen, To-Dos und Hashtags oder am Rand neben der Notiz) schiebt diese Leiste oben heraus und rastet ein, sodass die Notiz den ganzen Bereich nutzt – wie beim Kanban. Hochscrollen dort holt sie zurück. In der Notiz und in der Ordnerleiste scrollt das Mausrad wie bisher nur deren Inhalt.
- Fokus-Modus: Die Ordnerleiste links und die Leiste oben haben einen Pin. Angepinnt bleiben sie stehen, statt wieder wegzugleiten; die Einstellung bleibt erhalten.
### Changed
- Fokus-Modus: Die obere Leiste wirft nur noch einen leichten Schatten.
- Notizen: Die Rückfrage beim Löschen eines Ordners ist kürzer.
- Kanban in „Alle Aufgaben“: Bei Spalten, die es nur in Aufgabenlisten gibt, ist „Aufgabe hinzufügen“ jetzt ausgegraut sichtbar statt einfach zu fehlen. Ein Hinweis beim Drüberfahren nennt die Listen, in denen man dort Aufgaben anlegen kann.
## [0.66.0] - 2026-11-02 ## [0.66.0] - 2026-11-02
### Added ### Added
@@ -936,7 +947,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.66.0...dev [Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.67.0...dev
[0.67.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.66.0...v0.67.0
[0.66.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.65.0...v0.66.0 [0.66.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.65.0...v0.66.0
[0.65.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.64.1...v0.65.0 [0.65.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.64.1...v0.65.0
[0.64.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.64.0...v0.64.1 [0.64.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.64.0...v0.64.1
+2 -2
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.66.0-16a34a?style=flat-square)](CHANGELOG.md) [![Version](https://img.shields.io/badge/Version-v0.67.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.
@@ -68,7 +68,7 @@ Die meisten Notiz- und Aufgaben-Apps verlangen Struktur im falschen Moment: Wenn
### 📝 Workspace: Outliner-Notizen ### 📝 Workspace: Outliner-Notizen
* Jeder Gedanke ein Bullet: `Enter`, `Tab`/`Shift+Tab`, Ein-/Ausklappen, Drag & Drop ganzer Äste; am Handy eine Leiste über der Tastatur zum Ein-/Ausrücken und Verschieben. Text lässt sich mit der Maus über mehrere Punkte markieren und wie in einem Texteditor löschen, ersetzen oder kopieren. * Jeder Gedanke ein Bullet: `Enter`, `Tab`/`Shift+Tab`, Ein-/Ausklappen, Drag & Drop ganzer Äste; am Handy eine Leiste über der Tastatur zum Ein-/Ausrücken und Verschieben. Text lässt sich mit der Maus über mehrere Punkte markieren und wie in einem Texteditor löschen, ersetzen oder kopieren.
* **Zoom-Fokus** per Klick auf den Bullet, Breadcrumbs zum Zurücknavigieren; **Fokus-Modus** (`Strg+Umschalt+F`) zeigt nur die Notiz in Lesebreite, Ordner gleiten bei Bedarf von links herein. * **Zoom-Fokus** per Klick auf den Bullet, Breadcrumbs zum Zurücknavigieren; **Fokus-Modus** (`Strg+Umschalt+F`) zeigt nur die Notiz in Lesebreite, Ordner gleiten bei Bedarf von links herein, Ordnerleiste und obere Leiste lassen sich anpinnen. Am Desktop schiebt Scrollen im Hintergrund (neben der Notiz) die Leiste mit Notizen, To-Dos und Hashtags nach oben heraus.
* **Journal:** eine Notiz pro Tag, alle Tage untereinander (heute oben, ältere laden beim Scrollen nach), mit Kalender zum Springen; einzelne Tage lassen sich teilen. Pro Gerät abschaltbar. * **Journal:** eine Notiz pro Tag, alle Tage untereinander (heute oben, ältere laden beim Scrollen nach), mit Kalender zum Springen; einzelne Tage lassen sich teilen. Pro Gerät abschaltbar.
* Blocktypen und Slash-Befehle: `/todo`, `/number` (nummerierte Liste: 1., a., i.), `/link`, `/h1`–`/h3`, `/code` (mit Syntax-Hervorhebung), `/callout`, `/divider`, `/extract`, `/image`, `/audio`, `/diktat`. * Blocktypen und Slash-Befehle: `/todo`, `/number` (nummerierte Liste: 1., a., i.), `/link`, `/h1`–`/h3`, `/code` (mit Syntax-Hervorhebung), `/callout`, `/divider`, `/extract`, `/image`, `/audio`, `/diktat`.
* **Bilder & Audio in Notizen:** `/image` (Datei, Kamera, Zwischenablage oder Link), `/audio` (Sprachnotiz oder Datei-Upload) und `/diktat` (Live-Transkription). * **Bilder & Audio in Notizen:** `/image` (Datei, Kamera, Zwischenablage oder Link), `/audio` (Sprachnotiz oder Datei-Upload) und `/diktat` (Live-Transkription).
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.66.0", "version": "0.67.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.66.0", "version": "0.67.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.66.0", "version": "0.67.0",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"type": "module", "type": "module",
"scripts": { "scripts": {
+16 -4
View File
@@ -3138,13 +3138,25 @@ export const TasksView: React.FC<TasksViewProps> = ({
)} )}
</div> </div>
{/* Quick Add Task to Column */} {/* Quick Add Task to Column. In "Alle Aufgaben" a new task has no list: a column only lists
{!isSelectedListReadOnly && activeColumns.some((c) => boardIdOf(c) === col.id) && ( have shows the button greyed out, with a hint where the column is */}
<div className="p-2 border-t border-border bg-panel"> {!isSelectedListReadOnly && (
<div
className="p-2 border-t border-border bg-panel"
title={
activeColumns.some((c) => boardIdOf(c) === col.id)
? undefined
: `Neue Aufgaben hier landen ohne Liste, und dort gibt es diese Spalte nicht. Zum Anlegen die Liste öffnen: ${safeTaskLists
.filter((l) => (Array.isArray(l.columns) ? l.columns : []).some((c) => boardIdOf(c) === col.id))
.map((l) => `„${l.title}“`)
.join(', ')}`
}
>
<button <button
type="button" type="button"
disabled={!activeColumns.some((c) => boardIdOf(c) === col.id)}
onClick={() => handleQuickAddTaskToColumn(col.id)} onClick={() => handleQuickAddTaskToColumn(col.id)}
className="w-full h-7 px-2 text-xs border border-dashed border-border rounded-btn text-muted hover:text-main hover:bg-canvas flex items-center justify-center gap-1 transition-colors" className="w-full h-7 px-2 text-xs border border-dashed border-border rounded-btn text-muted hover:text-main hover:bg-canvas flex items-center justify-center gap-1 transition-colors disabled:opacity-40 disabled:pointer-events-none"
> >
<Plus className="w-3 h-3" /> <Plus className="w-3 h-3" />
<span>Aufgabe hinzufügen</span> <span>Aufgabe hinzufügen</span>
+122 -17
View File
@@ -25,6 +25,7 @@ import {
Loader2, Loader2,
CalendarDays, CalendarDays,
Minimize2, Minimize2,
PinOff,
} from 'lucide-react'; } from 'lucide-react';
import { import {
Document, Document,
@@ -474,6 +475,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
// Focus mode: the folder sidebar slides in from the left (button, title click or mouse at the edge) // Focus mode: the folder sidebar slides in from the left (button, title click or mouse at the edge)
const [zenSidebar, setZenSidebar] = useState<'closed' | 'click' | 'hover'>('closed'); const [zenSidebar, setZenSidebar] = useState<'closed' | 'click' | 'hover'>('closed');
const [zenActionsTarget, setZenActionsTarget] = useState<HTMLDivElement | null>(null); const [zenActionsTarget, setZenActionsTarget] = useState<HTMLDivElement | null>(null);
// Focus mode: header and sidebar can be pinned so they stay instead of sliding back (kept across sessions)
const [zenPinTop, setZenPinTop] = useState(() => localStorage.getItem('mindsift_zen_pin_top') === '1');
const [zenPinSide, setZenPinSide] = useState(() => localStorage.getItem('mindsift_zen_pin_side') === '1');
useEffect(() => {
localStorage.setItem('mindsift_zen_pin_top', zenPinTop ? '1' : '0');
localStorage.setItem('mindsift_zen_pin_side', zenPinSide ? '1' : '0');
}, [zenPinTop, zenPinSide]);
const zenSideOpen = zenPinSide || zenSidebar !== 'closed';
// Drag and Drop state // Drag and Drop state
@@ -1232,6 +1241,49 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
return () => window.removeEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey);
}, [canFocus, focusMode, zenSidebar]); }, [canFocus, focusMode, zenSidebar]);
// Desktop: a wheel step down over the background (the tab row, the margins beside and below the workspace)
// pushes the tab row (notes, to-dos, hashtags) out at the top and snaps, so the note fills the whole area,
// like the bar above the Kanban board; a step up brings it back. Over the note and the folder sidebar the
// wheel only scrolls them. The workspace is higher than the app's scroll area by the row (wsBar); that
// area stays overflow-hidden and is only scrolled from here.
const workspaceRootRef = useRef<HTMLDivElement>(null);
const tabRowRef = useRef<HTMLDivElement>(null);
const [wsBar, setWsBar] = useState(0);
useEffect(() => {
const row = tabRowRef.current;
if (!row || typeof ResizeObserver === 'undefined') return;
// offsetHeight is 0 on phones (the row is hidden there); 12 px is the gap below it
const measure = () => setWsBar(row.offsetHeight ? row.offsetHeight + 12 : 0);
measure();
const ro = new ResizeObserver(measure);
ro.observe(row);
return () => ro.disconnect();
}, []);
useEffect(() => {
const root = workspaceRootRef.current;
const area = root?.closest<HTMLElement>('[data-app-scroll]');
if (!root || !area || !wsBar || focusMode) return;
let pending = 0; // trackpads send many small steps: move only once they add up
let lockedUntil = 0;
const onWheel = (e: WheelEvent) => {
if (e.ctrlKey || e.shiftKey || isPhoneWidth()) return;
if ((e.target as HTMLElement).closest?.('[data-ws-pane]')) return;
const dy = e.deltaMode === 1 ? e.deltaY * 16 : e.deltaY;
if (dy === 0) return;
const max = area.scrollHeight - area.clientHeight;
if (dy > 0 ? area.scrollTop >= max - 1 : area.scrollTop <= 0) return;
e.preventDefault();
if (Date.now() < lockedUntil) return;
pending += dy;
if (Math.abs(pending) < 40) return;
pending = 0;
lockedUntil = Date.now() + 450;
area.scrollTo({ top: dy > 0 ? max : 0, behavior: 'smooth' });
};
area.addEventListener('wheel', onWheel, { passive: false });
return () => area.removeEventListener('wheel', onWheel);
}, [wsBar, focusMode]);
const allFolderPaths = useMemo(() => { const allFolderPaths = useMemo(() => {
const set = new Set<string>(folders); const set = new Set<string>(folders);
for (const d of docs) { for (const d of docs) {
@@ -1612,10 +1664,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
}, [tagsSummary, tagSearchQuery, tagSortBy]); }, [tagsSummary, tagSearchQuery, tagSortBy]);
return ( return (
<div className="w-full h-full min-h-0 flex flex-col gap-3"> <div
ref={workspaceRootRef}
className="w-full h-full min-h-0 flex flex-col gap-3"
style={wsBar ? { height: `calc(100% + ${wsBar}px)`, flexShrink: 0 } : undefined}
>
{/* Top Header: Workspace Tabs & Actions */} {/* Top Header: Workspace Tabs & Actions */}
{/* Phones: the views are switched in the menu at the top, so the row is hidden there */} {/* Phones: the views are switched in the menu at the top, so the row is hidden there */}
<div className="hidden sm:flex flex-wrap items-center justify-between gap-2 border-b border-border pb-2"> <div ref={tabRowRef} className="hidden sm:flex flex-wrap items-center justify-between gap-2 border-b border-border pb-2">
{/* Workspace selector tabs & left sidebar toggle */} {/* Workspace selector tabs & left sidebar toggle */}
<div className="grid grid-cols-3 w-full gap-1 sm:flex sm:flex-wrap sm:items-center sm:w-auto"> <div className="grid grid-cols-3 w-full gap-1 sm:flex sm:flex-wrap sm:items-center sm:w-auto">
<button <button
@@ -1843,24 +1899,38 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* Main Workspace Split Pane */} {/* Main Workspace Split Pane */}
<div <div
data-ws-pane
className={ className={
focusMode focusMode
? 'fixed inset-0 z-[45] flex bg-editor focus-mode-enter' ? `fixed inset-0 z-[45] flex bg-editor focus-mode-enter ${zenPinTop ? 'lg:pt-[45px]' : ''} ${
zenPinSide ? 'lg:pl-80' : ''
}`
: 'flex flex-1 min-h-0 bg-editor rounded-card border border-border overflow-hidden' : 'flex flex-1 min-h-0 bg-editor rounded-card border border-border overflow-hidden'
} }
> >
{focusMode && ( {focusMode && (
<> <>
{/* Header slides in when the mouse touches the top edge (large screens) */} {/* Header slides in when the mouse touches the top edge (large screens); pinned, it stays */}
<div className="hidden lg:block fixed top-0 inset-x-0 h-3 z-[46] group"> {/* A pinned sidebar takes the full height unless the header is pinned too: the header starts beside it */}
<div className="absolute top-0 inset-x-0 flex items-center gap-2 px-6 py-2 bg-panel/95 backdrop-blur-sm border-b border-border shadow-panel -translate-y-full group-hover:translate-y-0 group-focus-within:translate-y-0 transition-transform duration-200"> <div
className={`hidden lg:block fixed top-0 right-0 h-3 z-[46] group ${
zenPinSide && !zenPinTop ? 'left-80' : 'left-0'
}`}
>
<div
className={`absolute top-0 inset-x-0 flex items-center gap-2 px-6 py-2 bg-panel/95 backdrop-blur-sm border-b border-border shadow-sm transition-transform duration-200 ${
zenPinTop ? 'translate-y-0' : '-translate-y-full group-hover:translate-y-0 group-focus-within:translate-y-0'
}`}
>
<button <button
type="button" type="button"
onClick={() => setZenSidebar((v) => (v === 'closed' ? 'click' : 'closed'))} onClick={() =>
zenPinSide ? setZenPinSide(false) : setZenSidebar((v) => (v === 'closed' ? 'click' : 'closed'))
}
className="btn-icon h-7 w-7 shrink-0" className="btn-icon h-7 w-7 shrink-0"
title="Ordner und Notizen einblenden" title={zenSideOpen ? 'Ordner und Notizen ausblenden' : 'Ordner und Notizen einblenden'}
aria-label="Ordner und Notizen einblenden" aria-label={zenSideOpen ? 'Ordner und Notizen ausblenden' : 'Ordner und Notizen einblenden'}
aria-expanded={zenSidebar !== 'closed'} aria-expanded={zenSideOpen}
> >
<FolderOpen className="w-3.5 h-3.5" /> <FolderOpen className="w-3.5 h-3.5" />
</button> </button>
@@ -1873,6 +1943,20 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{[selectedDocObj?.icon, selectedDocObj?.title].filter(Boolean).join(' ')} {[selectedDocObj?.icon, selectedDocObj?.title].filter(Boolean).join(' ')}
</button> </button>
<div ref={setZenActionsTarget} className="flex items-center" /> <div ref={setZenActionsTarget} className="flex items-center" />
<button
type="button"
onClick={(e) => {
// Unpinned, the header slides away once the mouse leaves (focus would keep it open)
if (zenPinTop) e.currentTarget.blur();
setZenPinTop((v) => !v);
}}
className={`btn-icon h-7 w-7 shrink-0 ${zenPinTop ? 'text-accent' : ''}`}
title={zenPinTop ? 'Leiste lösen' : 'Leiste anpinnen'}
aria-label={zenPinTop ? 'Leiste lösen' : 'Leiste anpinnen'}
aria-pressed={zenPinTop}
>
{zenPinTop ? <PinOff className="w-3.5 h-3.5" /> : <Pin className="w-3.5 h-3.5" />}
</button>
<button <button
type="button" type="button"
onClick={() => setFocusMode(false)} onClick={() => setFocusMode(false)}
@@ -1886,11 +1970,11 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</div> </div>
{/* Wide screens: touching the left edge opens the sidebar (the note sits far from it) */} {/* Wide screens: touching the left edge opens the sidebar (the note sits far from it) */}
<div <div
className="hidden xl:block fixed left-0 inset-y-0 w-2 z-[46]" className={`${zenPinSide ? 'hidden' : 'hidden xl:block'} fixed left-0 inset-y-0 w-2 z-[46]`}
onMouseEnter={() => setZenSidebar((v) => (v === 'closed' ? 'hover' : v))} onMouseEnter={() => setZenSidebar((v) => (v === 'closed' ? 'hover' : v))}
aria-hidden aria-hidden
/> />
{zenSidebar === 'click' && ( {zenSidebar === 'click' && !zenPinSide && (
<div className="fixed inset-0 z-[46] bg-overlay/30" onClick={() => setZenSidebar('closed')} aria-hidden /> <div className="fixed inset-0 z-[46] bg-overlay/30" onClick={() => setZenSidebar('closed')} aria-hidden />
)} )}
<button <button
@@ -1909,9 +1993,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
onMouseLeave={() => setZenSidebar((v) => (v === 'hover' ? 'closed' : v))} onMouseLeave={() => setZenSidebar((v) => (v === 'hover' ? 'closed' : v))}
className={ className={
focusMode focusMode
? `fixed left-0 inset-y-0 z-[47] w-80 max-w-[85vw] flex flex-col bg-sidebar border-r border-border shadow-panel transition-transform duration-300 ease-out ${ ? // Pinned: from lg on it stays open beside the note (below a pinned header), without shadow
zenSidebar !== 'closed' ? 'translate-x-0' : '-translate-x-full' `fixed left-0 inset-y-0 z-[47] w-80 max-w-[85vw] flex flex-col bg-sidebar border-r border-border transition-transform duration-300 ease-out ${
}` zenSidebar !== 'closed'
? 'translate-x-0 shadow-panel'
: zenPinSide
? '-translate-x-full lg:translate-x-0'
: '-translate-x-full'
} ${zenPinSide && zenPinTop ? 'lg:top-[45px]' : ''}`
: `${ : `${
(subView !== 'editor' && subView !== 'trash' && subView !== 'journal') || isSidebarCollapsed (subView !== 'editor' && subView !== 'trash' && subView !== 'journal') || isSidebarCollapsed
? 'hidden' ? 'hidden'
@@ -1956,6 +2045,23 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
> >
<FilePlus className="w-4 h-4" /> <FilePlus className="w-4 h-4" />
</button> </button>
{/* Focus mode: pin the sliding sidebar so it stays beside the note */}
{focusMode && (
<button
type="button"
onClick={() => {
// Unpinned, it stays until the mouse leaves it
setZenSidebar(zenPinSide ? 'hover' : 'closed');
setZenPinSide((v) => !v);
}}
className={`hidden lg:inline-flex btn-icon h-8 w-8 ${zenPinSide ? 'text-accent' : ''}`}
title={zenPinSide ? 'Seitenleiste lösen' : 'Seitenleiste anpinnen'}
aria-label={zenPinSide ? 'Seitenleiste lösen' : 'Seitenleiste anpinnen'}
aria-pressed={zenPinSide}
>
{zenPinSide ? <PinOff className="w-4 h-4" /> : <Pin className="w-4 h-4" />}
</button>
)}
</div> </div>
</div> </div>
@@ -2940,8 +3046,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
message={ message={
<> <>
Ordner <strong>„{name}“</strong> Ordner <strong>„{name}“</strong>
{notes > 0 ? ` mit ${notes === 1 ? '1 Notiz' : `${notes} Notizen`}` : ''} löschen? Er kommt mit allen {notes > 0 ? ` mit ${notes === 1 ? '1 Notiz' : `${notes} Notizen`}` : ''} wirklich löschen?
Unterordnern in den Papierkorb und lässt sich dort wiederherstellen.
</> </>
} }
onCancel={closeFolderDelete} onCancel={closeFolderDelete}