feat: unify navigation with shared PillNavigation component

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

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

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

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

View file

@ -1,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>

View file

@ -0,0 +1,4 @@
import { writable } from 'svelte/store';
export const isSidebarMode = writable(false);
export const isNavCollapsed = writable(false);

View file

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