mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 03:01:09 +02:00
refactor(todo/web): remove edit mode, rename pages, add inline editing & drag reorder
- Remove edit mode entirely (toolbar, context, Layout pill in PillNav) - Remove onAddColumn/add-sheet from all layouts (FokusLayout, Grid, Kanban) - Rename SecondaryPage → TodoPage, secondaryPage → page (i18n keys, CSS) - ViewColumnHeader: always-on inline editing (click name, click color dot) - TodoPage: contenteditable title (invisible, no input styling) - TodoPage: drag handle bar for reorder via HTML5 drag & drop - FokusLayout: add matching drag handle to board sheets - Clean up dead spotlight actions (/today, /upcoming, /kanban) - Remove /statistics reference from QuickInputBar Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
990ade352f
commit
933715c7d9
14 changed files with 603 additions and 640 deletions
|
|
@ -16,7 +16,6 @@
|
|||
onColumnMove?: (colIdx: number, dir: -1 | 1) => void;
|
||||
onColumnDelete?: (colIdx: number) => void;
|
||||
onColumnClose?: (colIdx: number) => void;
|
||||
onAddColumn?: () => void;
|
||||
trailing?: Snippet;
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +27,6 @@
|
|||
onColumnMove,
|
||||
onColumnDelete,
|
||||
onColumnClose,
|
||||
onAddColumn,
|
||||
trailing,
|
||||
}: Props = $props();
|
||||
|
||||
|
|
@ -105,7 +103,6 @@
|
|||
{onColumnMove}
|
||||
{onColumnDelete}
|
||||
{onColumnClose}
|
||||
{onAddColumn}
|
||||
{trailing}
|
||||
/>
|
||||
{:else if activeLayout === 'grid'}
|
||||
|
|
@ -119,7 +116,6 @@
|
|||
{onColumnColorChange}
|
||||
{onColumnMove}
|
||||
{onColumnDelete}
|
||||
{onAddColumn}
|
||||
/>
|
||||
{:else}
|
||||
<KanbanLayout
|
||||
|
|
@ -132,6 +128,5 @@
|
|||
{onColumnColorChange}
|
||||
{onColumnMove}
|
||||
{onColumnDelete}
|
||||
{onAddColumn}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -9,7 +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';
|
||||
import { X, DotsSixVertical } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
columns: GroupedColumn[];
|
||||
|
|
@ -22,7 +22,6 @@
|
|||
onColumnMove?: (colIdx: number, dir: -1 | 1) => void;
|
||||
onColumnDelete?: (colIdx: number) => void;
|
||||
onColumnClose?: (colIdx: number) => void;
|
||||
onAddColumn?: () => void;
|
||||
trailing?: Snippet;
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +36,6 @@
|
|||
onColumnMove,
|
||||
onColumnDelete,
|
||||
onColumnClose,
|
||||
onAddColumn,
|
||||
trailing,
|
||||
}: Props = $props();
|
||||
|
||||
|
|
@ -135,6 +133,11 @@
|
|||
{#each columns as column, i (column.id)}
|
||||
{@const tasks = localTasksByColumn[column.id] || column.tasks}
|
||||
<div class="fokus-sheet" class:sheet-completed={column.name === 'Erledigt'}>
|
||||
<div class="drag-handle-bar">
|
||||
<span class="drag-handle">
|
||||
<DotsSixVertical size={14} />
|
||||
</span>
|
||||
</div>
|
||||
<div class="sheet-header-row">
|
||||
<ViewColumnHeader
|
||||
name={column.name}
|
||||
|
|
@ -206,16 +209,7 @@
|
|||
</div>
|
||||
{/each}
|
||||
|
||||
{#if onAddColumn}
|
||||
<div class="fokus-sheet add-sheet">
|
||||
<button class="add-sheet-btn" onclick={onAddColumn}>
|
||||
<span class="add-sheet-icon">+</span>
|
||||
<span class="add-sheet-label">Neues Board</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Trailing content (Neue Seite, secondary pages) -->
|
||||
<!-- Trailing content (pages) -->
|
||||
{#if trailing}
|
||||
{@render trailing()}
|
||||
{/if}
|
||||
|
|
@ -248,6 +242,36 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.drag-handle-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0.25rem 0 0;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 14px;
|
||||
color: #d1d5db;
|
||||
cursor: grab;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.drag-handle:hover {
|
||||
color: #9ca3af;
|
||||
}
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
:global(.dark) .drag-handle {
|
||||
color: #3f3b38;
|
||||
}
|
||||
:global(.dark) .drag-handle:hover {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.fokus-sheet {
|
||||
flex: 0 0 auto;
|
||||
width: var(--sheet-width, min(840px, 85vw));
|
||||
|
|
@ -337,42 +361,6 @@
|
|||
background: color-mix(in srgb, var(--color-primary) 4%, transparent);
|
||||
}
|
||||
|
||||
/* Add sheet */
|
||||
.add-sheet {
|
||||
border: 2px dashed color-mix(in srgb, var(--color-primary) 30%, transparent) !important;
|
||||
background: color-mix(in srgb, var(--color-primary) 2%, transparent) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.add-sheet:hover {
|
||||
border-color: var(--color-primary) !important;
|
||||
background: color-mix(in srgb, var(--color-primary) 6%, transparent) !important;
|
||||
}
|
||||
|
||||
.add-sheet-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.add-sheet-icon {
|
||||
font-size: 2rem;
|
||||
font-weight: 300;
|
||||
color: var(--color-primary);
|
||||
line-height: 1;
|
||||
}
|
||||
.add-sheet-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Heute erledigt section */
|
||||
.completed-today {
|
||||
padding: 0.75rem 1rem 1rem;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
onColumnColorChange?: (colIdx: number, color: string) => void;
|
||||
onColumnMove?: (colIdx: number, dir: -1 | 1) => void;
|
||||
onColumnDelete?: (colIdx: number) => void;
|
||||
onAddColumn?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -26,7 +25,6 @@
|
|||
onColumnColorChange,
|
||||
onColumnMove,
|
||||
onColumnDelete,
|
||||
onAddColumn,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
|
|
@ -48,15 +46,6 @@
|
|||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if onAddColumn}
|
||||
<div class="grid-cell">
|
||||
<button class="add-column-card" onclick={onAddColumn}>
|
||||
<span class="add-icon">+</span>
|
||||
<span class="add-label">Neues Board</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
onColumnColorChange?: (colIdx: number, color: string) => void;
|
||||
onColumnMove?: (colIdx: number, dir: -1 | 1) => void;
|
||||
onColumnDelete?: (colIdx: number) => void;
|
||||
onAddColumn?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -26,7 +25,6 @@
|
|||
onColumnColorChange,
|
||||
onColumnMove,
|
||||
onColumnDelete,
|
||||
onAddColumn,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
|
|
@ -48,15 +46,6 @@
|
|||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if onAddColumn}
|
||||
<div class="kanban-column-wrapper">
|
||||
<button class="add-column-card" onclick={onAddColumn}>
|
||||
<span class="add-column-icon">+</span>
|
||||
<span class="add-column-label">Neues Board</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { ArrowLeft, ArrowRight, Trash } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -26,11 +25,9 @@
|
|||
onDelete,
|
||||
}: Props = $props();
|
||||
|
||||
const editModeCtx: { readonly active: boolean } | undefined = getContext('editMode');
|
||||
let editMode = $derived(editModeCtx?.active ?? false);
|
||||
let editable = $derived(editMode && !!onRename);
|
||||
|
||||
let showColorPicker = $state(false);
|
||||
let isEditingName = $state(false);
|
||||
let editInputEl = $state<HTMLInputElement | null>(null);
|
||||
|
||||
const PRESET_COLORS = [
|
||||
'#EF4444',
|
||||
|
|
@ -50,6 +47,25 @@
|
|||
'#6B7280',
|
||||
'#334155',
|
||||
];
|
||||
|
||||
function startEditing() {
|
||||
if (!onRename) return;
|
||||
isEditingName = true;
|
||||
requestAnimationFrame(() => {
|
||||
editInputEl?.select();
|
||||
});
|
||||
}
|
||||
|
||||
function finishEditing() {
|
||||
isEditingName = false;
|
||||
}
|
||||
|
||||
function handleNameKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' || e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
finishEditing();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
|
|
@ -57,13 +73,12 @@
|
|||
<div class="picker-backdrop" onclick={() => (showColorPicker = false)}></div>
|
||||
{/if}
|
||||
|
||||
<div class="column-header" class:editing={editable}>
|
||||
{#if editable}
|
||||
<!-- Edit mode: same layout, color dot is clickable, name is input -->
|
||||
<div class="header-left">
|
||||
<div class="column-header">
|
||||
<div class="header-left">
|
||||
{#if onColorChange}
|
||||
<div class="color-dot-wrapper">
|
||||
<button
|
||||
class="color-dot editable"
|
||||
class="color-dot clickable"
|
||||
style="background-color: {color}"
|
||||
onclick={() => (showColorPicker = !showColorPicker)}
|
||||
title="Farbe ändern"
|
||||
|
|
@ -78,7 +93,7 @@
|
|||
class:active={color === c}
|
||||
style="background-color: {c}"
|
||||
onclick={() => {
|
||||
onColorChange?.(c);
|
||||
onColorChange(c);
|
||||
showColorPicker = false;
|
||||
}}
|
||||
></button>
|
||||
|
|
@ -89,56 +104,41 @@
|
|||
<input
|
||||
type="color"
|
||||
value={color}
|
||||
oninput={(e) => onColorChange?.(e.currentTarget.value)}
|
||||
oninput={(e) => onColorChange(e.currentTarget.value)}
|
||||
class="custom-color-input"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<span class="color-dot" style="background-color: {color}"></span>
|
||||
{/if}
|
||||
|
||||
{#if isEditingName && onRename}
|
||||
<input
|
||||
bind:this={editInputEl}
|
||||
class="name-input"
|
||||
type="text"
|
||||
value={name}
|
||||
oninput={(e) => onRename?.(e.currentTarget.value)}
|
||||
oninput={(e) => onRename(e.currentTarget.value)}
|
||||
onblur={finishEditing}
|
||||
onkeydown={handleNameKeydown}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="edit-actions">
|
||||
<button
|
||||
class="act-btn"
|
||||
onclick={() => onMove?.(-1)}
|
||||
disabled={columnIndex === 0}
|
||||
title="Nach links"
|
||||
{:else}
|
||||
<span
|
||||
class="column-name"
|
||||
class:editable={!!onRename}
|
||||
role={onRename ? 'button' : undefined}
|
||||
tabindex={onRename ? 0 : undefined}
|
||||
onclick={startEditing}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') startEditing();
|
||||
}}>{name}</span
|
||||
>
|
||||
<ArrowLeft size={12} />
|
||||
</button>
|
||||
<button
|
||||
class="act-btn"
|
||||
onclick={() => onMove?.(1)}
|
||||
disabled={columnIndex >= totalColumns - 1}
|
||||
title="Nach rechts"
|
||||
>
|
||||
<ArrowRight size={12} />
|
||||
</button>
|
||||
<button
|
||||
class="act-btn del-btn"
|
||||
onclick={() => onDelete?.()}
|
||||
disabled={totalColumns <= 1}
|
||||
title="Spalte löschen"
|
||||
>
|
||||
<Trash size={12} />
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Normal mode -->
|
||||
<div class="header-left">
|
||||
<span class="color-dot" style="background-color: {color}"></span>
|
||||
<span class="column-name">{name}</span>
|
||||
</div>
|
||||
<span class="task-count">{taskCount}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<span class="task-count">{taskCount}</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
@ -164,6 +164,27 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.color-dot-wrapper {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.color-dot.clickable {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
padding: 0;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.color-dot.clickable:hover {
|
||||
transform: scale(1.25);
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
:global(.dark) .color-dot.clickable:hover {
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.column-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
|
|
@ -172,11 +193,42 @@
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.column-name.editable {
|
||||
cursor: text;
|
||||
border-radius: 0.125rem;
|
||||
padding: 0.0625rem 0.25rem;
|
||||
margin: -0.0625rem -0.25rem;
|
||||
}
|
||||
.column-name.editable:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
:global(.dark) .column-name.editable:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
:global(.dark) .column-name {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.name-input {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(139, 92, 246, 0.4);
|
||||
padding: 0.125rem 0;
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
}
|
||||
.name-input:focus {
|
||||
border-bottom-color: #8b5cf6;
|
||||
}
|
||||
:global(.dark) .name-input {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.task-count {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
|
|
@ -192,80 +244,6 @@
|
|||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* ── Edit mode ────────────────────────────────────────── */
|
||||
|
||||
.color-dot-wrapper {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.color-dot.editable {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
padding: 0;
|
||||
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.5);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.color-dot.editable:hover {
|
||||
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.7);
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
.name-input {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(139, 92, 246, 0.3);
|
||||
padding: 0.125rem 0;
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
}
|
||||
.name-input:focus {
|
||||
border-bottom-color: #8b5cf6;
|
||||
}
|
||||
:global(.dark) .name-input {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.edit-actions {
|
||||
display: flex;
|
||||
gap: 0.125rem;
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.375rem;
|
||||
}
|
||||
|
||||
.act-btn {
|
||||
padding: 0.2rem;
|
||||
border-radius: 0.25rem;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
background: transparent;
|
||||
border: none;
|
||||
line-height: 0;
|
||||
}
|
||||
.act-btn:hover:not(:disabled) {
|
||||
color: #374151;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.act-btn:disabled {
|
||||
opacity: 0.25;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
:global(.dark) .act-btn:hover:not(:disabled) {
|
||||
color: #f3f4f6;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.del-btn:hover:not(:disabled) {
|
||||
color: #ef4444 !important;
|
||||
background: rgba(239, 68, 68, 0.1) !important;
|
||||
}
|
||||
|
||||
/* ── Color Picker Popup ──────────────────────────────── */
|
||||
|
||||
.picker-backdrop {
|
||||
|
|
|
|||
|
|
@ -3,21 +3,45 @@
|
|||
import { isToday, isPast, startOfDay, addDays, subHours, format } from 'date-fns';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { Task } from '@todo/shared';
|
||||
import { X, Circle, Minus } from '@manacore/shared-icons';
|
||||
import { X, Circle, Minus, DotsSixVertical } 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;
|
||||
title?: string;
|
||||
onClose: () => void;
|
||||
onMinimize?: () => void;
|
||||
onRename?: (name: string) => void;
|
||||
}
|
||||
|
||||
let { pageId, onClose, onMinimize }: Props = $props();
|
||||
let { pageId, title: customTitle, onClose, onMinimize, onRename }: Props = $props();
|
||||
|
||||
const tasksCtx: { readonly value: Task[] } = getContext('tasks');
|
||||
|
||||
let titleEl = $state<HTMLSpanElement | null>(null);
|
||||
let isTitleFocused = $state(false);
|
||||
|
||||
// Set initial text content without reactive binding (avoids cursor jump)
|
||||
$effect(() => {
|
||||
if (titleEl && !isTitleFocused) {
|
||||
titleEl.textContent = displayTitle;
|
||||
}
|
||||
});
|
||||
|
||||
function handleTitleInput() {
|
||||
const text = titleEl?.textContent?.trim() ?? '';
|
||||
if (text && onRename) onRename(text);
|
||||
}
|
||||
|
||||
function handleTitleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
(e.target as HTMLElement).blur();
|
||||
}
|
||||
}
|
||||
|
||||
const PAGE_META: Record<string, { title: string; color: string }> = {
|
||||
todo: { title: 'To Do', color: '#6B7280' },
|
||||
completed: { title: 'Erledigt', color: '#22C55E' },
|
||||
|
|
@ -29,7 +53,8 @@
|
|||
'no-date': { title: 'Ohne Datum', color: '#6B7280' },
|
||||
};
|
||||
|
||||
let meta = $derived(PAGE_META[pageId] ?? { title: pageId, color: '#6B7280' });
|
||||
let pageMeta = $derived(PAGE_META[pageId] ?? { title: pageId, color: '#6B7280' });
|
||||
let displayTitle = $derived(customTitle ?? pageMeta.title);
|
||||
|
||||
let filteredTasks = $derived.by(() => {
|
||||
const tasks = tasksCtx.value;
|
||||
|
|
@ -127,9 +152,9 @@
|
|||
const time = format(date, 'HH:mm');
|
||||
if (pageId === 'completed') {
|
||||
const dateStr = format(date, 'dd.MM.');
|
||||
return $t('secondaryPage.completedAtDateTime', { values: { date: dateStr, time } });
|
||||
return $t('page.completedAtDateTime', { values: { date: dateStr, time } });
|
||||
}
|
||||
return $t('secondaryPage.completedAtTime', { values: { time } });
|
||||
return $t('page.completedAtTime', { values: { time } });
|
||||
}
|
||||
|
||||
let newTaskTitle = $state('');
|
||||
|
|
@ -150,11 +175,27 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="secondary-page" style="width: {sheetWidth}">
|
||||
<div class="page-header">
|
||||
<div class="todo-page" style="width: {sheetWidth}">
|
||||
<div class="drag-handle-bar">
|
||||
<span class="drag-handle">
|
||||
<DotsSixVertical size={14} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="page-header" ondragstart={(e) => e.preventDefault()}>
|
||||
<div class="header-left">
|
||||
<span class="color-dot" style="background-color: {meta.color}"></span>
|
||||
<span class="page-title">{meta.title}</span>
|
||||
<span class="color-dot" style="background-color: {pageMeta.color}"></span>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<span
|
||||
bind:this={titleEl}
|
||||
class="page-title"
|
||||
contenteditable={!!onRename}
|
||||
oninput={handleTitleInput}
|
||||
onkeydown={handleTitleKeydown}
|
||||
onfocus={() => (isTitleFocused = true)}
|
||||
onblur={() => (isTitleFocused = false)}
|
||||
></span>
|
||||
<span class="task-count">{filteredTasks.length}</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
|
|
@ -169,7 +210,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-body">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="page-body" ondragstart={(e) => e.preventDefault()}>
|
||||
{#if pageId === 'completed'}
|
||||
{#each filteredTasks as task (task.id)}
|
||||
<div class="task-card-wrapper completed-task">
|
||||
|
|
@ -198,7 +240,7 @@
|
|||
|
||||
{#if recentlyCompleted.length > 0}
|
||||
<div class="completed-section">
|
||||
<span class="completed-label">{$t('secondaryPage.recentlyCompleted')}</span>
|
||||
<span class="completed-label">{$t('page.recentlyCompleted')}</span>
|
||||
{#each recentlyCompleted as task (task.id)}
|
||||
<div class="task-card-wrapper completed-task">
|
||||
<KanbanTaskCard
|
||||
|
|
@ -223,7 +265,7 @@
|
|||
bind:this={inputEl}
|
||||
bind:value={newTaskTitle}
|
||||
class="inline-create-input"
|
||||
placeholder={$t('secondaryPage.newTaskPlaceholder')}
|
||||
placeholder={$t('page.newTaskPlaceholder')}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') handleInlineCreate();
|
||||
}}
|
||||
|
|
@ -234,7 +276,7 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.secondary-page {
|
||||
.todo-page {
|
||||
flex: 0 0 auto;
|
||||
min-height: 60vh;
|
||||
background: #fffef5;
|
||||
|
|
@ -246,13 +288,43 @@
|
|||
flex-direction: column;
|
||||
animation: fadeIn 0.25s ease-out;
|
||||
}
|
||||
:global(.dark) .secondary-page {
|
||||
:global(.dark) .todo-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);
|
||||
}
|
||||
|
||||
.drag-handle-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0.25rem 0 0;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 14px;
|
||||
color: #d1d5db;
|
||||
cursor: grab;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.drag-handle:hover {
|
||||
color: #9ca3af;
|
||||
}
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
:global(.dark) .drag-handle {
|
||||
color: #3f3b38;
|
||||
}
|
||||
:global(.dark) .drag-handle:hover {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
|
@ -288,6 +360,11 @@
|
|||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
outline: none;
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
.page-title[contenteditable='true'] {
|
||||
cursor: text;
|
||||
}
|
||||
:global(.dark) .page-title {
|
||||
color: #f3f4f6;
|
||||
|
|
@ -225,7 +225,7 @@
|
|||
"taskOptions": "Aufgaben-Optionen",
|
||||
"switchView": "Ansicht wechseln"
|
||||
},
|
||||
"secondaryPage": {
|
||||
"page": {
|
||||
"recentlyCompleted": "Kürzlich erledigt",
|
||||
"completedAtTime": "{time} Uhr",
|
||||
"completedAtDateTime": "{date}, {time} Uhr",
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@
|
|||
"taskOptions": "Task options",
|
||||
"switchView": "Switch view"
|
||||
},
|
||||
"secondaryPage": {
|
||||
"page": {
|
||||
"recentlyCompleted": "Recently completed",
|
||||
"completedAtTime": "{time}",
|
||||
"completedAtDateTime": "{date}, {time}",
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@
|
|||
"taskOptions": "Opciones de tareas",
|
||||
"switchView": "Cambiar vista"
|
||||
},
|
||||
"secondaryPage": {
|
||||
"page": {
|
||||
"recentlyCompleted": "Completadas recientemente",
|
||||
"completedAtTime": "{time}",
|
||||
"completedAtDateTime": "{date}, {time}",
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@
|
|||
"taskOptions": "Options des tâches",
|
||||
"switchView": "Changer de vue"
|
||||
},
|
||||
"secondaryPage": {
|
||||
"page": {
|
||||
"recentlyCompleted": "Récemment terminées",
|
||||
"completedAtTime": "{time} h",
|
||||
"completedAtDateTime": "{date}, {time} h",
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@
|
|||
"taskOptions": "Opzioni attività",
|
||||
"switchView": "Cambia vista"
|
||||
},
|
||||
"secondaryPage": {
|
||||
"page": {
|
||||
"recentlyCompleted": "Completate di recente",
|
||||
"completedAtTime": "ore {time}",
|
||||
"completedAtDateTime": "{date}, ore {time}",
|
||||
|
|
|
|||
51
apps/todo/apps/web/src/lib/stores/minimized-pages.svelte.ts
Normal file
51
apps/todo/apps/web/src/lib/stores/minimized-pages.svelte.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Minimized pages context — layout owns the state, page reads/writes via context.
|
||||
*
|
||||
* Layout calls `createMinimizedPagesContext()` + `setContext`.
|
||||
* Page calls `getContext('minimizedPages')` to get the same object.
|
||||
*/
|
||||
import type { MinimizedPage } from '@manacore/shared-ui';
|
||||
|
||||
export const PAGE_META: Record<string, { title: string; color: string }> = {
|
||||
todo: { title: 'To Do', color: '#6B7280' },
|
||||
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' },
|
||||
};
|
||||
|
||||
export interface MinimizedPagesContext {
|
||||
readonly pages: MinimizedPage[];
|
||||
readonly hasPages: boolean;
|
||||
/** Called by page to sync its openPages state */
|
||||
sync(openPages: { id: string; minimized: boolean }[]): void;
|
||||
/** Called by page on unmount */
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
export function createMinimizedPagesContext(): MinimizedPagesContext {
|
||||
let pages = $state<MinimizedPage[]>([]);
|
||||
|
||||
return {
|
||||
get pages() {
|
||||
return pages;
|
||||
},
|
||||
get hasPages() {
|
||||
return pages.length > 0;
|
||||
},
|
||||
sync(openPages: { id: string; minimized: boolean }[]) {
|
||||
pages = openPages
|
||||
.filter((p) => p.minimized)
|
||||
.map((p) => {
|
||||
const meta = PAGE_META[p.id] ?? { title: p.id, color: '#6B7280' };
|
||||
return { id: p.id, title: meta.title, color: meta.color };
|
||||
});
|
||||
},
|
||||
clear() {
|
||||
pages = [];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -3,7 +3,13 @@
|
|||
import { page } from '$app/stores';
|
||||
import { setContext } from 'svelte';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { PillNavigation, QuickInputBar, ImmersiveModeToggle } from '@manacore/shared-ui';
|
||||
import {
|
||||
PillNavigation,
|
||||
QuickInputBar,
|
||||
ImmersiveModeToggle,
|
||||
BottomStack,
|
||||
MinimizedTabs,
|
||||
} from '@manacore/shared-ui';
|
||||
import {
|
||||
SplitPaneContainer,
|
||||
setSplitPanelContext,
|
||||
|
|
@ -54,6 +60,7 @@
|
|||
import { useAllTasks, useAllBoardViews } from '$lib/data/task-queries';
|
||||
import SyncIndicator from '$lib/components/SyncIndicator.svelte';
|
||||
import { List, X } from '@manacore/shared-icons';
|
||||
import { createMinimizedPagesContext } from '$lib/stores/minimized-pages.svelte';
|
||||
|
||||
// Live queries — auto-update when IndexedDB changes (local writes, sync, other tabs)
|
||||
const allTasks = useAllTasks();
|
||||
|
|
@ -86,20 +93,6 @@
|
|||
},
|
||||
});
|
||||
|
||||
// Edit mode state — shared between layout (PillNav button) and page (editor)
|
||||
let editMode = $state(false);
|
||||
setContext('editMode', {
|
||||
get active() {
|
||||
return editMode;
|
||||
},
|
||||
toggle() {
|
||||
editMode = !editMode;
|
||||
},
|
||||
set(val: boolean) {
|
||||
editMode = val;
|
||||
},
|
||||
});
|
||||
|
||||
// Guest welcome modal state
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
|
|
@ -187,6 +180,10 @@
|
|||
// FilterStrip visibility (toggle via Filter button in PillNav)
|
||||
let isFilterStripVisible = $derived(!todoSettings.filterStripCollapsed);
|
||||
|
||||
// Minimized page tabs add extra height to the bottom bar stack
|
||||
let hasMinimizedTabs = $derived(minimizedPagesStore.hasPages);
|
||||
const MINIMIZED_TABS_HEIGHT = 36; // px
|
||||
|
||||
// Use theme store's isDark directly
|
||||
let isDark = $derived(theme.isDark);
|
||||
|
||||
|
|
@ -242,7 +239,7 @@
|
|||
// Keep navRoutes for keyboard shortcuts (Ctrl+1-3)
|
||||
const viewRoutes: Record<string, string> = { fokus: '/' };
|
||||
|
||||
// Tags and Layout stay as standalone pills (toggle behavior, not navigation)
|
||||
// Tags pill (toggle behavior, not navigation)
|
||||
let baseNavItems = $derived<PillNavItem[]>([
|
||||
{
|
||||
href: '/',
|
||||
|
|
@ -251,19 +248,6 @@
|
|||
onClick: handleTagStripToggle,
|
||||
active: isFilterStripVisible,
|
||||
},
|
||||
...($page.url.pathname === '/' || $page.url.pathname === ''
|
||||
? [
|
||||
{
|
||||
href: '/',
|
||||
label: editMode ? 'Fertig' : 'Layout',
|
||||
icon: editMode ? 'check' : 'grid',
|
||||
onClick: () => {
|
||||
editMode = !editMode;
|
||||
},
|
||||
active: editMode,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
|
||||
// Navigation items filtered by visibility settings (with fallback for guest mode)
|
||||
|
|
@ -341,25 +325,18 @@
|
|||
category: 'Erstellen',
|
||||
onExecute: () => goto('/'),
|
||||
},
|
||||
{ id: 'today', label: 'Heute', category: 'Navigation', onExecute: () => goto('/today') },
|
||||
{
|
||||
id: 'upcoming',
|
||||
label: 'Demnächst',
|
||||
category: 'Navigation',
|
||||
onExecute: () => goto('/upcoming'),
|
||||
},
|
||||
{
|
||||
id: 'kanban',
|
||||
label: 'Kanban Board',
|
||||
category: 'Navigation',
|
||||
onExecute: () => goto('/kanban'),
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'Einstellungen',
|
||||
category: 'Navigation',
|
||||
onExecute: () => goto('/settings'),
|
||||
},
|
||||
{
|
||||
id: 'tags',
|
||||
label: 'Tags verwalten',
|
||||
category: 'Navigation',
|
||||
onExecute: () => goto('/tags'),
|
||||
},
|
||||
];
|
||||
|
||||
async function handleAuthReady() {
|
||||
|
|
@ -476,8 +453,54 @@
|
|||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Minimized Page Tabs (between PillNav and QuickInputBar) -->
|
||||
{#if hasMinimizedTabs}
|
||||
<div
|
||||
class="minimized-tabs-bar"
|
||||
style="--tabs-bottom: {(() => {
|
||||
let offset = 16;
|
||||
if (!isPillNavCollapsed) offset += 68;
|
||||
if (!isPillNavCollapsed && isFilterStripVisible) offset += 50;
|
||||
return `${offset}px`;
|
||||
})()}"
|
||||
>
|
||||
<div class="minimized-tabs-inner">
|
||||
{#each minimizedPagesStore.pages as pg (pg.id)}
|
||||
<div
|
||||
class="minimized-tab"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={() => {
|
||||
window.dispatchEvent(new CustomEvent('restore-page', { detail: pg.id }));
|
||||
}}
|
||||
>
|
||||
<span class="minimized-tab-dot" style="background-color: {pg.color}"></span>
|
||||
<span class="minimized-tab-title">{pg.title}</span>
|
||||
<button
|
||||
class="minimized-tab-close"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.dispatchEvent(new CustomEvent('remove-page', { detail: pg.id }));
|
||||
}}
|
||||
title="Schließen"
|
||||
>
|
||||
<X size={10} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
<button
|
||||
class="minimized-tab-add"
|
||||
onclick={() => window.dispatchEvent(new CustomEvent('toggle-page-picker'))}
|
||||
title="Neue Seite hinzufügen"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Global Quick Input Bar -->
|
||||
{#if $page.url.pathname === '/' || $page.url.pathname === '/statistics'}
|
||||
{#if $page.url.pathname === '/'}
|
||||
<QuickInputBar
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
|
|
@ -492,13 +515,26 @@
|
|||
locale={$locale || 'de'}
|
||||
appIcon="todo"
|
||||
hasFabRight={true}
|
||||
bottomOffset={isPillNavCollapsed ? '16px' : isFilterStripVisible ? '180px' : '110px'}
|
||||
bottomOffset={(() => {
|
||||
let offset = 16;
|
||||
if (!isPillNavCollapsed) offset += 68;
|
||||
if (!isPillNavCollapsed && isFilterStripVisible) offset += 50;
|
||||
if (hasMinimizedTabs) offset += MINIMIZED_TABS_HEIGHT;
|
||||
return `${offset}px`;
|
||||
})()}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- FAB to toggle PillNav visibility -->
|
||||
<button
|
||||
class="pillnav-fab"
|
||||
style="--fab-bottom: {(() => {
|
||||
let offset = 20;
|
||||
if (!isPillNavCollapsed) offset += 68;
|
||||
if (!isPillNavCollapsed && isFilterStripVisible) offset += 50;
|
||||
if (hasMinimizedTabs) offset += MINIMIZED_TABS_HEIGHT;
|
||||
return `${offset}px`;
|
||||
})()}"
|
||||
onclick={handlePillNavToggle}
|
||||
title={isPillNavCollapsed ? 'Navigation anzeigen' : 'Navigation ausblenden'}
|
||||
aria-label={isPillNavCollapsed ? 'Navigation anzeigen' : 'Navigation ausblenden'}
|
||||
|
|
@ -506,10 +542,10 @@
|
|||
>
|
||||
{#if isPillNavCollapsed}
|
||||
<!-- Menu icon -->
|
||||
<List size={20} class="fab-icon" />
|
||||
<List size="48" weight="bold" />
|
||||
{:else}
|
||||
<!-- Close icon -->
|
||||
<X size={20} class="fab-icon" />
|
||||
<X size="48" weight="bold" />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
|
@ -635,13 +671,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* FAB to toggle PillNav */
|
||||
/* FAB to toggle PillNav — sits right next to the centered QuickInputBar */
|
||||
.pillnav-fab {
|
||||
position: fixed;
|
||||
bottom: calc(16px + env(safe-area-inset-bottom, 0px));
|
||||
right: 1rem;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
bottom: calc(var(--fab-bottom, 16px) + env(safe-area-inset-bottom, 0px));
|
||||
/* Anchor to center, then offset by half of InputBar max-width (350px) + gap */
|
||||
left: calc(50% + 350px + 0.75rem);
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-surface-elevated-2);
|
||||
border: 1px solid var(--color-border);
|
||||
|
|
@ -654,6 +691,14 @@
|
|||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* On narrower screens, FAB sits at the right edge of the padded input area */
|
||||
@media (max-width: 900px) {
|
||||
.pillnav-fab {
|
||||
left: auto;
|
||||
right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.pillnav-fab:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
|
@ -662,9 +707,112 @@
|
|||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
.pillnav-fab :global(svg) {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
/* ── Minimized Page Tabs Bar ─────────────────────────── */
|
||||
.minimized-tabs-bar {
|
||||
position: fixed;
|
||||
bottom: calc(var(--tabs-bottom, 16px) + env(safe-area-inset-bottom, 0px));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.minimized-tabs-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
background: var(--color-surface-elevated, #fffef5);
|
||||
border: 1px solid var(--color-border, rgba(0, 0, 0, 0.12));
|
||||
border-radius: 0.625rem;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
:global(.dark) .minimized-tabs-inner {
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.minimized-tabs-inner::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.minimized-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0.3rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
font-family: inherit;
|
||||
}
|
||||
.minimized-tab:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
:global(.dark) .minimized-tab:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.minimized-tab-dot {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 9999px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.minimized-tab-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-muted-foreground, #6b7280);
|
||||
}
|
||||
|
||||
.minimized-tab-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-muted-foreground, #d1d5db);
|
||||
border-radius: 0.125rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: all 0.15s;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.minimized-tab-close:hover {
|
||||
opacity: 1;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
:global(.dark) .minimized-tab-close:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.minimized-tab-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 0.3rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-muted-foreground, #9ca3af);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.15s;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.minimized-tab-add:hover {
|
||||
opacity: 1;
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,34 +1,59 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { getContext, onDestroy } from 'svelte';
|
||||
import type { LocalBoardView } from '$lib/data/local-store';
|
||||
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, Minus, X } from '@manacore/shared-icons';
|
||||
import { Plus } from '@manacore/shared-icons';
|
||||
import PagePicker from '$lib/components/pages/PagePicker.svelte';
|
||||
import SecondaryPage from '$lib/components/pages/SecondaryPage.svelte';
|
||||
import TodoPage from '$lib/components/pages/TodoPage.svelte';
|
||||
import { minimizedPagesStore } from '$lib/stores/minimized-pages.svelte';
|
||||
|
||||
// Get active view + edit mode from layout context
|
||||
// Get active view from layout context
|
||||
const activeViewCtx: { readonly value: LocalBoardView | null } = getContext('activeView');
|
||||
const editModeCtx: { readonly active: boolean; toggle(): void; set(val: boolean): void } =
|
||||
getContext('editMode');
|
||||
|
||||
let editMode = $derived(editModeCtx.active);
|
||||
let activeView = $derived(activeViewCtx.value);
|
||||
let pageTitle = $derived(activeView?.name ?? 'Aufgaben');
|
||||
|
||||
// ── Secondary Pages ─────────────────────────────────────
|
||||
// ── Pages ───────────────────────────────────────────────
|
||||
let showPagePicker = $state(false);
|
||||
let openPages = $state<{ id: string; minimized: boolean }[]>([]);
|
||||
let openPages = $state<{ id: string; minimized: boolean; customTitle?: string }[]>([
|
||||
{ id: 'todo', minimized: false },
|
||||
]);
|
||||
|
||||
let expandedPages = $derived(openPages.filter((p) => !p.minimized));
|
||||
let minimizedPages = $derived(openPages.filter((p) => p.minimized));
|
||||
|
||||
// Sync minimized pages to shared store so layout can render tabs
|
||||
$effect(() => {
|
||||
minimizedPagesStore.set(openPages);
|
||||
});
|
||||
onDestroy(() => minimizedPagesStore.clear());
|
||||
|
||||
// Listen for events from layout's minimized tab bar
|
||||
function onRestorePage(e: Event) {
|
||||
handleRestorePage((e as CustomEvent).detail);
|
||||
}
|
||||
function onRemovePage(e: Event) {
|
||||
handleRemovePage((e as CustomEvent).detail);
|
||||
}
|
||||
function onTogglePagePicker() {
|
||||
togglePagePicker();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
window.addEventListener('restore-page', onRestorePage);
|
||||
window.addEventListener('remove-page', onRemovePage);
|
||||
window.addEventListener('toggle-page-picker', onTogglePagePicker);
|
||||
return () => {
|
||||
window.removeEventListener('restore-page', onRestorePage);
|
||||
window.removeEventListener('remove-page', onRemovePage);
|
||||
window.removeEventListener('toggle-page-picker', onTogglePagePicker);
|
||||
};
|
||||
});
|
||||
|
||||
function handleAddPage(pageId: string) {
|
||||
if (!openPages.some((p) => p.id === pageId)) {
|
||||
openPages = [...openPages, { id: pageId, minimized: false }];
|
||||
} else {
|
||||
// Restore if minimized
|
||||
openPages = openPages.map((p) => (p.id === pageId ? { ...p, minimized: false } : p));
|
||||
}
|
||||
showPagePicker = false;
|
||||
|
|
@ -46,6 +71,44 @@
|
|||
openPages = openPages.map((p) => (p.id === pageId ? { ...p, minimized: false } : p));
|
||||
}
|
||||
|
||||
function handleRenamePage(pageId: string, name: string) {
|
||||
openPages = openPages.map((p) => (p.id === pageId ? { ...p, customTitle: name } : p));
|
||||
}
|
||||
|
||||
// ── Page drag reorder ───────────────────────────────────
|
||||
let dragPageId = $state<string | null>(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;
|
||||
}
|
||||
|
|
@ -56,33 +119,7 @@
|
|||
updateView({ columns });
|
||||
}
|
||||
|
||||
// ── Edit helpers ────────────────────────────────────────
|
||||
|
||||
const GROUPBY_OPTIONS = [
|
||||
{ value: 'status', label: 'Status' },
|
||||
{ value: 'priority', label: 'Priorität' },
|
||||
{ value: 'dueDate', label: 'Fälligkeit' },
|
||||
{ value: 'custom', label: 'Benutzerdefiniert' },
|
||||
];
|
||||
|
||||
const WIDTH_OPTIONS: { value: PageWidth; label: string }[] = [
|
||||
{ value: 'narrow', label: 'S' },
|
||||
{ value: 'medium', label: 'M' },
|
||||
{ value: 'wide', label: 'L' },
|
||||
{ value: 'full', label: 'XL' },
|
||||
];
|
||||
|
||||
const COLUMN_COLORS = [
|
||||
'#EF4444',
|
||||
'#F59E0B',
|
||||
'#22C55E',
|
||||
'#3B82F6',
|
||||
'#8B5CF6',
|
||||
'#EC4899',
|
||||
'#14B8A6',
|
||||
'#F97316',
|
||||
'#6B7280',
|
||||
];
|
||||
// ── Column helpers ──────────────────────────────────────
|
||||
|
||||
async function updateView(data: Partial<LocalBoardView>) {
|
||||
if (!activeView) return;
|
||||
|
|
@ -102,17 +139,6 @@
|
|||
updateView({ columns });
|
||||
}
|
||||
|
||||
function addColumn() {
|
||||
if (!activeView) return;
|
||||
const newCol = {
|
||||
id: `col-${crypto.randomUUID().slice(0, 8)}`,
|
||||
name: 'Neue Spalte',
|
||||
color: COLUMN_COLORS[activeView.columns.length % COLUMN_COLORS.length],
|
||||
match: { type: 'custom' as const, value: `custom-${Date.now()}` },
|
||||
};
|
||||
updateView({ columns: [...$state.snapshot(activeView.columns), newCol] });
|
||||
}
|
||||
|
||||
function moveColumn(colIdx: number, dir: -1 | 1) {
|
||||
if (!activeView) return;
|
||||
const cols = $state.snapshot(activeView.columns);
|
||||
|
|
@ -126,17 +152,6 @@
|
|||
activeView?.groupBy === 'status' || activeView?.groupBy === 'custom'
|
||||
);
|
||||
|
||||
const PAGE_META: Record<string, { title: string; color: string }> = {
|
||||
todo: { title: 'To Do', color: '#6B7280' },
|
||||
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 pagePickerEl = $state<HTMLDivElement | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
|
|
@ -144,95 +159,13 @@
|
|||
pagePickerEl.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
||||
}
|
||||
});
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && editMode) {
|
||||
editModeCtx.set(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<svelte:head>
|
||||
<title>{pageTitle} - Todo</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="board-page" class:edit-mode={editMode}>
|
||||
<!-- Edit Toolbar -->
|
||||
{#if editMode && activeView}
|
||||
<div class="edit-toolbar">
|
||||
<input
|
||||
class="edit-name-input"
|
||||
type="text"
|
||||
value={activeView.name}
|
||||
oninput={(e) => updateView({ name: e.currentTarget.value })}
|
||||
placeholder="View-Name"
|
||||
/>
|
||||
|
||||
<div class="edit-group">
|
||||
<span class="edit-label">Gruppierung</span>
|
||||
<div class="edit-pills">
|
||||
{#each GROUPBY_OPTIONS as opt}
|
||||
<button
|
||||
class="edit-pill"
|
||||
class:active={activeView.groupBy === opt.value}
|
||||
onclick={() => updateView({ groupBy: opt.value as LocalBoardView['groupBy'] })}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="edit-group">
|
||||
<span class="edit-label">Breite</span>
|
||||
<div class="edit-pills">
|
||||
{#each WIDTH_OPTIONS as opt}
|
||||
<button
|
||||
class="edit-pill"
|
||||
class:active={todoSettings.pageWidth === opt.value}
|
||||
onclick={() => todoSettings.set('pageWidth', opt.value)}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Minimized Page Tabs -->
|
||||
{#if minimizedPages.length > 0}
|
||||
<div class="minimized-tabs">
|
||||
{#each minimizedPages as page (page.id)}
|
||||
{@const meta = PAGE_META[page.id] ?? { title: page.id, color: '#6B7280' }}
|
||||
<div
|
||||
class="minimized-tab"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={() => handleRestorePage(page.id)}
|
||||
>
|
||||
<span class="minimized-tab-dot" style="background-color: {meta.color}"></span>
|
||||
<span class="minimized-tab-title">{meta.title}</span>
|
||||
<button
|
||||
class="minimized-tab-close"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemovePage(page.id);
|
||||
}}
|
||||
title="Schließen"
|
||||
>
|
||||
<X size={10} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
<button class="minimized-tab-add" onclick={togglePagePicker} title="Neue Seite hinzufügen">
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="board-page">
|
||||
<!-- Board Content -->
|
||||
{#if activeView}
|
||||
<BoardViewRenderer
|
||||
|
|
@ -243,37 +176,43 @@
|
|||
onColumnMove={columnsEditable ? moveColumn : undefined}
|
||||
onColumnDelete={columnsEditable ? removeColumn : undefined}
|
||||
onColumnClose={handleColumnClose}
|
||||
onAddColumn={columnsEditable && editMode ? addColumn : undefined}
|
||||
>
|
||||
{#snippet trailing()}
|
||||
<!-- Secondary Pages -->
|
||||
<!-- Pages -->
|
||||
{#each expandedPages as page (page.id)}
|
||||
<SecondaryPage
|
||||
pageId={page.id}
|
||||
onClose={() => handleRemovePage(page.id)}
|
||||
onMinimize={() => handleMinimizePage(page.id)}
|
||||
/>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="page-drag-wrapper"
|
||||
class:dragging={dragPageId === page.id}
|
||||
draggable="true"
|
||||
ondragstart={(e) => handlePageDragStart(e, page.id)}
|
||||
ondragover={handlePageDragOver}
|
||||
ondrop={(e) => handlePageDrop(e, page.id)}
|
||||
ondragend={handlePageDragEnd}
|
||||
>
|
||||
<TodoPage
|
||||
pageId={page.id}
|
||||
title={page.customTitle}
|
||||
onClose={() => handleRemovePage(page.id)}
|
||||
onMinimize={() => handleMinimizePage(page.id)}
|
||||
onRename={(name) => handleRenamePage(page.id, name)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<!-- Neue Seite button (always last in track) -->
|
||||
{#if !editMode}
|
||||
{#if showPagePicker}
|
||||
<div bind:this={pagePickerEl}>
|
||||
<PagePicker
|
||||
onSelect={handleAddPage}
|
||||
onClose={() => (showPagePicker = false)}
|
||||
activePageIds={openPages.map((p) => p.id)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="neue-seite-card"
|
||||
onclick={togglePagePicker}
|
||||
title="Neue Seite hinzufügen"
|
||||
>
|
||||
<Plus size={18} />
|
||||
</button>
|
||||
{/if}
|
||||
<!-- Page picker -->
|
||||
{#if showPagePicker}
|
||||
<div bind:this={pagePickerEl}>
|
||||
<PagePicker
|
||||
onSelect={handleAddPage}
|
||||
onClose={() => (showPagePicker = false)}
|
||||
activePageIds={openPages.map((p) => p.id)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<button class="neue-seite-card" onclick={togglePagePicker} title="Neue Seite hinzufügen">
|
||||
<Plus size={18} />
|
||||
</button>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</BoardViewRenderer>
|
||||
|
|
@ -291,6 +230,14 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.page-drag-wrapper {
|
||||
flex: 0 0 auto;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.page-drag-wrapper.dragging {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.neue-seite-card {
|
||||
flex: 0 0 auto;
|
||||
width: 48px;
|
||||
|
|
@ -320,209 +267,10 @@
|
|||
background: color-mix(in srgb, var(--color-primary, #8b5cf6) 8%, transparent);
|
||||
}
|
||||
|
||||
/* ── Minimized Tabs ──────────────────────────────────── */
|
||||
.minimized-tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 1.5rem 0.25rem;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.minimized-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.minimized-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.4rem 0.75rem 0.4rem 0.75rem;
|
||||
background: #fffef5;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.minimized-tab:hover {
|
||||
background: #fffdf0;
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
:global(.dark) .minimized-tab {
|
||||
background: #252220;
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
:global(.dark) .minimized-tab:hover {
|
||||
background: #2a2725;
|
||||
border-color: rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
|
||||
.minimized-tab-dot {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 9999px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.minimized-tab-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
}
|
||||
:global(.dark) .minimized-tab-title {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.minimized-tab-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #d1d5db;
|
||||
border-radius: 0.125rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.minimized-tab-close:hover {
|
||||
color: #6b7280;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
:global(.dark) .minimized-tab-close {
|
||||
color: #4b5563;
|
||||
}
|
||||
:global(.dark) .minimized-tab-close:hover {
|
||||
color: #9ca3af;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.minimized-tab-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px dashed rgba(0, 0, 0, 0.15);
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.minimized-tab-add: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) .minimized-tab-add {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: #4b5563;
|
||||
}
|
||||
:global(.dark) .minimized-tab-add: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;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ── Edit Toolbar ─────────────────────────────────────── */
|
||||
.edit-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: rgba(139, 92, 246, 0.06);
|
||||
border-bottom: 1px solid rgba(139, 92, 246, 0.15);
|
||||
flex-shrink: 0;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.edit-toolbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
:global(.dark) .edit-toolbar {
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
border-bottom-color: rgba(139, 92, 246, 0.2);
|
||||
}
|
||||
|
||||
.edit-name-input {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid rgba(139, 92, 246, 0.3);
|
||||
padding: 0.25rem 0;
|
||||
outline: none;
|
||||
min-width: 120px;
|
||||
max-width: 200px;
|
||||
}
|
||||
.edit-name-input:focus {
|
||||
border-bottom-color: #8b5cf6;
|
||||
}
|
||||
:global(.dark) .edit-name-input {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.edit-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.edit-label {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #6b7280;
|
||||
white-space: nowrap;
|
||||
}
|
||||
:global(.dark) .edit-label {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.edit-pills {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.edit-pill {
|
||||
padding: 0.25rem 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
:global(.dark) .edit-pill {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: #d1d5db;
|
||||
}
|
||||
.edit-pill:hover {
|
||||
border-color: rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
.edit-pill.active {
|
||||
background: #8b5cf6;
|
||||
border-color: #8b5cf6;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue