mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
feat(workbench): dynamic page height + tighter bottom-stack spacing
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 <main>: 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) <noreply@anthropic.com>
This commit is contained in:
parent
68c2442419
commit
474ba93d70
2 changed files with 262 additions and 143 deletions
|
|
@ -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 <main>:
|
||||
- --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);
|
||||
|
|
|
|||
|
|
@ -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<PillBarConfig | null>(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,10 +741,14 @@
|
|||
{/if}
|
||||
|
||||
<div class="min-h-screen bg-background">
|
||||
<!-- Bottom Stack: all fixed-bottom elements in one flex container -->
|
||||
<!-- Bottom Stack: all fixed-bottom elements in one flex container.
|
||||
Hidden entirely when fullscreen mode is active (press "f"). -->
|
||||
{#if !isFullscreen}
|
||||
<div class="bottom-stack" style:--bottom-chrome-height="{bottomChromeHeight}px">
|
||||
<!-- Page-injected bottom bar (e.g. workbench scene+app tabs) -->
|
||||
{#if bottomBarStore.component}
|
||||
<!-- Page-injected bottom bar (e.g. workbench scene+app tabs).
|
||||
Gated by isBottomBarVisible so the "workbench tabs" pill can
|
||||
toggle it without unmounting the owning page. -->
|
||||
{#if isBottomBarVisible && bottomBarStore.component}
|
||||
{@const BarComponent = bottomBarStore.component}
|
||||
<BarComponent {...bottomBarStore.props} />
|
||||
{/if}
|
||||
|
|
@ -735,7 +810,8 @@
|
|||
<SuggestionToast />
|
||||
</div>
|
||||
|
||||
<!-- QuickInputBar with inline nav toggle -->
|
||||
<!-- QuickInputBar with inline nav toggle — gated by the "search" pill -->
|
||||
{#if isQuickInputVisible}
|
||||
<QuickInputBar
|
||||
onSearch={inputBarAdapter.onSearch}
|
||||
onSelect={inputBarAdapter.onSelect}
|
||||
|
|
@ -765,6 +841,7 @@
|
|||
</button>
|
||||
{/snippet}
|
||||
</QuickInputBar>
|
||||
{/if}
|
||||
|
||||
<!-- TagStrip (between QuickInputBar and PillNav) -->
|
||||
{#if isTagStripVisible}
|
||||
|
|
@ -784,8 +861,23 @@
|
|||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Dropdown-as-bar: shows the items of the currently opened
|
||||
PillNavigation dropdown (theme / AI / sync / user) as
|
||||
horizontal pills directly above the PillNav. -->
|
||||
{#if activeBar}
|
||||
<PillDropdownBar
|
||||
items={activeBar.items}
|
||||
label={activeBar.label}
|
||||
icon={activeBar.icon}
|
||||
onClose={closeActiveBar}
|
||||
positioning="static"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- PillNav (bottom of stack) -->
|
||||
<PillNavigation
|
||||
onOpenBar={handleOpenBar}
|
||||
activeBarId={activeBar?.id ?? null}
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Mana"
|
||||
|
|
@ -830,13 +922,22 @@
|
|||
positioning="static"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- DnD: floating preview -->
|
||||
<DragPreview />
|
||||
|
||||
<!-- Main content -->
|
||||
<main style="padding-bottom: {bottomChromeHeight + 32}px">
|
||||
<div class="mx-auto max-w-7xl px-3 py-4 sm:px-6 sm:py-8 lg:px-8">
|
||||
<!-- Main content.
|
||||
Publish layout offsets as CSS variables so descendants (esp.
|
||||
PageShell in the carousel) can compute their available
|
||||
height against viewport + bottom chrome without prop
|
||||
drilling. `--workbench-top-offset` must match the vertical
|
||||
padding on the inner max-w-7xl wrapper below. -->
|
||||
<main
|
||||
style="padding-bottom: {bottomChromeHeight +
|
||||
8}px; --bottom-chrome-height: {bottomChromeHeight}px; --workbench-reserved-y: 2.5rem;"
|
||||
>
|
||||
<div class="mx-auto max-w-7xl px-3 py-2 sm:px-6 sm:py-3 lg:px-8">
|
||||
{#if routeBlocked && routeAppId}
|
||||
<!-- Per-route tier gate. The wrapping AuthGate only fires
|
||||
onMount + only for authenticated users, so this is the
|
||||
|
|
@ -948,6 +1049,9 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
/* Uniform small gap between bars instead of each wrapper
|
||||
providing its own ad-hoc padding-bottom. */
|
||||
gap: 0.25rem;
|
||||
pointer-events: none;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
|
|
@ -992,6 +1096,9 @@
|
|||
.bottom-stack-notification {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0 1rem 0.5rem;
|
||||
/* Only horizontal padding — vertical spacing comes from the
|
||||
parent .bottom-stack `gap`, so all bars are evenly spaced
|
||||
regardless of how many children they nest. */
|
||||
padding: 0 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue