From 637333051b2e56cf76d24990895953a82b2677a9 Mon Sep 17 00:00:00 2001 From: Till JS Date: Sat, 11 Apr 2026 17:40:19 +0200 Subject: [PATCH] feat(pill-nav): collapse user pills into account dropdown + solid pill backgrounds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Profile/Settings/Spiral/Credits move out of the standalone nav pills and into the user-menu dropdown so the bottom bar stays compact. The dropdown now also renders for guests (login users) — auth-only items (Profil, Mana, Feedback, Logout) get filtered out, and a primary-styled "Anmelden" entry replaces Logout. Themes is dropped from the dropdown since it already has its own theme-variant pill. New PillNavigation props: creditsHref, guestMenuLabel. New PillDropdown icon paths: creditCard, spiral. New PillDropdownItem flag: primary (prominent CTA styling), used for the guest Anmelden item. All .glass-pill classes across PillNavigation, PillDropdown, PillTabGroup, PillTagSelector, PillViewSwitcher, PillTimeRangeSelector, PillToolbar, AppDrawer and ExpandableToolbar move from rgba+backdrop-blur to solid theme tokens (hsl(var(--color-card)) / --color-border / --color-foreground) so pills are fully opaque and follow the active theme variant instead of having a frosted look that varied by background. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../apps/web/src/routes/(app)/+layout.svelte | 31 +--- .../shared-ui/src/navigation/AppDrawer.svelte | 33 ++--- .../src/navigation/PillDropdown.svelte | 54 ++++--- .../src/navigation/PillNavigation.svelte | 136 ++++++++---------- .../src/navigation/PillTabGroup.svelte | 17 +-- .../src/navigation/PillTagSelector.svelte | 33 ++--- .../navigation/PillTimeRangeSelector.svelte | 14 +- .../src/navigation/PillToolbar.svelte | 15 +- .../src/navigation/PillViewSwitcher.svelte | 8 +- .../ExpandableToolbar.svelte | 10 +- packages/shared-ui/src/navigation/types.ts | 2 + 11 files changed, 140 insertions(+), 213 deletions(-) diff --git a/apps/mana/apps/web/src/routes/(app)/+layout.svelte b/apps/mana/apps/web/src/routes/(app)/+layout.svelte index e8608b154..c59a72bd9 100644 --- a/apps/mana/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/mana/apps/web/src/routes/(app)/+layout.svelte @@ -369,6 +369,11 @@ } // ── Navigation ────────────────────────────────────────── + // Note: spiral, credits, profile and settings used to live here as + // standalone pills but were moved into the user-menu dropdown so the + // nav stays compact. They are still routable via the dropdown items + // the layout passes as `spiralHref` / `creditsHref` / `profileHref` / + // `settingsHref` below. let baseNavItems = $derived([ { href: '/', @@ -378,30 +383,6 @@ active: isTagStripVisible, }, { href: '/', label: $_('nav.home'), icon: 'home', onContextMenu: makeNavContextMenu('/') }, - { - href: '/spiral', - label: $_('nav.spiral'), - icon: 'spiral', - onContextMenu: makeNavContextMenu('/spiral'), - }, - { - href: '/credits', - label: $_('nav.credits'), - icon: 'creditCard', - onContextMenu: makeNavContextMenu('/credits'), - }, - { - href: '/profile', - label: $_('nav.profile'), - icon: 'user', - onContextMenu: makeNavContextMenu('/profile'), - }, - { - href: '/settings', - label: $_('nav.settings'), - icon: 'settings', - onContextMenu: makeNavContextMenu('/settings'), - }, ]); let isAdmin = $derived(authStore.user?.role === 'admin'); @@ -839,6 +820,8 @@ settingsHref="/settings" manaHref="/mana" profileHref="/profile" + spiralHref="/spiral" + creditsHref="/credits" themesHref="/themes" helpHref="/help" allAppsHref="/apps" diff --git a/packages/shared-ui/src/navigation/AppDrawer.svelte b/packages/shared-ui/src/navigation/AppDrawer.svelte index cda3127f4..b4feb1599 100644 --- a/packages/shared-ui/src/navigation/AppDrawer.svelte +++ b/packages/shared-ui/src/navigation/AppDrawer.svelte @@ -318,36 +318,23 @@ transform: rotate(180deg); } - /* Glass pill - matches PillDropdown exactly */ + /* Solid theme-tokened pill (formerly the "glass" frosted pill). */ .glass-pill { - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(0, 0, 0, 0.1); + background: hsl(var(--color-card)); + border: 1px solid hsl(var(--color-border)); box-shadow: - 0 4px 6px -1px rgba(0, 0, 0, 0.1), - 0 2px 4px -1px rgba(0, 0, 0, 0.06); - color: #374151; - } - - :global(.dark) .glass-pill { - background: rgba(255, 255, 255, 0.12); - border: 1px solid rgba(255, 255, 255, 0.15); - color: #f3f4f6; + 0 1px 2px hsl(0 0% 0% / 0.05), + 0 2px 6px hsl(0 0% 0% / 0.04); + color: hsl(var(--color-foreground)); } .glass-pill:hover { - background: rgba(255, 255, 255, 0.95); - border-color: rgba(0, 0, 0, 0.15); + background: hsl(var(--color-surface-hover)); + border-color: hsl(var(--color-border-strong, var(--color-border))); transform: translateY(-2px); box-shadow: - 0 10px 15px -3px rgba(0, 0, 0, 0.1), - 0 4px 6px -2px rgba(0, 0, 0, 0.05); - } - - :global(.dark) .glass-pill:hover { - background: rgba(255, 255, 255, 0.2); - border-color: rgba(255, 255, 255, 0.25); + 0 6px 12px hsl(0 0% 0% / 0.08), + 0 2px 4px hsl(0 0% 0% / 0.05); } /* Backdrop */ diff --git a/packages/shared-ui/src/navigation/PillDropdown.svelte b/packages/shared-ui/src/navigation/PillDropdown.svelte index 4dfd892ce..ae03656ce 100644 --- a/packages/shared-ui/src/navigation/PillDropdown.svelte +++ b/packages/shared-ui/src/navigation/PillDropdown.svelte @@ -136,6 +136,8 @@ 'M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z', power: 'M12 3v9m6.364-6.364a9 9 0 11-12.728 0', download: 'M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5 5-5M12 15V3', + creditCard: 'M3 10h18M5 6h14a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2zM7 15h2', + spiral: 'M12 4a8 8 0 108 8 6 6 0 00-6-6 4 4 0 00-4 4 2 2 0 002 2 1 1 0 001-1', }; function getIcon(iconName: string) { @@ -214,6 +216,7 @@ onclick={(e) => handleItemClick(item, e)} class="pill glass-pill fan-pill" class:danger-pill={item.danger} + class:primary-pill={item.primary} class:active-pill={item.active} class:has-submenu={item.submenu && item.submenu.length > 0} class:submenu-open={openSubmenuId === item.id} @@ -388,37 +391,25 @@ cursor: pointer; } + /* Solid theme-tokened pill (formerly the "glass" frosted pill). + Class name kept for backwards compatibility. */ .glass-pill, :global(.fan-container .glass-pill) { - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(0, 0, 0, 0.1); + background: hsl(var(--color-card)); + border: 1px solid hsl(var(--color-border)); box-shadow: - 0 4px 6px -1px rgba(0, 0, 0, 0.1), - 0 2px 4px -1px rgba(0, 0, 0, 0.06); - color: #374151; - } - - :global(.dark) .glass-pill, - :global(.dark .fan-container .glass-pill) { - background: rgba(255, 255, 255, 0.12); - border: 1px solid rgba(255, 255, 255, 0.15); - color: #f3f4f6; + 0 1px 2px hsl(0 0% 0% / 0.05), + 0 2px 6px hsl(0 0% 0% / 0.04); + color: hsl(var(--color-foreground)); } .glass-pill:hover { - background: rgba(255, 255, 255, 0.95); - border-color: rgba(0, 0, 0, 0.15); + background: hsl(var(--color-surface-hover)); + border-color: hsl(var(--color-border-strong, var(--color-border))); transform: translateY(-2px); box-shadow: - 0 10px 15px -3px rgba(0, 0, 0, 0.1), - 0 4px 6px -2px rgba(0, 0, 0, 0.05); - } - - :global(.dark) .glass-pill:hover { - background: rgba(255, 255, 255, 0.2); - border-color: rgba(255, 255, 255, 0.25); + 0 6px 12px hsl(0 0% 0% / 0.08), + 0 2px 4px hsl(0 0% 0% / 0.05); } .active-pill { @@ -444,6 +435,23 @@ border-color: rgba(220, 38, 38, 0.3); } + /* Primary CTA pill — used for the guest "Anmelden" entry */ + .primary-pill { + background: hsl(var(--color-primary)); + border-color: hsl(var(--color-primary)); + color: hsl(var(--color-primary-foreground, 0 0% 100%)); + font-weight: 600; + } + + .primary-pill:hover { + background: hsl(var(--color-primary) / 0.92); + border-color: hsl(var(--color-primary) / 0.92); + color: hsl(var(--color-primary-foreground, 0 0% 100%)); + box-shadow: + 0 6px 14px hsl(var(--color-primary) / 0.3), + 0 2px 4px hsl(0 0% 0% / 0.06); + } + .pill-icon { width: 1rem; height: 1rem; diff --git a/packages/shared-ui/src/navigation/PillNavigation.svelte b/packages/shared-ui/src/navigation/PillNavigation.svelte index 0920b6fc1..aa2098db5 100644 --- a/packages/shared-ui/src/navigation/PillNavigation.svelte +++ b/packages/shared-ui/src/navigation/PillNavigation.svelte @@ -318,6 +318,10 @@ themesHref?: string; /** Spiral page href (shown in user dropdown). Set to empty string to hide. */ spiralHref?: string; + /** Credits page href (shown in user dropdown). Set to empty string to hide. */ + creditsHref?: string; + /** Trigger label for the user dropdown when no one is signed in. */ + guestMenuLabel?: string; /** Help page href (shown in user dropdown). Set to empty string to hide. */ helpHref?: string; /** Bottom offset from viewport bottom (default: '0px'). Use to position above other fixed bars. */ @@ -378,6 +382,8 @@ feedbackHref = '/feedback', themesHref, spiralHref, + creditsHref, + guestMenuLabel = 'Menü', helpHref, bottomOffset = '0px', }: Props = $props(); @@ -708,11 +714,14 @@ {/if} - - {#if userEmail} + + {#if userEmail || loginHref} { + window.location.href = spiralHref; + }, + active: currentPath === spiralHref, + }, + ] + : []), + ...(creditsHref + ? [ + { + id: 'credits', + label: 'Credits', + icon: 'creditCard', + onClick: () => { + window.location.href = creditsHref; + }, + active: currentPath === creditsHref, + }, + ] + : []), + ...(userEmail && feedbackHref ? [ { id: 'feedback', @@ -760,32 +795,6 @@ }, ] : []), - ...(themesHref - ? [ - { - id: 'themes', - label: 'Themes', - icon: 'palette', - onClick: () => { - window.location.href = themesHref; - }, - active: currentPath === themesHref, - }, - ] - : []), - ...(spiralHref - ? [ - { - id: 'spiral', - label: 'Spiral', - icon: 'sparkles', - onClick: () => { - window.location.href = spiralHref; - }, - active: currentPath === spiralHref, - }, - ] - : []), ...(helpHref ? [ { @@ -813,7 +822,7 @@ ] : []), { id: 'auth-divider', label: '', divider: true }, - ...(showLogout && onLogout + ...(userEmail && showLogout && onLogout ? [ { id: 'logout', @@ -823,12 +832,13 @@ danger: true, }, ] - : loginHref + : !userEmail && loginHref ? [ { id: 'login', - label: 'Login', + label: 'Anmelden', icon: 'user', + primary: true, onClick: () => { window.location.href = loginHref; }, @@ -837,21 +847,15 @@ : []), ]} direction={dropdownDirection} - label={truncateEmail(userEmail)} + label={userEmail ? truncateEmail(userEmail) : guestMenuLabel} icon="user" /> {:else if onLogout && showLogout} - + - {:else if loginHref && !userEmail} - - {/if} @@ -955,36 +959,24 @@ cursor: pointer; } - /* Glass effect */ + /* Solid theme-tokened pill (formerly the "glass" frosted pill). + The class name is kept for backwards compatibility. */ .glass-pill { - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(0, 0, 0, 0.1); + background: hsl(var(--color-card)); + border: 1px solid hsl(var(--color-border)); box-shadow: - 0 4px 6px -1px rgba(0, 0, 0, 0.1), - 0 2px 4px -1px rgba(0, 0, 0, 0.06); - color: #374151; - } - - :global(.dark) .glass-pill { - background: rgba(255, 255, 255, 0.12); - border: 1px solid rgba(255, 255, 255, 0.15); - color: #f3f4f6; + 0 1px 2px hsl(0 0% 0% / 0.05), + 0 2px 6px hsl(0 0% 0% / 0.04); + color: hsl(var(--color-foreground)); } .glass-pill:hover { - background: rgba(255, 255, 255, 0.95); - border-color: rgba(0, 0, 0, 0.15); + background: hsl(var(--color-surface-hover)); + border-color: hsl(var(--color-border-strong, var(--color-border))); transform: translateY(-2px); box-shadow: - 0 10px 15px -3px rgba(0, 0, 0, 0.1), - 0 4px 6px -2px rgba(0, 0, 0, 0.05); - } - - :global(.dark) .glass-pill:hover { - background: rgba(255, 255, 255, 0.2); - border-color: rgba(255, 255, 255, 0.25); + 0 6px 12px hsl(0 0% 0% / 0.08), + 0 2px 4px hsl(0 0% 0% / 0.05); } /* Active state - uses CSS custom property for theming */ @@ -1036,20 +1028,6 @@ border-color: rgba(220, 38, 38, 0.3); } - /* Guest login pill — prominent with primary color */ - .login-pill { - background: var(--pill-primary-color, var(--color-primary-500, #3b82f6)); - color: #fff; - border-color: transparent; - font-weight: 600; - text-decoration: none; - } - - .login-pill:hover { - filter: brightness(1.1); - transform: scale(1.03); - } - .pill-icon { width: 1rem; height: 1rem; diff --git a/packages/shared-ui/src/navigation/PillTabGroup.svelte b/packages/shared-ui/src/navigation/PillTabGroup.svelte index 809047be1..88f078f7e 100644 --- a/packages/shared-ui/src/navigation/PillTabGroup.svelte +++ b/packages/shared-ui/src/navigation/PillTabGroup.svelte @@ -118,20 +118,13 @@ border-radius: 9999px; } - /* Glass effect */ + /* Solid theme-tokened pill (formerly the "glass" frosted pill). */ .glass-pill { - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(0, 0, 0, 0.1); + background: hsl(var(--color-card)); + border: 1px solid hsl(var(--color-border)); box-shadow: - 0 4px 6px -1px rgba(0, 0, 0, 0.1), - 0 2px 4px -1px rgba(0, 0, 0, 0.06); - } - - :global(.dark) .glass-pill { - background: rgba(255, 255, 255, 0.12); - border: 1px solid rgba(255, 255, 255, 0.15); + 0 1px 2px hsl(0 0% 0% / 0.05), + 0 2px 6px hsl(0 0% 0% / 0.04); } .tab-btn { diff --git a/packages/shared-ui/src/navigation/PillTagSelector.svelte b/packages/shared-ui/src/navigation/PillTagSelector.svelte index e0acb3461..1cee223d4 100644 --- a/packages/shared-ui/src/navigation/PillTagSelector.svelte +++ b/packages/shared-ui/src/navigation/PillTagSelector.svelte @@ -200,36 +200,23 @@ cursor: pointer; } - /* Glass effect */ + /* Solid theme-tokened pill (formerly the "glass" frosted pill). */ .glass-pill { - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(0, 0, 0, 0.1); + background: hsl(var(--color-card)); + border: 1px solid hsl(var(--color-border)); box-shadow: - 0 4px 6px -1px rgba(0, 0, 0, 0.1), - 0 2px 4px -1px rgba(0, 0, 0, 0.06); - color: #374151; - } - - :global(.dark) .glass-pill { - background: rgba(255, 255, 255, 0.12); - border: 1px solid rgba(255, 255, 255, 0.15); - color: #f3f4f6; + 0 1px 2px hsl(0 0% 0% / 0.05), + 0 2px 6px hsl(0 0% 0% / 0.04); + color: hsl(var(--color-foreground)); } .glass-pill:hover { - background: rgba(255, 255, 255, 0.95); - border-color: rgba(0, 0, 0, 0.15); + background: hsl(var(--color-surface-hover)); + border-color: hsl(var(--color-border-strong, var(--color-border))); transform: translateY(-2px); box-shadow: - 0 10px 15px -3px rgba(0, 0, 0, 0.1), - 0 4px 6px -2px rgba(0, 0, 0, 0.05); - } - - :global(.dark) .glass-pill:hover { - background: rgba(255, 255, 255, 0.2); - border-color: rgba(255, 255, 255, 0.25); + 0 6px 12px hsl(0 0% 0% / 0.08), + 0 2px 4px hsl(0 0% 0% / 0.05); } /* Active selection state */ diff --git a/packages/shared-ui/src/navigation/PillTimeRangeSelector.svelte b/packages/shared-ui/src/navigation/PillTimeRangeSelector.svelte index fe0e12441..ea738d8f0 100644 --- a/packages/shared-ui/src/navigation/PillTimeRangeSelector.svelte +++ b/packages/shared-ui/src/navigation/PillTimeRangeSelector.svelte @@ -255,22 +255,20 @@ } .glass-pill { - background: hsl(var(--color-surface) / 0.85); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); + background: hsl(var(--color-card)); border: 1px solid hsl(var(--color-border)); box-shadow: - 0 4px 6px -1px hsl(var(--color-foreground) / 0.1), - 0 2px 4px -1px hsl(var(--color-foreground) / 0.06); + 0 1px 2px hsl(0 0% 0% / 0.05), + 0 2px 6px hsl(0 0% 0% / 0.04); color: hsl(var(--color-foreground)); } .glass-pill:hover { - background: hsl(var(--color-surface) / 0.95); + background: hsl(var(--color-surface-hover)); transform: translateY(-1px); box-shadow: - 0 10px 15px -3px hsl(var(--color-foreground) / 0.1), - 0 4px 6px -2px hsl(var(--color-foreground) / 0.05); + 0 6px 12px hsl(0 0% 0% / 0.08), + 0 2px 4px hsl(0 0% 0% / 0.05); } .pill-label { diff --git a/packages/shared-ui/src/navigation/PillToolbar.svelte b/packages/shared-ui/src/navigation/PillToolbar.svelte index e4388b9ed..a9bf1ca30 100644 --- a/packages/shared-ui/src/navigation/PillToolbar.svelte +++ b/packages/shared-ui/src/navigation/PillToolbar.svelte @@ -73,18 +73,11 @@ } .glass-pill { - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(0, 0, 0, 0.1); + background: hsl(var(--color-card)); + border: 1px solid hsl(var(--color-border)); box-shadow: - 0 4px 6px -1px rgba(0, 0, 0, 0.1), - 0 2px 4px -1px rgba(0, 0, 0, 0.06); + 0 1px 2px hsl(0 0% 0% / 0.05), + 0 2px 6px hsl(0 0% 0% / 0.04); border-radius: 9999px; } - - :global(.dark) .glass-pill { - background: rgba(255, 255, 255, 0.12); - border: 1px solid rgba(255, 255, 255, 0.15); - } diff --git a/packages/shared-ui/src/navigation/PillViewSwitcher.svelte b/packages/shared-ui/src/navigation/PillViewSwitcher.svelte index c86beb2fe..e317ace86 100644 --- a/packages/shared-ui/src/navigation/PillViewSwitcher.svelte +++ b/packages/shared-ui/src/navigation/PillViewSwitcher.svelte @@ -112,13 +112,11 @@ } .glass-pill { - background: hsl(var(--color-surface) / 0.85); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); + background: hsl(var(--color-card)); border: 1px solid hsl(var(--color-border)); box-shadow: - 0 4px 6px -1px hsl(var(--color-foreground) / 0.1), - 0 2px 4px -1px hsl(var(--color-foreground) / 0.06); + 0 1px 2px hsl(0 0% 0% / 0.05), + 0 2px 6px hsl(0 0% 0% / 0.04); } /* Embedded mode - no background/border */ diff --git a/packages/shared-ui/src/navigation/expandable-toolbar/ExpandableToolbar.svelte b/packages/shared-ui/src/navigation/expandable-toolbar/ExpandableToolbar.svelte index 6126764f2..6a3f543e9 100644 --- a/packages/shared-ui/src/navigation/expandable-toolbar/ExpandableToolbar.svelte +++ b/packages/shared-ui/src/navigation/expandable-toolbar/ExpandableToolbar.svelte @@ -145,13 +145,13 @@ overflow: visible; } - /* Glass styling */ + /* Solid theme-tokened pill (formerly the "glass" frosted pill). */ .glass-pill { - background: hsl(var(--color-surface) / 0.85); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); + background: hsl(var(--color-card)); border: 1px solid hsl(var(--color-border)); - box-shadow: 0 2px 8px hsl(var(--color-foreground) / 0.08); + box-shadow: + 0 1px 2px hsl(0 0% 0% / 0.05), + 0 2px 6px hsl(0 0% 0% / 0.04); border-radius: 9999px; } diff --git a/packages/shared-ui/src/navigation/types.ts b/packages/shared-ui/src/navigation/types.ts index 7ac1d9125..b47f9e700 100644 --- a/packages/shared-ui/src/navigation/types.ts +++ b/packages/shared-ui/src/navigation/types.ts @@ -43,6 +43,8 @@ export interface PillDropdownItem { disabled?: boolean; /** Whether item should be styled as danger/destructive */ danger?: boolean; + /** Whether item should be styled prominently with the primary color */ + primary?: boolean; /** Whether this item is currently active/selected */ active?: boolean; /** Whether this item is a divider */