feat(kanban): wide board animates, keeps header width and caps column width

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-31 10:04:16 +01:00
1 parent 25bb787a8e
commit 945be2c1ad
6 files changed
+30 -19

No files matched your search

+7 -1
View File
@@ -6,6 +6,11 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
## [Unreleased] ## [Unreleased]
## [0.17.1] - 2026-10-31
### Changed
- Kanban maximieren: Nur noch das Board wird breiter, Listen-Reiter und „Neue Aufgabe anlegen“ behalten ihre Breite. Das Board wächst weich animiert, jede Spalte wird höchstens 380 px breit und das Board bleibt mittig (bei wenigen Spalten also schmaler als das Fenster).
## [0.17.0] - 2026-10-31 ## [0.17.0] - 2026-10-31
### Added ### Added
@@ -395,7 +400,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.17.0...dev [Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.17.1...dev
[0.17.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.17.0...v0.17.1
[0.17.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.16.1...v0.17.0 [0.17.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.16.1...v0.17.0
[0.16.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.16.0...v0.16.1 [0.16.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.16.0...v0.16.1
[0.16.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.1...v0.16.0 [0.16.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.1...v0.16.0
+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.17.0-16a34a?style=flat-square)](CHANGELOG.md) [![Version](https://img.shields.io/badge/Version-v0.17.1-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.17.0", "version": "0.17.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.17.0", "version": "0.17.1",
"license": "Unlicense", "license": "Unlicense",
"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.17.0", "version": "0.17.1",
"license": "Unlicense", "license": "Unlicense",
"type": "module", "type": "module",
"scripts": { "scripts": {
+1 -6
View File
@@ -164,8 +164,6 @@ export const App: React.FC = () => {
const [deck, setDeck] = useState<Thought[]>([]); const [deck, setDeck] = useState<Thought[]>([]);
const [lists, setLists] = useState<SharedList[]>([]); const [lists, setLists] = useState<SharedList[]>([]);
const [activeTab, setActiveTab] = useState<ActiveTab>('triage'); const [activeTab, setActiveTab] = useState<ActiveTab>('triage');
// Kanban asked for the full window width (reported by the tasks view)
const [tasksWide, setTasksWide] = useState(false);
const [previousTab, setPreviousTab] = useState<ActiveTab>('triage'); const [previousTab, setPreviousTab] = useState<ActiveTab>('triage');
const [selectedDocId, setSelectedDocId] = useState<string | null>(null); const [selectedDocId, setSelectedDocId] = useState<string | null>(null);
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null); const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
@@ -1170,9 +1168,7 @@ export const App: React.FC = () => {
{/* Main Container */} {/* Main Container */}
{/* The workspace fills the window exactly and scrolls only inside the note, never the page */} {/* The workspace fills the window exactly and scrolls only inside the note, never the page */}
<main <main
className={`flex-1 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 === 'tasks' && tasksWide ? 'lg:max-w-none lg:px-6' : 'max-w-6xl'
} ${
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' : 'sm:pb-6'
}`} }`}
> >
@@ -1196,7 +1192,6 @@ export const App: React.FC = () => {
refreshTrigger={refreshRevision} refreshTrigger={refreshRevision}
initialSelectedTaskId={selectedTaskId} initialSelectedTaskId={selectedTaskId}
openTasksCount={stats.tasksOpenCount} openTasksCount={stats.tasksOpenCount}
onWideChange={setTasksWide}
/> />
)} )}
+18 -8
View File
@@ -62,10 +62,25 @@ interface TasksViewProps {
refreshTrigger?: number; refreshTrigger?: number;
initialSelectedTaskId?: string | null; initialSelectedTaskId?: string | null;
openTasksCount?: number; // all open tasks incl. shared lists (same as the nav bar counter) openTasksCount?: number; // all open tasks incl. shared lists (same as the nav bar counter)
onWideChange?: (wide: boolean) => void; // Kanban stretched to the full window width
} }
const KANBAN_WIDE_KEY = 'mindsift:kanban_wide'; const KANBAN_WIDE_KEY = 'mindsift:kanban_wide';
// Wide Kanban: a column grows to at most this width, the board stays centred
const KANBAN_COL_MAX = 380;
const KANBAN_GAP = 12; // gap-3
// Only the board leaves the page column (header and "new task" keep their width): its width follows
// the columns (at most KANBAN_COL_MAX each, at most the window) and a margin keeps it centred – a
// negative one when it is wider than the page column. Width and margin are animated.
const kanbanBoardStyle = (wide: boolean, columns: number): React.CSSProperties => {
if (!wide) return { '--kanban-w': '100%', '--kanban-ml': '0px' } as React.CSSProperties;
const w = `min(100vw - 4rem, ${columns * KANBAN_COL_MAX + (columns - 1) * KANBAN_GAP}px)`;
return {
'--kanban-cols': columns,
'--kanban-w': w,
'--kanban-ml': `calc((100% - ${w}) / 2)`,
} as React.CSSProperties;
};
type TasksSession = { listId: string; viewMode: 'list' | 'kanban' }; type TasksSession = { listId: string; viewMode: 'list' | 'kanban' };
@@ -76,7 +91,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
refreshTrigger = 0, refreshTrigger = 0,
initialSelectedTaskId, initialSelectedTaskId,
openTasksCount, openTasksCount,
onWideChange,
}) => { }) => {
// Open list and view survive switching to other tabs (like the workspace's open note) // Open list and view survive switching to other tabs (like the workspace's open note)
const [viewMode, setViewMode] = useState<'list' | 'kanban'>(() => const [viewMode, setViewMode] = useState<'list' | 'kanban'>(() =>
@@ -100,10 +114,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
} }
}; };
const isKanbanWide = viewMode === 'kanban' && kanbanWide; const isKanbanWide = viewMode === 'kanban' && kanbanWide;
useEffect(() => {
onWideChange?.(isKanbanWide);
}, [isKanbanWide, onWideChange]);
useEffect(() => () => onWideChange?.(false), [onWideChange]);
const [filter, setFilter] = useState<'open' | 'done' | 'discarded'>('open'); const [filter, setFilter] = useState<'open' | 'done' | 'discarded'>('open');
const [tasks, setTasks] = useState<Thought[]>([]); const [tasks, setTasks] = useState<Thought[]>([]);
const [taskLists, setTaskLists] = useState<TaskList[]>([]); const [taskLists, setTaskLists] = useState<TaskList[]>([]);
@@ -2214,10 +2224,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
/* DYNAMIC KANBAN BOARD */ /* DYNAMIC KANBAN BOARD */
<div className="space-y-2"> <div className="space-y-2">
<div <div
className={`grid grid-cols-1 md:grid-cols-2 gap-3 pt-1 ${ className={`grid grid-cols-1 md:grid-cols-2 gap-3 pt-1 lg:w-[var(--kanban-w)] lg:ml-[var(--kanban-ml)] transition-[width,margin] duration-300 ease-out ${
isKanbanWide ? 'lg:grid-cols-[repeat(var(--kanban-cols),minmax(0,1fr))]' : 'lg:grid-cols-4' isKanbanWide ? 'lg:grid-cols-[repeat(var(--kanban-cols),minmax(0,1fr))]' : 'lg:grid-cols-4'
}`} }`}
style={isKanbanWide ? ({ '--kanban-cols': activeColumns.length } as React.CSSProperties) : undefined} style={kanbanBoardStyle(isKanbanWide, activeColumns.length)}
> >
{activeColumns.map((col, colIdx) => { {activeColumns.map((col, colIdx) => {
const colTasks = kanbanTasks.filter((t) => { const colTasks = kanbanTasks.filter((t) => {