63 lines
2 KiB
TypeScript
63 lines
2 KiB
TypeScript
|
|
import { useState } from "react";
|
||
|
|
|
||
|
|
// Reusable horizontal pill tab-rail for in-page sub-navigation (Configuration, Users & roles…).
|
||
|
|
// The active tab is persisted to localStorage so a reload (F5) keeps the user where they were,
|
||
|
|
// per the app's "persist UI state across reload" convention.
|
||
|
|
|
||
|
|
export interface TabDef {
|
||
|
|
id: string;
|
||
|
|
label: string;
|
||
|
|
badge?: number; // optional count pill (e.g. pending requests); hidden when 0/undefined
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Persisted active-tab state. Validation is deferred to the caller (it clamps to a valid id at
|
||
|
|
* render) so this can be called before an async-loaded tab list is known, keeping hook order stable. */
|
||
|
|
export function usePersistedTab(storageKey: string, fallback = ""): [string, (id: string) => void] {
|
||
|
|
const [tab, setTabState] = useState<string>(() => localStorage.getItem(storageKey) ?? fallback);
|
||
|
|
const setTab = (id: string) => {
|
||
|
|
setTabState(id);
|
||
|
|
localStorage.setItem(storageKey, id);
|
||
|
|
};
|
||
|
|
return [tab, setTab];
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function Tabs({
|
||
|
|
tabs,
|
||
|
|
active,
|
||
|
|
onChange,
|
||
|
|
}: {
|
||
|
|
tabs: TabDef[];
|
||
|
|
active: string;
|
||
|
|
onChange: (id: string) => void;
|
||
|
|
}) {
|
||
|
|
return (
|
||
|
|
<div className="flex flex-wrap gap-1 mb-4">
|
||
|
|
{tabs.map((tb) => {
|
||
|
|
const on = tb.id === active;
|
||
|
|
return (
|
||
|
|
<button
|
||
|
|
key={tb.id}
|
||
|
|
onClick={() => onChange(tb.id)}
|
||
|
|
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm transition ${
|
||
|
|
on
|
||
|
|
? "bg-accent text-accent-fg font-medium shadow-md shadow-accent/20"
|
||
|
|
: "text-muted hover:text-fg hover:bg-card/60"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{tb.label}
|
||
|
|
{tb.badge != null && tb.badge > 0 && (
|
||
|
|
<span
|
||
|
|
className={`text-[11px] leading-none px-1.5 py-0.5 rounded-full ${
|
||
|
|
on ? "bg-accent-fg/20" : "bg-accent/20 text-accent"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{tb.badge}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</button>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|