mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 00:01:10 +02:00
refactor(ui): unified bottom-stack container for PillNav, QuickInput, TagStrip
Replace 4 independent position:fixed elements with one flex container that stacks them naturally from bottom to top. Elements push each other automatically — no more hardcoded offsets or z-index conflicts. Stack order (bottom → top): 1. PillNavigation (collapsible) 2. TagStrip (togglable) 3. QuickInputBar + toggle button row Shared-UI changes: - PillNavigation: add positioning='fixed'|'static' prop - QuickInputBar: add positioning='fixed'|'static' prop - TagStrip: add positioning='fixed'|'static' prop - All default to 'fixed' for backward compatibility Layout changes: - Wrap all bottom elements in .bottom-stack (position:fixed, flex-column) - Remove hardcoded bottomOffset calculations - Toggle button is now inline next to QuickInputBar (not separately positioned) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bed2060ba5
commit
b415567dfa
4 changed files with 154 additions and 85 deletions
|
|
@ -375,89 +375,93 @@
|
|||
{/if}
|
||||
|
||||
<div class="min-h-screen bg-background">
|
||||
<!-- Pill Navigation -->
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="ManaCore"
|
||||
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}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
themesHref="/themes"
|
||||
helpHref="/help"
|
||||
allAppsHref="/apps"
|
||||
{spotlightActions}
|
||||
{contentSearcher}
|
||||
/>
|
||||
<!-- Bottom Stack: all fixed-bottom elements in one flex container -->
|
||||
<div class="bottom-stack">
|
||||
<!-- QuickInputBar + toggle button row -->
|
||||
<div class="bottom-stack-row">
|
||||
<QuickInputBar
|
||||
onSearch={inputBarAdapter.onSearch}
|
||||
onSelect={inputBarAdapter.onSelect}
|
||||
onParseCreate={inputBarAdapter.onParseCreate}
|
||||
onCreate={inputBarAdapter.onCreate}
|
||||
onSearchChange={inputBarAdapter.onSearchChange}
|
||||
placeholder={inputBarAdapter.placeholder}
|
||||
appIcon={inputBarAdapter.appIcon}
|
||||
emptyText={inputBarAdapter.emptyText}
|
||||
createText={inputBarAdapter.createText}
|
||||
deferSearch={inputBarAdapter.deferSearch}
|
||||
locale={$locale || 'de'}
|
||||
defaultOptions={inputBarAdapter.defaultOptions}
|
||||
selectedDefaultId={inputBarAdapter.selectedDefaultId}
|
||||
defaultOptionLabel={inputBarAdapter.defaultOptionLabel}
|
||||
onDefaultChange={inputBarAdapter.onDefaultChange}
|
||||
highlightPatterns={inputBarAdapter.highlightPatterns}
|
||||
positioning="static"
|
||||
/>
|
||||
<button
|
||||
class="pill-nav-toggle"
|
||||
onclick={() => handleCollapsedChange(!isCollapsed)}
|
||||
title={isCollapsed ? 'Navigation einblenden' : 'Navigation ausblenden'}
|
||||
>
|
||||
<span class="pill-nav-toggle-icon" class:collapsed={isCollapsed}>▼</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- QuickInputBar (context-aware per module) -->
|
||||
<QuickInputBar
|
||||
onSearch={inputBarAdapter.onSearch}
|
||||
onSelect={inputBarAdapter.onSelect}
|
||||
onParseCreate={inputBarAdapter.onParseCreate}
|
||||
onCreate={inputBarAdapter.onCreate}
|
||||
onSearchChange={inputBarAdapter.onSearchChange}
|
||||
placeholder={inputBarAdapter.placeholder}
|
||||
appIcon={inputBarAdapter.appIcon}
|
||||
emptyText={inputBarAdapter.emptyText}
|
||||
createText={inputBarAdapter.createText}
|
||||
deferSearch={inputBarAdapter.deferSearch}
|
||||
locale={$locale || 'de'}
|
||||
defaultOptions={inputBarAdapter.defaultOptions}
|
||||
selectedDefaultId={inputBarAdapter.selectedDefaultId}
|
||||
defaultOptionLabel={inputBarAdapter.defaultOptionLabel}
|
||||
onDefaultChange={inputBarAdapter.onDefaultChange}
|
||||
highlightPatterns={inputBarAdapter.highlightPatterns}
|
||||
bottomOffset={isCollapsed ? '12px' : '70px'}
|
||||
/>
|
||||
<!-- TagStrip (between QuickInputBar and PillNav) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={(allTags.value ?? []).map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
}))}
|
||||
selectedIds={[]}
|
||||
onToggle={() => {}}
|
||||
onClear={() => {}}
|
||||
onTagDrop={tagDropHandler ?? undefined}
|
||||
managementHref="/tags"
|
||||
loading={allTags.loading}
|
||||
positioning="static"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- PillNav toggle button (next to QuickInputBar) -->
|
||||
<button
|
||||
class="pill-nav-toggle"
|
||||
style:bottom={isCollapsed ? '20px' : '78px'}
|
||||
onclick={() => handleCollapsedChange(!isCollapsed)}
|
||||
title={isCollapsed ? 'Navigation einblenden' : 'Navigation ausblenden'}
|
||||
>
|
||||
<span class="pill-nav-toggle-icon" class:collapsed={isCollapsed}>▼</span>
|
||||
</button>
|
||||
|
||||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={(allTags.value ?? []).map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
}))}
|
||||
selectedIds={[]}
|
||||
onToggle={() => {}}
|
||||
onClear={() => {}}
|
||||
onTagDrop={tagDropHandler ?? undefined}
|
||||
managementHref="/tags"
|
||||
loading={allTags.loading}
|
||||
<!-- PillNav (bottom of stack) -->
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="ManaCore"
|
||||
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}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
themesHref="/themes"
|
||||
helpHref="/help"
|
||||
allAppsHref="/apps"
|
||||
{spotlightActions}
|
||||
{contentSearcher}
|
||||
positioning="static"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- DnD: floating preview -->
|
||||
<DragPreview />
|
||||
|
|
@ -499,12 +503,44 @@
|
|||
</AuthGate>
|
||||
|
||||
<style>
|
||||
.pill-nav-toggle {
|
||||
.bottom-stack {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
z-index: 91;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 90;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
pointer-events: none;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
|
||||
.bottom-stack > :global(*) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.bottom-stack-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
padding-right: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bottom-stack-row > :global(*) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.bottom-stack-row > :global(:first-child) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pill-nav-toggle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
|
|
@ -515,9 +551,9 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
transition:
|
||||
bottom 0.3s ease,
|
||||
background 0.2s ease,
|
||||
color 0.2s ease;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.pill-nav-toggle:hover {
|
||||
|
|
|
|||
|
|
@ -228,6 +228,8 @@
|
|||
onToggleTheme?: () => void;
|
||||
/** Whether dark mode is active */
|
||||
isDark?: boolean;
|
||||
/** Use 'static' when inside a flex container (bottom-stack pattern). Default: 'fixed'. */
|
||||
positioning?: 'fixed' | 'static';
|
||||
/** Whether navigation is collapsed */
|
||||
isCollapsed?: boolean;
|
||||
/** Called when collapsed state changes */
|
||||
|
|
@ -318,6 +320,7 @@
|
|||
onLogout,
|
||||
onToggleTheme,
|
||||
isDark = false,
|
||||
positioning = 'fixed',
|
||||
isCollapsed: externalCollapsed,
|
||||
onCollapsedChange,
|
||||
languageItems = [],
|
||||
|
|
@ -403,6 +406,7 @@
|
|||
{#if !(externalCollapsed ?? false)}
|
||||
<nav
|
||||
class="pill-nav"
|
||||
class:pill-nav-static={positioning === 'static'}
|
||||
style="{primaryColor
|
||||
? `--pill-primary-color: ${primaryColor};`
|
||||
: ''}--pill-nav-bottom: {bottomOffset}"
|
||||
|
|
@ -828,6 +832,12 @@
|
|||
container-name: pillnav;
|
||||
}
|
||||
|
||||
.pill-nav-static {
|
||||
position: relative;
|
||||
bottom: auto;
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
.pill-nav-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@
|
|||
createHref?: string;
|
||||
/** Whether the filter strip below is visible (adjusts bottom position) */
|
||||
aboveFilterStrip?: boolean;
|
||||
/** Use 'static' when inside a flex container (bottom-stack pattern). Default: 'fixed'. */
|
||||
positioning?: 'fixed' | 'static';
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -48,6 +50,7 @@
|
|||
showCreateButton = true,
|
||||
createHref,
|
||||
aboveFilterStrip = false,
|
||||
positioning = 'fixed',
|
||||
}: Props = $props();
|
||||
|
||||
const resolvedCreateHref = $derived(createHref ?? managementHref + '?new=true');
|
||||
|
|
@ -63,7 +66,11 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="tag-strip-wrapper" class:above-filter-strip={aboveFilterStrip}>
|
||||
<div
|
||||
class="tag-strip-wrapper"
|
||||
class:above-filter-strip={aboveFilterStrip}
|
||||
class:tag-strip-static={positioning === 'static'}
|
||||
>
|
||||
<div class="tag-strip-container">
|
||||
<!-- Clear Filter Button (always rendered to prevent layout shift) -->
|
||||
<button
|
||||
|
|
@ -140,6 +147,12 @@
|
|||
left: 0;
|
||||
right: 0;
|
||||
z-index: 49;
|
||||
}
|
||||
|
||||
.tag-strip-static {
|
||||
position: relative;
|
||||
bottom: auto;
|
||||
z-index: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@
|
|||
highlightPatterns?: HighlightPattern[];
|
||||
/** Locale for syntax highlighting keywords (e.g., 'de', 'en'). Default: 'de'. */
|
||||
locale?: string;
|
||||
/** Use 'static' when inside a flex container (bottom-stack pattern). Default: 'fixed'. */
|
||||
positioning?: 'fixed' | 'static';
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -107,6 +109,7 @@
|
|||
leftAction,
|
||||
highlightPatterns,
|
||||
locale = 'de',
|
||||
positioning = 'fixed',
|
||||
}: Props = $props();
|
||||
|
||||
// Use settings for autoFocus
|
||||
|
|
@ -382,6 +385,7 @@
|
|||
class="quick-input-bar"
|
||||
class:has-fab-right={hasFabRight}
|
||||
class:has-fab-left={hasFabLeft}
|
||||
class:quick-input-static={positioning === 'static'}
|
||||
style="--bottom-offset: {bottomOffset}"
|
||||
>
|
||||
<!-- Results Panel (above input) -->
|
||||
|
|
@ -566,6 +570,12 @@
|
|||
transition: bottom 0.3s ease;
|
||||
}
|
||||
|
||||
.quick-input-static {
|
||||
position: relative;
|
||||
bottom: auto;
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
/* Leave space for FAB on mobile */
|
||||
@media (max-width: 900px) {
|
||||
.quick-input-bar.has-fab-right {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue