From 1cd89af80d5eccf1fb20f407db9169882b8e6e66 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 2 Apr 2026 12:44:14 +0200 Subject: [PATCH] feat(todo/web): add custom pages with inline visual editor Bring back the custom pages system from the old standalone todo app: - Edit FAB (pencil icon) toggles inline edit mode on the homepage - Custom pages with configurable filter rules (priority, date range, completed) - Inline PageEditBar with icon picker (10 icons), filter pills, reorder arrows - Width pills (S/M/L/XL) visible in edit mode to resize all sheets - Custom pages persisted to todoSettings.customPages - Auto-enable edit mode when creating a new custom page - PagePicker now includes "Eigene Seite" creation option Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/lib/components/pages/TodoPage.svelte | 2 +- .../apps/web/src/routes/(app)/+page.svelte | 267 +++++++++++++++++- 2 files changed, 259 insertions(+), 10 deletions(-) diff --git a/apps/todo/apps/web/src/lib/components/pages/TodoPage.svelte b/apps/todo/apps/web/src/lib/components/pages/TodoPage.svelte index 0baab80d3..22ba49d06 100644 --- a/apps/todo/apps/web/src/lib/components/pages/TodoPage.svelte +++ b/apps/todo/apps/web/src/lib/components/pages/TodoPage.svelte @@ -261,7 +261,7 @@ pageId === 'todo' ? filteredTasks.filter((t) => t.isCompleted) : [] ); - function formatCompletedTime(completedAt: string): string { + function formatCompletedTime(completedAt: string | Date): string { const date = new Date(completedAt); const time = format(date, 'HH:mm'); if (pageId === 'completed' || showCompleted) { diff --git a/apps/todo/apps/web/src/routes/(app)/+page.svelte b/apps/todo/apps/web/src/routes/(app)/+page.svelte index c6b1d68ed..420485269 100644 --- a/apps/todo/apps/web/src/routes/(app)/+page.svelte +++ b/apps/todo/apps/web/src/routes/(app)/+page.svelte @@ -3,7 +3,9 @@ import type { LocalBoardView } from '$lib/data/local-store'; import { BoardViewRenderer } from '$lib/components/board-views'; import { boardViewsStore } from '$lib/stores/board-views.svelte'; - import { Plus } from '@manacore/shared-icons'; + import { todoSettings } from '$lib/stores/settings.svelte'; + import type { PageConfig, PageWidth } from '$lib/stores/settings.svelte'; + import { Plus, PencilSimple, X } from '@manacore/shared-icons'; import PagePicker from '$lib/components/pages/PagePicker.svelte'; import TodoPage from '$lib/components/pages/TodoPage.svelte'; import type { MinimizedPagesContext } from '$lib/stores/minimized-pages.svelte'; @@ -15,6 +17,9 @@ let activeView = $derived(activeViewCtx.value); let pageTitle = $derived(activeView?.name ?? 'Aufgaben'); + // ── Edit mode ────────────────────────────────────────── + let editMode = $state(false); + // ── Pages ─────────────────────────────────────────────── let showPagePicker = $state(false); let openPages = $state< @@ -23,9 +28,12 @@ let expandedPages = $derived(openPages.filter((p) => !p.minimized)); + // Custom pages from settings + let customPages = $derived(todoSettings.customPages); + // Sync minimized pages to layout via context $effect(() => { - minimizedPages.sync(openPages); + minimizedPages.sync(openPages, customPages); }); // Register handlers so layout can delegate tab actions back to us @@ -61,6 +69,11 @@ function handleRenamePage(pageId: string, name: string) { openPages = openPages.map((p) => (p.id === pageId ? { ...p, customTitle: name } : p)); + // Also update custom page config label + if (pageId.startsWith('custom-')) { + const updated = customPages.map((cp) => (cp.id === pageId ? { ...cp, label: name } : cp)); + todoSettings.update({ customPages: updated }); + } } function handleMaximizePage(pageId: string) { @@ -69,6 +82,61 @@ ); } + // ── Custom page CRUD ──────────────────────────────────── + function handleCreateCustomPage() { + const id = `custom-${crypto.randomUUID().slice(0, 8)}`; + const newPage: PageConfig = { + id, + label: 'Neue Seite', + icon: 'star', + filter: {}, + }; + todoSettings.update({ customPages: [...customPages, newPage] }); + openPages = [...openPages, { id, minimized: false }]; + showPagePicker = false; + // Auto-enable edit mode so the user can configure the new page + editMode = true; + } + + function handleUpdateCustomPage(pageId: string, data: Partial) { + const updated = customPages.map((cp) => { + if (cp.id !== pageId) return cp; + return { ...cp, ...data, filter: data.filter ?? cp.filter }; + }); + todoSettings.update({ customPages: updated }); + } + + function handleDeletePage(pageId: string) { + // Remove from open pages + openPages = openPages.filter((p) => p.id !== pageId); + // If custom, also remove from settings + if (pageId.startsWith('custom-')) { + const updated = customPages.filter((cp) => cp.id !== pageId); + todoSettings.update({ customPages: updated }); + } + } + + function getCustomPageConfig(pageId: string): PageConfig | undefined { + return customPages.find((cp) => cp.id === pageId); + } + + // ── Page reorder (in edit mode, with arrows) ──────────── + function handleMovePageLeft(pageId: string) { + const idx = openPages.findIndex((p) => p.id === pageId); + if (idx <= 0) return; + const pages = [...openPages]; + [pages[idx - 1], pages[idx]] = [pages[idx], pages[idx - 1]]; + openPages = pages; + } + + function handleMovePageRight(pageId: string) { + const idx = openPages.findIndex((p) => p.id === pageId); + if (idx === -1 || idx >= openPages.length - 1) return; + const pages = [...openPages]; + [pages[idx], pages[idx + 1]] = [pages[idx + 1], pages[idx]]; + openPages = pages; + } + // ── Page drag reorder ─────────────────────────────────── let dragPageId = $state(null); @@ -107,14 +175,30 @@ showPagePicker = !showPagePicker; } + function toggleEditMode() { + editMode = !editMode; + if (!editMode) showPagePicker = false; + } + + // ── Width pills ───────────────────────────────────────── + const WIDTH_OPTIONS: { id: PageWidth; label: string }[] = [ + { id: 'narrow', label: 'S' }, + { id: 'medium', label: 'M' }, + { id: 'wide', label: 'L' }, + { id: 'full', label: 'XL' }, + ]; + + function setPageWidth(width: PageWidth) { + todoSettings.update({ pageWidth: width }); + } + + // ── Column helpers ────────────────────────────────────── function handleColumnClose(colIdx: number) { if (!activeView) return; const columns = $state.snapshot(activeView.columns).filter((_, i) => i !== colIdx); updateView({ columns }); } - // ── Column helpers ────────────────────────────────────── - async function updateView(data: Partial) { if (!activeView) return; await boardViewsStore.updateView(activeView.id, data); @@ -160,6 +244,23 @@
+ + {#if editMode} +
+
+ {#each WIDTH_OPTIONS as opt (opt.id)} + + {/each} +
+
+ {/if} + {#if activeView} {#snippet trailing()} - {#each expandedPages as page (page.id)} + {#each expandedPages as page, pageIdx (page.id)} + {@const config = getCustomPageConfig(page.id)} + {@const isCustom = page.id.startsWith('custom-')}
handlePageDragStart(e, page.id)} ondragover={handlePageDragOver} ondrop={(e) => handlePageDrop(e, page.id)} @@ -186,24 +289,37 @@ > handleRemovePage(page.id)} onMinimize={() => handleMinimizePage(page.id)} onMaximize={() => handleMaximizePage(page.id)} onRename={(name) => handleRenamePage(page.id, name)} + onUpdateConfig={isCustom + ? (data) => handleUpdateCustomPage(page.id, data) + : undefined} + onMoveLeft={editMode ? () => handleMovePageLeft(page.id) : undefined} + onMoveRight={editMode ? () => handleMovePageRight(page.id) : undefined} + onDelete={editMode ? () => handleDeletePage(page.id) : undefined} />
{/each} {#if expandedPages.length === 0} -
{#if showPagePicker} (showPagePicker = false)} + onCreateCustom={handleCreateCustomPage} activePageIds={openPages.map((p) => p.id)} /> {:else} @@ -222,6 +338,7 @@ (showPagePicker = false)} + onCreateCustom={handleCreateCustomPage} activePageIds={openPages.map((p) => p.id)} />
@@ -237,6 +354,20 @@

Views werden geladen...

{/if} + + +