feat(kanban): button to stretch the board to the full window width
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m30s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-31 09:44:12 +01:00
1 parent 7888a255b1
commit fc637bc25d
5 files changed
+74 -18

No files matched your search

+6 -3
View File
@@ -6,7 +6,10 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
## [Unreleased] ## [Unreleased]
## [0.15.2] - 2026-10-31 ## [0.16.0] - 2026-10-31
### Added
- Kanban: Neben „Spalten anpassen“ streckt ein kleiner Knopf (diagonale Pfeile) das Board auf die volle Fensterbreite; alle Spalten stehen dann nebeneinander. Die Einstellung merkt sich der Browser, am Handy gibt es den Knopf nicht.
### Fixed ### Fixed
- Aufgaben: Ein neuer Quick-Dump-Eintrag (z. B. per Spracheingabe über das gehaltene Plus) tauchte kurz oben in den Aufgaben auf, obwohl er in die Triage gehört. Live-Änderungen nehmen jetzt nur noch echte Aufgaben auf; eine per „Rückgängig“ zurück in die Triage geholte Aufgabe verschwindet auch sofort aus der Liste. - Aufgaben: Ein neuer Quick-Dump-Eintrag (z. B. per Spracheingabe über das gehaltene Plus) tauchte kurz oben in den Aufgaben auf, obwohl er in die Triage gehört. Live-Änderungen nehmen jetzt nur noch echte Aufgaben auf; eine per „Rückgängig“ zurück in die Triage geholte Aufgabe verschwindet auch sofort aus der Liste.
@@ -371,8 +374,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.15.2...dev [Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.16.0...dev
[0.15.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.1...v0.15.2 [0.16.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.1...v0.16.0
[0.15.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.0...v0.15.1 [0.15.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.0...v0.15.1
[0.15.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.3...v0.15.0 [0.15.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.3...v0.15.0
[0.14.3]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.2...v0.14.3 [0.14.3]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.2...v0.14.3
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.15.2", "version": "0.16.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.15.2", "version": "0.16.0",
"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.15.2", "version": "0.16.0",
"license": "Unlicense", "license": "Unlicense",
"type": "module", "type": "module",
"scripts": { "scripts": {
+6 -1
View File
@@ -164,6 +164,8 @@ 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);
@@ -1168,7 +1170,9 @@ 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 max-w-6xl w-full mx-auto px-3 sm:px-4 pt-3 pb-24 ${ className={`flex-1 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'
}`} }`}
> >
@@ -1192,6 +1196,7 @@ export const App: React.FC = () => {
refreshTrigger={refreshRevision} refreshTrigger={refreshRevision}
initialSelectedTaskId={selectedTaskId} initialSelectedTaskId={selectedTaskId}
openTasksCount={stats.tasksOpenCount} openTasksCount={stats.tasksOpenCount}
onWideChange={setTasksWide}
/> />
)} )}
+59 -11
View File
@@ -8,6 +8,8 @@ import {
FileText, FileText,
LayoutList, LayoutList,
Columns, Columns,
Maximize2,
Minimize2,
ArrowRight, ArrowRight,
ArrowLeft, ArrowLeft,
Edit3, Edit3,
@@ -59,8 +61,11 @@ 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';
export const TasksView: React.FC<TasksViewProps> = ({ export const TasksView: React.FC<TasksViewProps> = ({
currentUser, currentUser,
onRefreshStats, onRefreshStats,
@@ -68,8 +73,31 @@ export const TasksView: React.FC<TasksViewProps> = ({
refreshTrigger = 0, refreshTrigger = 0,
initialSelectedTaskId, initialSelectedTaskId,
openTasksCount, openTasksCount,
onWideChange,
}) => { }) => {
const [viewMode, setViewMode] = useState<'list' | 'kanban'>('list'); const [viewMode, setViewMode] = useState<'list' | 'kanban'>('list');
// Wide Kanban: the board takes the whole window and shows every column in one row
const [kanbanWide, setKanbanWide] = useState(() => {
try {
return localStorage.getItem(KANBAN_WIDE_KEY) === '1';
} catch {
return false;
}
});
const toggleKanbanWide = () => {
const next = !kanbanWide;
setKanbanWide(next);
try {
localStorage.setItem(KANBAN_WIDE_KEY, next ? '1' : '0');
} catch {
// only a convenience
}
};
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[]>([]);
@@ -1650,16 +1678,31 @@ export const TasksView: React.FC<TasksViewProps> = ({
{/* Group By & Sort By Dropdowns & Manage Columns button */} {/* Group By & Sort By Dropdowns & Manage Columns button */}
<div className="flex items-center gap-2 flex-shrink-0 ml-auto"> <div className="flex items-center gap-2 flex-shrink-0 ml-auto">
{viewMode === 'kanban' ? ( {viewMode === 'kanban' ? (
!isSelectedListReadOnly && ( <>
<button {!isSelectedListReadOnly && (
onClick={() => setShowManageColumnsModal(true)} <button
className="btn-action h-7 px-2.5 text-xs flex items-center gap-1.5" onClick={() => setShowManageColumnsModal(true)}
title="Kanban-Spalten hinzufügen, umbenennen oder neu anordnen" className="btn-action h-7 px-2.5 text-xs flex items-center gap-1.5"
> title="Kanban-Spalten hinzufügen, umbenennen oder neu anordnen"
<Columns className="w-3.5 h-3.5 text-accent" /> >
<span>Spalten anpassen</span> <Columns className="w-3.5 h-3.5 text-accent" />
</button> <span>Spalten anpassen</span>
) </button>
)}
<button
type="button"
onClick={toggleKanbanWide}
aria-pressed={kanbanWide}
className="btn-action h-7 w-7 p-0 hidden lg:inline-flex items-center justify-center"
title={kanbanWide ? 'Board wieder normal breit' : 'Board auf volle Breite strecken'}
>
{kanbanWide ? (
<Minimize2 className="w-3.5 h-3.5 text-accent" />
) : (
<Maximize2 className="w-3.5 h-3.5 text-accent" />
)}
</button>
</>
) : ( ) : (
<> <>
{/* Group By */} {/* Group By */}
@@ -2112,7 +2155,12 @@ export const TasksView: React.FC<TasksViewProps> = ({
{viewMode === 'kanban' ? ( {viewMode === 'kanban' ? (
/* DYNAMIC KANBAN BOARD */ /* DYNAMIC KANBAN BOARD */
<div className="space-y-2"> <div className="space-y-2">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 pt-1"> <div
className={`grid grid-cols-1 md:grid-cols-2 gap-3 pt-1 ${
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}
>
{activeColumns.map((col, colIdx) => { {activeColumns.map((col, colIdx) => {
const colTasks = kanbanTasks.filter((t) => { const colTasks = kanbanTasks.filter((t) => {
if (col.isDone) { if (col.isDone) {