forked from cardosofelipe/pragma-stack
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:
170
frontend/src/components/layout/AppLayout.tsx
Normal file
170
frontend/src/components/layout/AppLayout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user