feat(pill-nav): collapse user pills into account dropdown + solid pill backgrounds

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) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-11 17:40:19 +02:00
parent 3ac3a4bae4
commit 637333051b
11 changed files with 140 additions and 213 deletions

View file

@ -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 */

View file

@ -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;

View file

@ -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 @@
</button>
{/if}
<!-- User Menu Dropdown -->
{#if userEmail}
<!-- User Menu Dropdown — rendered for both authenticated users and
guests. Auth-only items (profile/settings/logout) are filtered
out when userEmail is empty; spiral/credits/themes/help stay
available either way so guests can still navigate. -->
{#if userEmail || loginHref}
<PillDropdown
items={[
...(profileHref
...(userEmail && profileHref
? [
{
id: 'profile',
@ -734,7 +743,7 @@
},
active: currentPath === settingsHref,
},
...(manaHref
...(userEmail && manaHref
? [
{
id: 'mana',
@ -747,7 +756,33 @@
},
]
: []),
...(feedbackHref
...(spiralHref
? [
{
id: 'spiral',
label: 'Spiral',
icon: 'spiral',
onClick: () => {
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}
<!-- Fallback to standalone logout if no user email -->
<!-- Fallback to standalone logout if no user email and no loginHref -->
<button onclick={onLogout} class="pill glass-pill logout-pill" title="Logout">
<SignOut size={18} class="pill-icon" />
<span class="pill-label">Logout</span>
</button>
{:else if loginHref && !userEmail}
<!-- Guest mode: prominent login button -->
<a href={loginHref} class="pill glass-pill login-pill" title="Anmelden">
<User size={18} class="pill-icon" />
<span class="pill-label">Anmelden</span>
</a>
{/if}
</div>
</nav>
@ -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;

View file

@ -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 {

View file

@ -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 */

View file

@ -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 {

View file

@ -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);
}
</style>

View file

@ -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 */

View file

@ -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;
}

View file

@ -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 */