mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 20:46:41 +02:00
feat(todo/web): add page maximize/minimize/close controls and default todo page
- Show "To Do" page by default on app load - Add fullscreen (maximize) button to TodoPage with centered content - Consistent button order across all states: Minimize → Fullscreen → Close - Add maximize/restore/close buttons to minimized page tabs - Use ArrowLineUp icon for restore in tabs to differentiate from minimize - Larger tap targets for tab action buttons (24px) - Centered empty state when no pages are open with "Seite hinzufügen" button - Wire maximize through MinimizedPagesContext for tab-to-fullscreen flow Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bd67e8d20b
commit
c81b636f2f
9 changed files with 809 additions and 87 deletions
|
|
@ -3,7 +3,7 @@
|
|||
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, DotsSixVertical } from '@manacore/shared-icons';
|
||||
import { X, Circle, Minus, DotsSixVertical, CornersOut, CornersIn } from '@manacore/shared-icons';
|
||||
import KanbanTaskCard from '../kanban/KanbanTaskCard.svelte';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
import { todoSettings } from '$lib/stores/settings.svelte';
|
||||
|
|
@ -11,12 +11,22 @@
|
|||
interface Props {
|
||||
pageId: string;
|
||||
title?: string;
|
||||
maximized?: boolean;
|
||||
onClose: () => void;
|
||||
onMinimize?: () => void;
|
||||
onMaximize?: () => void;
|
||||
onRename?: (name: string) => void;
|
||||
}
|
||||
|
||||
let { pageId, title: customTitle, onClose, onMinimize, onRename }: Props = $props();
|
||||
let {
|
||||
pageId,
|
||||
title: customTitle,
|
||||
maximized = false,
|
||||
onClose,
|
||||
onMinimize,
|
||||
onMaximize,
|
||||
onRename,
|
||||
}: Props = $props();
|
||||
|
||||
const tasksCtx: { readonly value: Task[] } = getContext('tasks');
|
||||
|
||||
|
|
@ -175,7 +185,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="todo-page" style="width: {sheetWidth}">
|
||||
<div class="todo-page" class:maximized style="width: {maximized ? '100%' : sheetWidth}">
|
||||
<div class="drag-handle-bar">
|
||||
<span class="drag-handle">
|
||||
<DotsSixVertical size={14} />
|
||||
|
|
@ -204,6 +214,19 @@
|
|||
<Minus size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if onMaximize}
|
||||
<button
|
||||
class="header-btn"
|
||||
onclick={onMaximize}
|
||||
title={maximized ? 'Verkleinern' : 'Maximieren'}
|
||||
>
|
||||
{#if maximized}
|
||||
<CornersIn size={14} />
|
||||
{:else}
|
||||
<CornersOut size={14} />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<button class="header-btn" onclick={onClose} title="Seite schließen">
|
||||
<X size={14} />
|
||||
</button>
|
||||
|
|
@ -295,6 +318,42 @@
|
|||
0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.todo-page.maximized {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
width: 100% !important;
|
||||
min-height: 100vh;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
animation: fadeInScale 0.2s ease-out;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.todo-page.maximized .page-header,
|
||||
.todo-page.maximized .page-body {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.todo-page.maximized .page-header {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.todo-page.maximized .page-body {
|
||||
margin: 0 auto;
|
||||
}
|
||||
@keyframes fadeInScale {
|
||||
from {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.97);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* Minimized pages context — layout owns the state, page reads/writes via context.
|
||||
* Minimized pages context — shared between layout and page via Svelte context.
|
||||
*
|
||||
* Layout calls `createMinimizedPagesContext()` + `setContext`.
|
||||
* Page calls `getContext('minimizedPages')` to get the same object.
|
||||
* Layout creates the context, renders MinimizedTabs using its reactive state.
|
||||
* Page registers callbacks (restore/remove/togglePicker) and syncs its openPages.
|
||||
*/
|
||||
import type { MinimizedPage } from '@manacore/shared-ui';
|
||||
|
||||
|
|
@ -18,16 +18,39 @@ export const PAGE_META: Record<string, { title: string; color: string }> = {
|
|||
};
|
||||
|
||||
export interface MinimizedPagesContext {
|
||||
/** Reactive list of minimized pages (read by layout for rendering) */
|
||||
readonly pages: MinimizedPage[];
|
||||
/** Whether there are any minimized pages */
|
||||
readonly hasPages: boolean;
|
||||
/** Called by page to sync its openPages state */
|
||||
/** Sync open pages state from page component */
|
||||
sync(openPages: { id: string; minimized: boolean }[]): void;
|
||||
/** Called by page on unmount */
|
||||
/** Clear all pages (called on page unmount) */
|
||||
clear(): void;
|
||||
/** Restore a minimized page — delegates to registered callback */
|
||||
restore(pageId: string): void;
|
||||
/** Remove a page — delegates to registered callback */
|
||||
remove(pageId: string): void;
|
||||
/** Maximize a minimized page — delegates to registered callback */
|
||||
maximize(pageId: string): void;
|
||||
/** Toggle page picker — delegates to registered callback */
|
||||
togglePicker(): void;
|
||||
/** Page registers its handlers here */
|
||||
registerHandlers(handlers: {
|
||||
restore: (id: string) => void;
|
||||
remove: (id: string) => void;
|
||||
maximize: (id: string) => void;
|
||||
togglePicker: () => void;
|
||||
}): void;
|
||||
}
|
||||
|
||||
export function createMinimizedPagesContext(): MinimizedPagesContext {
|
||||
let pages = $state<MinimizedPage[]>([]);
|
||||
let handlers = $state<{
|
||||
restore: (id: string) => void;
|
||||
remove: (id: string) => void;
|
||||
maximize: (id: string) => void;
|
||||
togglePicker: () => void;
|
||||
} | null>(null);
|
||||
|
||||
return {
|
||||
get pages() {
|
||||
|
|
@ -36,7 +59,7 @@ export function createMinimizedPagesContext(): MinimizedPagesContext {
|
|||
get hasPages() {
|
||||
return pages.length > 0;
|
||||
},
|
||||
sync(openPages: { id: string; minimized: boolean }[]) {
|
||||
sync(openPages) {
|
||||
pages = openPages
|
||||
.filter((p) => p.minimized)
|
||||
.map((p) => {
|
||||
|
|
@ -46,6 +69,22 @@ export function createMinimizedPagesContext(): MinimizedPagesContext {
|
|||
},
|
||||
clear() {
|
||||
pages = [];
|
||||
handlers = null;
|
||||
},
|
||||
restore(pageId) {
|
||||
handlers?.restore(pageId);
|
||||
},
|
||||
remove(pageId) {
|
||||
handlers?.remove(pageId);
|
||||
},
|
||||
maximize(pageId) {
|
||||
handlers?.maximize(pageId);
|
||||
},
|
||||
togglePicker() {
|
||||
handlers?.togglePicker();
|
||||
},
|
||||
registerHandlers(h) {
|
||||
handlers = h;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
ImmersiveModeToggle,
|
||||
BottomStack,
|
||||
MinimizedTabs,
|
||||
NotificationBar,
|
||||
} from '@manacore/shared-ui';
|
||||
import type { BottomNotification } from '@manacore/shared-ui';
|
||||
import {
|
||||
SplitPaneContainer,
|
||||
setSplitPanelContext,
|
||||
|
|
@ -52,14 +54,18 @@
|
|||
SessionExpiredBanner,
|
||||
AuthGate,
|
||||
GuestWelcomeModal,
|
||||
GuestRegistrationNudge,
|
||||
startGuestSession,
|
||||
shouldShowGuestNudge,
|
||||
dismissGuestNudge,
|
||||
} from '@manacore/shared-auth-ui';
|
||||
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
import { UserPlus } from '@manacore/shared-icons';
|
||||
import { TodoEvents } from '@manacore/shared-utils/analytics';
|
||||
import { todoStore, taskCollection } from '$lib/data/local-store';
|
||||
import type { LocalBoardView } from '$lib/data/local-store';
|
||||
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)
|
||||
|
|
@ -119,6 +125,48 @@
|
|||
},
|
||||
});
|
||||
|
||||
// ── Bottom notifications ────────────────────────────────
|
||||
let bottomNotifications = $state<BottomNotification[]>([]);
|
||||
let guestNudgeInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function initGuestNudge() {
|
||||
if (authStore.isAuthenticated) return;
|
||||
startGuestSession('todo');
|
||||
|
||||
function checkNudge() {
|
||||
if (shouldShowGuestNudge('todo', 3)) {
|
||||
bottomNotifications = [
|
||||
{
|
||||
id: 'guest-nudge',
|
||||
message:
|
||||
($locale || 'de') === 'de'
|
||||
? 'Gefällt es dir? Sichere deine Daten geräteübergreifend.'
|
||||
: 'Like what you see? Save your data across devices.',
|
||||
type: 'info' as const,
|
||||
action: {
|
||||
label: ($locale || 'de') === 'de' ? 'Konto erstellen' : 'Create account',
|
||||
icon: UserPlus,
|
||||
onClick: () => {
|
||||
dismissGuestNudge('todo');
|
||||
bottomNotifications = [];
|
||||
goto('/register');
|
||||
},
|
||||
},
|
||||
dismissible: true,
|
||||
onDismiss: () => {
|
||||
dismissGuestNudge('todo');
|
||||
bottomNotifications = bottomNotifications.filter((n) => n.id !== 'guest-nudge');
|
||||
},
|
||||
},
|
||||
];
|
||||
if (guestNudgeInterval) clearInterval(guestNudgeInterval);
|
||||
}
|
||||
}
|
||||
|
||||
checkNudge();
|
||||
guestNudgeInterval = setInterval(checkNudge, 30_000);
|
||||
}
|
||||
|
||||
// Guest welcome modal state
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
|
|
@ -200,12 +248,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Bottom bar visibility (controlled by FAB)
|
||||
let isBottomBarsVisible = $state(true);
|
||||
|
||||
// FilterStrip visibility (toggle via Filter button in PillNav)
|
||||
let isFilterStripVisible = $derived(!todoSettings.filterStripCollapsed);
|
||||
|
||||
// BottomStack computes these offsets automatically
|
||||
let pillNavOffset = $state('0px');
|
||||
let tagStripOffset = $state('72px');
|
||||
let fabOffset = $state('20px');
|
||||
|
||||
// Use theme store's isDark directly
|
||||
let isDark = $derived(theme.isDark);
|
||||
|
|
@ -314,6 +366,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
function handleBottomBarsToggle() {
|
||||
isBottomBarsVisible = !isBottomBarsVisible;
|
||||
}
|
||||
|
||||
function handleToggleTheme() {
|
||||
theme.toggleMode();
|
||||
}
|
||||
|
|
@ -377,6 +433,9 @@
|
|||
// Show guest welcome modal on first visit
|
||||
initGuestWelcome();
|
||||
|
||||
// Start guest registration nudge timer
|
||||
initGuestNudge();
|
||||
|
||||
// Tags and projects are now loaded reactively via useLiveQuery — no fetch needed
|
||||
|
||||
// User settings need auth
|
||||
|
|
@ -415,10 +474,11 @@
|
|||
{#if !todoSettings.immersiveModeEnabled}
|
||||
<!-- BottomStack: computes offsets for the entire bottom bar stack -->
|
||||
<BottomStack
|
||||
pillNavVisible={true}
|
||||
tagStripVisible={isFilterStripVisible}
|
||||
pillNavVisible={isBottomBarsVisible}
|
||||
tagStripVisible={isBottomBarsVisible && isFilterStripVisible}
|
||||
bind:pillNavOffset
|
||||
bind:tagStripOffset
|
||||
bind:fabOffset
|
||||
>
|
||||
<MinimizedTabs
|
||||
pages={minimizedPages.pages}
|
||||
|
|
@ -427,6 +487,9 @@
|
|||
onMaximize={(id) => minimizedPages.maximize(id)}
|
||||
onAdd={() => minimizedPages.togglePicker()}
|
||||
/>
|
||||
{#snippet top()}
|
||||
<NotificationBar notifications={bottomNotifications} />
|
||||
{/snippet}
|
||||
</BottomStack>
|
||||
|
||||
<!-- 1. QuickInputBar (always at bottom) -->
|
||||
|
|
@ -444,52 +507,71 @@
|
|||
deferSearch={true}
|
||||
locale={$locale || 'de'}
|
||||
appIcon="todo"
|
||||
hasFabRight={true}
|
||||
bottomOffset="16px"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- 2. PillNav (above InputBar, always visible) -->
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Todo"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={authStore.isAuthenticated}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#8b5cf6"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
themesHref="/themes"
|
||||
spiralHref="/spiral"
|
||||
helpHref="/help"
|
||||
onOpenInPanel={handleOpenInPanel}
|
||||
ariaLabel="Hauptnavigation"
|
||||
{spotlightActions}
|
||||
bottomOffset={pillNavOffset}
|
||||
/>
|
||||
<!-- 2. PillNav + TagStrip (toggled by FAB) -->
|
||||
{#if isBottomBarsVisible}
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Todo"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={authStore.isAuthenticated}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#8b5cf6"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
themesHref="/themes"
|
||||
spiralHref="/spiral"
|
||||
helpHref="/help"
|
||||
onOpenInPanel={handleOpenInPanel}
|
||||
ariaLabel="Hauptnavigation"
|
||||
{spotlightActions}
|
||||
bottomOffset={pillNavOffset}
|
||||
/>
|
||||
|
||||
<!-- 3. TagStrip (above PillNav) -->
|
||||
{#if isFilterStripVisible}
|
||||
<TagStrip bottomOffset={tagStripOffset} />
|
||||
<!-- 3. TagStrip (above PillNav) -->
|
||||
{#if isFilterStripVisible}
|
||||
<TagStrip bottomOffset={tagStripOffset} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- FAB to toggle bottom bars -->
|
||||
<button
|
||||
class="pillnav-fab"
|
||||
style="--fab-bottom: {fabOffset}"
|
||||
onclick={handleBottomBarsToggle}
|
||||
title={isBottomBarsVisible ? 'Navigation ausblenden' : 'Navigation anzeigen'}
|
||||
aria-label={isBottomBarsVisible ? 'Navigation ausblenden' : 'Navigation anzeigen'}
|
||||
data-umami-event="bottom-bars-toggle"
|
||||
>
|
||||
{#if isBottomBarsVisible}
|
||||
<X size={20} weight="bold" />
|
||||
{:else}
|
||||
<List size={20} weight="bold" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- DnD: floating preview + action zones -->
|
||||
<DragPreview />
|
||||
<ActionZone
|
||||
|
|
@ -543,13 +625,6 @@
|
|||
|
||||
{#if authStore.isAuthenticated}
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
|
||||
{:else}
|
||||
<GuestRegistrationNudge
|
||||
appId="todo"
|
||||
onRegister={() => goto('/register')}
|
||||
locale={($locale || 'de') === 'de' ? 'de' : 'en'}
|
||||
delayMinutes={3}
|
||||
/>
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
||||
|
|
@ -620,4 +695,42 @@
|
|||
padding-bottom: calc(100px + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
||||
/* FAB to toggle bottom bars — next to QuickInputBar */
|
||||
.pillnav-fab {
|
||||
position: fixed;
|
||||
bottom: calc(var(--fab-bottom, 20px) + env(safe-area-inset-bottom, 0px));
|
||||
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);
|
||||
box-shadow: var(--shadow-xl);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1002;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.pillnav-fab {
|
||||
left: auto;
|
||||
right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.pillnav-fab:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.pillnav-fab:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.pillnav-fab :global(svg) {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -6,50 +6,38 @@
|
|||
import { Plus } from '@manacore/shared-icons';
|
||||
import PagePicker from '$lib/components/pages/PagePicker.svelte';
|
||||
import TodoPage from '$lib/components/pages/TodoPage.svelte';
|
||||
import { minimizedPagesStore } from '$lib/stores/minimized-pages.svelte';
|
||||
import type { MinimizedPagesContext } from '$lib/stores/minimized-pages.svelte';
|
||||
|
||||
// Get active view from layout context
|
||||
const activeViewCtx: { readonly value: LocalBoardView | null } = getContext('activeView');
|
||||
const minimizedPages: MinimizedPagesContext = getContext('minimizedPages');
|
||||
|
||||
let activeView = $derived(activeViewCtx.value);
|
||||
let pageTitle = $derived(activeView?.name ?? 'Aufgaben');
|
||||
|
||||
// ── Pages ───────────────────────────────────────────────
|
||||
let showPagePicker = $state(false);
|
||||
let openPages = $state<{ id: string; minimized: boolean; customTitle?: string }[]>([
|
||||
{ id: 'todo', minimized: false },
|
||||
]);
|
||||
let openPages = $state<
|
||||
{ id: string; minimized: boolean; maximized?: boolean; customTitle?: string }[]
|
||||
>([{ id: 'todo', minimized: false }]);
|
||||
|
||||
let expandedPages = $derived(openPages.filter((p) => !p.minimized));
|
||||
|
||||
// Sync minimized pages to shared store so layout can render tabs
|
||||
// Sync minimized pages to layout via context
|
||||
$effect(() => {
|
||||
minimizedPagesStore.set(openPages);
|
||||
minimizedPages.sync(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);
|
||||
};
|
||||
// Register handlers so layout can delegate tab actions back to us
|
||||
minimizedPages.registerHandlers({
|
||||
restore: (id) => handleRestorePage(id),
|
||||
remove: (id) => handleRemovePage(id),
|
||||
maximize: (id) => handleMaximizePage(id),
|
||||
togglePicker: () => togglePagePicker(),
|
||||
});
|
||||
|
||||
onDestroy(() => minimizedPages.clear());
|
||||
|
||||
function handleAddPage(pageId: string) {
|
||||
if (!openPages.some((p) => p.id === pageId)) {
|
||||
openPages = [...openPages, { id: pageId, minimized: false }];
|
||||
|
|
@ -75,6 +63,12 @@
|
|||
openPages = openPages.map((p) => (p.id === pageId ? { ...p, customTitle: name } : p));
|
||||
}
|
||||
|
||||
function handleMaximizePage(pageId: string) {
|
||||
openPages = openPages.map((p) =>
|
||||
p.id === pageId ? { ...p, maximized: !p.maximized, minimized: false } : p
|
||||
);
|
||||
}
|
||||
|
||||
// ── Page drag reorder ───────────────────────────────────
|
||||
let dragPageId = $state<string | null>(null);
|
||||
|
||||
|
|
@ -193,15 +187,37 @@
|
|||
<TodoPage
|
||||
pageId={page.id}
|
||||
title={page.customTitle}
|
||||
maximized={page.maximized}
|
||||
onClose={() => handleRemovePage(page.id)}
|
||||
onMinimize={() => handleMinimizePage(page.id)}
|
||||
onMaximize={() => handleMaximizePage(page.id)}
|
||||
onRename={(name) => handleRenamePage(page.id, name)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<!-- Page picker -->
|
||||
{#if showPagePicker}
|
||||
<!-- Page picker / add button -->
|
||||
{#if expandedPages.length === 0}
|
||||
<!-- Wrapper matches sheet width so fokus-track centering works -->
|
||||
<div class="empty-pages-wrapper">
|
||||
{#if showPagePicker}
|
||||
<PagePicker
|
||||
onSelect={handleAddPage}
|
||||
onClose={() => (showPagePicker = false)}
|
||||
activePageIds={openPages.map((p) => p.id)}
|
||||
/>
|
||||
{:else}
|
||||
<button
|
||||
class="neue-seite-card alone"
|
||||
onclick={togglePagePicker}
|
||||
title="Neue Seite hinzufügen"
|
||||
>
|
||||
<Plus size={24} />
|
||||
<span class="neue-seite-label">Seite hinzufügen</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if showPagePicker}
|
||||
<div bind:this={pagePickerEl}>
|
||||
<PagePicker
|
||||
onSelect={handleAddPage}
|
||||
|
|
@ -243,8 +259,10 @@
|
|||
width: 48px;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
border: 2px dashed rgba(0, 0, 0, 0.08);
|
||||
border-radius: 0.375rem;
|
||||
background: transparent;
|
||||
|
|
@ -252,6 +270,21 @@
|
|||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.empty-pages-wrapper {
|
||||
flex: 0 0 auto;
|
||||
/* Match the sheet width from fokus-track so centering padding works */
|
||||
width: var(--sheet-width, min(480px, 85vw));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.neue-seite-card.alone {
|
||||
width: 100%;
|
||||
min-height: 60vh;
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.neue-seite-card:hover {
|
||||
border-color: var(--color-primary, #8b5cf6);
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
|
|
@ -261,12 +294,22 @@
|
|||
border-color: rgba(255, 255, 255, 0.06);
|
||||
color: #4b5563;
|
||||
}
|
||||
:global(.dark) .neue-seite-card.alone {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: #6b7280;
|
||||
}
|
||||
: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);
|
||||
}
|
||||
|
||||
.neue-seite-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue