From 85257212af10726af1d2c1a25bf75d86a506628d Mon Sep 17 00:00:00 2001 From: Till JS Date: Wed, 1 Apr 2026 16:37:16 +0200 Subject: [PATCH] feat(todo/web): add secondary pages system with centered layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add "Neue Seite" button to open filtered task pages (Erledigt, Heute, Überfällig, etc.) alongside the main board view. Sheets are centered via carousel-style padding, and all pages are closeable with X buttons. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../board-views/BoardViewRenderer.svelte | 8 +- .../components/board-views/FokusLayout.svelte | 89 +++++- .../lib/components/pages/PagePicker.svelte | 265 ++++++++++++++++++ .../lib/components/pages/SecondaryPage.svelte | 259 +++++++++++++++++ .../apps/web/src/routes/(app)/+page.svelte | 96 ++++++- 5 files changed, 697 insertions(+), 20 deletions(-) create mode 100644 apps/todo/apps/web/src/lib/components/pages/PagePicker.svelte create mode 100644 apps/todo/apps/web/src/lib/components/pages/SecondaryPage.svelte diff --git a/apps/todo/apps/web/src/lib/components/board-views/BoardViewRenderer.svelte b/apps/todo/apps/web/src/lib/components/board-views/BoardViewRenderer.svelte index f725dc737..820f4b9a0 100644 --- a/apps/todo/apps/web/src/lib/components/board-views/BoardViewRenderer.svelte +++ b/apps/todo/apps/web/src/lib/components/board-views/BoardViewRenderer.svelte @@ -1,5 +1,5 @@ + +
+
+

Neue Seite

+ +
+ +
+ {#each availableOptions as option, i (option.id)} + {#if i > 0} +
+ {/if} + + {/each} + + {#if availableOptions.length === 0} +
+

Alle Seiten sind bereits geöffnet

+
+ {/if} +
+
+ + diff --git a/apps/todo/apps/web/src/lib/components/pages/SecondaryPage.svelte b/apps/todo/apps/web/src/lib/components/pages/SecondaryPage.svelte new file mode 100644 index 000000000..8dcc174c8 --- /dev/null +++ b/apps/todo/apps/web/src/lib/components/pages/SecondaryPage.svelte @@ -0,0 +1,259 @@ + + +
+ + +
+ {#if filteredTasks.length === 0} +
+

Keine Aufgaben

+
+ {:else} + {#each filteredTasks as task (task.id)} +
+ handleToggle(task)} + onSave={(data) => handleUpdate(task.id, data)} + onDelete={() => handleDelete(task.id)} + /> +
+ {/each} + {/if} +
+
+ + diff --git a/apps/todo/apps/web/src/routes/(app)/+page.svelte b/apps/todo/apps/web/src/routes/(app)/+page.svelte index 2ef0eb829..bf791aed2 100644 --- a/apps/todo/apps/web/src/routes/(app)/+page.svelte +++ b/apps/todo/apps/web/src/routes/(app)/+page.svelte @@ -4,6 +4,9 @@ import { BoardViewRenderer } from '$lib/components/board-views'; import { todoSettings, type PageWidth } from '$lib/stores/settings.svelte'; import { boardViewsStore } from '$lib/stores/board-views.svelte'; + import { Plus } from '@manacore/shared-icons'; + import PagePicker from '$lib/components/pages/PagePicker.svelte'; + import SecondaryPage from '$lib/components/pages/SecondaryPage.svelte'; // Get active view + edit mode from layout context const activeViewCtx: { readonly value: LocalBoardView | null } = getContext('activeView'); @@ -14,6 +17,31 @@ let activeView = $derived(activeViewCtx.value); let pageTitle = $derived(activeView?.name ?? 'Aufgaben'); + // ── Secondary Pages ───────────────────────────────────── + let showPagePicker = $state(false); + let openPages = $state([]); + + function handleAddPage(pageId: string) { + if (!openPages.includes(pageId)) { + openPages = [...openPages, pageId]; + } + showPagePicker = false; + } + + function handleRemovePage(pageId: string) { + openPages = openPages.filter((p) => p !== pageId); + } + + function togglePagePicker() { + showPagePicker = !showPagePicker; + } + + function handleColumnClose(colIdx: number) { + if (!activeView || activeView.columns.length <= 1) return; + const columns = $state.snapshot(activeView.columns).filter((_, i) => i !== colIdx); + updateView({ columns }); + } + // ── Edit helpers ──────────────────────────────────────── const GROUPBY_OPTIONS = [ @@ -49,13 +77,15 @@ function updateColumn(colIdx: number, data: Record) { if (!activeView) return; - const cols = activeView.columns.map((c, i) => (i === colIdx ? { ...c, ...data } : { ...c })); + const raw = $state.snapshot(activeView.columns); + const cols = raw.map((c, i) => (i === colIdx ? { ...c, ...data } : c)); updateView({ columns: cols }); } function removeColumn(colIdx: number) { if (!activeView || activeView.columns.length <= 1) return; - updateView({ columns: activeView.columns.filter((_, i) => i !== colIdx) }); + const columns = $state.snapshot(activeView.columns).filter((_, i) => i !== colIdx); + updateView({ columns }); } function addColumn() { @@ -66,12 +96,12 @@ color: COLUMN_COLORS[activeView.columns.length % COLUMN_COLORS.length], match: { type: 'custom' as const, value: `custom-${Date.now()}` }, }; - updateView({ columns: [...activeView.columns, newCol] }); + updateView({ columns: [...$state.snapshot(activeView.columns), newCol] }); } function moveColumn(colIdx: number, dir: -1 | 1) { if (!activeView) return; - const cols = [...activeView.columns]; + const cols = $state.snapshot(activeView.columns); const target = colIdx + dir; if (target < 0 || target >= cols.length) return; [cols[colIdx], cols[target]] = [cols[target], cols[colIdx]]; @@ -148,8 +178,35 @@ onColumnColorChange={columnsEditable ? (i, color) => updateColumn(i, { color }) : undefined} onColumnMove={columnsEditable ? moveColumn : undefined} onColumnDelete={columnsEditable ? removeColumn : undefined} + onColumnClose={handleColumnClose} onAddColumn={columnsEditable && editMode ? addColumn : undefined} - /> + > + {#snippet trailing()} + + {#each openPages as pageId (pageId)} + handleRemovePage(pageId)} /> + {/each} + + + {#if !editMode} + {#if showPagePicker} + (showPagePicker = false)} + activePageIds={openPages} + /> + {:else} + + {/if} + {/if} + {/snippet} + {:else}

Views werden geladen...

@@ -164,6 +221,35 @@ flex-direction: column; } + .neue-seite-card { + flex: 0 0 auto; + width: 48px; + align-self: stretch; + display: flex; + align-items: center; + justify-content: center; + border: 2px dashed rgba(0, 0, 0, 0.08); + border-radius: 0.375rem; + background: transparent; + color: #9ca3af; + cursor: pointer; + transition: all 0.2s; + } + .neue-seite-card:hover { + border-color: var(--color-primary, #8b5cf6); + color: var(--color-primary, #8b5cf6); + background: color-mix(in srgb, var(--color-primary, #8b5cf6) 4%, transparent); + } + :global(.dark) .neue-seite-card { + border-color: rgba(255, 255, 255, 0.06); + color: #4b5563; + } + :global(.dark) .neue-seite-card:hover { + border-color: var(--color-primary, #8b5cf6); + color: var(--color-primary, #8b5cf6); + background: color-mix(in srgb, var(--color-primary, #8b5cf6) 8%, transparent); + } + .empty-state { display: flex; align-items: center;