mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
feat(todo/web): add secondary pages system with centered layout
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) <noreply@anthropic.com>
This commit is contained in:
parent
d3807b4bea
commit
85257212af
5 changed files with 697 additions and 20 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { getContext, type Snippet } from 'svelte';
|
||||
import type { Task } from '@todo/shared';
|
||||
import type { LocalBoardView } from '$lib/data/local-store';
|
||||
import { groupTasksByView, getDropActionUpdate } from '$lib/data/view-grouping';
|
||||
|
|
@ -15,7 +15,9 @@
|
|||
onColumnColorChange?: (colIdx: number, color: string) => void;
|
||||
onColumnMove?: (colIdx: number, dir: -1 | 1) => void;
|
||||
onColumnDelete?: (colIdx: number) => void;
|
||||
onColumnClose?: (colIdx: number) => void;
|
||||
onAddColumn?: () => void;
|
||||
trailing?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -25,7 +27,9 @@
|
|||
onColumnColorChange,
|
||||
onColumnMove,
|
||||
onColumnDelete,
|
||||
onColumnClose,
|
||||
onAddColumn,
|
||||
trailing,
|
||||
}: Props = $props();
|
||||
|
||||
let activeLayout = $derived(layoutOverride || view.layout);
|
||||
|
|
@ -89,7 +93,9 @@
|
|||
{onColumnColorChange}
|
||||
{onColumnMove}
|
||||
{onColumnDelete}
|
||||
{onColumnClose}
|
||||
{onAddColumn}
|
||||
{trailing}
|
||||
/>
|
||||
{:else if activeLayout === 'grid'}
|
||||
<GridLayout
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID, type DndEvent } from 'svelte-dnd-action';
|
||||
import { getContext } from 'svelte';
|
||||
import { getContext, type Snippet } from 'svelte';
|
||||
import { isToday } from 'date-fns';
|
||||
import type { Task } from '@todo/shared';
|
||||
import type { GroupedColumn } from '$lib/data/view-grouping';
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
import ViewColumnHeader from './ViewColumnHeader.svelte';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
import { todoSettings } from '$lib/stores/settings.svelte';
|
||||
import { X } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
columns: GroupedColumn[];
|
||||
|
|
@ -20,7 +21,9 @@
|
|||
onColumnColorChange?: (colIdx: number, color: string) => void;
|
||||
onColumnMove?: (colIdx: number, dir: -1 | 1) => void;
|
||||
onColumnDelete?: (colIdx: number) => void;
|
||||
onColumnClose?: (colIdx: number) => void;
|
||||
onAddColumn?: () => void;
|
||||
trailing?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -33,7 +36,9 @@
|
|||
onColumnColorChange,
|
||||
onColumnMove,
|
||||
onColumnDelete,
|
||||
onColumnClose,
|
||||
onAddColumn,
|
||||
trailing,
|
||||
}: Props = $props();
|
||||
|
||||
// Today's completed tasks — shown at the bottom of every sheet
|
||||
|
|
@ -130,17 +135,28 @@
|
|||
{#each columns as column, i (column.id)}
|
||||
{@const tasks = localTasksByColumn[column.id] || column.tasks}
|
||||
<div class="fokus-sheet" class:sheet-completed={column.name === 'Erledigt'}>
|
||||
<ViewColumnHeader
|
||||
name={column.name}
|
||||
color={column.color}
|
||||
taskCount={tasks.length}
|
||||
columnIndex={i}
|
||||
totalColumns={columns.length}
|
||||
onRename={onColumnRename ? (name) => onColumnRename(i, name) : undefined}
|
||||
onColorChange={onColumnColorChange ? (c) => onColumnColorChange(i, c) : undefined}
|
||||
onMove={onColumnMove ? (dir) => onColumnMove(i, dir) : undefined}
|
||||
onDelete={onColumnDelete ? () => onColumnDelete(i) : undefined}
|
||||
/>
|
||||
<div class="sheet-header-row">
|
||||
<ViewColumnHeader
|
||||
name={column.name}
|
||||
color={column.color}
|
||||
taskCount={tasks.length}
|
||||
columnIndex={i}
|
||||
totalColumns={columns.length}
|
||||
onRename={onColumnRename ? (name) => onColumnRename(i, name) : undefined}
|
||||
onColorChange={onColumnColorChange ? (c) => onColumnColorChange(i, c) : undefined}
|
||||
onMove={onColumnMove ? (dir) => onColumnMove(i, dir) : undefined}
|
||||
onDelete={onColumnDelete ? () => onColumnDelete(i) : undefined}
|
||||
/>
|
||||
{#if onColumnClose}
|
||||
<button
|
||||
class="sheet-close-btn"
|
||||
onclick={() => onColumnClose(i)}
|
||||
title="Seite schließen"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="sheet-body">
|
||||
<div
|
||||
|
|
@ -198,6 +214,11 @@
|
|||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Trailing content (Neue Seite, secondary pages) -->
|
||||
{#if trailing}
|
||||
{@render trailing()}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Page dots -->
|
||||
|
|
@ -220,8 +241,10 @@
|
|||
gap: 1.5rem;
|
||||
overflow-x: auto;
|
||||
scroll-snap-type: x mandatory;
|
||||
scroll-padding: 1.5rem;
|
||||
padding: 1rem 1.5rem 1rem;
|
||||
/* Centering padding: pushes first sheet to viewport center.
|
||||
Works like a carousel — padding is scrollable. */
|
||||
padding: 1rem calc(50% - var(--sheet-width) / 2);
|
||||
scroll-padding: calc(50% - var(--sheet-width) / 2);
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.fokus-track::-webkit-scrollbar {
|
||||
|
|
@ -251,6 +274,44 @@
|
|||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.sheet-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Let ViewColumnHeader fill available space */
|
||||
.sheet-header-row > :global(:first-child) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sheet-close-btn {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin-right: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #d1d5db;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.sheet-close-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #6b7280;
|
||||
}
|
||||
:global(.dark) .sheet-close-btn {
|
||||
color: #4b5563;
|
||||
}
|
||||
:global(.dark) .sheet-close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.sheet-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
|
|
|||
265
apps/todo/apps/web/src/lib/components/pages/PagePicker.svelte
Normal file
265
apps/todo/apps/web/src/lib/components/pages/PagePicker.svelte
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
CheckCircle,
|
||||
CalendarCheck,
|
||||
Warning,
|
||||
ListChecks,
|
||||
Flag,
|
||||
Calendar,
|
||||
TagSimple,
|
||||
X,
|
||||
} from '@manacore/shared-icons';
|
||||
|
||||
export interface PageOption {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: typeof CheckCircle;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onSelect: (pageId: string) => void;
|
||||
onClose: () => void;
|
||||
activePageIds?: string[];
|
||||
}
|
||||
|
||||
let { onSelect, onClose, activePageIds = [] }: Props = $props();
|
||||
|
||||
const PAGE_OPTIONS: PageOption[] = [
|
||||
{
|
||||
id: 'completed',
|
||||
title: 'Erledigt',
|
||||
description: 'Alle abgeschlossenen Aufgaben',
|
||||
icon: CheckCircle,
|
||||
color: '#22C55E',
|
||||
},
|
||||
{
|
||||
id: 'today',
|
||||
title: 'Heute',
|
||||
description: 'Fällig heute & überfällig',
|
||||
icon: CalendarCheck,
|
||||
color: '#F59E0B',
|
||||
},
|
||||
{
|
||||
id: 'overdue',
|
||||
title: 'Überfällig',
|
||||
description: 'Aufgaben nach Fälligkeitsdatum',
|
||||
icon: Warning,
|
||||
color: '#EF4444',
|
||||
},
|
||||
{
|
||||
id: 'all',
|
||||
title: 'Alle Aufgaben',
|
||||
description: 'Vollständige Aufgabenliste',
|
||||
icon: ListChecks,
|
||||
color: '#3B82F6',
|
||||
},
|
||||
{
|
||||
id: 'high-priority',
|
||||
title: 'Hohe Priorität',
|
||||
description: 'Dringend & hoch priorisiert',
|
||||
icon: Flag,
|
||||
color: '#EF4444',
|
||||
},
|
||||
{
|
||||
id: 'this-week',
|
||||
title: 'Diese Woche',
|
||||
description: 'Aufgaben der nächsten 7 Tage',
|
||||
icon: Calendar,
|
||||
color: '#8B5CF6',
|
||||
},
|
||||
{
|
||||
id: 'no-date',
|
||||
title: 'Ohne Datum',
|
||||
description: 'Aufgaben ohne Fälligkeitsdatum',
|
||||
icon: TagSimple,
|
||||
color: '#6B7280',
|
||||
},
|
||||
];
|
||||
|
||||
let availableOptions = $derived(PAGE_OPTIONS.filter((opt) => !activePageIds.includes(opt.id)));
|
||||
</script>
|
||||
|
||||
<div class="page-picker">
|
||||
<div class="picker-header">
|
||||
<h3 class="picker-title">Neue Seite</h3>
|
||||
<button class="close-btn" onclick={onClose} title="Schließen">
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="picker-list">
|
||||
{#each availableOptions as option, i (option.id)}
|
||||
{#if i > 0}
|
||||
<div class="divider"></div>
|
||||
{/if}
|
||||
<button class="page-option" onclick={() => onSelect(option.id)}>
|
||||
<div class="option-icon" style="color: {option.color}">
|
||||
<option.icon size={20} />
|
||||
</div>
|
||||
<div class="option-text">
|
||||
<span class="option-title">{option.title}</span>
|
||||
<span class="option-desc">{option.description}</span>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
{#if availableOptions.length === 0}
|
||||
<div class="empty-state">
|
||||
<p>Alle Seiten sind bereits geöffnet</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page-picker {
|
||||
flex: 0 0 auto;
|
||||
width: min(320px, 85vw);
|
||||
min-height: 60vh;
|
||||
scroll-snap-align: center;
|
||||
background: #fffef5;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.08),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: slideIn 0.25s ease-out;
|
||||
}
|
||||
:global(.dark) .page-picker {
|
||||
background-color: #252220;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.picker-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.picker-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
}
|
||||
:global(.dark) .picker-title {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.close-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.picker-list {
|
||||
flex: 1;
|
||||
padding: 0 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
:global(.dark) .divider {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.page-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 0.5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 0.375rem;
|
||||
transition: background 0.15s;
|
||||
text-align: left;
|
||||
}
|
||||
.page-option:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
:global(.dark) .page-option:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 0.5rem;
|
||||
background: color-mix(in srgb, currentColor 10%, transparent);
|
||||
}
|
||||
|
||||
.option-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.option-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .option-title {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.option-desc {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
:global(.dark) .option-desc {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 2rem 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.empty-state p {
|
||||
font-size: 0.8125rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
259
apps/todo/apps/web/src/lib/components/pages/SecondaryPage.svelte
Normal file
259
apps/todo/apps/web/src/lib/components/pages/SecondaryPage.svelte
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { isToday, isPast, startOfDay, addDays } from 'date-fns';
|
||||
import type { Task } from '@todo/shared';
|
||||
import { X } from '@manacore/shared-icons';
|
||||
import KanbanTaskCard from '../kanban/KanbanTaskCard.svelte';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
import { todoSettings } from '$lib/stores/settings.svelte';
|
||||
|
||||
interface Props {
|
||||
pageId: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { pageId, onClose }: Props = $props();
|
||||
|
||||
const tasksCtx: { readonly value: Task[] } = getContext('tasks');
|
||||
|
||||
const PAGE_META: Record<string, { title: string; color: string }> = {
|
||||
completed: { title: 'Erledigt', color: '#22C55E' },
|
||||
today: { title: 'Heute', color: '#F59E0B' },
|
||||
overdue: { title: 'Überfällig', color: '#EF4444' },
|
||||
all: { title: 'Alle Aufgaben', color: '#3B82F6' },
|
||||
'high-priority': { title: 'Hohe Priorität', color: '#EF4444' },
|
||||
'this-week': { title: 'Diese Woche', color: '#8B5CF6' },
|
||||
'no-date': { title: 'Ohne Datum', color: '#6B7280' },
|
||||
};
|
||||
|
||||
let meta = $derived(PAGE_META[pageId] ?? { title: pageId, color: '#6B7280' });
|
||||
|
||||
let filteredTasks = $derived.by(() => {
|
||||
const tasks = tasksCtx.value;
|
||||
const today = startOfDay(new Date());
|
||||
const weekEnd = addDays(today, 7);
|
||||
|
||||
switch (pageId) {
|
||||
case 'completed':
|
||||
return tasks.filter((t) => t.isCompleted);
|
||||
case 'today':
|
||||
return tasks.filter(
|
||||
(t) =>
|
||||
!t.isCompleted &&
|
||||
t.dueDate &&
|
||||
(isToday(new Date(t.dueDate)) || isPast(startOfDay(new Date(t.dueDate))))
|
||||
);
|
||||
case 'overdue':
|
||||
return tasks.filter(
|
||||
(t) =>
|
||||
!t.isCompleted &&
|
||||
t.dueDate &&
|
||||
isPast(startOfDay(new Date(t.dueDate))) &&
|
||||
!isToday(new Date(t.dueDate))
|
||||
);
|
||||
case 'all':
|
||||
return tasks;
|
||||
case 'high-priority':
|
||||
return tasks.filter(
|
||||
(t) => !t.isCompleted && (t.priority === 'urgent' || t.priority === 'high')
|
||||
);
|
||||
case 'this-week':
|
||||
return tasks.filter(
|
||||
(t) =>
|
||||
!t.isCompleted &&
|
||||
t.dueDate &&
|
||||
new Date(t.dueDate) <= weekEnd &&
|
||||
new Date(t.dueDate) >= today
|
||||
);
|
||||
case 'no-date':
|
||||
return tasks.filter((t) => !t.isCompleted && !t.dueDate);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
function handleToggle(task: Task) {
|
||||
if (task.isCompleted) {
|
||||
tasksStore.uncompleteTask(task.id);
|
||||
} else {
|
||||
tasksStore.completeTask(task.id);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(taskId: string) {
|
||||
tasksStore.deleteTask(taskId);
|
||||
}
|
||||
|
||||
function handleUpdate(taskId: string, data: Partial<Task>) {
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (data.title !== undefined) updateData.title = data.title;
|
||||
if (data.description !== undefined) updateData.description = data.description;
|
||||
if (data.priority !== undefined) updateData.priority = data.priority;
|
||||
if (data.dueDate !== undefined) {
|
||||
updateData.dueDate = data.dueDate instanceof Date ? data.dueDate.toISOString() : data.dueDate;
|
||||
}
|
||||
tasksStore.updateTask(taskId, updateData);
|
||||
}
|
||||
|
||||
const PAGE_WIDTH_MAP: Record<string, string> = {
|
||||
narrow: 'min(360px, 85vw)',
|
||||
medium: 'min(480px, 85vw)',
|
||||
wide: 'min(640px, 90vw)',
|
||||
full: 'min(840px, 95vw)',
|
||||
};
|
||||
|
||||
let sheetWidth = $derived(PAGE_WIDTH_MAP[todoSettings.pageWidth] || PAGE_WIDTH_MAP.medium);
|
||||
</script>
|
||||
|
||||
<div class="secondary-page" style="width: {sheetWidth}">
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<span class="color-dot" style="background-color: {meta.color}"></span>
|
||||
<span class="page-title">{meta.title}</span>
|
||||
<span class="task-count">{filteredTasks.length}</span>
|
||||
</div>
|
||||
<button class="close-btn" onclick={onClose} title="Seite schließen">
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="page-body">
|
||||
{#if filteredTasks.length === 0}
|
||||
<div class="empty-state">
|
||||
<p>Keine Aufgaben</p>
|
||||
</div>
|
||||
{:else}
|
||||
{#each filteredTasks as task (task.id)}
|
||||
<div class="task-card-wrapper">
|
||||
<KanbanTaskCard
|
||||
{task}
|
||||
onToggleComplete={() => handleToggle(task)}
|
||||
onSave={(data) => handleUpdate(task.id, data)}
|
||||
onDelete={() => handleDelete(task.id)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.secondary-page {
|
||||
flex: 0 0 auto;
|
||||
min-height: 60vh;
|
||||
scroll-snap-align: center;
|
||||
background: #fffef5;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.08),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: fadeIn 0.25s ease-out;
|
||||
}
|
||||
:global(.dark) .secondary-page {
|
||||
background-color: #252220;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.color-dot {
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
border-radius: 9999px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .page-title {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.task-count {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: #9ca3af;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
:global(.dark) .task-count {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 0.25rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.close-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.page-body {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.task-card-wrapper {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.task-card-wrapper:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 120px;
|
||||
}
|
||||
.empty-state p {
|
||||
font-size: 0.8125rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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<string[]>([]);
|
||||
|
||||
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<string, unknown>) {
|
||||
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()}
|
||||
<!-- Secondary Pages -->
|
||||
{#each openPages as pageId (pageId)}
|
||||
<SecondaryPage {pageId} onClose={() => handleRemovePage(pageId)} />
|
||||
{/each}
|
||||
|
||||
<!-- Neue Seite button (always last in track) -->
|
||||
{#if !editMode}
|
||||
{#if showPagePicker}
|
||||
<PagePicker
|
||||
onSelect={handleAddPage}
|
||||
onClose={() => (showPagePicker = false)}
|
||||
activePageIds={openPages}
|
||||
/>
|
||||
{:else}
|
||||
<button
|
||||
class="neue-seite-card"
|
||||
onclick={togglePagePicker}
|
||||
title="Neue Seite hinzufügen"
|
||||
>
|
||||
<Plus size={18} />
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</BoardViewRenderer>
|
||||
{:else}
|
||||
<div class="empty-state">
|
||||
<p class="text-muted-foreground">Views werden geladen...</p>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue