feat(manacore/web): add minimized tabs bar to todo pages

Wire up the minimize/restore/maximize flow for todo pages:
- Minimized pages appear as a floating tab bar at the bottom
- Each tab shows color dot + title, with maximize and close buttons
- Click tab title to restore, click arrows-out to maximize
- Tab bar has paper-sheet aesthetic matching the pages
- Plus button on tab bar opens page picker
- Tabs slide up with animation when pages are minimized

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-02 13:25:08 +02:00
parent 97730cd9f2
commit 509a541b70

View file

@ -13,7 +13,7 @@
tasksStore,
taskTable,
} from '$lib/modules/todo';
import { Plus, PencilSimple, X, Gear } from '@manacore/shared-icons';
import { Plus, PencilSimple, X, Gear, ArrowsOut } from '@manacore/shared-icons';
import { ShareModal } from '@manacore/shared-uload';
// Components
@ -103,6 +103,45 @@
let expandedPages = $derived(openPages.filter((p) => !p.minimized));
let customPages = $derived(todoSettings.customPages);
// Minimized pages for tab bar
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' },
};
const ICON_COLORS: Record<string, string> = {
warning: '#EF4444',
calendar: '#3B82F6',
'calendar-dots': '#8B5CF6',
check: '#22C55E',
star: '#F59E0B',
lightning: '#F97316',
clock: '#6B7280',
fire: '#EF4444',
leaf: '#22C55E',
heart: '#EC4899',
};
let minimizedPages = $derived(
openPages
.filter((p) => p.minimized)
.map((p) => {
const config = getCustomPageConfig(p.id);
const preset = PAGE_META[p.id];
const title = p.customTitle ?? config?.label ?? preset?.title ?? p.id;
const color = config?.icon
? (ICON_COLORS[config.icon] ?? '#8B5CF6')
: (preset?.color ?? '#6B7280');
return { id: p.id, title, color };
})
);
function handleAddPage(pageId: string) {
if (!openPages.some((p) => p.id === pageId)) {
openPages = [...openPages, { id: pageId, minimized: false }];
@ -120,6 +159,10 @@
openPages = openPages.map((p) => (p.id === pageId ? { ...p, minimized: true } : p));
}
function handleRestorePage(pageId: string) {
openPages = openPages.map((p) => (p.id === pageId ? { ...p, minimized: false } : p));
}
function handleMaximizePage(pageId: string) {
openPages = openPages.map((p) =>
p.id === pageId ? { ...p, maximized: !p.maximized, minimized: false } : p
@ -378,6 +421,33 @@
{/if}
</div>
<!-- Minimized tabs bar -->
{#if minimizedPages.length > 0}
<div class="minimized-tabs">
{#each minimizedPages as page (page.id)}
<div class="minimized-tab group">
<span class="tab-dot" style="background-color: {page.color}"></span>
<button class="tab-title" onclick={() => handleRestorePage(page.id)}>
{page.title}
</button>
<button
class="tab-maximize"
onclick={() => handleMaximizePage(page.id)}
title="Maximieren"
>
<ArrowsOut size={12} />
</button>
<button class="tab-close" onclick={() => handleRemovePage(page.id)} title="Schließen">
<X size={12} />
</button>
</div>
{/each}
<button class="tab-add" onclick={togglePagePicker} title="Seite hinzufügen">
<Plus size={14} />
</button>
</div>
{/if}
<!-- Edit FAB -->
<button
class="edit-fab"
@ -603,6 +673,129 @@
letter-spacing: 0.01em;
}
/* Minimized tabs */
.minimized-tabs {
position: fixed;
bottom: 4.5rem;
left: 50%;
transform: translateX(-50%);
z-index: 45;
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.375rem 0.5rem;
border-radius: 0.75rem;
background: #fffef5;
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
animation: slideUp 0.25s ease-out;
}
:global(.dark) .minimized-tabs {
background: #252220;
border-color: rgba(255, 255, 255, 0.08);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateX(-50%) translateY(12px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
.minimized-tab {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
transition: background 0.15s;
}
.minimized-tab:hover {
background: rgba(0, 0, 0, 0.04);
}
:global(.dark) .minimized-tab:hover {
background: rgba(255, 255, 255, 0.06);
}
.tab-dot {
width: 8px;
height: 8px;
border-radius: 9999px;
flex-shrink: 0;
}
.tab-title {
border: none;
background: none;
color: #374151;
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0;
}
.tab-title:hover {
color: var(--color-primary, #8b5cf6);
}
:global(.dark) .tab-title {
color: #e5e7eb;
}
:global(.dark) .tab-title:hover {
color: var(--color-primary, #8b5cf6);
}
.tab-maximize,
.tab-close {
border: none;
background: none;
color: #9ca3af;
cursor: pointer;
padding: 0.125rem;
border-radius: 0.25rem;
display: flex;
align-items: center;
opacity: 0;
transition: all 0.15s;
}
:global(.group:hover) .tab-maximize,
:global(.group:hover) .tab-close,
.minimized-tab:hover .tab-maximize,
.minimized-tab:hover .tab-close {
opacity: 1;
}
.tab-maximize:hover {
color: var(--color-primary, #8b5cf6);
background: rgba(139, 92, 246, 0.08);
}
.tab-close:hover {
color: #ef4444;
background: rgba(239, 68, 68, 0.08);
}
.tab-add {
border: none;
background: none;
color: #9ca3af;
cursor: pointer;
padding: 0.25rem;
border-radius: 0.25rem;
display: flex;
align-items: center;
transition: all 0.15s;
margin-left: 0.125rem;
}
.tab-add:hover {
color: var(--color-primary, #8b5cf6);
background: rgba(139, 92, 246, 0.08);
}
/* Edit FAB */
.edit-fab {
position: fixed;