diff --git a/apps/manacore/apps/web/src/lib/modules/todo/components/pages/PageEditBar.svelte b/apps/manacore/apps/web/src/lib/modules/todo/components/pages/PageEditBar.svelte new file mode 100644 index 000000000..bd95af9a8 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/todo/components/pages/PageEditBar.svelte @@ -0,0 +1,369 @@ + + +
+
+ {#each ICONS as icon (icon.id)} + + {/each} +
+ + + + {#if showFilters} +
+
+ Priorität +
+ {#each PRIORITIES as p (p.id)} + + {/each} +
+
+ +
+ Zeitraum +
+ {#each DATE_RANGES as dr (dr.id)} + + {/each} +
+
+ +
+ +
+
+ {/if} + +
+
+ {#if !isFirst && onMoveLeft} + + {/if} + {#if !isLast && onMoveRight} + + {/if} +
+ +
+
+ + diff --git a/apps/manacore/apps/web/src/lib/modules/todo/components/pages/PagePicker.svelte b/apps/manacore/apps/web/src/lib/modules/todo/components/pages/PagePicker.svelte new file mode 100644 index 000000000..51c89ba2d --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/todo/components/pages/PagePicker.svelte @@ -0,0 +1,255 @@ + + +
+
+

Neue Seite

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

Alle Seiten sind bereits geöffnet

+ {/if} +
+
+ + diff --git a/apps/manacore/apps/web/src/lib/modules/todo/components/pages/TodoPage.svelte b/apps/manacore/apps/web/src/lib/modules/todo/components/pages/TodoPage.svelte new file mode 100644 index 000000000..1e78a3134 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/todo/components/pages/TodoPage.svelte @@ -0,0 +1,606 @@ + + +
+
+ +
+ + {#if editMode && isCustom && customPageConfig && onUpdateConfig && onDelete} + + {/if} + + + + + +
e.preventDefault()}> + {#each openTasks as task (task.id)} +
+ tasksStore.toggleComplete(task.id)} + onClick={() => onOpenTask?.(task)} + onContextMenu={() => {}} + /> +
+ {/each} + + {#if recentlyCompleted.length > 0} +
+ Kürzlich erledigt + {#each recentlyCompleted as task (task.id)} +
+ onOpenTask(task) : undefined} + /> +
+ {/each} +
+ {/if} + + {#if !editMode && !showCompleted && pageId !== 'completed'} +
+ + { + if (e.key === 'Enter') handleInlineCreate(); + }} + /> +
+ {/if} +
+
+ + diff --git a/apps/manacore/apps/web/src/lib/modules/todo/stores/settings.svelte.ts b/apps/manacore/apps/web/src/lib/modules/todo/stores/settings.svelte.ts index f2837d6c8..261a4c244 100644 --- a/apps/manacore/apps/web/src/lib/modules/todo/stores/settings.svelte.ts +++ b/apps/manacore/apps/web/src/lib/modules/todo/stores/settings.svelte.ts @@ -12,6 +12,29 @@ export type KanbanCardSize = 'compact' | 'normal' | 'large'; export type LayoutMode = 'fokus' | 'uebersicht' | 'matrix'; export type PageWidth = 'narrow' | 'medium' | 'wide' | 'full'; +export type PageIcon = + | 'warning' + | 'calendar' + | 'calendar-dots' + | 'check' + | 'star' + | 'lightning' + | 'clock' + | 'fire' + | 'leaf' + | 'heart'; + +export interface PageConfig { + id: string; + label: string; + icon?: PageIcon; + filter: { + priorities?: ('low' | 'medium' | 'high' | 'urgent')[]; + completed?: boolean; + dateRange?: 'overdue' | 'today' | 'tomorrow' | 'upcoming' | 'any'; + }; +} + export interface TodoAppSettings extends Record { // Task Behavior defaultPriority: TaskPriority; @@ -57,6 +80,9 @@ export interface TodoAppSettings extends Record { // Page width pageWidth: PageWidth; + + // Custom pages + customPages: PageConfig[]; } const DEFAULT_SETTINGS: TodoAppSettings = { @@ -91,6 +117,7 @@ const DEFAULT_SETTINGS: TodoAppSettings = { filterStripCollapsed: false, activeLayoutMode: 'fokus' as LayoutMode, pageWidth: 'medium' as PageWidth, + customPages: [] as PageConfig[], }; const baseStore = createAppSettingsStore('todo-settings', DEFAULT_SETTINGS); @@ -149,6 +176,10 @@ export const todoSettings = { return baseStore.settings.filterStripCollapsed; }, + get customPages() { + return baseStore.settings.customPages; + }, + toggleFilterStrip() { baseStore.update({ filterStripCollapsed: !baseStore.settings.filterStripCollapsed }); }, diff --git a/apps/manacore/apps/web/src/routes/(app)/todo/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/todo/+page.svelte index e4f16890b..d3965d1c4 100644 --- a/apps/manacore/apps/web/src/routes/(app)/todo/+page.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/todo/+page.svelte @@ -12,54 +12,28 @@ type LocalTodoProject, tasksStore, taskTable, - viewStore, - filterIncomplete, - filterCompleted, - filterOverdue, - filterToday, - filterUpcoming, - filterByProject, - searchTasks, - sortTasks, - getTaskStats, } from '$lib/modules/todo'; - import { - Tray, - CalendarBlank, - CalendarCheck, - CheckCircle, - MagnifyingGlass, - Gear, - } from '@manacore/shared-icons'; + import { Plus, PencilSimple, X, Gear } from '@manacore/shared-icons'; import { ShareModal } from '@manacore/shared-uload'; // Components - import TaskList from '$lib/modules/todo/components/TaskList.svelte'; import TaskEditModal from '$lib/modules/todo/components/TaskEditModal.svelte'; import QuickAddTask from '$lib/modules/todo/components/QuickAddTask.svelte'; - import TodoToolbar from '$lib/modules/todo/components/TodoToolbar.svelte'; - import TagStrip from '$lib/modules/todo/components/TagStrip.svelte'; import SyncIndicator from '$lib/modules/todo/components/SyncIndicator.svelte'; import SyntaxHelpOverlay from '$lib/modules/todo/components/SyntaxHelpOverlay.svelte'; import OnboardingModal from '$lib/modules/todo/components/OnboardingModal.svelte'; - import { TaskListSkeleton, StatisticsSkeleton } from '$lib/modules/todo/components/skeletons'; - import { - BoardViewRenderer, - ViewSelector, - ViewEditorModal, - } from '$lib/modules/todo/components/board-views'; + import TodoPage from '$lib/modules/todo/components/pages/TodoPage.svelte'; + import PagePicker from '$lib/modules/todo/components/pages/PagePicker.svelte'; import { todoSettings } from '$lib/modules/todo/stores/settings.svelte'; + import type { PageConfig, PageWidth } from '$lib/modules/todo/stores/settings.svelte'; + import { getTaskStats } from '$lib/modules/todo'; // Get data from layout context const allTasks$: Observable = getContext('tasks'); const allLabels$: Observable = getContext('labels'); - const allProjects$: Observable = getContext('projects'); - const allBoardViews$: Observable = getContext('boardViews'); let allTasks = $state([]); let allLabels = $state([]); - let allProjects = $state([]); - let allBoardViews = $state([]); let isLoaded = $state(false); $effect(() => { @@ -75,69 +49,13 @@ return () => sub.unsubscribe(); }); - $effect(() => { - const sub = allProjects$.subscribe((p) => (allProjects = p)); - return () => sub.unsubscribe(); - }); - - $effect(() => { - const sub = allBoardViews$.subscribe((v) => (allBoardViews = v)); - return () => sub.unsubscribe(); - }); - - // Tags for resolving labelIds - const globalTags = useAllTags(); - const tagList = $derived( - (globalTags.value ?? []).map((t) => ({ id: t.id, name: t.name, color: t.color })) - ); - // Stats let stats = $derived(getTaskStats(allTasks)); - // Filtered tasks - let displayTasks = $derived.by(() => { - let tasks = allTasks; - switch (viewStore.currentView) { - case 'today': - tasks = [...filterOverdue(allTasks), ...filterToday(allTasks)]; - break; - case 'upcoming': - tasks = filterUpcoming(allTasks); - break; - case 'completed': - tasks = filterCompleted(allTasks); - break; - case 'search': - tasks = searchTasks(allTasks, viewStore.searchQuery); - break; - case 'label': - tasks = filterIncomplete(allTasks).filter((t) => { - const ids: string[] = (t.metadata as { labelIds?: string[] })?.labelIds ?? []; - return ids.includes(viewStore.currentLabelId ?? ''); - }); - break; - default: - tasks = filterIncomplete(allTasks); - } - if (viewStore.showCompleted && viewStore.currentView !== 'completed') { - tasks = [...tasks, ...filterCompleted(allTasks)]; - } - return sortTasks(tasks, viewStore.sortBy, viewStore.sortOrder); - }); - - // Board view state - let isBoardView = $state(false); - let activeBoardId = $state(null); - let activeBoard = $derived( - allBoardViews.find((v) => v.id === activeBoardId) ?? allBoardViews[0] ?? null - ); - // Modal states let editTask = $state(null); let shareTask = $state(null); let showSyntaxHelp = $state(false); - let showBoardEditor = $state(false); - let editBoardView = $state(null); let showOnboarding = $state(false); let shareUrl = $derived( @@ -146,7 +64,6 @@ : '' ); - // Check onboarding onMount(() => { try { if (!localStorage.getItem('todo-onboarding-done')) { @@ -163,7 +80,7 @@ onMount(() => { tagDropCtx?.set(async (tagId: string, payload: DragPayload) => { - const taskData = payload.data as TagDragData; + const taskData = payload.data as { id: string }; const task = await taskTable.get(taskData.id); if (!task) return; const currentLabels: string[] = (task.metadata as { labelIds?: string[] })?.labelIds ?? []; @@ -174,217 +91,306 @@ return () => tagDropCtx?.clear(); }); - // Board view callbacks - async function handleBoardQuickAdd(title: string, _columnId: string) { - await tasksStore.createTask({ title }); + // ── Edit mode ────────────────────────────────────────── + let editMode = $state(false); + + // ── Pages ─────────────────────────────────────────────── + let showPagePicker = $state(false); + let openPages = $state< + { id: string; minimized: boolean; maximized?: boolean; customTitle?: string }[] + >([{ id: 'todo', minimized: false }]); + + let expandedPages = $derived(openPages.filter((p) => !p.minimized)); + let customPages = $derived(todoSettings.customPages); + + function handleAddPage(pageId: string) { + if (!openPages.some((p) => p.id === pageId)) { + openPages = [...openPages, { id: pageId, minimized: false }]; + } else { + openPages = openPages.map((p) => (p.id === pageId ? { ...p, minimized: false } : p)); + } + showPagePicker = false; } - // View navigation - let views = $derived([ - { id: 'inbox', label: $_('todo.inbox'), icon: Tray }, - { id: 'today', label: $_('todo.todayView'), icon: CalendarBlank }, - { id: 'upcoming', label: $_('todo.upcoming'), icon: CalendarCheck }, - { id: 'completed', label: $_('todo.completedView'), icon: CheckCircle }, - ] as const); + function handleRemovePage(pageId: string) { + openPages = openPages.filter((p) => p.id !== pageId); + } + + function handleMinimizePage(pageId: string) { + openPages = openPages.map((p) => (p.id === pageId ? { ...p, minimized: true } : p)); + } + + function handleMaximizePage(pageId: string) { + openPages = openPages.map((p) => + p.id === pageId ? { ...p, maximized: !p.maximized, minimized: false } : p + ); + } + + function handleRenamePage(pageId: string, name: string) { + openPages = openPages.map((p) => (p.id === pageId ? { ...p, customTitle: name } : p)); + if (pageId.startsWith('custom-')) { + const updated = customPages.map((cp) => (cp.id === pageId ? { ...cp, label: name } : cp)); + todoSettings.update({ customPages: updated }); + } + } + + // ── 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; + 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) { + openPages = openPages.filter((p) => p.id !== pageId); + if (pageId.startsWith('custom-')) { + todoSettings.update({ customPages: customPages.filter((cp) => cp.id !== pageId) }); + } + } + + function getCustomPageConfig(pageId: string): PageConfig | undefined { + return customPages.find((cp) => cp.id === pageId); + } + + // ── Page reorder ──────────────────────────────────────── + 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); + + function handlePageDragStart(e: DragEvent, pageId: string) { + dragPageId = pageId; + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', pageId); + } + } + + function handlePageDragOver(e: DragEvent) { + if (!dragPageId) return; + e.preventDefault(); + if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'; + } + + function handlePageDrop(e: DragEvent, targetPageId: string) { + e.preventDefault(); + if (!dragPageId || dragPageId === targetPageId) return; + const fromIdx = openPages.findIndex((p) => p.id === dragPageId); + const toIdx = openPages.findIndex((p) => p.id === targetPageId); + if (fromIdx === -1 || toIdx === -1) return; + const pages = [...openPages]; + const [moved] = pages.splice(fromIdx, 1); + pages.splice(toIdx, 0, moved); + openPages = pages; + dragPageId = null; + } + + function handlePageDragEnd() { + dragPageId = null; + } + + function togglePagePicker() { + 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 }); + } + + const PAGE_WIDTH_MAP: Record = { + narrow: 'min(360px, 85vw)', + medium: 'min(480px, 85vw)', + wide: 'min(640px, 90vw)', + full: 'min(840px, 95vw)', + }; + + let sheetWidthVar = $derived(PAGE_WIDTH_MAP[todoSettings.pageWidth] || PAGE_WIDTH_MAP.medium); + + let pagePickerEl = $state(null); + + $effect(() => { + if (showPagePicker && pagePickerEl) { + pagePickerEl.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); + } + }); Todo - ManaCore -
+
-
+
-

{$_('todo.title')}

+

Todo

{#if isLoaded} -
+
{stats.total} {$_('todo.tasks')} {stats.completed} {$_('todo.completed')} {#if stats.overdue > 0} - {stats.overdue} {$_('todo.overdue')} - {/if} - {#if stats.today > 0} - {stats.today} {$_('todo.today')} + {stats.overdue} überfällig {/if}
- {:else} - {/if}
-
+
- -
-
- {#each views as view} - - {/each} -
- - 0} - {isBoardView} - onToggleBoard={() => (isBoardView = !isBoardView)} - /> + +
+ (showSyntaxHelp = true)} />
- - todoSettings.toggleFilterStrip()} - /> - - - {#if viewStore.currentView === 'search'} -
- - viewStore.updateSearchQuery(e.currentTarget.value)} - class="w-full rounded-lg border border-border bg-card py-2.5 pl-10 pr-4 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20" - autofocus - /> -
- {/if} - - - {#if isBoardView} -
- (activeBoardId = id)} - onCreateView={() => { - editBoardView = null; - showBoardEditor = true; - }} - /> -
- {/if} - - - {#if !isBoardView} - (showSyntaxHelp = true)} /> - {/if} - - - {#if !isLoaded} - - {:else if isBoardView && activeBoard} - tasksStore.toggleComplete(id)} - onSaveTask={(id, data) => tasksStore.updateTask(id, data)} - onDeleteTask={(id) => tasksStore.deleteTask(id)} - onQuickAdd={handleBoardQuickAdd} - onOpenTask={(task) => (editTask = task)} - /> - {:else if displayTasks.length === 0} -
-
- -
-

- {#if viewStore.currentView === 'completed'} - {$_('todo.noTasksCompleted')} - {:else if viewStore.currentView === 'today'} - {$_('todo.noTasksToday')} - {:else if viewStore.currentView === 'upcoming'} - {$_('todo.noTasksUpcoming')} - {:else if viewStore.currentView === 'search'} - {$_('todo.noTasks')} - {:else} - {$_('todo.noTasksInbox')} - {/if} -

-

- {#if viewStore.currentView === 'inbox'} - {$_('todo.firstTaskHint')} - {/if} -

-
- {:else} - (editTask = task)} - /> - -

- {displayTasks.length} - {$_('todo.tasks')} -

- {/if} - - - {#if !isBoardView && allProjects.length > 0} -
-

- {$_('todo.projects')} -

-
- {#each allProjects as project (project.id)} + + {#if editMode} +
+
+ {#each WIDTH_OPTIONS as opt (opt.id)} {/each}
{/if} + + +
+ {#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)} + ondragend={handlePageDragEnd} + > + 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} + onOpenTask={(task) => (editTask = task)} + /> +
+ {/each} + + + {#if expandedPages.length === 0} +
+ {#if showPagePicker} + (showPagePicker = false)} + onCreateCustom={handleCreateCustomPage} + activePageIds={openPages.map((p) => p.id)} + /> + {:else} + + {/if} +
+ {:else if showPagePicker} +
+ (showPagePicker = false)} + onCreateCustom={handleCreateCustomPage} + activePageIds={openPages.map((p) => p.id)} + /> +
+ {:else} + + {/if} +
+ + +
@@ -392,13 +398,6 @@ (editTask = null)} /> {/if} - - (showBoardEditor = false)} -/> - (showSyntaxHelp = false)} /> @@ -416,17 +415,257 @@ />