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:
149
frontend/src/components/layout/AppBreadcrumbs.tsx
Normal file
149
frontend/src/components/layout/AppBreadcrumbs.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Application Breadcrumbs
|
||||
* Displays navigation breadcrumb trail for application pages
|
||||
* Supports dynamic breadcrumb items and project context
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
import { usePathname } from '@/lib/i18n/routing';
|
||||
import { ChevronRight, Home } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
/** Display label */
|
||||
label: string;
|
||||
/** Navigation href */
|
||||
href: string;
|
||||
/** If true, this is the current page */
|
||||
current?: boolean;
|
||||
}
|
||||
|
||||
interface AppBreadcrumbsProps {
|
||||
/** Custom breadcrumb items (overrides auto-generation) */
|
||||
items?: BreadcrumbItem[];
|
||||
/** Show home icon as first item */
|
||||
showHome?: boolean;
|
||||
/** Additional className */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** Path segment to label mappings */
|
||||
const pathLabels: Record<string, string> = {
|
||||
projects: 'Projects',
|
||||
issues: 'Issues',
|
||||
sprints: 'Sprints',
|
||||
agents: 'Agents',
|
||||
settings: 'Settings',
|
||||
dashboard: 'Dashboard',
|
||||
admin: 'Admin',
|
||||
'agent-types': 'Agent Types',
|
||||
users: 'Users',
|
||||
organizations: 'Organizations',
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate breadcrumb items from the current pathname
|
||||
*/
|
||||
function generateBreadcrumbs(pathname: string): BreadcrumbItem[] {
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
const breadcrumbs: BreadcrumbItem[] = [];
|
||||
|
||||
let currentPath = '';
|
||||
|
||||
segments.forEach((segment, index) => {
|
||||
currentPath += `/${segment}`;
|
||||
|
||||
// Skip locale segments (2-letter codes)
|
||||
if (segment.length === 2 && /^[a-z]{2}$/i.test(segment)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if segment is a dynamic ID/slug (not in pathLabels)
|
||||
const label = pathLabels[segment] || segment;
|
||||
const isLast = index === segments.length - 1;
|
||||
|
||||
breadcrumbs.push({
|
||||
label,
|
||||
href: currentPath,
|
||||
current: isLast,
|
||||
});
|
||||
});
|
||||
|
||||
return breadcrumbs;
|
||||
}
|
||||
|
||||
export function AppBreadcrumbs({
|
||||
items,
|
||||
showHome = true,
|
||||
className,
|
||||
}: AppBreadcrumbsProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
// Use provided items or generate from pathname
|
||||
const breadcrumbs = items ?? generateBreadcrumbs(pathname);
|
||||
|
||||
// Don't render if no breadcrumbs or only home
|
||||
if (breadcrumbs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label="Breadcrumb"
|
||||
className={cn('border-b bg-background px-4 py-3 lg:px-6', className)}
|
||||
data-testid="breadcrumbs"
|
||||
>
|
||||
<ol className="flex flex-wrap items-center gap-1 text-sm">
|
||||
{/* Home link */}
|
||||
{showHome && (
|
||||
<li className="flex items-center">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-sm"
|
||||
aria-label="Home"
|
||||
data-testid="breadcrumb-home"
|
||||
>
|
||||
<Home className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{/* Breadcrumb items */}
|
||||
{breadcrumbs.map((breadcrumb, index) => {
|
||||
const showSeparator = showHome || index > 0;
|
||||
const testId = `breadcrumb-${breadcrumb.label.toLowerCase().replace(/\s+/g, '-')}`;
|
||||
|
||||
return (
|
||||
<li key={breadcrumb.href} className="flex items-center">
|
||||
{showSeparator && (
|
||||
<ChevronRight
|
||||
className="mx-1 h-4 w-4 text-muted-foreground flex-shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{breadcrumb.current ? (
|
||||
<span
|
||||
className="font-medium text-foreground"
|
||||
aria-current="page"
|
||||
data-testid={testId}
|
||||
>
|
||||
{breadcrumb.label}
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href={breadcrumb.href}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-sm"
|
||||
data-testid={testId}
|
||||
>
|
||||
{breadcrumb.label}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user