'use client'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { createContext, type ReactNode, useContext, useEffect, useId, useRef } from 'react'; import { cn } from '@/lib/cn'; export type TabDef = { id: string; label: string; count?: number | null; hidden?: boolean }; const Ctx = createContext<{ active: string; uid: string }>({ active: '', uid: '' }); /** * URL-driven tabs (`?tab=`); all panels are rendered server-side, only the active one is shown. Arrow keys move. * The strip scrolls horizontally on mobile. */ export function Tabs({ tabs, defaultTab, children, param = 'tab', className, sticky = false }: { tabs: TabDef[]; defaultTab?: string; children: ReactNode; param?: string; className?: string; sticky?: boolean }) { const visible = tabs.filter((t) => !t.hidden); const first = defaultTab ?? visible[0]?.id ?? ''; const router = useRouter(); const pathname = usePathname(); const sp = useSearchParams(); const stripRef = useRef(null); const uid = useId(); const requested = sp.get(param) ?? ''; const active = visible.some((t) => t.id === requested) ? requested : first; const select = (id: string) => { const next = new URLSearchParams(sp.toString()); if (id === first) next.delete(param); else next.set(param, id); const q = next.toString(); router.replace(q ? `${pathname}?${q}` : pathname, { scroll: false }); }; useEffect(() => { // Horizontal reveal of the active tab only — never scrollIntoView (it scrolls the page vertically on load, spec: pages open at the top). const strip = stripRef.current; const el = strip?.querySelector(`[data-tab="${active}"]`); if (!strip || !el) return; const left = el.offsetLeft - (strip.clientWidth - el.offsetWidth) / 2; if (Math.abs(strip.scrollLeft - left) > 4) strip.scrollTo({ left: Math.max(0, left) }); }, [active]); const onKey = (e: React.KeyboardEvent) => { const idx = visible.findIndex((t) => t.id === active); if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') { e.preventDefault(); const n = visible[(idx + (e.key === 'ArrowRight' ? 1 : visible.length - 1)) % visible.length]; if (n) { select(n.id); stripRef.current?.querySelector(`[data-tab="${n.id}"]`)?.focus(); } } }; return (
{visible.map((t) => { const on = t.id === active; return ( ); })}
{children}
); } export function TabPanel({ id, children, className }: { id: string; children: ReactNode; className?: string }) { const { active, uid } = useContext(Ctx); const on = active === id; return ( ); } /** Simple segmented control (not URL-bound) for local state like windows/kinds. */ export function Segmented({ options, value, onChange, className, size = 'md', ariaLabel }: { options: { id: T; label: string }[]; value: T; onChange: (v: T) => void; className?: string; size?: 'sm' | 'md'; ariaLabel?: string }) { return (
{options.map((o) => { const on = o.id === value; return ( ); })}
); }