From 474ba93d70571f1628dcdc641e340049f32927d3 Mon Sep 17 00:00:00 2001 From: Till JS Date: Sun, 12 Apr 2026 14:15:47 +0200 Subject: [PATCH] feat(workbench): dynamic page height + tighter bottom-stack spacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PageShell cards now fill the available viewport between the workbench top padding and the bottom chrome instead of using a static 60vh. Height is calculated via two CSS vars published by the layout
: height: calc(100dvh - var(--bottom-chrome-height) - var(--workbench-reserved-y)) --bottom-chrome-height reacts to pill-nav collapse, tag strip toggle and bottom-bar mount state. --workbench-reserved-y (2.5rem) folds the wrapper padding + buffer into a single non-chrome offset. dvh handles Safari's retractable address bar. Inline height from resize-drag still overrides as before. Bottom-stack bars now use a uniform `gap: 0.25rem` instead of ad-hoc per-child padding-bottom, giving consistent 4px spacing between all bars. Wrapper vertical padding reduced from py-4/py-8 to py-2/py-3 and main's bottom buffer from +32px to +8px — cards gain ~72px of usable vertical space on a typical viewport. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/page-carousel/PageShell.svelte | 14 +- .../apps/web/src/routes/(app)/+layout.svelte | 391 +++++++++++------- 2 files changed, 262 insertions(+), 143 deletions(-) diff --git a/apps/mana/apps/web/src/lib/components/page-carousel/PageShell.svelte b/apps/mana/apps/web/src/lib/components/page-carousel/PageShell.svelte index 20255e349..143999d72 100644 --- a/apps/mana/apps/web/src/lib/components/page-carousel/PageShell.svelte +++ b/apps/mana/apps/web/src/lib/components/page-carousel/PageShell.svelte @@ -243,7 +243,19 @@ THEME_DEFINITIONS to change the texture for a whole theme. */ .page-shell { flex: 0 0 auto; - min-height: 60vh; + /* Default page height fills the viewport between the workbench + top padding and the bottom chrome (pill nav + tag strip + + bottom bar). Two CSS vars cascade from the layout's
: + - --bottom-chrome-height reacts to pill-nav collapse, tag + strip visibility and bottom-bar mount state + - --workbench-reserved-y collapses the py-* wrapper padding + plus a small buffer into a single "non-chrome vertical" + number so this calc doesn't have to mirror DOM padding + `dvh` accounts for mobile Safari's retractable address bar. + An inline `height: {px}px` style from the resize-drag prop + overrides this value (same specificity rule as before). */ + height: calc(100dvh - var(--bottom-chrome-height, 80px) - var(--workbench-reserved-y, 2.5rem)); + min-height: 320px; max-width: calc(100vw - 2rem); background-color: hsl(var(--color-card)); background-image: var(--paper-texture, none); diff --git a/apps/mana/apps/web/src/routes/(app)/+layout.svelte b/apps/mana/apps/web/src/routes/(app)/+layout.svelte index c59a72bd9..0d30726e5 100644 --- a/apps/mana/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/mana/apps/web/src/routes/(app)/+layout.svelte @@ -13,6 +13,7 @@ import { locale, _ } from 'svelte-i18n'; import { PillNavigation, + PillDropdownBar, TagStrip, DragPreview, ActionZone, @@ -21,6 +22,7 @@ import type { PillNavItem, PillDropdownItem, + PillBarConfig, SpotlightAction, ContentSearcher, ContextMenuItem, @@ -330,9 +332,47 @@ isTagStripVisible = !isTagStripVisible; } + // ── QuickInputBar visibility (toggled by the "search" pill) ── + let isQuickInputVisible = $state(true); + function handleQuickInputToggle() { + isQuickInputVisible = !isQuickInputVisible; + } + + // ── Workbench tab bar visibility (toggled by the "tabs" pill) ── + // Controls whether the page-injected bottomBar (SceneAppBar on /) is rendered. + let isBottomBarVisible = $state(true); + function handleBottomBarToggle() { + isBottomBarVisible = !isBottomBarVisible; + } + + // ── Dropdown-as-bar ────────────────────────────────────── + // Theme / AI tier / Sync / User-menu dropdowns are surfaced as + // bars in the bottom stack instead of floating popovers. PillNavigation + // calls handleOpenBar with a PillBarConfig (or null to close); we + // render the items via PillDropdownBar just above the PillNav. + let activeBar = $state(null); + function handleOpenBar(config: PillBarConfig | null) { + activeBar = config; + } + function closeActiveBar() { + activeBar = null; + } + + // ── Fullscreen mode (press "f" to toggle) ─────────────── + // Hides the entire bottom-stack (pill nav, QuickInputBar, TagStrip, + // notifications, bottom bar) so only the routed page content + // (e.g. workbench pages) is visible. Esc exits. + let isFullscreen = $state(false); + // Bottom chrome height: calculated from state, not measured (avoids reflow loop) const bottomChromeHeight = $derived( - (isCollapsed ? 0 : 80) + (isTagStripVisible ? 44 : 0) + 72 + (bottomBarStore.component ? 36 : 0) + isFullscreen + ? 0 + : (isCollapsed ? 0 : 80) + + (activeBar ? 56 : 0) + + (isTagStripVisible ? 44 : 0) + + (isQuickInputVisible ? 72 : 0) + + (isBottomBarVisible && bottomBarStore.component ? 36 : 0) ); // ── DnD context ───────────────────────────────────────── @@ -379,10 +419,26 @@ href: '/', label: $_('nav.tags'), icon: 'tag', + iconOnly: true, onClick: handleTagStripToggle, active: isTagStripVisible, }, - { href: '/', label: $_('nav.home'), icon: 'home', onContextMenu: makeNavContextMenu('/') }, + { + href: '/', + label: 'Suche', + icon: 'search', + iconOnly: true, + onClick: handleQuickInputToggle, + active: isQuickInputVisible, + }, + { + href: '/', + label: 'Workbench-Tabs', + icon: 'columns', + iconOnly: true, + onClick: handleBottomBarToggle, + active: isBottomBarVisible, + }, ]); let isAdmin = $derived(authStore.user?.role === 'admin'); @@ -401,6 +457,21 @@ showShortcuts = !showShortcuts; return; } + if ( + (event.key === 'f' || event.key === 'F') && + !event.ctrlKey && + !event.metaKey && + !event.altKey + ) { + event.preventDefault(); + isFullscreen = !isFullscreen; + return; + } + if (event.key === 'Escape' && isFullscreen) { + event.preventDefault(); + isFullscreen = false; + return; + } if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) { const num = parseInt(event.key); if (num >= 1 && num <= navRoutes.length) { @@ -670,173 +741,203 @@ {/if}
- -
- - {#if bottomBarStore.component} - {@const BarComponent = bottomBarStore.component} - - {/if} + + {#if !isFullscreen} +
+ + {#if isBottomBarVisible && bottomBarStore.component} + {@const BarComponent = bottomBarStore.component} + + {/if} - -
- -
- - - {#if syncBilling.paused}
-
- Cloud Sync pausiert — Credits reichen nicht aus. - + + + {#if syncBilling.paused} +
+
+ Cloud Sync pausiert — Credits reichen nicht aus. +
-
- {/if} + {/if} - - {#if (guestMode && guestMode.notifications.length > 0) || guestPrompt.notifications.length > 0} -
- -
- {/if} + {#if (guestMode && guestMode.notifications.length > 0) || guestPrompt.notifications.length > 0} +
+ +
+ {/if} - - {#if authStore.isAuthenticated} -
- -
- {/if} + {#if authStore.isAuthenticated} +
+ +
+ {/if} - -
- -
+
+ +
- - - {#snippet rightAction()} - - {/snippet} - + {#snippet rightAction()} + + {/snippet} + + {/if} - - {#if isTagStripVisible} - ({ - id: t.id, - name: t.name, - color: t.color || '#3b82f6', - }))} - selectedIds={[]} - onToggle={() => {}} - onClear={() => {}} - onTagDrop={tagDropHandler ?? undefined} - managementHref="/tags" - loading={allTags.loading} + + {#if isTagStripVisible} + ({ + id: t.id, + name: t.name, + color: t.color || '#3b82f6', + }))} + selectedIds={[]} + onToggle={() => {}} + onClear={() => {}} + onTagDrop={tagDropHandler ?? undefined} + managementHref="/tags" + loading={allTags.loading} + positioning="static" + /> + {/if} + + + {#if activeBar} + + {/if} + + + - {/if} - - - -
+
+ {/if} - -
-
+ +
+
{#if routeBlocked && routeAppId}