diff --git a/manacore/apps/web/src/lib/stores/navigation.ts b/manacore/apps/web/src/lib/stores/navigation.ts new file mode 100644 index 000000000..f4072e926 --- /dev/null +++ b/manacore/apps/web/src/lib/stores/navigation.ts @@ -0,0 +1,4 @@ +import { writable } from 'svelte/store'; + +export const isSidebarMode = writable(false); +export const isNavCollapsed = writable(false); diff --git a/manacore/apps/web/src/routes/(app)/+layout.svelte b/manacore/apps/web/src/routes/(app)/+layout.svelte index 05bdf87d2..94ded0c52 100644 --- a/manacore/apps/web/src/routes/(app)/+layout.svelte +++ b/manacore/apps/web/src/routes/(app)/+layout.svelte @@ -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; + }); -
- - - - -
- {@render children()} -
-
+ + +{/if} diff --git a/manadeck/apps/web/src/lib/components/layout/Navbar.svelte b/manadeck/apps/web/src/lib/components/layout/Navbar.svelte deleted file mode 100644 index 8a57b83c7..000000000 --- a/manadeck/apps/web/src/lib/components/layout/Navbar.svelte +++ /dev/null @@ -1,83 +0,0 @@ - - - diff --git a/manadeck/apps/web/src/lib/stores/navigation.ts b/manadeck/apps/web/src/lib/stores/navigation.ts new file mode 100644 index 000000000..f4072e926 --- /dev/null +++ b/manadeck/apps/web/src/lib/stores/navigation.ts @@ -0,0 +1,4 @@ +import { writable } from 'svelte/store'; + +export const isSidebarMode = writable(false); +export const isNavCollapsed = writable(false); diff --git a/manadeck/apps/web/src/routes/(app)/+layout.svelte b/manadeck/apps/web/src/routes/(app)/+layout.svelte index e71df0125..f82800f74 100644 --- a/manadeck/apps/web/src/routes/(app)/+layout.svelte +++ b/manadeck/apps/web/src/routes/(app)/+layout.svelte @@ -1,32 +1,140 @@ + + {#if authStore.loading} -
+
-
-

Loading...

+
+

Loading...

{:else if authStore.isAuthenticated} -
- -
- {@render children()} +
+ + + + +
+
+ {@render children()} +
{/if} diff --git a/memoro/apps/web/src/lib/components/PillNavigation.svelte b/memoro/apps/web/src/lib/components/PillNavigation.svelte deleted file mode 100644 index 09271ce7b..000000000 --- a/memoro/apps/web/src/lib/components/PillNavigation.svelte +++ /dev/null @@ -1,535 +0,0 @@ - - -{#if !isCollapsed} - -{/if} - - -{#if isCollapsed} - -{/if} - - diff --git a/memoro/apps/web/src/lib/components/Sidebar.svelte b/memoro/apps/web/src/lib/components/Sidebar.svelte deleted file mode 100644 index 1262e53ea..000000000 --- a/memoro/apps/web/src/lib/components/Sidebar.svelte +++ /dev/null @@ -1,774 +0,0 @@ - - - - - diff --git a/memoro/apps/web/src/routes/(protected)/+layout.svelte b/memoro/apps/web/src/routes/(protected)/+layout.svelte index 9b1afdbca..94f195cf9 100644 --- a/memoro/apps/web/src/routes/(protected)/+layout.svelte +++ b/memoro/apps/web/src/routes/(protected)/+layout.svelte @@ -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 @@
- + + {#snippet logo()} + + + + Memoro + {/snippet} +
- 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 = { + 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 = { - 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; } @@ -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 { diff --git a/packages/shared-ui/src/navigation/PillNavigation.svelte b/packages/shared-ui/src/navigation/PillNavigation.svelte new file mode 100644 index 000000000..1404c4da2 --- /dev/null +++ b/packages/shared-ui/src/navigation/PillNavigation.svelte @@ -0,0 +1,551 @@ + + +{#if !isCollapsed} + +{/if} + + +{#if isCollapsed} + +{/if} + + diff --git a/packages/shared-ui/src/navigation/index.ts b/packages/shared-ui/src/navigation/index.ts index 7571956e0..c4ff04ed1 100644 --- a/packages/shared-ui/src/navigation/index.ts +++ b/packages/shared-ui/src/navigation/index.ts @@ -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'; diff --git a/packages/shared-ui/src/navigation/types.ts b/packages/shared-ui/src/navigation/types.ts index 0bb1e0f72..e97d32051 100644 --- a/packages/shared-ui/src/navigation/types.ts +++ b/packages/shared-ui/src/navigation/types.ts @@ -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;