feat(frontend): Implement navigation and layout (#44)

Implements the main navigation and layout structure:

- Sidebar component with collapsible navigation and keyboard shortcut
- AppHeader with project switcher and user menu
- AppBreadcrumbs with auto-generation from pathname
- ProjectSwitcher dropdown for quick project navigation
- UserMenu with profile, settings, and logout
- AppLayout component combining all layout elements

Features:
- Responsive design (mobile sidebar sheet, desktop sidebar)
- Keyboard navigation (Cmd/Ctrl+B to toggle sidebar)
- Dark mode support
- WCAG AA accessible (ARIA labels, focus management)

All 125 tests passing. Follows design system guidelines.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 01:35:39 +01:00
parent d6db6af964
commit 6e645835dc
13 changed files with 2771 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
/**
* Application Layout Component
* Main layout wrapper that combines sidebar, header, breadcrumbs, and content area
* Provides the standard application shell for authenticated pages
*/
'use client';
import { ReactNode } from 'react';
import { cn } from '@/lib/utils';
import { Sidebar } from './Sidebar';
import { AppHeader } from './AppHeader';
import { AppBreadcrumbs, type BreadcrumbItem } from './AppBreadcrumbs';
interface Project {
id: string;
slug: string;
name: string;
}
interface AppLayoutProps {
/** Page content */
children: ReactNode;
/** Current project (for project-specific pages) */
currentProject?: Project;
/** List of available projects */
projects?: Project[];
/** Callback when project is changed */
onProjectChange?: (projectSlug: string) => void;
/** Custom breadcrumb items (overrides auto-generation) */
breadcrumbs?: BreadcrumbItem[];
/** Hide breadcrumbs */
hideBreadcrumbs?: boolean;
/** Hide sidebar */
hideSidebar?: boolean;
/** Additional className for main content area */
className?: string;
/** Additional className for the outer wrapper */
wrapperClassName?: string;
}
export function AppLayout({
children,
currentProject,
projects = [],
onProjectChange,
breadcrumbs,
hideBreadcrumbs = false,
hideSidebar = false,
className,
wrapperClassName,
}: AppLayoutProps) {
return (
<div
className={cn('flex min-h-screen flex-col bg-background', wrapperClassName)}
data-testid="app-layout"
>
{/* Header */}
<AppHeader
currentProject={currentProject}
projects={projects}
onProjectChange={onProjectChange}
/>
{/* Main content area with sidebar */}
<div className="flex flex-1">
{/* Sidebar */}
{!hideSidebar && <Sidebar projectSlug={currentProject?.slug} />}
{/* Content area */}
<div className="flex flex-1 flex-col">
{/* Breadcrumbs */}
{!hideBreadcrumbs && <AppBreadcrumbs items={breadcrumbs} />}
{/* Main content */}
<main
className={cn('flex-1', className)}
id="main-content"
tabIndex={-1}
>
{children}
</main>
</div>
</div>
</div>
);
}
/**
* Page Container Component
* Standard container for page content with consistent padding and max-width
*/
interface PageContainerProps {
/** Page content */
children: ReactNode;
/** Maximum width constraint */
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '4xl' | '6xl' | 'full';
/** Additional className */
className?: string;
}
const maxWidthClasses: Record<string, string> = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
'2xl': 'max-w-2xl',
'4xl': 'max-w-4xl',
'6xl': 'max-w-6xl',
full: 'max-w-full',
};
export function PageContainer({
children,
maxWidth = '6xl',
className,
}: PageContainerProps) {
return (
<div
className={cn(
'container mx-auto px-4 py-6 lg:px-6 lg:py-8',
maxWidthClasses[maxWidth],
className
)}
data-testid="page-container"
>
{children}
</div>
);
}
/**
* Page Header Component
* Consistent header for page titles and actions
*/
interface PageHeaderProps {
/** Page title */
title: string;
/** Optional description */
description?: string;
/** Action buttons or other content */
actions?: ReactNode;
/** Additional className */
className?: string;
}
export function PageHeader({
title,
description,
actions,
className,
}: PageHeaderProps) {
return (
<div
className={cn(
'flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between',
className
)}
data-testid="page-header"
>
<div className="space-y-1">
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">{title}</h1>
{description && (
<p className="text-muted-foreground">{description}</p>
)}
</div>
{actions && <div className="flex items-center gap-2">{actions}</div>}
</div>
);
}