managarten/packages/shared-ui/src/navigation/PillNavigation.svelte
Till-JS dde2d51778 feat(shared-ui): add app switcher dropdown to PillNavigation
- Add PillAppDropdown component for switching between Mana apps
- Add PillAppItem type for app configuration
- Add showAppSwitcher and appItems props to PillNavigation
- Add APP_URLS config and getPillAppItems helper to shared-branding
- Apps open in new tab with external link icon indicator

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 15:10:17 +01:00

857 lines
24 KiB
Svelte

<script lang="ts">
import type { Snippet } from 'svelte';
import type { PillNavItem, PillDropdownItem, PillNavElement, PillTabGroupConfig, PillAppItem } from './types';
import PillDropdown from './PillDropdown.svelte';
import PillTabGroup from './PillTabGroup.svelte';
import PillAppDropdown from './PillAppDropdown.svelte';
interface Props {
/** Navigation items */
items: PillNavItem[];
/** Current active path */
currentPath?: string;
/** Logo snippet */
logo?: Snippet;
/** App name */
appName?: string;
/** Home/default route */
homeRoute?: string;
/** Called when logout is clicked */
onLogout?: () => void;
/** Called when theme toggle is clicked */
onToggleTheme?: () => void;
/** Whether dark mode is active */
isDark?: boolean;
/** Whether sidebar mode is enabled */
isSidebarMode?: boolean;
/** Called when sidebar mode changes */
onModeChange?: (isSidebar: boolean) => void;
/** Whether navigation is collapsed */
isCollapsed?: boolean;
/** Called when collapsed state changes */
onCollapsedChange?: (isCollapsed: boolean) => void;
/** Language dropdown items */
languageItems?: PillDropdownItem[];
/** Current language label */
currentLanguageLabel?: string;
/** Show language switcher */
showLanguageSwitcher?: boolean;
/** Show theme toggle (standalone button, hidden if showThemeVariants is true) */
showThemeToggle?: boolean;
/** Primary color for active state (CSS custom property or hex) */
primaryColor?: string;
/** Additional elements (tab groups, dividers) to show after nav items */
elements?: PillNavElement[];
/** Show logout button */
showLogout?: boolean;
/** Theme variant dropdown items */
themeVariantItems?: PillDropdownItem[];
/** Current theme variant label */
currentThemeVariantLabel?: string;
/** Show theme variant selector */
showThemeVariants?: boolean;
/** Current theme mode ('light', 'dark', 'system') */
themeMode?: 'light' | 'dark' | 'system';
/** Called when theme mode changes */
onThemeModeChange?: (mode: 'light' | 'dark' | 'system') => void;
/** App items for app switcher dropdown */
appItems?: PillAppItem[];
/** Show app switcher dropdown */
showAppSwitcher?: boolean;
}
let {
items,
currentPath = '',
logo,
appName = 'App',
homeRoute = '/',
onLogout,
onToggleTheme,
isDark = false,
isSidebarMode: externalSidebarMode,
onModeChange,
isCollapsed: externalCollapsed,
onCollapsedChange,
languageItems = [],
currentLanguageLabel = 'Language',
showLanguageSwitcher = false,
showThemeToggle = true,
primaryColor,
elements = [],
showLogout = true,
themeVariantItems = [],
currentThemeVariantLabel = 'Theme',
showThemeVariants = false,
themeMode = 'system',
onThemeModeChange,
appItems = [],
showAppSwitcher = false,
}: Props = $props();
// Type guards for elements
function isTabGroup(element: PillNavElement): element is PillTabGroupConfig {
return 'type' in element && element.type === 'tabs';
}
function isDivider(element: PillNavElement): element is { type: 'divider' } {
return 'type' in element && element.type === 'divider';
}
function isNavItem(element: PillNavElement): element is PillNavItem {
return 'href' in element;
}
// Local state for uncontrolled mode
let internalSidebarMode = $state(false);
let internalCollapsed = $state(false);
// Use external or internal state
const isSidebarMode = $derived(
onModeChange !== undefined ? (externalSidebarMode ?? false) : internalSidebarMode
);
const isCollapsed = $derived(
onCollapsedChange !== undefined ? (externalCollapsed ?? false) : internalCollapsed
);
function toggleSidebarMode() {
const newValue = !isSidebarMode;
if (onModeChange) {
onModeChange(newValue);
} else {
internalSidebarMode = newValue;
}
}
function collapseNav() {
if (onCollapsedChange) {
onCollapsedChange(true);
} else {
internalCollapsed = true;
}
}
function expandNav() {
if (onCollapsedChange) {
onCollapsedChange(false);
} else {
internalCollapsed = false;
}
}
function isActive(path: string) {
return currentPath === path;
}
// Icon SVG paths
const icons: Record<string, string> = {
mic: 'M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z',
archive: 'M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4',
upload: 'M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12',
music:
'M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3',
tag: 'M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z',
document:
'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
chart:
'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z',
settings:
'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z',
settingsInner: 'M15 12a3 3 0 11-6 0 3 3 0 016 0z',
home: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6',
users:
'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z',
user: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z',
building:
'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4',
creditCard:
'M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z',
search: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z',
heart: 'M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z',
list: 'M4 6h16M4 10h16M4 14h16M4 18h16',
compass:
'M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22ZM16.24 7.76L14.12 14.12L7.76 16.24L9.88 9.88L16.24 7.76Z',
moon: 'M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z',
sun: 'M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z',
logout:
'M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1',
chevronDown: 'M19 9l-7 7-7-7',
chevronUp: 'M5 15l7-7 7 7',
chevronLeft: 'M15 19l-7-7 7-7',
menu: 'M4 6h16M4 12h16M4 18h16',
fire: 'M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7C14 5 16.09 5.777 17.656 7.343A7.975 7.975 0 0120 13a7.975 7.975 0 01-2.343 5.657z',
grid: 'M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z',
gridSmall:
'M4 5a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM10 5a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1V5zM16 5a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1V5zM4 11a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1v-2zM10 11a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2zM16 11a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2z',
palette:
'M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01',
};
function getIconPath(name: string): string {
return icons[name] || '';
}
</script>
{#if !isCollapsed}
<nav
class="pill-nav"
class:sidebar-mode={isSidebarMode}
style={primaryColor ? `--pill-primary-color: ${primaryColor}` : ''}
>
<div class="pill-nav-container" class:sidebar-container={isSidebarMode}>
<!-- Control Button (left position in horizontal mode) -->
{#if !isSidebarMode}
<div class="pill glass-pill segmented-control">
<button
onclick={toggleSidebarMode}
class="segment-btn"
title="Switch to sidebar navigation"
>
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('chevronDown')}
/>
</svg>
</button>
<div class="segment-divider"></div>
<button onclick={collapseNav} class="segment-btn" title="Collapse navigation">
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('chevronLeft')}
/>
</svg>
</button>
</div>
{/if}
<!-- Logo pill / App Switcher -->
{#if showAppSwitcher && appItems.length > 0}
<PillAppDropdown
apps={appItems}
currentAppName={appName}
{logo}
{homeRoute}
direction="down"
/>
{:else}
<a href={homeRoute} class="pill glass-pill logo-pill">
{#if logo}
{@render logo()}
{:else}
<span class="pill-label font-bold">{appName}</span>
{/if}
</a>
{/if}
<!-- Navigation Items -->
{#each items as item}
<a href={item.href} class="pill glass-pill" class:active={isActive(item.href)}>
{#if item.icon}
{#if item.icon === 'mana'}
<svg class="pill-icon" viewBox="0 0 24 24" fill="currentColor">
<path
d="M12.3047 1C12.3392 1.04573 19.608 10.6706 19.6084 14.6953C19.6084 18.7293 16.3386 21.9998 12.3047 22C8.27061 22 5 18.7294 5 14.6953C5.00041 10.661 12.3047 1 12.3047 1ZM12.3047 7.3916C12.2811 7.42276 8.65234 12.2288 8.65234 14.2393C8.65241 16.2562 10.2877 17.8916 12.3047 17.8916C14.3217 17.8916 15.957 16.2562 15.957 14.2393C15.957 12.2301 12.3331 7.42917 12.3047 7.3916Z"
/>
</svg>
{:else if item.icon === 'settings'}
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('settings')}
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('settingsInner')}
/>
</svg>
{:else if item.iconSvg}
{@html item.iconSvg}
{:else}
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath(item.icon)}
/>
</svg>
{/if}
{/if}
<span class="pill-label">{item.label}</span>
</a>
{/each}
<!-- Additional Elements (Tab Groups, Dividers) -->
{#each elements as element}
{#if isTabGroup(element)}
<PillTabGroup
options={element.options}
value={element.value}
onChange={element.onChange}
sectionLabel={element.sectionLabel}
{isSidebarMode}
{primaryColor}
/>
{:else if isDivider(element)}
<div class="pill-divider" class:sidebar-divider={isSidebarMode}></div>
{:else if isNavItem(element)}
<a href={element.href} class="pill glass-pill" class:active={isActive(element.href)}>
{#if element.icon}
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath(element.icon)}
/>
</svg>
{/if}
<span class="pill-label">{element.label}</span>
</a>
{/if}
{/each}
<!-- Language Switcher -->
{#if showLanguageSwitcher && languageItems.length > 0}
<PillDropdown
items={languageItems}
direction="down"
label={currentLanguageLabel}
icon="globe"
/>
{/if}
<!-- Theme Variant Selector -->
{#if showThemeVariants && themeVariantItems.length > 0}
<PillDropdown
items={themeVariantItems}
direction="down"
label={currentThemeVariantLabel}
icon="palette"
>
{#snippet header()}
<div class="theme-mode-selector">
<button
type="button"
onclick={() => onThemeModeChange?.('light')}
class="mode-btn"
class:active={themeMode === 'light'}
title="Light mode"
>
<svg class="mode-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath('sun')} />
</svg>
</button>
<button
type="button"
onclick={() => onThemeModeChange?.('dark')}
class="mode-btn"
class:active={themeMode === 'dark'}
title="Dark mode"
>
<svg class="mode-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath('moon')} />
</svg>
</button>
<button
type="button"
onclick={() => onThemeModeChange?.('system')}
class="mode-btn"
class:active={themeMode === 'system'}
title="System mode"
>
<svg class="mode-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<rect x="2" y="3" width="20" height="14" rx="2" stroke-width="2" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 21h8M12 17v4" />
</svg>
</button>
</div>
{/snippet}
</PillDropdown>
{/if}
<!-- Theme Toggle (only show when not using theme variants dropdown) -->
{#if showThemeToggle && onToggleTheme && !showThemeVariants}
<button
onclick={onToggleTheme}
class="pill glass-pill"
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
{#if !isDark}
<svg class="pill-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('moon')}
/>
</svg>
{:else}
<svg class="pill-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('sun')}
/>
</svg>
{/if}
<span class="pill-label">{isDark ? 'Light' : 'Dark'}</span>
</button>
{/if}
<!-- Logout -->
{#if onLogout && showLogout}
<button onclick={onLogout} class="pill glass-pill logout-pill" title="Logout">
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('logout')}
/>
</svg>
<span class="pill-label">Logout</span>
</button>
{/if}
<!-- Control Button (bottom position in sidebar mode) -->
{#if isSidebarMode}
<div class="sidebar-spacer"></div>
<div class="pill glass-pill segmented-control sidebar-segmented">
<button onclick={toggleSidebarMode} class="segment-btn" title="Switch to top navigation">
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('chevronUp')}
/>
</svg>
</button>
<div class="segment-divider"></div>
<button onclick={collapseNav} class="segment-btn" title="Collapse navigation">
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('chevronLeft')}
/>
</svg>
</button>
</div>
{/if}
</div>
</nav>
{/if}
<!-- FAB for collapsed state -->
{#if isCollapsed}
<button onclick={expandNav} class="nav-fab glass-pill" title="Expand navigation">
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('menu')}
/>
</svg>
</button>
{/if}
<style>
.pill-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
padding: 0.75rem 0 1.5rem;
pointer-events: none;
}
.pill-nav-container {
display: flex;
align-items: center;
gap: 1rem;
overflow-x: auto;
scrollbar-width: none;
-ms-overflow-style: none;
pointer-events: auto;
padding: 0.5rem 2rem;
}
.pill-nav-container::-webkit-scrollbar {
display: none;
}
/* Base pill styles */
.pill {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 500;
white-space: nowrap;
text-decoration: none;
transition: all 0.2s;
border: none;
cursor: pointer;
}
/* Glass effect */
.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);
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;
}
.glass-pill:hover {
background: rgba(255, 255, 255, 0.95);
border-color: rgba(0, 0, 0, 0.15);
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);
}
/* Active state - uses CSS custom property for theming */
.pill.active {
background: var(--pill-primary-color, var(--color-primary-500, rgba(248, 214, 43, 0.9)));
background: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 20%,
white 80%
);
border-color: var(--pill-primary-color, var(--color-primary-500, rgba(248, 214, 43, 0.5)));
color: #1a1a1a;
}
:global(.dark) .pill.active {
background: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 30%,
transparent 70%
);
border-color: var(--pill-primary-color, var(--color-primary-500, rgba(248, 214, 43, 0.4)));
color: var(--pill-primary-color, var(--color-primary-500, #f8d62b));
}
/* Divider */
.pill-divider {
width: 1px;
height: 1.5rem;
background: rgba(0, 0, 0, 0.15);
flex-shrink: 0;
margin: 0 0.25rem;
}
:global(.dark) .pill-divider {
background: rgba(255, 255, 255, 0.2);
}
.sidebar-divider {
width: 100%;
height: 1px;
margin: 0.5rem 0;
}
/* Logout pill */
.logout-pill {
color: #dc2626;
}
:global(.dark) .logout-pill {
color: #ef4444;
}
.logout-pill:hover {
background: rgba(220, 38, 38, 0.15);
border-color: rgba(220, 38, 38, 0.3);
}
.pill-icon {
width: 1rem;
height: 1rem;
flex-shrink: 0;
}
.pill-label {
display: inline;
}
/* Sidebar mode styles */
.pill-nav.sidebar-mode {
top: 0;
left: 0;
bottom: 0;
right: auto;
width: 180px;
padding: 0.75rem 0;
background: transparent;
backdrop-filter: none;
-webkit-backdrop-filter: none;
border: none;
}
:global(.dark) .pill-nav.sidebar-mode {
background: transparent;
border: none;
}
.sidebar-container {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
overflow-y: auto;
overflow-x: hidden;
padding: 0.5rem 0.75rem;
height: 100%;
}
.sidebar-container .pill {
justify-content: flex-start;
width: 100%;
}
/* Transparent pills in sidebar mode */
.sidebar-container .glass-pill,
.sidebar-container :global(.pill-dropdown .trigger-button) {
background: transparent;
backdrop-filter: none;
-webkit-backdrop-filter: none;
border: 1px solid transparent;
box-shadow: none;
}
.sidebar-container .glass-pill:hover,
.sidebar-container :global(.pill-dropdown .trigger-button:hover) {
background: rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.1);
transform: none;
box-shadow: none;
}
:global(.dark) .sidebar-container .glass-pill:hover,
:global(.dark) .sidebar-container :global(.pill-dropdown .trigger-button:hover) {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.1);
}
/* Keep active state visible */
.sidebar-container .pill.active {
background: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 20%,
transparent 80%
);
border-color: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 30%,
transparent 70%
);
}
:global(.dark) .sidebar-container .pill.active {
background: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 15%,
transparent 85%
);
border-color: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 25%,
transparent 75%
);
}
/* Logo pill in sidebar - same as other pills (transparent) */
.sidebar-container .logo-pill {
background: transparent;
border-color: transparent;
}
.sidebar-container .logo-pill:hover {
background: rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.1);
}
:global(.dark) .sidebar-container .logo-pill:hover {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.1);
}
/* Spacer to push toggle button to bottom */
.sidebar-spacer {
flex: 1;
min-height: 1rem;
}
.sidebar-container .toggle-pill {
margin-top: auto;
}
/* Segmented control */
.segmented-control {
display: flex;
align-items: center;
padding: 0;
gap: 0;
}
.segment-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0.5rem 0.625rem;
background: transparent;
border: none;
cursor: pointer;
color: inherit;
transition: background 0.2s;
}
.segment-btn:hover {
background: rgba(0, 0, 0, 0.05);
}
:global(.dark) .segment-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.segment-divider {
width: 1px;
height: 1rem;
background: rgba(0, 0, 0, 0.15);
}
:global(.dark) .segment-divider {
background: rgba(255, 255, 255, 0.2);
}
.sidebar-segmented {
margin: 0 0.75rem;
}
/* FAB for collapsed state */
.nav-fab {
position: fixed;
top: 1rem;
left: 1rem;
z-index: 1001;
display: flex;
align-items: center;
justify-content: center;
padding: 0.75rem;
border-radius: 9999px;
cursor: pointer;
border: none;
}
/* Transitions */
.pill-nav {
transition: all 0.3s ease;
}
.pill-nav-container {
transition: all 0.3s ease;
}
/* Theme mode selector in dropdown header */
.theme-mode-selector {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem;
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);
border-radius: 9999px;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
:global(.dark) .theme-mode-selector {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.15);
}
.mode-btn {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
padding: 0.375rem;
border: none;
background: transparent;
border-radius: 9999px;
cursor: pointer;
color: #374151;
transition: all 0.15s;
}
:global(.dark) .mode-btn {
color: #f3f4f6;
}
.mode-btn:hover:not(.active) {
background: rgba(0, 0, 0, 0.05);
}
:global(.dark) .mode-btn:hover:not(.active) {
background: rgba(255, 255, 255, 0.1);
}
.mode-btn.active {
background: var(--pill-primary-color, var(--color-primary-500, rgba(248, 214, 43, 0.2)));
background: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #3b82f6)) 20%,
white 80%
);
}
:global(.dark) .mode-btn.active {
background: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #3b82f6)) 30%,
transparent 70%
);
}
.mode-icon {
width: 1rem;
height: 1rem;
}
</style>