feat: unify navigation with shared PillNavigation component

- Add PillNavigation and PillDropdown to @manacore/shared-ui
- Features: glassmorphism design, horizontal/sidebar toggle, collapsible FAB
- Configurable per app: nav items, primary color, logo, language switcher
- Integrate into ManaCore, ManaDeck, and Memoro web apps
- Remove old local navigation components (Sidebar, Navbar, PillNavigation)
- Add navigation stores for persistent user preferences

Apps now share consistent navigation UX with app-specific branding.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-25 01:31:23 +01:00
parent cacbd61fe4
commit bd869dfe09
13 changed files with 984 additions and 1520 deletions

View file

@ -1,327 +0,0 @@
<script lang="ts">
interface DropdownItem {
id: string;
label: string;
icon?: string;
onClick: () => void;
disabled?: boolean;
danger?: boolean;
active?: boolean;
}
interface Props {
items: DropdownItem[];
direction?: 'up' | 'down';
label: string;
icon?: string;
isOpen?: boolean;
onToggle?: (open: boolean) => void;
}
let {
items,
direction = 'down',
label,
icon,
isOpen = false,
onToggle
}: Props = $props();
let internalOpen = $state(false);
let triggerButton: HTMLButtonElement;
let dropdownPosition = $state({ top: 0, left: 0 });
const open = $derived(onToggle ? isOpen : internalOpen);
function toggle() {
if (triggerButton) {
const rect = triggerButton.getBoundingClientRect();
if (direction === 'down') {
dropdownPosition = {
top: rect.bottom + 8,
left: rect.left
};
} else {
dropdownPosition = {
top: rect.top - 8,
left: rect.left
};
}
}
if (onToggle) {
onToggle(!isOpen);
} else {
internalOpen = !internalOpen;
}
}
function close() {
if (onToggle) {
onToggle(false);
} else {
internalOpen = false;
}
}
function handleItemClick(item: DropdownItem) {
item.onClick();
close();
}
function getIcon(iconName: string) {
const icons: Record<string, string> = {
language: 'M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129',
check: 'M5 13l4 4L19 7',
chevronDown: 'M19 9l-7 7-7-7',
globe: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9'
};
return icons[iconName] || '';
}
</script>
<div class="pill-dropdown">
<!-- Trigger Button -->
<button
bind:this={triggerButton}
onclick={toggle}
class="pill glass-pill trigger-button"
>
{#if 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={getIcon(icon)}
/>
</svg>
{/if}
<span class="pill-label">{label}</span>
<svg
class="chevron-icon"
class:rotated={open}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIcon('chevronDown')}
/>
</svg>
</button>
{#if open}
<!-- Backdrop -->
<button
class="menu-backdrop"
onclick={close}
onkeydown={(e) => e.key === 'Escape' && close()}
></button>
<!-- Dropdown items -->
<div
class="fan-container"
class:fan-up={direction === 'up'}
class:fan-down={direction === 'down'}
style="top: {dropdownPosition.top}px; left: {dropdownPosition.left}px;"
>
{#each items.filter(i => !i.disabled) as item, i (item.id)}
<button
onclick={() => handleItemClick(item)}
class="pill glass-pill fan-pill"
class:danger-pill={item.danger}
class:active-pill={item.active}
style="animation-delay: {i * 15}ms"
>
{#if item.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={getIcon(item.icon)}
/>
</svg>
{/if}
<span class="pill-label">{item.label}</span>
{#if item.active}
<svg
class="check-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIcon('check')}
/>
</svg>
{/if}
</button>
{/each}
</div>
{/if}
</div>
<style>
.pill-dropdown {
position: relative;
}
.trigger-button {
position: relative;
z-index: 10;
}
.chevron-icon {
width: 0.75rem;
height: 0.75rem;
transition: transform 0.2s;
margin-left: 0.25rem;
}
.chevron-icon.rotated {
transform: rotate(180deg);
}
.fan-container {
position: fixed;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 9999;
}
.fan-up {
flex-direction: column-reverse;
transform: translateY(-100%);
}
.fan-down {
flex-direction: column;
}
.fan-pill {
animation: fanIn 0.15s ease-out forwards;
opacity: 0;
transform: translateY(10px);
}
.fan-up .fan-pill {
transform: translateY(-10px);
}
@keyframes fanIn {
to {
opacity: 1;
transform: translateY(0);
}
}
.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-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-pill {
background: rgba(248, 214, 43, 0.2);
border-color: rgba(248, 214, 43, 0.3);
}
:global(.dark) .active-pill {
background: rgba(248, 214, 43, 0.15);
border-color: rgba(248, 214, 43, 0.25);
}
.danger-pill {
color: #dc2626;
}
:global(.dark) .danger-pill {
color: #ef4444;
}
.danger-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;
}
.check-icon {
width: 0.875rem;
height: 0.875rem;
margin-left: 0.25rem;
color: #f8d62b;
}
.pill-label {
display: inline;
}
.menu-backdrop {
position: fixed;
inset: 0;
z-index: 9998;
background: transparent;
border: none;
cursor: default;
}
</style>

View file

@ -1,535 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import { user } from '$lib/stores/auth';
import { theme } from '$lib/stores/theme';
import { locale } from 'svelte-i18n';
import { onMount } from 'svelte';
import PillDropdown from './PillDropdown.svelte';
interface Props {
onLogout: () => void;
onModeChange?: (isSidebar: boolean) => void;
onCollapsedChange?: (isCollapsed: boolean) => void;
}
let { onLogout, onModeChange, onCollapsedChange }: Props = $props();
// Sidebar mode state with localStorage persistence
let isSidebarMode = $state(false);
let isCollapsed = $state(false);
onMount(() => {
const savedSidebar = localStorage.getItem('memoro-nav-sidebar');
if (savedSidebar === 'true') {
isSidebarMode = true;
onModeChange?.(true);
}
const savedCollapsed = localStorage.getItem('memoro-nav-collapsed');
if (savedCollapsed === 'true') {
isCollapsed = true;
onCollapsedChange?.(true);
}
});
function toggleSidebarMode() {
isSidebarMode = !isSidebarMode;
localStorage.setItem('memoro-nav-sidebar', String(isSidebarMode));
onModeChange?.(isSidebarMode);
}
function collapseNav() {
isCollapsed = true;
localStorage.setItem('memoro-nav-collapsed', 'true');
onCollapsedChange?.(true);
}
function expandNav() {
isCollapsed = false;
localStorage.setItem('memoro-nav-collapsed', 'false');
onCollapsedChange?.(false);
}
function isActive(path: string) {
return $page.url.pathname === path;
}
function toggleTheme() {
theme.toggleMode();
}
let currentTheme = $derived($theme);
// Language state - sync with svelte-i18n locale
let currentLanguage = $derived($locale || 'de');
const languages = [
{ code: 'de', label: 'Deutsch' },
{ code: 'en', label: 'English' },
{ code: 'es', label: 'Español' },
{ code: 'fr', label: 'Français' },
{ code: 'it', label: 'Italiano' },
];
const languageItems = $derived(languages.map(lang => ({
id: lang.code,
label: lang.label,
onClick: () => {
locale.set(lang.code);
},
active: currentLanguage === lang.code
})));
const currentLanguageLabel = $derived(
languages.find(l => l.code === currentLanguage)?.label || 'Deutsch'
);
const navItems = [
{ path: '/record', label: 'Aufnehmen', icon: 'mic' },
{ path: '/memos', label: 'Memos', icon: 'archive' },
{ path: '/upload', label: 'Upload', icon: 'upload' },
{ path: '/audio-archive', label: 'Audio-Archiv', icon: 'music' },
{ path: '/tags', label: 'Tags', icon: 'tag' },
{ path: '/subscription', label: 'Mana', icon: 'mana' },
{ path: '/blueprints', label: 'Blueprints', icon: 'document' },
{ path: '/statistics', label: 'Statistics', icon: 'chart' },
{ path: '/settings', label: 'Settings', icon: 'settings' },
];
</script>
{#if !isCollapsed}
<nav class="pill-nav" class:sidebar-mode={isSidebarMode}>
<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="M19 9l-7 7-7-7" />
</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="M15 19l-7-7 7-7" />
</svg>
</button>
</div>
{/if}
<!-- Memoro Logo as first pill -->
<a href="/record" class="pill glass-pill logo-pill">
<svg class="pill-icon" width="16" height="16" viewBox="0 0 280 280" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M280 140C280 217.32 217.32 280 140 280C62.6801 280 0 217.32 0 140C0 62.6801 62.6801 0 140 0C217.32 0 280 62.6801 280 140ZM247.988 140C247.988 199.64 199.64 241.988 140 241.988C80.3598 241.988 32.0118 199.64 32.0118 140C32.0118 111.918 36.7308 95.3397 54.3005 76.1331C58.5193 71.5212 70.5 63 79.3937 74.511L119.781 131.788C134.5 149 149 147 160.218 131.788L200.605 74.5101C208 64 221.48 71.5203 225.699 76.1321C243.269 95.3388 247.988 111.918 247.988 140Z" fill="#F7D44C"/>
</svg>
<span class="pill-label">Memoro</span>
</a>
{#each navItems as item}
<a
href={item.path}
class="pill glass-pill"
class:active={isActive(item.path)}
>
{#if item.icon === 'mic'}
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
</svg>
{:else if item.icon === 'archive'}
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
</svg>
{:else if item.icon === 'upload'}
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
</svg>
{:else if item.icon === 'music'}
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
</svg>
{:else if item.icon === 'tag'}
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
</svg>
{:else 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 === 'document'}
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
</svg>
{:else if item.icon === 'chart'}
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
</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="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" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
{/if}
<span class="pill-label">{item.label}</span>
</a>
{/each}
<!-- Language Switcher -->
<PillDropdown
items={languageItems}
direction="down"
label={currentLanguageLabel}
icon="globe"
/>
<!-- Theme Toggle as pill -->
<button
onclick={toggleTheme}
class="pill glass-pill"
title={currentTheme.effectiveMode === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
>
{#if currentTheme.effectiveMode === 'light'}
<svg class="pill-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
</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="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" />
</svg>
{/if}
<span class="pill-label">{currentTheme.effectiveMode === 'light' ? 'Dark' : 'Light'}</span>
</button>
<!-- Logout as pill -->
<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="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" />
</svg>
<span class="pill-label">Logout</span>
</button>
<!-- 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="M5 15l7-7 7 7" />
</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="M15 19l-7-7 7-7" />
</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="M4 6h16M4 12h16M4 18h16" />
</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 */
.pill.active {
background: rgba(248, 214, 43, 0.9);
border-color: rgba(248, 214, 43, 0.5);
color: #1a1a1a;
}
:global(.dark) .pill.active {
background: rgba(248, 214, 43, 0.3);
border-color: rgba(248, 214, 43, 0.4);
color: #f8d62b;
}
/* 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;
}
/* Toggle button */
.toggle-pill {
flex-shrink: 0;
}
/* 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: rgba(248, 214, 43, 0.2);
border-color: rgba(248, 214, 43, 0.3);
}
:global(.dark) .sidebar-container .pill.active {
background: rgba(248, 214, 43, 0.15);
border-color: rgba(248, 214, 43, 0.25);
}
/* 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;
}
</style>

View file

@ -1,774 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import { user } from '$lib/stores/auth';
import { theme } from '$lib/stores/theme';
import { Text } from '@manacore/shared-ui';
interface Props {
onLogout: () => void;
}
let { onLogout }: Props = $props();
// Load minimized state from localStorage, default to true (minimized)
let isMinimized = $state(
typeof localStorage !== 'undefined'
? (localStorage.getItem('sidebar-minimized') ?? 'true') === 'true'
: true
);
let showShortcuts = $state(false);
const isMac = typeof navigator !== 'undefined' && navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const modKey = isMac ? '⌘' : 'Ctrl';
function toggleSidebar() {
isMinimized = !isMinimized;
// Save state to localStorage
if (typeof localStorage !== 'undefined') {
localStorage.setItem('sidebar-minimized', String(isMinimized));
}
}
function isActive(path: string) {
return $page.url.pathname === path;
}
function handleMouseEnter(e: MouseEvent) {
if (!isMinimized) return;
const target = e.currentTarget as HTMLElement;
const tooltip = target.querySelector('.tooltip') as HTMLElement;
if (tooltip) {
const rect = target.getBoundingClientRect();
// Center tooltip vertically - use transform for perfect centering
tooltip.style.top = `${rect.top + rect.height / 2}px`;
tooltip.style.transform = 'translateY(-50%)';
}
}
function toggleTheme() {
theme.toggleMode();
}
let currentTheme = $derived($theme);
</script>
<aside
class="sidebar transition-all duration-300 ease-in-out"
class:minimized={isMinimized}
>
<div class="flex h-full flex-col bg-menu border-r border-theme">
<!-- Logo -->
<div class="flex items-center p-4 border-b border-theme">
{#if !isMinimized}
<a href="/record" class="flex items-center gap-2">
<svg width="28" height="28" viewBox="0 0 280 280" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M280 140C280 217.32 217.32 280 140 280C62.6801 280 0 217.32 0 140C0 62.6801 62.6801 0 140 0C217.32 0 280 62.6801 280 140ZM247.988 140C247.988 199.64 199.64 241.988 140 241.988C80.3598 241.988 32.0118 199.64 32.0118 140C32.0118 111.918 36.7308 95.3397 54.3005 76.1331C58.5193 71.5212 70.5 63 79.3937 74.511L119.781 131.788C134.5 149 149 147 160.218 131.788L200.605 74.5101C208 64 221.48 71.5203 225.699 76.1321C243.269 95.3388 247.988 111.918 247.988 140Z" fill="#F7D44C"/>
</svg>
<Text variant="large" weight="bold" class="text-xl text-white">Memoro</Text>
</a>
{:else}
<a href="/record" class="flex items-center justify-center w-full">
<svg width="28" height="28" viewBox="0 0 280 280" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M280 140C280 217.32 217.32 280 140 280C62.6801 280 0 217.32 0 140C0 62.6801 62.6801 0 140 0C217.32 0 280 62.6801 280 140ZM247.988 140C247.988 199.64 199.64 241.988 140 241.988C80.3598 241.988 32.0118 199.64 32.0118 140C32.0118 111.918 36.7308 95.3397 54.3005 76.1331C58.5193 71.5212 70.5 63 79.3937 74.511L119.781 131.788C134.5 149 149 147 160.218 131.788L200.605 74.5101C208 64 221.48 71.5203 225.699 76.1321C243.269 95.3388 247.988 111.918 247.988 140Z" fill="#F7D44C"/>
</svg>
</a>
{/if}
</div>
<!-- Navigation Links -->
<nav class="flex-1 overflow-y-auto p-2">
<div class="space-y-1">
<a
href="/record"
class="nav-item"
class:active={isActive('/record')}
title="Aufnehmen"
onmouseenter={handleMouseEnter}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="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"
/>
</svg>
{#if !isMinimized}
<span>Aufnehmen</span>
{:else}
<span class="tooltip">Aufnehmen</span>
{/if}
</a>
<a
href="/memos"
class="nav-item"
class:active={isActive('/memos')}
title="Memos"
onmouseenter={handleMouseEnter}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
/>
</svg>
{#if !isMinimized}
<span>Memos</span>
{:else}
<span class="tooltip">Memos</span>
{/if}
</a>
<a
href="/upload"
class="nav-item"
class:active={isActive('/upload')}
title="Upload"
onmouseenter={handleMouseEnter}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="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"
/>
</svg>
{#if !isMinimized}
<span>Upload</span>
{:else}
<span class="tooltip">Upload</span>
{/if}
</a>
<a
href="/audio-archive"
class="nav-item"
class:active={isActive('/audio-archive')}
title="Audio-Archiv"
onmouseenter={handleMouseEnter}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="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"
/>
</svg>
{#if !isMinimized}
<span>Audio-Archiv</span>
{:else}
<span class="tooltip">Audio-Archiv</span>
{/if}
</a>
<a
href="/tags"
class="nav-item"
class:active={isActive('/tags')}
title="Tags"
onmouseenter={handleMouseEnter}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="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"
/>
</svg>
{#if !isMinimized}
<span>Tags</span>
{:else}
<span class="tooltip">Tags</span>
{/if}
</a>
<!-- Spaces temporarily hidden -->
<!-- <a
href="/spaces"
class="nav-item"
class:active={isActive('/spaces')}
title="Spaces"
onmouseenter={handleMouseEnter}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
/>
</svg>
{#if !isMinimized}
<span>Spaces</span>
{:else}
<span class="tooltip">Spaces</span>
{/if}
</a> -->
<a
href="/subscription"
class="nav-item"
class:active={isActive('/subscription')}
title="Mana"
onmouseenter={handleMouseEnter}
>
<!-- Mana Icon SVG (from mobile/assets/icons/mana-icon.svg) -->
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<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>
{#if !isMinimized}
<span>Mana</span>
{:else}
<span class="tooltip">Mana</span>
{/if}
</a>
<a
href="/blueprints"
class="nav-item"
class:active={isActive('/blueprints')}
title="Blueprints"
onmouseenter={handleMouseEnter}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="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"
/>
</svg>
{#if !isMinimized}
<span>Blueprints</span>
{:else}
<span class="tooltip">Blueprints</span>
{/if}
</a>
<a
href="/statistics"
class="nav-item"
class:active={isActive('/statistics')}
title="Statistics"
onmouseenter={handleMouseEnter}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="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"
/>
</svg>
{#if !isMinimized}
<span>Statistics</span>
{:else}
<span class="tooltip">Statistics</span>
{/if}
</a>
<a
href="/settings"
class="nav-item"
class:active={isActive('/settings')}
title="Settings"
onmouseenter={handleMouseEnter}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="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"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
{#if !isMinimized}
<span>Settings</span>
{:else}
<span class="tooltip">Settings</span>
{/if}
</a>
</div>
</nav>
<!-- Keyboard Shortcuts Panel -->
<div class="border-t border-theme">
<button
onclick={() => showShortcuts = !showShortcuts}
class="nav-item w-full"
title="Keyboard Shortcuts"
onmouseenter={handleMouseEnter}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
</svg>
{#if !isMinimized}
<span class="flex-1 text-left">Shortcuts</span>
<svg class="h-4 w-4 transition-transform {showShortcuts ? 'rotate-180' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
{:else}
<span class="tooltip">Shortcuts</span>
{/if}
</button>
{#if showShortcuts && !isMinimized}
<div class="px-3 py-2 space-y-3 text-xs bg-content">
<!-- Tab Navigation -->
<div>
<Text variant="muted" weight="semibold" class="mb-2 uppercase tracking-wide text-[10px]">Tab Navigation</Text>
<div class="space-y-1.5">
<div class="flex items-center justify-between">
<span class="text-theme-secondary">Close Tab</span>
<kbd class="kbd">{modKey} W</kbd>
</div>
<div class="flex items-center justify-between">
<span class="text-theme-secondary">Previous Tab</span>
<kbd class="kbd">{modKey} [</kbd>
</div>
<div class="flex items-center justify-between">
<span class="text-theme-secondary">Next Tab</span>
<kbd class="kbd">{modKey} ]</kbd>
</div>
</div>
</div>
<!-- Split Navigation -->
<div>
<Text variant="muted" weight="semibold" class="mb-2 uppercase tracking-wide text-[10px]">Split Navigation</Text>
<div class="space-y-1.5">
<div class="flex items-center justify-between">
<span class="text-theme-secondary">Focus Split 1-4</span>
<kbd class="kbd">{modKey} 1-4</kbd>
</div>
</div>
</div>
<!-- Mouse Shortcuts -->
<div>
<Text variant="muted" weight="semibold" class="mb-2 uppercase tracking-wide text-[10px]">Mouse Shortcuts</Text>
<div class="space-y-1.5">
<div class="flex items-center justify-between">
<span class="text-theme-secondary">Open Memo</span>
<kbd class="kbd">Click</kbd>
</div>
<div class="flex items-center justify-between">
<span class="text-theme-secondary">Open in Split</span>
<kbd class="kbd">Shift Click</kbd>
</div>
</div>
</div>
</div>
{/if}
{#if showShortcuts && isMinimized}
<div class="shortcuts-panel">
<div class="space-y-3">
<!-- Tab Navigation -->
<div>
<Text variant="muted" weight="semibold" class="mb-2 uppercase tracking-wide text-[10px]">Tab Navigation</Text>
<div class="space-y-1.5">
<div class="flex items-center justify-between gap-4">
<span class="text-theme-secondary">Close Tab</span>
<kbd class="kbd">{modKey} W</kbd>
</div>
<div class="flex items-center justify-between gap-4">
<span class="text-theme-secondary">Previous Tab</span>
<kbd class="kbd">{modKey} [</kbd>
</div>
<div class="flex items-center justify-between gap-4">
<span class="text-theme-secondary">Next Tab</span>
<kbd class="kbd">{modKey} ]</kbd>
</div>
</div>
</div>
<!-- Split Navigation -->
<div>
<Text variant="muted" weight="semibold" class="mb-2 uppercase tracking-wide text-[10px]">Split Navigation</Text>
<div class="space-y-1.5">
<div class="flex items-center justify-between gap-4">
<span class="text-theme-secondary">Focus Split 1-4</span>
<kbd class="kbd">{modKey} 1-4</kbd>
</div>
</div>
</div>
<!-- Mouse Shortcuts -->
<div>
<Text variant="muted" weight="semibold" class="mb-2 uppercase tracking-wide text-[10px]">Mouse Shortcuts</Text>
<div class="space-y-1.5">
<div class="flex items-center justify-between gap-4">
<span class="text-theme-secondary">Open Memo</span>
<kbd class="kbd">Click</kbd>
</div>
<div class="flex items-center justify-between gap-4">
<span class="text-theme-secondary">Open in Split</span>
<kbd class="kbd">Shift Click</kbd>
</div>
</div>
</div>
</div>
</div>
{/if}
</div>
<!-- User Section -->
<div class="border-t border-theme p-2">
<!-- Theme Toggle -->
<button
onclick={toggleTheme}
class="nav-item w-full mb-2"
title={currentTheme.effectiveMode === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
onmouseenter={handleMouseEnter}
>
{#if currentTheme.effectiveMode === 'light'}
<!-- Moon Icon (Dark Mode) -->
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="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"
/>
</svg>
{:else}
<!-- Sun Icon (Light Mode) -->
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="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"
/>
</svg>
{/if}
{#if !isMinimized}
<span>{currentTheme.effectiveMode === 'light' ? 'Dark Mode' : 'Light Mode'}</span>
{:else}
<span class="tooltip">{currentTheme.effectiveMode === 'light' ? 'Dark Mode' : 'Light Mode'}</span>
{/if}
</button>
{#if !isMinimized}
<!-- User Email -->
<div class="mb-2 px-3 py-2 text-xs text-theme-muted truncate">
{$user?.email || ''}
</div>
{/if}
<!-- Logout Button -->
<button
onclick={onLogout}
class="nav-item logout-button w-full"
title="Logout"
onmouseenter={handleMouseEnter}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="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"
/>
</svg>
{#if !isMinimized}
<span>Logout</span>
{:else}
<span class="tooltip">Logout</span>
{/if}
</button>
<!-- Toggle Sidebar Button -->
<button
onclick={toggleSidebar}
class="nav-item w-full mt-2"
title={isMinimized ? 'Expand sidebar' : 'Minimize sidebar'}
onmouseenter={handleMouseEnter}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{#if isMinimized}
<!-- Menu icon (expand) -->
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
{:else}
<!-- Arrow left icon (minimize) -->
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 19l-7-7 7-7"
/>
{/if}
</svg>
{#if !isMinimized}
<span>Minimize</span>
{:else}
<span class="tooltip">Expand</span>
{/if}
</button>
</div>
</div>
</aside>
<style>
.sidebar {
width: 240px;
flex-shrink: 0;
height: 100vh;
position: sticky;
top: 0;
left: 0;
overflow: visible;
z-index: 100;
}
.sidebar.minimized {
width: 64px;
overflow: visible;
}
/* Ensure inner container allows tooltips to overflow */
.sidebar > div {
overflow-x: visible;
overflow-y: auto;
}
/* Only the nav section should scroll */
.sidebar nav {
overflow-y: auto;
overflow-x: visible;
}
.nav-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border-radius: 0.5rem;
transition: all 0.2s;
text-decoration: none;
/* Normal text color */
color: #6b7280;
}
.dark .nav-item {
color: #d1d5db;
}
/* Remove opacity reduction - we use direct colors instead */
a.nav-item {
opacity: 1;
}
/* Hover state */
.nav-item:hover {
background-color: #f5f5f5;
opacity: 1;
}
.dark .nav-item:hover {
background-color: #333333;
opacity: 1;
}
/* Active state */
.nav-item.active {
background-color: rgba(248, 214, 43, 0.1);
color: #f8d62b;
font-weight: 600;
opacity: 1;
}
.dark .nav-item.active {
background-color: rgba(248, 214, 43, 0.15);
color: #f8d62b;
}
.nav-item.active svg {
stroke: #f8d62b;
}
/* Minimized layout */
.minimized .nav-item {
justify-content: center;
padding: 0.75rem;
position: relative;
overflow: visible;
}
/* Tooltip for minimized sidebar */
.tooltip {
display: block;
position: fixed;
left: 80px;
padding: 0.625rem 1rem;
background-color: #1E1E1E;
color: #ffffff;
font-size: 0.875rem;
font-weight: 500;
border-radius: 0.5rem;
white-space: nowrap;
pointer-events: none;
opacity: 0;
visibility: hidden;
transition: opacity 0.15s ease-out, visibility 0.15s ease-out;
z-index: 2147483647;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
}
/* Tooltip arrow */
.tooltip::before {
content: '';
position: absolute;
right: 100%;
top: 50%;
transform: translateY(-50%);
border: 5px solid transparent;
border-right-color: #1E1E1E;
}
:global(.dark) .tooltip {
background-color: #ffffff;
color: #1E1E1E;
}
:global(.dark) .tooltip::before {
border-right-color: #ffffff;
}
.minimized .nav-item:hover .tooltip {
opacity: 1 !important;
visibility: visible !important;
}
/* Logout button specific styling */
.logout-button {
color: #dc2626;
opacity: 0.8;
}
.dark .logout-button {
color: #ef4444;
}
.logout-button:hover {
opacity: 1;
}
/* Smooth transitions */
.sidebar * {
transition: opacity 0.2s ease-in-out;
}
/* Keyboard shortcut badge */
.kbd {
display: inline-block;
padding: 0.2rem 0.4rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.65rem;
font-weight: 600;
line-height: 1;
background-color: #f3f4f6;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
color: #374151;
}
.dark .kbd {
background-color: #374151;
border-color: #4b5563;
color: #e5e7eb;
}
/* Shortcuts panel for minimized sidebar */
.shortcuts-panel {
position: fixed;
left: 80px;
bottom: 120px;
width: 280px;
padding: 1rem;
background-color: #1E1E1E;
color: #ffffff;
font-size: 0.75rem;
border-radius: 0.5rem;
z-index: 2147483647;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
animation: slideIn 0.2s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
:global(.dark) .shortcuts-panel {
background-color: #ffffff;
color: #1E1E1E;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.shortcuts-panel .text-theme-secondary {
color: rgba(255, 255, 255, 0.8);
}
:global(.dark) .shortcuts-panel .text-theme-secondary {
color: rgba(0, 0, 0, 0.7);
}
.shortcuts-panel .kbd {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
color: #ffffff;
}
:global(.dark) .shortcuts-panel .kbd {
background-color: #f3f4f6;
border-color: #d1d5db;
color: #374151;
}
.shortcuts-panel h4 {
color: rgba(255, 255, 255, 0.6);
}
:global(.dark) .shortcuts-panel h4 {
color: rgba(0, 0, 0, 0.5);
font-weight: 600;
}
.shortcuts-panel::before {
content: '';
position: absolute;
right: 100%;
bottom: 20px;
border: 8px solid transparent;
border-right-color: #1E1E1E;
}
:global(.dark) .shortcuts-panel::before {
border-right-color: #ffffff;
}
</style>

View file

@ -3,8 +3,11 @@
import { page } from '$app/stores';
import { auth, isAuthenticated } from '$lib/stores/auth';
import { isSidebarMode as sidebarModeStore, isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
import { theme } from '$lib/stores/theme';
import { locale } from 'svelte-i18n';
import { onMount } from 'svelte';
import PillNavigation from '$lib/components/PillNavigation.svelte';
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
// Navigation shortcuts (Ctrl+1-9)
const navRoutes = [
@ -19,6 +22,19 @@
'/settings', // Ctrl+9
];
// Navigation items for Memoro
const navItems: PillNavItem[] = [
{ href: '/record', label: 'Aufnehmen', icon: 'mic' },
{ href: '/memos', label: 'Memos', icon: 'archive' },
{ href: '/upload', label: 'Upload', icon: 'upload' },
{ href: '/audio-archive', label: 'Audio-Archiv', icon: 'music' },
{ href: '/tags', label: 'Tags', icon: 'tag' },
{ href: '/subscription', label: 'Mana', icon: 'mana' },
{ href: '/blueprints', label: 'Blueprints', icon: 'document' },
{ href: '/statistics', label: 'Statistics', icon: 'chart' },
{ href: '/settings', label: 'Settings', icon: 'settings' },
];
function handleKeydown(event: KeyboardEvent) {
// Don't handle if user is typing in an input
const target = event.target as HTMLElement;
@ -48,19 +64,56 @@
let isSidebarMode = $state(false);
let isCollapsed = $state(false);
// Get theme state
let effectiveMode = $derived(theme.effectiveMode);
// Check if current page needs full height (no scroll container)
const isFullHeightPage = $derived(
$page.url.pathname === '/record' || $page.url.pathname === '/memos' || $page.url.pathname === '/dashboard'
);
// Language state - sync with svelte-i18n locale
let currentLanguage = $derived($locale || 'de');
const languages = [
{ code: 'de', label: 'Deutsch' },
{ code: 'en', label: 'English' },
{ code: 'es', label: 'Español' },
{ code: 'fr', label: 'Français' },
{ code: 'it', label: 'Italiano' },
];
const languageItems: PillDropdownItem[] = $derived(languages.map(lang => ({
id: lang.code,
label: lang.label,
onClick: () => {
locale.set(lang.code);
},
active: currentLanguage === lang.code
})));
const currentLanguageLabel = $derived(
languages.find(l => l.code === currentLanguage)?.label || 'Deutsch'
);
function handleModeChange(isSidebar: boolean) {
isSidebarMode = isSidebar;
sidebarModeStore.set(isSidebar);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('memoro-nav-sidebar', String(isSidebar));
}
}
function handleCollapsedChange(collapsed: boolean) {
isCollapsed = collapsed;
collapsedStore.set(collapsed);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('memoro-nav-collapsed', String(collapsed));
}
}
function handleToggleTheme() {
theme.toggleMode();
}
// Client-side auth guard
@ -104,7 +157,31 @@
<!-- Navigation Layout -->
<div class="flex flex-col min-h-screen">
<!-- Floating/Sidebar Pill Navigation -->
<PillNavigation onLogout={handleLogout} onModeChange={handleModeChange} onCollapsedChange={handleCollapsedChange} />
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Memoro"
homeRoute="/record"
onLogout={handleLogout}
onToggleTheme={handleToggleTheme}
isDark={effectiveMode === 'dark'}
isSidebarMode={isSidebarMode}
onModeChange={handleModeChange}
isCollapsed={isCollapsed}
onCollapsedChange={handleCollapsedChange}
showThemeToggle={true}
showLanguageSwitcher={true}
languageItems={languageItems}
currentLanguageLabel={currentLanguageLabel}
primaryColor="#F7D44C"
>
{#snippet logo()}
<svg class="pill-icon" width="16" height="16" viewBox="0 0 280 280" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M280 140C280 217.32 217.32 280 140 280C62.6801 280 0 217.32 0 140C0 62.6801 62.6801 0 140 0C217.32 0 280 62.6801 280 140ZM247.988 140C247.988 199.64 199.64 241.988 140 241.988C80.3598 241.988 32.0118 199.64 32.0118 140C32.0118 111.918 36.7308 95.3397 54.3005 76.1331C58.5193 71.5212 70.5 63 79.3937 74.511L119.781 131.788C134.5 149 149 147 160.218 131.788L200.605 74.5101C208 64 221.48 71.5203 225.699 76.1321C243.269 95.3388 247.988 111.918 247.988 140Z" fill="#F7D44C"/>
</svg>
<span class="pill-label font-bold">Memoro</span>
{/snippet}
</PillNavigation>
<!-- Main Content with dynamic padding based on nav mode -->
<main