React + Vite + TypeScript SPA covering the full ABS feature set (library browsing, item detail, metadata/cover editing, podcasts, player with session sync, admin: users/libraries/scanner/server settings). Dev uses a dynamic CORS proxy; production is served by server/index.mjs (static + reverse proxy to ABS_URL). Includes systemd unit and installer under deploy/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
39 lines
928 B
TypeScript
39 lines
928 B
TypeScript
import { cn } from '@/lib/cn'
|
|
|
|
export interface TabDef {
|
|
key: string
|
|
label: string
|
|
}
|
|
|
|
interface Props {
|
|
tabs: TabDef[]
|
|
active: string
|
|
onChange: (key: string) => void
|
|
}
|
|
|
|
export function Tabs({ tabs, active, onChange }: Props) {
|
|
return (
|
|
<div role="tablist" className="flex gap-1 border-b border-border">
|
|
{tabs.map((t) => (
|
|
<button
|
|
key={t.key}
|
|
role="tab"
|
|
aria-selected={active === t.key}
|
|
onClick={() => onChange(t.key)}
|
|
className={cn(
|
|
'relative -mb-px px-4 py-2.5 text-sm font-medium transition-colors',
|
|
active === t.key
|
|
? 'text-text'
|
|
: 'text-text-muted hover:text-text',
|
|
)}
|
|
>
|
|
{t.label}
|
|
{active === t.key && (
|
|
<span className="absolute inset-x-2 -bottom-px h-0.5 rounded-full bg-accent" />
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|