diff --git a/apps/console/src/App.tsx b/apps/console/src/App.tsx index 4a369c82..2ac4588e 100644 --- a/apps/console/src/App.tsx +++ b/apps/console/src/App.tsx @@ -62,6 +62,7 @@ const HomeLayout = lazy(() => import('./pages/home/HomeLayout').then(m => ({ def import { ThemeProvider } from './components/theme-provider'; import { ConsoleToaster } from './components/ConsoleToaster'; +import { NavigationProvider } from './context/NavigationContext'; /** * ConnectedShell @@ -518,6 +519,7 @@ export function App() { + }> @@ -569,6 +571,7 @@ export function App() { + ); diff --git a/apps/console/src/__tests__/HomeLayout.test.tsx b/apps/console/src/__tests__/HomeLayout.test.tsx index 13bda676..8ec20ef3 100644 --- a/apps/console/src/__tests__/HomeLayout.test.tsx +++ b/apps/console/src/__tests__/HomeLayout.test.tsx @@ -1,12 +1,12 @@ /** - * Tests for HomeLayout — lightweight nav shell for /home. - * Validates: layout rendering, user avatar, navigation links. + * Tests for HomeLayout — unified sidebar nav shell for /home. + * Validates: layout rendering, sidebar presence, navigation context. * * Note: Radix DropdownMenu portal rendering is limited in jsdom, * so we test the trigger and visible elements rather than dropdown contents. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import { MemoryRouter } from 'react-router-dom'; import { HomeLayout } from '../pages/home/HomeLayout'; @@ -40,6 +40,9 @@ vi.mock('@object-ui/i18n', () => ({ direction: 'ltr', i18n: {}, }), + useObjectLabel: () => ({ + objectLabel: ({ name, label }: any) => label || name, + }), })); // Mock @object-ui/components to keep most components @@ -51,6 +54,77 @@ vi.mock('@object-ui/components', async (importOriginal) => { }; }); +// Mock @object-ui/layout AppShell +vi.mock('@object-ui/layout', () => ({ + AppShell: ({ children, sidebar }: any) => ( +
+
{sidebar}
+
{children}
+
+ ), + useAppShellBranding: () => {}, +})); + +// Mock NavigationContext +const mockSetContext = vi.fn(); +vi.mock('../context/NavigationContext', () => ({ + useNavigationContext: () => ({ + context: 'home', + setContext: mockSetContext, + currentAppName: undefined, + setCurrentAppName: vi.fn(), + }), +})); + +// Mock MetadataProvider +vi.mock('../context/MetadataProvider', () => ({ + useMetadata: () => ({ + apps: [], + objects: [], + loading: false, + }), +})); + +// Mock other required contexts +vi.mock('../context/ExpressionProvider', () => ({ + useExpressionContext: () => ({ + evaluator: {}, + }), + evaluateVisibility: () => true, +})); + +vi.mock('@object-ui/permissions', () => ({ + usePermissions: () => ({ + can: () => true, + }), +})); + +vi.mock('../hooks/useRecentItems', () => ({ + useRecentItems: () => ({ + recentItems: [], + addRecentItem: vi.fn(), + }), +})); + +vi.mock('../hooks/useFavorites', () => ({ + useFavorites: () => ({ + favorites: [], + addFavorite: vi.fn(), + removeFavorite: vi.fn(), + }), +})); + +vi.mock('../hooks/useNavPins', () => ({ + useNavPins: () => ({ + togglePin: vi.fn(), + applyPins: (items: any[]) => items, + }), +})); + +vi.mock('../hooks/useResponsiveSidebar', () => ({ + useResponsiveSidebar: () => {}, +})); + describe('HomeLayout', () => { beforeEach(() => { vi.clearAllMocks(); @@ -64,57 +138,20 @@ describe('HomeLayout', () => { ); }; - it('renders the layout shell with data-testid', () => { + it('renders the AppShell with sidebar and content', () => { renderLayout(); - expect(screen.getByTestId('home-layout')).toBeInTheDocument(); + expect(screen.getByTestId('app-shell')).toBeInTheDocument(); + expect(screen.getByTestId('sidebar')).toBeInTheDocument(); + expect(screen.getByTestId('content')).toBeInTheDocument(); }); - it('renders the Home branding button in the top bar', () => { + it('sets navigation context to "home" on mount', () => { renderLayout(); - const brand = screen.getByTestId('home-layout-brand'); - expect(brand).toBeInTheDocument(); - expect(brand).toHaveTextContent('Home'); + expect(mockSetContext).toHaveBeenCalledWith('home'); }); it('renders children inside the layout', () => { renderLayout(
Hello World
); expect(screen.getByTestId('child-content')).toBeInTheDocument(); }); - - it('renders user avatar with initials fallback', () => { - renderLayout(); - // The avatar trigger should show user initials "AD" (Alice Dev) - expect(screen.getByTestId('home-layout-user-trigger')).toBeInTheDocument(); - expect(screen.getByText('AD')).toBeInTheDocument(); - }); - - it('renders Settings button in the top bar', () => { - renderLayout(); - expect(screen.getByTestId('home-layout-settings-btn')).toBeInTheDocument(); - }); - - it('navigates to /system when Settings button is clicked', () => { - renderLayout(); - fireEvent.click(screen.getByTestId('home-layout-settings-btn')); - expect(mockNavigate).toHaveBeenCalledWith('/system'); - }); - - it('navigates to /home when brand button is clicked', () => { - renderLayout(); - fireEvent.click(screen.getByTestId('home-layout-brand')); - expect(mockNavigate).toHaveBeenCalledWith('/home'); - }); - - it('renders sticky header element', () => { - renderLayout(); - const header = screen.getByTestId('home-layout').querySelector('header'); - expect(header).toBeInTheDocument(); - expect(header?.className).toContain('sticky'); - }); - - it('renders user menu trigger as a round button', () => { - renderLayout(); - const trigger = screen.getByTestId('home-layout-user-trigger'); - expect(trigger.className).toContain('rounded-full'); - }); }); diff --git a/apps/console/src/components/ConsoleLayout.tsx b/apps/console/src/components/ConsoleLayout.tsx index 8e5f2391..b74985fa 100644 --- a/apps/console/src/components/ConsoleLayout.tsx +++ b/apps/console/src/components/ConsoleLayout.tsx @@ -2,18 +2,20 @@ * ConsoleLayout * * Root layout shell for the console application. Composes the AppShell - * with the sidebar, header, and main content area. + * with the UnifiedSidebar, header, and main content area. * Includes the global floating chatbot (FAB) widget. + * Sets navigation context to 'app' for app-specific routes. * @module */ -import React from 'react'; +import React, { useEffect } from 'react'; import { AppShell } from '@object-ui/layout'; import { FloatingChatbot, useObjectChat, type ChatMessage } from '@object-ui/plugin-chatbot'; import { useDiscovery } from '@object-ui/react'; -import { AppSidebar } from './AppSidebar'; +import { UnifiedSidebar } from './UnifiedSidebar'; import { AppHeader } from './AppHeader'; import { useResponsiveSidebar } from '../hooks/useResponsiveSidebar'; +import { useNavigationContext } from '../context/NavigationContext'; import { resolveI18nLabel } from '../utils'; import type { ConnectionState } from '../dataSource'; @@ -88,9 +90,9 @@ function ConsoleFloatingChatbot({ appLabel, objects }: { appLabel: string; objec ); } -export function ConsoleLayout({ - children, - activeAppName, +export function ConsoleLayout({ + children, + activeAppName, activeApp, onAppChange, objects, @@ -98,18 +100,25 @@ export function ConsoleLayout({ }: ConsoleLayoutProps) { const appLabel = resolveI18nLabel(activeApp?.label) || activeAppName; const { isAiEnabled } = useDiscovery(); + const { setContext, setCurrentAppName } = useNavigationContext(); + + // Set navigation context to 'app' when this layout mounts + useEffect(() => { + setContext('app'); + setCurrentAppName(activeAppName); + }, [setContext, setCurrentAppName, activeAppName]); return ( } navbar={ - diff --git a/apps/console/src/components/UnifiedSidebar.tsx b/apps/console/src/components/UnifiedSidebar.tsx new file mode 100644 index 00000000..a29c4b36 --- /dev/null +++ b/apps/console/src/components/UnifiedSidebar.tsx @@ -0,0 +1,680 @@ +/** + * UnifiedSidebar + * + * Airtable-style contextual sidebar that dynamically switches between Home and App navigation. + * Features: + * - Persistent across all authenticated routes + * - Context-aware navigation (Home vs App) + * - Pinned bottom area (Settings, Help, User Profile) + * - Smooth transitions between contexts + * - Back to Home navigation from App context + * - App switcher dropdown + * + * @module + */ + +import * as React from 'react'; +import { useNavigate, Link, useLocation } from 'react-router-dom'; +import * as LucideIcons from 'lucide-react'; +import { + Sidebar, + SidebarHeader, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupLabel, + SidebarGroupContent, + SidebarMenu, + SidebarMenuItem, + SidebarMenuButton, + SidebarMenuAction, + SidebarInput, + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuGroup, + Avatar, + AvatarImage, + AvatarFallback, + useSidebar, + Button, +} from '@object-ui/components'; +import { + ChevronsUpDown, + Plus, + Settings, + LogOut, + Database, + Clock, + Star, + StarOff, + Search, + Pencil, + ChevronRight, + Home, + Grid3x3, + HelpCircle, + ArrowLeft, + Layers, +} from 'lucide-react'; +import { NavigationRenderer } from '@object-ui/layout'; +import type { NavigationItem } from '@object-ui/types'; +import { useMetadata } from '../context/MetadataProvider'; +import { useExpressionContext, evaluateVisibility } from '../context/ExpressionProvider'; +import { useAuth, getUserInitials } from '@object-ui/auth'; +import { usePermissions } from '@object-ui/permissions'; +import { useRecentItems } from '../hooks/useRecentItems'; +import { useFavorites } from '../hooks/useFavorites'; +import { useNavPins } from '../hooks/useNavPins'; +import { resolveI18nLabel } from '../utils'; +import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n'; +import { useNavigationContext } from '../context/NavigationContext'; + +// --------------------------------------------------------------------------- +// useNavOrder – localStorage-persisted drag-and-drop reorder for nav items +// --------------------------------------------------------------------------- + +function useNavOrder(appName: string) { + const storageKey = `objectui-nav-order-${appName}`; + + const [orderMap, setOrderMap] = React.useState>(() => { + try { + const raw = localStorage.getItem(storageKey); + if (!raw) return {}; + const parsed: unknown = JSON.parse(raw); + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return {}; + const result: Record = {}; + for (const [k, v] of Object.entries(parsed as Record)) { + if (Array.isArray(v) && v.every((i: unknown) => typeof i === 'string')) { + result[k] = v as string[]; + } + } + return result; + } catch { + return {}; + } + }); + + const persist = React.useCallback( + (next: Record) => { + setOrderMap(next); + try { localStorage.setItem(storageKey, JSON.stringify(next)); } catch { /* full */ } + }, + [storageKey], + ); + + const applyOrder = React.useCallback( + (items: NavigationItem[]): NavigationItem[] => { + const saved = orderMap['__root__']; + if (!saved) return items; + const byId = new Map(items.map(i => [i.id, i])); + const ordered: NavigationItem[] = []; + for (const id of saved) { + const item = byId.get(id); + if (item) { ordered.push(item); byId.delete(id); } + } + byId.forEach(item => ordered.push(item)); + return ordered; + }, + [orderMap], + ); + + const handleReorder = React.useCallback( + (reorderedItems: NavigationItem[]) => { + const ids = reorderedItems.map(i => i.id); + persist({ ...orderMap, __root__: ids }); + }, + [orderMap, persist], + ); + + return { applyOrder, handleReorder }; +} + +/** + * Resolve a Lucide icon component by name string. + */ +function getIcon(name?: string): React.ComponentType { + if (!name) return LucideIcons.Database; + + if ((LucideIcons as any)[name]) { + return (LucideIcons as any)[name]; + } + + const pascalName = name + .split('-') + .map(part => part.charAt(0).toUpperCase() + part.slice(1)) + .join(''); + + if ((LucideIcons as any)[pascalName]) { + return (LucideIcons as any)[pascalName]; + } + + return LucideIcons.Database; +} + +interface UnifiedSidebarProps { + /** When in app context, the active app name */ + activeAppName?: string; + /** Callback when user switches apps */ + onAppChange?: (name: string) => void; +} + +export function UnifiedSidebar({ activeAppName, onAppChange }: UnifiedSidebarProps) { + const { isMobile } = useSidebar(); + const { user, signOut } = useAuth(); + const navigate = useNavigate(); + const location = useLocation(); + const { t } = useObjectTranslation(); + const { objectLabel: resolveNavObjectLabel } = useObjectLabel(); + const { context, currentAppName } = useNavigationContext(); + + // Swipe-from-left-edge gesture to open sidebar on mobile + React.useEffect(() => { + const EDGE_THRESHOLD = 30; + const SWIPE_DISTANCE = 50; + let touchStartX = 0; + const handleTouchStart = (e: TouchEvent) => { + touchStartX = e.touches[0].clientX; + }; + const handleTouchEnd = (e: TouchEvent) => { + const deltaX = e.changedTouches[0].clientX - touchStartX; + if (touchStartX < EDGE_THRESHOLD && deltaX > SWIPE_DISTANCE && isMobile) { + document.querySelector('[data-sidebar="trigger"]')?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + } + }; + document.addEventListener('touchstart', handleTouchStart, { passive: true }); + document.addEventListener('touchend', handleTouchEnd, { passive: true }); + return () => { + document.removeEventListener('touchstart', handleTouchStart); + document.removeEventListener('touchend', handleTouchEnd); + }; + }, [isMobile]); + + const { recentItems } = useRecentItems(); + const { favorites, removeFavorite } = useFavorites(); + + const { apps: metadataApps } = useMetadata(); + const apps = metadataApps || []; + const activeApps = apps.filter((a: any) => a.active !== false); + const activeApp = activeApps.find((a: any) => a.name === (activeAppName || currentAppName)) || activeApps[0]; + + const logo = activeApp?.branding?.logo; + const primaryColor = activeApp?.branding?.primaryColor; + + // Drag-reorder and pin persistence + const { applyOrder, handleReorder } = useNavOrder(activeApp?.name || 'home'); + const { togglePin, applyPins } = useNavPins(); + + // Area management + const areas: any[] = activeApp?.areas || []; + const [activeAreaId, setActiveAreaId] = React.useState( + () => areas.length > 0 ? areas[0].id : null, + ); + + React.useEffect(() => { + if (areas.length > 0) { + setActiveAreaId(prev => areas.some((a: any) => a.id === prev) ? prev : areas[0].id); + } else { + setActiveAreaId(null); + } + }, [activeApp?.name, areas.length]); + + // Resolve navigation items + const activeArea = areas.find((a: any) => a.id === activeAreaId); + const appNavigation: NavigationItem[] = activeArea?.navigation || activeApp?.navigation || []; + + // Home navigation items + const homeNavigation: NavigationItem[] = React.useMemo(() => [ + { id: 'home-dashboard', label: 'Home', type: 'url' as const, url: '/home', icon: 'home' }, + ], []); + + // Determine which navigation to show based on context + const navigationItems = context === 'home' ? homeNavigation : appNavigation; + + // Apply saved order and pin state + const processedNavigation = React.useMemo(() => { + const ordered = applyOrder(navigationItems); + return applyPins(ordered); + }, [navigationItems, applyOrder, applyPins]); + + // Search filter state + const [navSearchQuery, setNavSearchQuery] = React.useState(''); + + // Recent section collapsed by default + const [recentExpanded, setRecentExpanded] = React.useState(false); + + // Visibility evaluation + const { evaluator } = useExpressionContext(); + const evalVis = React.useCallback( + (expr: string | boolean | undefined) => evaluateVisibility(expr, evaluator), + [evaluator], + ); + + // Permission check + const { can } = usePermissions(); + const checkPerm = React.useCallback( + (permissions: string[]) => permissions.every((perm: string) => { + const parts = perm.split(':'); + const [object, action] = parts.length >= 2 + ? [parts[0], parts[1]] + : [perm, 'read']; + return can(object, action as any); + }), + [can], + ); + + const basePath = context === 'app' && activeApp ? `/apps/${activeApp.name}` : ''; + + return ( + <> + + + + + {context === 'app' && activeApp ? ( + + + +
+ {logo ? ( + {resolveI18nLabel(activeApp.label, + ) : ( + React.createElement(getIcon(activeApp.icon), { className: "size-4" }) + )} +
+
+ {resolveI18nLabel(activeApp.label, t)} + + {resolveI18nLabel(activeApp.description, t) || `${activeApps.length} Apps Available`} + +
+ +
+
+ + + Switch Application + + {activeApps.map((app: any) => ( + onAppChange?.(app.name)} + className="gap-2 p-2" + > +
+ {app.icon ? React.createElement(getIcon(app.icon), { className: "size-3" }) : } +
+ {resolveI18nLabel(app.label, t)} + {activeApp.name === app.name && } +
+ ))} + + navigate('/home')} data-testid="home-link-btn"> +
+ +
+
Back to Home
+
+ + navigate(`/apps/${activeApp.name}/edit-app/${activeApp.name}`)} data-testid="edit-app-btn"> +
+ +
+
Edit App
+
+
+
+ ) : ( + /* Home context header - Workspace selector */ + +
+ +
+
+ Workspace + {activeApps.length} Apps +
+
+ )} +
+
+
+ + +
+ {context === 'app' && activeApp ? ( + <> + {/* Back to Home button */} + + + + + + + Back to Home + + + + + + + {/* Area Switcher */} + {areas.length > 1 && ( + + + + Area + + + + {areas.map((area: any) => { + const AreaIcon = getIcon(area.icon); + const isActiveArea = area.id === activeAreaId; + return ( + + setActiveAreaId(area.id)} + > + + {area.label} + + + ); + })} + + + + )} + + {/* Navigation Search */} + + + + setNavSearchQuery(e.target.value)} + className="pl-8" + /> + + + + {/* App Navigation tree */} + resolveNavObjectLabel({ name: objectName, label: fallback })} + t={t} + /> + + {/* Recent Items */} + {recentItems.length > 0 && ( + + setRecentExpanded(prev => !prev)} + > + + + Recent + + {recentExpanded && ( + + + {recentItems.slice(0, 5).map(item => ( + + + + + {item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄'} + + {item.label} + + + + ))} + + + )} + + )} + + {/* Favorites */} + {favorites.length > 0 && ( + + + + Favorites + + + + {favorites.slice(0, 8).map(item => ( + + + + + {item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋'} + + {item.label} + + + { e.stopPropagation(); removeFavorite(item.id); }} + aria-label={`Remove ${item.label} from favorites`} + > + + + + ))} + + + + )} + + ) : ( + /* Home Navigation */ + <> + + + + {homeNavigation.map((item) => { + const NavIcon = getIcon(item.icon); + const isActive = location.pathname === item.url; + return ( + + + + + {item.label as string} + + + + ); + })} + + + + + {/* Starred Apps */} + {favorites.filter(f => f.type === 'object' || f.type === 'dashboard' || f.type === 'page').length > 0 && ( + + + + Starred + + + + {favorites.filter(f => f.type === 'object' || f.type === 'dashboard' || f.type === 'page').slice(0, 8).map(item => ( + + + + + {item.type === 'dashboard' ? '📊' : item.type === 'page' ? '📄' : '📋'} + + {item.label} + + + { e.stopPropagation(); removeFavorite(item.id); }} + aria-label={`Remove ${item.label} from favorites`} + > + + + + ))} + + + + )} + + )} +
+
+ + {/* Pinned Bottom Area - Always visible */} + + + {/* Settings */} + + + + + Settings + + + + + {/* Help */} + + + + + Help + + + + + {/* User Profile */} + + + + + + + + {getUserInitials(user)} + + +
+ {user?.name ?? 'User'} + {user?.email ?? ''} +
+ +
+
+ + +
+ + + + {getUserInitials(user)} + + +
+ {user?.name ?? 'User'} + {user?.email ?? ''} +
+
+
+ + + navigate('/system/profile')} + > + + Profile + + navigate(context === 'app' && activeApp ? `/apps/${activeApp.name}/system` : '/system')} + > + + Settings + + + + signOut()} + > + + Log out + +
+
+
+
+
+
+ {isMobile && context === 'app' && ( +
+ {processedNavigation.filter((n: any) => n.type !== 'group').slice(0, 5).map((item: any) => { + const NavIcon = getIcon(item.icon); + let href = item.url || '#'; + if (item.type === 'object') { + href = `${basePath}/${item.objectName}`; + if (item.viewName) href += `/view/${item.viewName}`; + } + else if (item.type === 'dashboard') href = item.dashboardName ? `${basePath}/dashboard/${item.dashboardName}` : '#'; + else if (item.type === 'page') href = item.pageName ? `${basePath}/page/${item.pageName}` : '#'; + return ( + + + {resolveI18nLabel(item.label, t)} + + ); + })} +
+ )} + + ); +} diff --git a/apps/console/src/context/NavigationContext.tsx b/apps/console/src/context/NavigationContext.tsx new file mode 100644 index 00000000..c0f3c82a --- /dev/null +++ b/apps/console/src/context/NavigationContext.tsx @@ -0,0 +1,62 @@ +/** + * NavigationContext + * + * Provides global navigation state for the unified sidebar. + * Tracks whether the user is in "Home" context (workspace view) or "App" context (specific app). + * Used to determine which navigation menu to display in the UnifiedSidebar. + * + * @module + */ + +import { createContext, useContext, useState, useMemo, type ReactNode } from 'react'; + +export type NavigationContextType = 'home' | 'app'; + +interface NavigationContextValue { + /** Current navigation context (home or app) */ + context: NavigationContextType; + /** Set the navigation context */ + setContext: (context: NavigationContextType) => void; + /** Current app name when in app context */ + currentAppName?: string; + /** Set the current app name */ + setCurrentAppName: (appName?: string) => void; +} + +const NavigationContext = createContext(undefined); + +interface NavigationProviderProps { + children: ReactNode; +} + +export function NavigationProvider({ children }: NavigationProviderProps) { + const [context, setContext] = useState('home'); + const [currentAppName, setCurrentAppName] = useState(); + + const value = useMemo( + () => ({ + context, + setContext, + currentAppName, + setCurrentAppName, + }), + [context, currentAppName] + ); + + return ( + + {children} + + ); +} + +/** + * Hook to access navigation context + */ +export function useNavigationContext(): NavigationContextValue { + const context = useContext(NavigationContext); + if (!context) { + throw new Error('useNavigationContext must be used within a NavigationProvider'); + } + return context; +} diff --git a/apps/console/src/pages/home/HomeLayout.tsx b/apps/console/src/pages/home/HomeLayout.tsx index 77770ac3..4eceeed0 100644 --- a/apps/console/src/pages/home/HomeLayout.tsx +++ b/apps/console/src/pages/home/HomeLayout.tsx @@ -1,117 +1,53 @@ /** * HomeLayout * - * Lightweight layout shell for the Home Dashboard (`/home`). - * Provides a consistent navigation frame (header bar with Home branding - * and user menu) so the page is not rendered "bare" and can be extended - * in the future with notifications, global guide, or unified theming. + * Unified Home Dashboard layout with persistent sidebar. + * Uses AppShell + UnifiedSidebar for Airtable-style contextual navigation. + * The sidebar displays Home-context navigation (workspace-level items). * * @module */ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useObjectTranslation } from '@object-ui/i18n'; -import { useAuth, getUserInitials } from '@object-ui/auth'; -import { - Button, - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuGroup, - Avatar, - AvatarImage, - AvatarFallback, -} from '@object-ui/components'; -import { Settings, LogOut, User, Home } from 'lucide-react'; +import React, { useEffect } from 'react'; +import { AppShell } from '@object-ui/layout'; +import { UnifiedSidebar } from '../../components/UnifiedSidebar'; +import { AppHeader } from '../../components/AppHeader'; +import { useNavigationContext } from '../../context/NavigationContext'; +import { useResponsiveSidebar } from '../../hooks/useResponsiveSidebar'; interface HomeLayoutProps { children: React.ReactNode; } -export function HomeLayout({ children }: HomeLayoutProps) { - const navigate = useNavigate(); - const { t } = useObjectTranslation(); - const { user, signOut } = useAuth(); - - return ( -
- {/* Top Navigation Bar */} -
-
- {/* Left — Branding / Home link */} - +/** Inner component that can access SidebarProvider context */ +function HomeLayoutInner({ children }: { children: React.ReactNode }) { + useResponsiveSidebar(); + return <>{children}; +} - {/* Right — Settings + User Menu */} -
- +export function HomeLayout({ children }: HomeLayoutProps) { + const { setContext } = useNavigationContext(); - {/* User Menu Dropdown */} - - - - - - -
-

{user?.name ?? 'User'}

-

{user?.email ?? ''}

-
-
- - - navigate('/profile')} data-testid="home-layout-user-profile"> - - {t('common.profile', { defaultValue: 'Profile' })} - - navigate('/system')} data-testid="home-layout-user-settings"> - - {t('common.settings', { defaultValue: 'Settings' })} - - - - signOut()} - data-testid="home-layout-user-signout" - > - - {t('common.signOut', { defaultValue: 'Sign Out' })} - -
-
-
-
-
+ // Set navigation context to 'home' when this layout mounts + useEffect(() => { + setContext('home'); + }, [setContext]); - {/* Page Content */} - {children} -
+ return ( + } + navbar={ + + } + className="p-0 overflow-hidden bg-muted/5" + > + + {children} + + ); } diff --git a/apps/console/src/pages/home/HomePage.tsx b/apps/console/src/pages/home/HomePage.tsx index 79e0d7c8..95e04a59 100644 --- a/apps/console/src/pages/home/HomePage.tsx +++ b/apps/console/src/pages/home/HomePage.tsx @@ -90,7 +90,7 @@ export function HomePage() { return (
{/* Page Title */} -
+

{t('home.title', { defaultValue: 'Home' })}

@@ -100,7 +100,7 @@ export function HomePage() {
{/* Main Content */} -
+
{/* Quick Actions */}