diff --git a/apps/web/src/features/dashboard/application-shell.test.tsx b/apps/web/src/features/dashboard/application-shell.test.tsx new file mode 100644 index 0000000..38acf46 --- /dev/null +++ b/apps/web/src/features/dashboard/application-shell.test.tsx @@ -0,0 +1,37 @@ +// Verifies the application shell exposes accessible responsive navigation and active state. +import { RouterProvider } from '@tanstack/react-router'; +import { cleanup, render, screen, within } from '@testing-library/react'; +import { afterEach, describe, expect, it } from 'vitest'; + +import { router } from '../../router'; + +afterEach(cleanup); + +describe('application shell', () => { + it('renders desktop and mobile navigation with the current page selected', async () => { + await router.navigate({ to: '/dashboard' }); + render(); + + expect(screen.getByRole('heading', { name: 'Dashboard' })).toBeVisible(); + + const navigationRegions = screen.getAllByRole('navigation', { + name: /primary navigation/i, + }); + expect(navigationRegions).toHaveLength(2); + + for (const navigation of navigationRegions) { + expect( + within(navigation).getByRole('link', { name: 'Dashboard' }), + ).toHaveAttribute('aria-current', 'page'); + expect( + within(navigation).getByRole('link', { name: 'Routines' }), + ).toHaveAttribute('href', '/routines'); + expect( + within(navigation).getByRole('link', { name: 'History' }), + ).toHaveAttribute('href', '/history'); + expect( + within(navigation).getByRole('link', { name: 'Settings' }), + ).toHaveAttribute('href', '/settings'); + } + }); +}); diff --git a/apps/web/src/features/dashboard/application-shell.tsx b/apps/web/src/features/dashboard/application-shell.tsx new file mode 100644 index 0000000..bcb5d02 --- /dev/null +++ b/apps/web/src/features/dashboard/application-shell.tsx @@ -0,0 +1,133 @@ +// Provides the responsive desktop and mobile navigation frame for application pages. +import { + Dumbbell, + History, + LayoutDashboard, + Settings, + type LucideIcon, +} from 'lucide-react'; +import { Link } from '@tanstack/react-router'; +import type { ReactNode } from 'react'; + +import wordmarkUrl from '../../assets/goforlift-wordmark.png'; +import { cn } from '../../lib/utils'; + +const navigationItems = [ + { href: '/dashboard', icon: LayoutDashboard, label: 'Dashboard' }, + { href: '/routines', icon: Dumbbell, label: 'Routines' }, + { href: '/history', icon: History, label: 'History' }, + { href: '/settings', icon: Settings, label: 'Settings' }, +] as const; + +type ApplicationShellProps = { + children: ReactNode; +}; + +export function ApplicationShell({ children }: ApplicationShellProps) { + return ( +
+ + +
+
+ +
+
+ {children} +
+
+ + +
+ ); +} + +function Brand({ className }: { className?: string }) { + return ( + + GoForLift + + ); +} + +type NavigationProps = { + className?: string; +}; + +function Navigation({ className }: NavigationProps) { + return ( + + ); +} + +type NavigationItemProps = { + href: (typeof navigationItems)[number]['href']; + icon: LucideIcon; + label: string; +}; + +function NavigationItem({ href, icon: Icon, label }: NavigationItemProps) { + return ( + +