mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
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:
parent
cacbd61fe4
commit
bd869dfe09
13 changed files with 984 additions and 1520 deletions
4
manacore/apps/web/src/lib/stores/navigation.ts
Normal file
4
manacore/apps/web/src/lib/stores/navigation.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
export const isSidebarMode = writable(false);
|
||||
export const isNavCollapsed = writable(false);
|
||||
|
|
@ -2,9 +2,79 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { PillNavigation } from '@manacore/shared-ui';
|
||||
import type { PillNavItem } from '@manacore/shared-ui';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { isSidebarMode as sidebarModeStore, isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
|
||||
|
||||
let { data, children }: { data: any; children: Snippet } = $props();
|
||||
let mobileMenuOpen = $state(false);
|
||||
|
||||
let loading = $state(true);
|
||||
let isSidebarMode = $state(false);
|
||||
let isCollapsed = $state(false);
|
||||
|
||||
// Get theme state
|
||||
let effectiveMode = $derived(theme.effectiveMode);
|
||||
|
||||
// Navigation items for ManaCore
|
||||
const navItems: PillNavItem[] = [
|
||||
{ href: '/dashboard', label: 'Dashboard', icon: 'home' },
|
||||
{ href: '/organizations', label: 'Organizations', icon: 'building' },
|
||||
{ href: '/teams', label: 'Teams', icon: 'users' },
|
||||
{ href: '/subscription', label: 'Subscription', icon: 'creditCard' },
|
||||
{ href: '/settings', label: 'Settings', icon: 'settings' }
|
||||
];
|
||||
|
||||
// Navigation shortcuts (Ctrl+1-5)
|
||||
const navRoutes = navItems.map(item => item.href);
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (
|
||||
target.tagName === 'INPUT' ||
|
||||
target.tagName === 'TEXTAREA' ||
|
||||
target.isContentEditable
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
|
||||
const num = parseInt(event.key);
|
||||
if (num >= 1 && num <= navRoutes.length) {
|
||||
event.preventDefault();
|
||||
const route = navRoutes[num - 1];
|
||||
if (route) {
|
||||
goto(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleModeChange(isSidebar: boolean) {
|
||||
isSidebarMode = isSidebar;
|
||||
sidebarModeStore.set(isSidebar);
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('manacore-nav-sidebar', String(isSidebar));
|
||||
}
|
||||
}
|
||||
|
||||
function handleCollapsedChange(collapsed: boolean) {
|
||||
isCollapsed = collapsed;
|
||||
collapsedStore.set(collapsed);
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('manacore-nav-collapsed', String(collapsed));
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleTheme() {
|
||||
theme.toggleMode();
|
||||
}
|
||||
|
||||
async function handleSignOut() {
|
||||
await data.supabase.auth.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!data.session) {
|
||||
|
|
@ -12,99 +82,61 @@
|
|||
}
|
||||
});
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', href: '/dashboard' },
|
||||
{ name: 'Organizations', href: '/organizations' },
|
||||
{ name: 'Teams', href: '/teams' },
|
||||
{ name: 'Subscription', href: '/subscription' },
|
||||
{ name: 'Settings', href: '/settings' }
|
||||
];
|
||||
onMount(() => {
|
||||
// Initialize sidebar mode from localStorage
|
||||
const savedSidebar = localStorage.getItem('manacore-nav-sidebar');
|
||||
if (savedSidebar === 'true') {
|
||||
isSidebarMode = true;
|
||||
sidebarModeStore.set(true);
|
||||
}
|
||||
|
||||
async function handleSignOut() {
|
||||
await data.supabase.auth.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
// Initialize collapsed state from localStorage
|
||||
const savedCollapsed = localStorage.getItem('manacore-nav-collapsed');
|
||||
if (savedCollapsed === 'true') {
|
||||
isCollapsed = true;
|
||||
collapsedStore.set(true);
|
||||
}
|
||||
|
||||
loading = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<!-- Navigation -->
|
||||
<nav class="border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex h-16 justify-between">
|
||||
<div class="flex">
|
||||
<div class="flex flex-shrink-0 items-center">
|
||||
<h1 class="text-xl font-bold text-primary-600">ManaCore</h1>
|
||||
</div>
|
||||
<div class="hidden sm:ml-6 sm:flex sm:space-x-8">
|
||||
{#each navigation as item}
|
||||
<a
|
||||
href={item.href}
|
||||
class="inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium {$page.url.pathname.startsWith(item.href)
|
||||
? 'border-primary-500 text-gray-900 dark:text-white'
|
||||
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'}"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden sm:ml-6 sm:flex sm:items-center">
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleSignOut}
|
||||
class="rounded-lg px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
<div class="-mr-2 flex items-center sm:hidden">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (mobileMenuOpen = !mobileMenuOpen)}
|
||||
class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 dark:hover:bg-gray-700"
|
||||
>
|
||||
<span class="sr-only">Open main menu</span>
|
||||
{#if !mobileMenuOpen}
|
||||
<svg class="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg class="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if loading}
|
||||
<div class="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900">
|
||||
<div class="text-center">
|
||||
<div class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary-500 border-r-transparent"></div>
|
||||
<p class="text-gray-500 dark:text-gray-400">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<!-- Pill Navigation -->
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="ManaCore"
|
||||
homeRoute="/dashboard"
|
||||
onLogout={handleSignOut}
|
||||
onToggleTheme={handleToggleTheme}
|
||||
isDark={effectiveMode === 'dark'}
|
||||
isSidebarMode={isSidebarMode}
|
||||
onModeChange={handleModeChange}
|
||||
isCollapsed={isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
showLanguageSwitcher={false}
|
||||
primaryColor="#6366f1"
|
||||
/>
|
||||
|
||||
{#if mobileMenuOpen}
|
||||
<div class="sm:hidden">
|
||||
<div class="space-y-1 pb-3 pt-2">
|
||||
{#each navigation as item}
|
||||
<a
|
||||
href={item.href}
|
||||
class="block border-l-4 py-2 pl-3 pr-4 text-base font-medium {$page.url.pathname.startsWith(item.href)
|
||||
? 'border-primary-500 bg-primary-50 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400'
|
||||
: 'border-transparent text-gray-600 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-800 dark:text-gray-400 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
{/each}
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleSignOut}
|
||||
class="block w-full border-l-4 border-transparent py-2 pl-3 pr-4 text-left text-base font-medium text-gray-600 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-800 dark:text-gray-400 dark:hover:bg-gray-700"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
<!-- Main content with dynamic padding -->
|
||||
<main
|
||||
class="transition-all duration-300 {isCollapsed ? '' : (isSidebarMode ? 'pl-[180px]' : 'pt-20')}"
|
||||
>
|
||||
<div class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,83 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/authStore.svelte';
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Decks', href: '/decks', icon: '📚' },
|
||||
{ label: 'Explore', href: '/explore', icon: '🔍' },
|
||||
{ label: 'Progress', href: '/progress', icon: '📊' },
|
||||
{ label: 'Mana', href: '/subscription', icon: '⚡' },
|
||||
{ label: 'Profile', href: '/profile', icon: '👤' }
|
||||
];
|
||||
|
||||
async function handleSignOut() {
|
||||
await authStore.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
function isActive(href: string) {
|
||||
return $page.url.pathname.startsWith(href);
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="border-b border-border bg-surface-elevated">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center">
|
||||
<a href="/decks" class="flex items-center space-x-2">
|
||||
<span class="text-2xl">🎴</span>
|
||||
<span class="text-xl font-bold">Manadeck</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div class="hidden md:flex items-center space-x-1">
|
||||
{#each navItems as item}
|
||||
<a
|
||||
href={item.href}
|
||||
class={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
isActive(item.href)
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
}`}
|
||||
>
|
||||
<span class="mr-2">{item.icon}</span>
|
||||
{item.label}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- User Menu -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{authStore.user?.email || 'Guest'}
|
||||
</div>
|
||||
<button
|
||||
onclick={handleSignOut}
|
||||
class="px-3 py-2 text-sm text-destructive hover:bg-destructive/10 rounded-md transition-colors"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Navigation -->
|
||||
<div class="md:hidden border-t border-border">
|
||||
<div class="flex justify-around py-2">
|
||||
{#each navItems as item}
|
||||
<a
|
||||
href={item.href}
|
||||
class={`flex flex-col items-center px-3 py-2 text-xs ${
|
||||
isActive(item.href) ? 'text-primary' : 'text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
<span class="text-xl mb-1">{item.icon}</span>
|
||||
{item.label}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
4
manadeck/apps/web/src/lib/stores/navigation.ts
Normal file
4
manadeck/apps/web/src/lib/stores/navigation.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
export const isSidebarMode = writable(false);
|
||||
export const isNavCollapsed = writable(false);
|
||||
|
|
@ -1,32 +1,140 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { authStore } from '$lib/stores/authStore.svelte';
|
||||
import Navbar from '$lib/components/layout/Navbar.svelte';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { isSidebarMode as sidebarModeStore, isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
|
||||
import { PillNavigation } from '@manacore/shared-ui';
|
||||
import type { PillNavItem } from '@manacore/shared-ui';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let isSidebarMode = $state(false);
|
||||
let isCollapsed = $state(false);
|
||||
|
||||
// Get theme state
|
||||
let effectiveMode = $derived(theme.effectiveMode);
|
||||
|
||||
// Navigation items for ManaDeck
|
||||
const navItems: PillNavItem[] = [
|
||||
{ href: '/decks', label: 'Decks', icon: 'archive' },
|
||||
{ href: '/explore', label: 'Explore', icon: 'search' },
|
||||
{ href: '/progress', label: 'Progress', icon: 'chart' },
|
||||
{ href: '/subscription', label: 'Mana', icon: 'mana' },
|
||||
{ href: '/profile', label: 'Profile', icon: 'user' }
|
||||
];
|
||||
|
||||
// Navigation shortcuts (Ctrl+1-5)
|
||||
const navRoutes = navItems.map(item => item.href);
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (
|
||||
target.tagName === 'INPUT' ||
|
||||
target.tagName === 'TEXTAREA' ||
|
||||
target.isContentEditable
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
|
||||
const num = parseInt(event.key);
|
||||
if (num >= 1 && num <= navRoutes.length) {
|
||||
event.preventDefault();
|
||||
const route = navRoutes[num - 1];
|
||||
if (route) {
|
||||
goto(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleModeChange(isSidebar: boolean) {
|
||||
isSidebarMode = isSidebar;
|
||||
sidebarModeStore.set(isSidebar);
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('manadeck-nav-sidebar', String(isSidebar));
|
||||
}
|
||||
}
|
||||
|
||||
function handleCollapsedChange(collapsed: boolean) {
|
||||
isCollapsed = collapsed;
|
||||
collapsedStore.set(collapsed);
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('manadeck-nav-collapsed', String(collapsed));
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleTheme() {
|
||||
theme.toggleMode();
|
||||
}
|
||||
|
||||
async function handleSignOut() {
|
||||
await authStore.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize sidebar mode from localStorage
|
||||
const savedSidebar = localStorage.getItem('manadeck-nav-sidebar');
|
||||
if (savedSidebar === 'true') {
|
||||
isSidebarMode = true;
|
||||
sidebarModeStore.set(true);
|
||||
}
|
||||
|
||||
// Initialize collapsed state from localStorage
|
||||
const savedCollapsed = localStorage.getItem('manadeck-nav-collapsed');
|
||||
if (savedCollapsed === 'true') {
|
||||
isCollapsed = true;
|
||||
collapsedStore.set(true);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if authStore.loading}
|
||||
<div class="min-h-screen flex items-center justify-center">
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
|
||||
<div class="text-center">
|
||||
<div class="inline-block animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
|
||||
<p class="mt-4 text-muted-foreground">Loading...</p>
|
||||
<div class="inline-block animate-spin h-8 w-8 border-4 border-primary-500 border-t-transparent rounded-full"></div>
|
||||
<p class="mt-4 text-gray-500 dark:text-gray-400">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if authStore.isAuthenticated}
|
||||
<div class="min-h-screen bg-background">
|
||||
<Navbar />
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{@render children()}
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<!-- Pill Navigation -->
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="ManaDeck"
|
||||
homeRoute="/decks"
|
||||
onLogout={handleSignOut}
|
||||
onToggleTheme={handleToggleTheme}
|
||||
isDark={effectiveMode === 'dark'}
|
||||
isSidebarMode={isSidebarMode}
|
||||
onModeChange={handleModeChange}
|
||||
isCollapsed={isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
showLanguageSwitcher={false}
|
||||
primaryColor="#6366f1"
|
||||
/>
|
||||
|
||||
<!-- Main content with dynamic padding -->
|
||||
<main
|
||||
class="transition-all duration-300 {isCollapsed ? '' : (isSidebarMode ? 'pl-[180px]' : 'pt-20')}"
|
||||
>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -28,5 +28,14 @@ export { Modal, ConfirmationModal, FormModal, AppSlider } from './organisms';
|
|||
export type { AppItem } from './organisms';
|
||||
|
||||
// Navigation
|
||||
export { NavLink, Navbar, Sidebar, SidebarSection } from './navigation';
|
||||
export type { NavItem, NavbarProps, SidebarProps, NavLinkProps, KeyboardShortcut } from './navigation';
|
||||
export { NavLink, Navbar, Sidebar, SidebarSection, PillNavigation, PillDropdown } from './navigation';
|
||||
export type {
|
||||
NavItem,
|
||||
NavbarProps,
|
||||
SidebarProps,
|
||||
NavLinkProps,
|
||||
KeyboardShortcut,
|
||||
PillNavItem,
|
||||
PillDropdownItem,
|
||||
PillNavigationProps
|
||||
} from './navigation';
|
||||
|
|
|
|||
|
|
@ -1,19 +1,11 @@
|
|||
<script lang="ts">
|
||||
interface DropdownItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
danger?: boolean;
|
||||
active?: boolean;
|
||||
}
|
||||
import type { PillDropdownItem } from './types';
|
||||
|
||||
interface Props {
|
||||
items: DropdownItem[];
|
||||
items: PillDropdownItem[];
|
||||
direction?: 'up' | 'down';
|
||||
label: string;
|
||||
icon?: string;
|
||||
icon?: 'globe' | 'language' | 'chevronDown' | 'check' | string;
|
||||
isOpen?: boolean;
|
||||
onToggle?: (open: boolean) => void;
|
||||
}
|
||||
|
|
@ -64,19 +56,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
function handleItemClick(item: DropdownItem) {
|
||||
function handleItemClick(item: PillDropdownItem) {
|
||||
item.onClick();
|
||||
close();
|
||||
}
|
||||
|
||||
const iconPaths: 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'
|
||||
};
|
||||
|
||||
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] || '';
|
||||
return iconPaths[iconName] || iconName;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -277,13 +270,13 @@
|
|||
}
|
||||
|
||||
.active-pill {
|
||||
background: rgba(248, 214, 43, 0.2);
|
||||
border-color: rgba(248, 214, 43, 0.3);
|
||||
background: var(--color-primary-100, rgba(248, 214, 43, 0.2));
|
||||
border-color: var(--color-primary-200, 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);
|
||||
background: var(--color-primary-900, rgba(248, 214, 43, 0.15));
|
||||
border-color: var(--color-primary-800, rgba(248, 214, 43, 0.25));
|
||||
}
|
||||
|
||||
.danger-pill {
|
||||
|
|
@ -309,7 +302,7 @@
|
|||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
margin-left: 0.25rem;
|
||||
color: #f8d62b;
|
||||
color: var(--color-primary-500, #f8d62b);
|
||||
}
|
||||
|
||||
.pill-label {
|
||||
551
packages/shared-ui/src/navigation/PillNavigation.svelte
Normal file
551
packages/shared-ui/src/navigation/PillNavigation.svelte
Normal file
|
|
@ -0,0 +1,551 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { PillNavItem, PillDropdownItem } from './types';
|
||||
import PillDropdown from './PillDropdown.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 */
|
||||
showThemeToggle?: boolean;
|
||||
/** Primary color for active state (CSS custom property or hex) */
|
||||
primaryColor?: string;
|
||||
}
|
||||
|
||||
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
|
||||
}: Props = $props();
|
||||
|
||||
// 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',
|
||||
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'
|
||||
};
|
||||
|
||||
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 -->
|
||||
<a href={homeRoute} class="pill glass-pill logo-pill">
|
||||
{#if logo}
|
||||
{@render logo()}
|
||||
{:else}
|
||||
<span class="pill-label font-bold">{appName}</span>
|
||||
{/if}
|
||||
</a>
|
||||
|
||||
<!-- 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}
|
||||
|
||||
<!-- Language Switcher -->
|
||||
{#if showLanguageSwitcher && languageItems.length > 0}
|
||||
<PillDropdown
|
||||
items={languageItems}
|
||||
direction="down"
|
||||
label={currentLanguageLabel}
|
||||
icon="globe"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
{#if showThemeToggle && onToggleTheme}
|
||||
<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}
|
||||
<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));
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,4 +2,15 @@ export { default as NavLink } from './NavLink.svelte';
|
|||
export { default as Navbar } from './Navbar.svelte';
|
||||
export { default as Sidebar } from './Sidebar.svelte';
|
||||
export { default as SidebarSection } from './SidebarSection.svelte';
|
||||
export type { NavItem, NavbarProps, SidebarProps, NavLinkProps, KeyboardShortcut } from './types';
|
||||
export { default as PillNavigation } from './PillNavigation.svelte';
|
||||
export { default as PillDropdown } from './PillDropdown.svelte';
|
||||
export type {
|
||||
NavItem,
|
||||
NavbarProps,
|
||||
SidebarProps,
|
||||
NavLinkProps,
|
||||
KeyboardShortcut,
|
||||
PillNavItem,
|
||||
PillDropdownItem,
|
||||
PillNavigationProps
|
||||
} from './types';
|
||||
|
|
|
|||
|
|
@ -9,6 +9,73 @@ export interface KeyboardShortcut {
|
|||
category?: string;
|
||||
}
|
||||
|
||||
// ============ Pill Navigation Types ============
|
||||
|
||||
export interface PillNavItem {
|
||||
/** Display label for the navigation item */
|
||||
label: string;
|
||||
/** URL to navigate to */
|
||||
href: string;
|
||||
/** Icon name (predefined) or 'mana' for special mana icon */
|
||||
icon?: string;
|
||||
/** Custom SVG icon HTML (for custom icons) */
|
||||
iconSvg?: string;
|
||||
}
|
||||
|
||||
export interface PillDropdownItem {
|
||||
/** Unique identifier */
|
||||
id: string;
|
||||
/** Display label */
|
||||
label: string;
|
||||
/** Icon name */
|
||||
icon?: string;
|
||||
/** Click handler */
|
||||
onClick: () => void;
|
||||
/** Whether item is disabled */
|
||||
disabled?: boolean;
|
||||
/** Whether item should be styled as danger/destructive */
|
||||
danger?: boolean;
|
||||
/** Whether this item is currently active/selected */
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface PillNavigationProps {
|
||||
/** 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 (controlled) */
|
||||
isSidebarMode?: boolean;
|
||||
/** Called when sidebar mode changes */
|
||||
onModeChange?: (isSidebar: boolean) => void;
|
||||
/** Whether navigation is collapsed (controlled) */
|
||||
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 */
|
||||
showThemeToggle?: boolean;
|
||||
/** Primary color for active state */
|
||||
primaryColor?: string;
|
||||
}
|
||||
|
||||
export interface NavItem {
|
||||
/** Display label for the navigation item */
|
||||
label: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue