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. */
|
THEME_DEFINITIONS to change the texture for a whole theme. */
|
||||||
.page-shell {
|
.page-shell {
|
||||||
flex: 0 0 auto;
|
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);
|
max-width: calc(100vw - 2rem);
|
||||||
background-color: hsl(var(--color-card));
|
background-color: hsl(var(--color-card));
|
||||||
background-image: var(--paper-texture, none);
|
background-image: var(--paper-texture, none);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
import { locale, _ } from 'svelte-i18n';
|
import { locale, _ } from 'svelte-i18n';
|
||||||
import {
|
import {
|
||||||
PillNavigation,
|
PillNavigation,
|
||||||
|
PillDropdownBar,
|
||||||
TagStrip,
|
TagStrip,
|
||||||
DragPreview,
|
DragPreview,
|
||||||
ActionZone,
|
ActionZone,
|
||||||
|
|
@ -21,6 +22,7 @@
|
||||||
import type {
|
import type {
|
||||||
PillNavItem,
|
PillNavItem,
|
||||||
PillDropdownItem,
|
PillDropdownItem,
|
||||||
|
PillBarConfig,
|
||||||
SpotlightAction,
|
SpotlightAction,
|
||||||
ContentSearcher,
|
ContentSearcher,
|
||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
|
|
@ -330,9 +332,47 @@
|
||||||
isTagStripVisible = !isTagStripVisible;
|
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)
|
// Bottom chrome height: calculated from state, not measured (avoids reflow loop)
|
||||||
const bottomChromeHeight = $derived(
|
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 ─────────────────────────────────────────
|
// ── DnD context ─────────────────────────────────────────
|
||||||
|
|
@ -379,10 +419,26 @@
|
||||||
href: '/',
|
href: '/',
|
||||||
label: $_('nav.tags'),
|
label: $_('nav.tags'),
|
||||||
icon: 'tag',
|
icon: 'tag',
|
||||||
|
iconOnly: true,
|
||||||
onClick: handleTagStripToggle,
|
onClick: handleTagStripToggle,
|
||||||
active: isTagStripVisible,
|
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');
|
let isAdmin = $derived(authStore.user?.role === 'admin');
|
||||||
|
|
@ -401,6 +457,21 @@
|
||||||
showShortcuts = !showShortcuts;
|
showShortcuts = !showShortcuts;
|
||||||
return;
|
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) {
|
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
|
||||||
const num = parseInt(event.key);
|
const num = parseInt(event.key);
|
||||||
if (num >= 1 && num <= navRoutes.length) {
|
if (num >= 1 && num <= navRoutes.length) {
|
||||||
|
|
@ -670,173 +741,203 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="min-h-screen bg-background">
|
<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.
|
||||||
<div class="bottom-stack" style:--bottom-chrome-height="{bottomChromeHeight}px">
|
Hidden entirely when fullscreen mode is active (press "f"). -->
|
||||||
<!-- Page-injected bottom bar (e.g. workbench scene+app tabs) -->
|
{#if !isFullscreen}
|
||||||
{#if bottomBarStore.component}
|
<div class="bottom-stack" style:--bottom-chrome-height="{bottomChromeHeight}px">
|
||||||
{@const BarComponent = bottomBarStore.component}
|
<!-- Page-injected bottom bar (e.g. workbench scene+app tabs).
|
||||||
<BarComponent {...bottomBarStore.props} />
|
Gated by isBottomBarVisible so the "workbench tabs" pill can
|
||||||
{/if}
|
toggle it without unmounting the owning page. -->
|
||||||
|
{#if isBottomBarVisible && bottomBarStore.component}
|
||||||
|
{@const BarComponent = bottomBarStore.component}
|
||||||
|
<BarComponent {...bottomBarStore.props} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- One-time encryption intro — sits at the top of the stack so
|
<!-- One-time encryption intro — sits at the top of the stack so
|
||||||
it can't be obscured by the QuickInputBar / TagStrip / PillNav.
|
it can't be obscured by the QuickInputBar / TagStrip / PillNav.
|
||||||
Self-gates on isVaultUnlocked() so guests never see it. -->
|
Self-gates on isVaultUnlocked() so guests never see it. -->
|
||||||
<div class="bottom-stack-notification">
|
|
||||||
<EncryptionIntroBanner />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sync pause banner — shown when sync was paused due to insufficient credits -->
|
|
||||||
{#if syncBilling.paused}
|
|
||||||
<div class="bottom-stack-notification">
|
<div class="bottom-stack-notification">
|
||||||
<div
|
<EncryptionIntroBanner />
|
||||||
class="flex items-center justify-between gap-3 rounded-lg bg-amber-50 px-4 py-3 text-sm text-amber-800 dark:bg-amber-900/20 dark:text-amber-200"
|
</div>
|
||||||
>
|
|
||||||
<span>Cloud Sync pausiert — Credits reichen nicht aus.</span>
|
<!-- Sync pause banner — shown when sync was paused due to insufficient credits -->
|
||||||
<div class="flex gap-2">
|
{#if syncBilling.paused}
|
||||||
<a href="/credits?tab=packages" class="font-medium underline hover:no-underline">
|
<div class="bottom-stack-notification">
|
||||||
Credits aufladen
|
<div
|
||||||
</a>
|
class="flex items-center justify-between gap-3 rounded-lg bg-amber-50 px-4 py-3 text-sm text-amber-800 dark:bg-amber-900/20 dark:text-amber-200"
|
||||||
<a href="/settings/sync" class="font-medium underline hover:no-underline">
|
>
|
||||||
Sync-Einstellungen
|
<span>Cloud Sync pausiert — Credits reichen nicht aus.</span>
|
||||||
</a>
|
<div class="flex gap-2">
|
||||||
|
<a href="/credits?tab=packages" class="font-medium underline hover:no-underline">
|
||||||
|
Credits aufladen
|
||||||
|
</a>
|
||||||
|
<a href="/settings/sync" class="font-medium underline hover:no-underline">
|
||||||
|
Sync-Einstellungen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Guest notifications — combines the time-based nudge from
|
<!-- Guest notifications — combines the time-based nudge from
|
||||||
createGuestMode (one-shot after N minutes) with the
|
createGuestMode (one-shot after N minutes) with the
|
||||||
event-driven prompts pushed by guestPrompt.requireAccount
|
event-driven prompts pushed by guestPrompt.requireAccount
|
||||||
(e.g. server feature called while signed out, 401 from
|
(e.g. server feature called while signed out, 401 from
|
||||||
the auth-aware fetch). Both flow into the same bar so
|
the auth-aware fetch). Both flow into the same bar so
|
||||||
the user only ever sees one stripe instead of stacking. -->
|
the user only ever sees one stripe instead of stacking. -->
|
||||||
{#if (guestMode && guestMode.notifications.length > 0) || guestPrompt.notifications.length > 0}
|
{#if (guestMode && guestMode.notifications.length > 0) || guestPrompt.notifications.length > 0}
|
||||||
<div class="bottom-stack-notification">
|
<div class="bottom-stack-notification">
|
||||||
<NotificationBar
|
<NotificationBar
|
||||||
notifications={[...(guestMode?.notifications ?? []), ...guestPrompt.notifications]}
|
notifications={[...(guestMode?.notifications ?? []), ...guestPrompt.notifications]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Session expiry warning (auth only). Self-gates on the
|
<!-- Session expiry warning (auth only). Self-gates on the
|
||||||
secondsLeft countdown and only renders inside the stack
|
secondsLeft countdown and only renders inside the stack
|
||||||
when actually warning, so the wrapper is no-op otherwise. -->
|
when actually warning, so the wrapper is no-op otherwise. -->
|
||||||
{#if authStore.isAuthenticated}
|
{#if authStore.isAuthenticated}
|
||||||
<div class="bottom-stack-notification">
|
<div class="bottom-stack-notification">
|
||||||
<SessionWarning />
|
<SessionWarning />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Cross-module automation suggestions. Lives in the (app)
|
<!-- Cross-module automation suggestions. Lives in the (app)
|
||||||
stack because automationsStore is an (app)-only module
|
stack because automationsStore is an (app)-only module
|
||||||
and the toast doesn't make sense on auth/landing pages
|
and the toast doesn't make sense on auth/landing pages
|
||||||
anyway. Self-gates on visible state. -->
|
anyway. Self-gates on visible state. -->
|
||||||
<div class="bottom-stack-notification">
|
<div class="bottom-stack-notification">
|
||||||
<SuggestionToast />
|
<SuggestionToast />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- QuickInputBar with inline nav toggle -->
|
<!-- QuickInputBar with inline nav toggle — gated by the "search" pill -->
|
||||||
<QuickInputBar
|
{#if isQuickInputVisible}
|
||||||
onSearch={inputBarAdapter.onSearch}
|
<QuickInputBar
|
||||||
onSelect={inputBarAdapter.onSelect}
|
onSearch={inputBarAdapter.onSearch}
|
||||||
onParseCreate={inputBarAdapter.onParseCreate}
|
onSelect={inputBarAdapter.onSelect}
|
||||||
onCreate={inputBarAdapter.onCreate}
|
onParseCreate={inputBarAdapter.onParseCreate}
|
||||||
onSearchChange={inputBarAdapter.onSearchChange}
|
onCreate={inputBarAdapter.onCreate}
|
||||||
placeholder={inputBarAdapter.placeholder}
|
onSearchChange={inputBarAdapter.onSearchChange}
|
||||||
appIcon={inputBarAdapter.appIcon}
|
placeholder={inputBarAdapter.placeholder}
|
||||||
emptyText={inputBarAdapter.emptyText}
|
appIcon={inputBarAdapter.appIcon}
|
||||||
createText={inputBarAdapter.createText}
|
emptyText={inputBarAdapter.emptyText}
|
||||||
deferSearch={inputBarAdapter.deferSearch}
|
createText={inputBarAdapter.createText}
|
||||||
locale={$locale || 'de'}
|
deferSearch={inputBarAdapter.deferSearch}
|
||||||
defaultOptions={inputBarAdapter.defaultOptions}
|
locale={$locale || 'de'}
|
||||||
selectedDefaultId={inputBarAdapter.selectedDefaultId}
|
defaultOptions={inputBarAdapter.defaultOptions}
|
||||||
defaultOptionLabel={inputBarAdapter.defaultOptionLabel}
|
selectedDefaultId={inputBarAdapter.selectedDefaultId}
|
||||||
onDefaultChange={inputBarAdapter.onDefaultChange}
|
defaultOptionLabel={inputBarAdapter.defaultOptionLabel}
|
||||||
highlightPatterns={inputBarAdapter.highlightPatterns}
|
onDefaultChange={inputBarAdapter.onDefaultChange}
|
||||||
positioning="static"
|
highlightPatterns={inputBarAdapter.highlightPatterns}
|
||||||
>
|
positioning="static"
|
||||||
{#snippet rightAction()}
|
|
||||||
<button
|
|
||||||
class="pill-nav-toggle"
|
|
||||||
onclick={() => handleCollapsedChange(!isCollapsed)}
|
|
||||||
title={isCollapsed ? 'Navigation einblenden' : 'Navigation ausblenden'}
|
|
||||||
>
|
>
|
||||||
<span class="pill-nav-toggle-icon" class:collapsed={isCollapsed}>▼</span>
|
{#snippet rightAction()}
|
||||||
</button>
|
<button
|
||||||
{/snippet}
|
class="pill-nav-toggle"
|
||||||
</QuickInputBar>
|
onclick={() => handleCollapsedChange(!isCollapsed)}
|
||||||
|
title={isCollapsed ? 'Navigation einblenden' : 'Navigation ausblenden'}
|
||||||
|
>
|
||||||
|
<span class="pill-nav-toggle-icon" class:collapsed={isCollapsed}>▼</span>
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
</QuickInputBar>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- TagStrip (between QuickInputBar and PillNav) -->
|
<!-- TagStrip (between QuickInputBar and PillNav) -->
|
||||||
{#if isTagStripVisible}
|
{#if isTagStripVisible}
|
||||||
<TagStrip
|
<TagStrip
|
||||||
tags={(allTags.value ?? []).map((t) => ({
|
tags={(allTags.value ?? []).map((t) => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
name: t.name,
|
name: t.name,
|
||||||
color: t.color || '#3b82f6',
|
color: t.color || '#3b82f6',
|
||||||
}))}
|
}))}
|
||||||
selectedIds={[]}
|
selectedIds={[]}
|
||||||
onToggle={() => {}}
|
onToggle={() => {}}
|
||||||
onClear={() => {}}
|
onClear={() => {}}
|
||||||
onTagDrop={tagDropHandler ?? undefined}
|
onTagDrop={tagDropHandler ?? undefined}
|
||||||
managementHref="/tags"
|
managementHref="/tags"
|
||||||
loading={allTags.loading}
|
loading={allTags.loading}
|
||||||
|
positioning="static"
|
||||||
|
/>
|
||||||
|
{/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"
|
||||||
|
homeRoute="/"
|
||||||
|
onLogout={handleSignOut}
|
||||||
|
onToggleTheme={handleToggleTheme}
|
||||||
|
{isDark}
|
||||||
|
{isCollapsed}
|
||||||
|
onCollapsedChange={handleCollapsedChange}
|
||||||
|
showThemeToggle={true}
|
||||||
|
showThemeVariants={true}
|
||||||
|
{themeVariantItems}
|
||||||
|
{currentThemeVariantLabel}
|
||||||
|
themeMode={theme.mode}
|
||||||
|
onThemeModeChange={handleThemeModeChange}
|
||||||
|
showLanguageSwitcher={true}
|
||||||
|
{languageItems}
|
||||||
|
{currentLanguageLabel}
|
||||||
|
showLogout={authStore.isAuthenticated}
|
||||||
|
loginHref="/login"
|
||||||
|
primaryColor="#6366f1"
|
||||||
|
showAppSwitcher={true}
|
||||||
|
showAiTierSelector={true}
|
||||||
|
{aiTierItems}
|
||||||
|
{currentAiTierLabel}
|
||||||
|
{currentAiTierIcon}
|
||||||
|
showSyncStatus={authStore.isAuthenticated}
|
||||||
|
{syncStatusItems}
|
||||||
|
{currentSyncLabel}
|
||||||
|
{appItems}
|
||||||
|
{userEmail}
|
||||||
|
settingsHref="/settings"
|
||||||
|
manaHref="/mana"
|
||||||
|
profileHref="/profile"
|
||||||
|
spiralHref="/spiral"
|
||||||
|
creditsHref="/credits"
|
||||||
|
themesHref="/themes"
|
||||||
|
helpHref="/help"
|
||||||
|
allAppsHref="/apps"
|
||||||
|
{spotlightActions}
|
||||||
|
{contentSearcher}
|
||||||
positioning="static"
|
positioning="static"
|
||||||
/>
|
/>
|
||||||
{/if}
|
</div>
|
||||||
|
{/if}
|
||||||
<!-- PillNav (bottom of stack) -->
|
|
||||||
<PillNavigation
|
|
||||||
items={navItems}
|
|
||||||
currentPath={$page.url.pathname}
|
|
||||||
appName="Mana"
|
|
||||||
homeRoute="/"
|
|
||||||
onLogout={handleSignOut}
|
|
||||||
onToggleTheme={handleToggleTheme}
|
|
||||||
{isDark}
|
|
||||||
{isCollapsed}
|
|
||||||
onCollapsedChange={handleCollapsedChange}
|
|
||||||
showThemeToggle={true}
|
|
||||||
showThemeVariants={true}
|
|
||||||
{themeVariantItems}
|
|
||||||
{currentThemeVariantLabel}
|
|
||||||
themeMode={theme.mode}
|
|
||||||
onThemeModeChange={handleThemeModeChange}
|
|
||||||
showLanguageSwitcher={true}
|
|
||||||
{languageItems}
|
|
||||||
{currentLanguageLabel}
|
|
||||||
showLogout={authStore.isAuthenticated}
|
|
||||||
loginHref="/login"
|
|
||||||
primaryColor="#6366f1"
|
|
||||||
showAppSwitcher={true}
|
|
||||||
showAiTierSelector={true}
|
|
||||||
{aiTierItems}
|
|
||||||
{currentAiTierLabel}
|
|
||||||
{currentAiTierIcon}
|
|
||||||
showSyncStatus={authStore.isAuthenticated}
|
|
||||||
{syncStatusItems}
|
|
||||||
{currentSyncLabel}
|
|
||||||
{appItems}
|
|
||||||
{userEmail}
|
|
||||||
settingsHref="/settings"
|
|
||||||
manaHref="/mana"
|
|
||||||
profileHref="/profile"
|
|
||||||
spiralHref="/spiral"
|
|
||||||
creditsHref="/credits"
|
|
||||||
themesHref="/themes"
|
|
||||||
helpHref="/help"
|
|
||||||
allAppsHref="/apps"
|
|
||||||
{spotlightActions}
|
|
||||||
{contentSearcher}
|
|
||||||
positioning="static"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- DnD: floating preview -->
|
<!-- DnD: floating preview -->
|
||||||
<DragPreview />
|
<DragPreview />
|
||||||
|
|
||||||
<!-- Main content -->
|
<!-- Main content.
|
||||||
<main style="padding-bottom: {bottomChromeHeight + 32}px">
|
Publish layout offsets as CSS variables so descendants (esp.
|
||||||
<div class="mx-auto max-w-7xl px-3 py-4 sm:px-6 sm:py-8 lg:px-8">
|
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}
|
{#if routeBlocked && routeAppId}
|
||||||
<!-- Per-route tier gate. The wrapping AuthGate only fires
|
<!-- Per-route tier gate. The wrapping AuthGate only fires
|
||||||
onMount + only for authenticated users, so this is the
|
onMount + only for authenticated users, so this is the
|
||||||
|
|
@ -948,6 +1049,9 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
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;
|
pointer-events: none;
|
||||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
}
|
}
|
||||||
|
|
@ -992,6 +1096,9 @@
|
||||||
.bottom-stack-notification {
|
.bottom-stack-notification {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
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>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue