mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 01:21:24 +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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue