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

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

View file

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