BillTracker/client/components/data/DataNav.tsx

79 lines
2.9 KiB
TypeScript

import type { ComponentType } from 'react';
import { cn } from '@/lib/utils';
const DOT_TONES: Record<string, string> = {
green: 'bg-emerald-500',
amber: 'bg-amber-500',
red: 'bg-rose-500',
gray: 'bg-muted-foreground/40',
};
export interface DataNavSection {
id: string;
label: string;
description?: string;
icon?: ComponentType<{ className?: string }>;
dot?: string;
badge?: number | null;
}
interface DataNavProps {
sections: DataNavSection[];
active: string;
onSelect: (id: string) => void;
}
/**
* Goal-oriented navigation for the Data page. Desktop (≥ lg): a sticky vertical
* list. Mobile: a horizontally-scrollable segmented control. One <nav> landmark,
* aria-current on the active item, fully keyboard-operable (native buttons).
*/
export default function DataNav({ sections, active, onSelect }: DataNavProps) {
return (
<nav aria-label="Data sections" className="lg:sticky lg:top-20">
<ul className="flex gap-1 overflow-x-auto pb-1 lg:flex-col lg:overflow-visible lg:pb-0">
{sections.map((s) => {
const isActive = s.id === active;
const Icon = s.icon;
return (
<li key={s.id} className="shrink-0 lg:shrink">
<button
type="button"
onClick={() => onSelect(s.id)}
aria-current={isActive ? 'page' : undefined}
className={cn(
'group flex w-full items-center gap-2.5 rounded-lg px-3 py-2.5 text-left text-sm transition-colors',
'whitespace-nowrap lg:whitespace-normal',
isActive
? 'bg-primary/10 font-semibold text-foreground lg:shadow-sm'
: 'text-muted-foreground hover:bg-muted/50 hover:text-foreground',
)}
>
{Icon && (
<Icon className={cn('h-4 w-4 shrink-0', isActive ? 'text-primary' : 'text-muted-foreground group-hover:text-foreground')} />
)}
<span className="min-w-0 flex-1">
<span className="flex items-center gap-1.5">
{s.dot && <span className={cn('h-1.5 w-1.5 shrink-0 rounded-full', DOT_TONES[s.dot] || DOT_TONES.gray)} />}
<span className="truncate">{s.label}</span>
</span>
{s.description && (
<span className="mt-0.5 hidden truncate text-xs font-normal text-muted-foreground lg:block">
{s.description}
</span>
)}
</span>
{s.badge != null && s.badge !== 0 && (
<span className="ml-auto shrink-0 rounded-full bg-amber-500/15 px-1.5 py-0.5 text-[11px] font-semibold tabular-nums text-amber-600 dark:text-amber-400">
{s.badge}
</span>
)}
</button>
</li>
);
})}
</ul>
</nav>
);
}