import { createSignal, onCleanup, onMount, Show, For, JSXElement } from "solid-js"; interface TabItem { title: string; badge?: number; content?: JSXElement; } interface TabGroupProps { items: TabItem[]; storageKey?: string; activeIndex?: number; onTabChange?: (index: number) => void; defaultIndex?: number; /** Optional content rendered in the right side of the tab bar (e.g. a PillSelect). */ actions?: JSXElement; /** Extra classes on the root element (e.g. `ui-tabs` for structured panel CSS). */ class?: string; /** When true, tabs fill available height and panels scroll internally (mobile POS). */ fill?: boolean; /** When true, tab buttons share the header row equally below md. Defaults to true when `actions` is omitted. */ stretch?: boolean; /** Tighter spacing for tabs above sibling panel content (use with PageLayout `tabs`). */ pageTabs?: boolean; } const TAB_BASE = "flex items-center gap-1.5 cursor-pointer py-2 px-4 text-sm font-medium bg-transparent border-0 border-b-2 transition-[color,border-color] duration-150"; const TAB_INACTIVE = "text-ink-muted border-line hover:text-ink"; const TAB_ACTIVE = "text-primary border-primary"; function resolveBadge(badge: number | undefined): number | undefined { return typeof badge === "function" ? (badge as () => number)() : badge; } export function TabGroup(props: TabGroupProps) { const getInitialIndex = () => { if (props.storageKey) { const stored = localStorage.getItem(props.storageKey); if (stored !== null) { const parsed = parseInt(stored, 10); if (!isNaN(parsed) && parsed >= 0 && parsed < props.items.length) { return parsed; } } } return props.defaultIndex ?? 0; }; const [_activeIndex, _setActiveIndex] = createSignal(getInitialIndex()); const activeIndex = (): number => { const controlled = props.activeIndex; if (controlled != null) { return typeof controlled === "function" ? (controlled as () => number)() : controlled; } return _activeIndex(); }; const setActiveIndex = (i: number) => { _setActiveIndex(i); if (props.storageKey) { const v = String(i); localStorage.setItem(props.storageKey, v); // localStorage writes don't fire `storage` events in the same // tab; synthesize one so other components syncing on this key // (e.g. an EventsSidebar tracking the active session) update. window.dispatchEvent(new StorageEvent("storage", { key: props.storageKey, newValue: v })); } props.onTabChange && props.onTabChange(i); }; // Sync uncontrolled tabs across components that share a storageKey: when // another part of the UI writes to it (and dispatches a synthetic storage // event), pick up the new value here too. onMount(() => { if (!props.storageKey) return; const handler = (e: StorageEvent) => { if (e.key !== props.storageKey || e.newValue == null) return; const n = parseInt(e.newValue, 10); if (!isNaN(n) && n >= 0 && n < props.items.length && n !== _activeIndex()) { _setActiveIndex(n); props.onTabChange && props.onTabChange(n); } }; window.addEventListener("storage", handler); onCleanup(() => window.removeEventListener("storage", handler)); }); const structured = () => props.fill || (props.class || "").includes("ui-tabs"); const pageTabs = () => props.pageTabs || (props.class || "").includes("page-tabs"); const rootCls = () => { const parts = props.fill ? ["ui-tabs flex w-full min-h-0 flex-1 flex-col overflow-hidden"] : pageTabs() ? ["w-full page-tabs"] : ["w-full pb-4"]; if (props.class) parts.push(props.class); return parts.join(" "); }; const headerCls = () => structured() ? "header overflow-x-auto flex flex-row w-full shrink-0 text-sm" : "overflow-x-auto flex flex-row w-full text-sm"; const panelCls = (index: number) => { const active = index === activeIndex(); if (!structured()) return active ? "" : "hidden"; if (!active) return "panel hidden"; return props.fill ? "panel flex min-h-0 flex-1 flex-col overflow-hidden" : "panel"; }; const stretchTabs = () => { if (props.actions != null) return props.stretch === true; return props.stretch !== false; }; const stretchCls = () => { if (!stretchTabs()) return ""; return " flex-1 justify-center md:flex-initial md:justify-start"; }; const tabBtnCls = (index: number) => () => TAB_BASE + stretchCls() + " " + (index === activeIndex() ? TAB_ACTIVE : TAB_INACTIVE); const hasInlinePanels = () => props.items.some((item) => item.content != null); const actionsContent = () => { if (props.actions == null) return null; return typeof props.actions === "function" ? (props.actions as () => JSXElement)() : props.actions; }; return (