Files
pragma-stack/frontend/src/components/layout/AppBreadcrumbs.tsx
Felipe Cardoso 6e645835dc 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>
2025-12-30 01:35:39 +01:00

150 lines
4.2 KiB
TypeScript

/**
* 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>
);
}