import { createSignal, onCleanup, onMount, Show, For, JSXElement } from "solid-js"; // CrmTabGroup / CrmSubTabGroup — drop-in, behaviourally identical siblings of // TabGroup (same props, storageKey syncing, controlled/uncontrolled index) with // a different look: // - CrmTabGroup → boxed tabs with a sky-blue top accent (ported from the old cdrl_2.0 tabs) // - CrmSubTabGroup → interlocking right-pointing arrows (a process-flow strip) interface CrmTabItem { title: string; badge?: number; content: JSXElement; } interface CrmTabGroupProps { items: CrmTabItem[]; storageKey?: string; activeIndex?: number; onTabChange?: (index: number) => void; defaultIndex?: number; } function resolveCrmBadge(badge: number | undefined): number | undefined { return typeof badge === "function" ? (badge as () => number)() : badge; } // Shared active-index state: mirrors TabGroup exactly (localStorage persistence, // cross-component sync via synthetic storage events, optional controlled index). function createCrmTabState(props: CrmTabGroupProps) { 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); window.dispatchEvent(new StorageEvent("storage", { key: props.storageKey, newValue: v })); } props.onTabChange && props.onTabChange(i); }; 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)); }); return { activeIndex, setActiveIndex }; } function crmTabContent(items: CrmTabItem[], activeIndex: () => number) { return {(item: CrmTabItem, index: () => number) => (
{item.content}
)}
; } // --- CrmTabGroup: boxed top-accent tabs ------------------------------------- // Ported from the old cdrl_2.0 TabGroup. Inactive tabs are flat with only a // bottom border (neutral-300) that forms the baseline; a trailing flex-1 filler // extends that baseline past the last tab to the right edge. The active tab // drops its bottom border and gains 1px left/right borders plus a 2px sky-700 // top edge, so it reads as a raised box connected to the content below. No body // panel — content sits flat beneath the row, as in the old design. // // Per-tab borders (no negative-margin overlap) mean the row's overflow-x-auto // can't clip anything: the baseline simply has a gap under the active tab. const CRM_TAB_ROW = "flex w-full overflow-x-auto text-sm"; const CRM_TAB_BASE = "flex items-center gap-1.5 cursor-pointer p-4 font-medium border-line-strong transition-colors"; const CRM_TAB_INACTIVE = "border-b text-ink-muted hover:text-ink"; const CRM_TAB_ACTIVE = "border-x border-t-2 border-t-sky-700 text-primary"; const CRM_TAB_BADGE = "inline-flex items-center justify-center min-w-5 h-5 px-1 text-xs font-semibold bg-primary text-white rounded-full"; export function CrmTabGroup(props: CrmTabGroupProps) { const { activeIndex, setActiveIndex } = createCrmTabState(props); return
{(item: CrmTabItem, index: () => number) => ( )}
{crmTabContent(props.items, activeIndex)}
; } // --- CrmSubTabGroup: segmented control -------------------------------------- // A single rounded, bordered group split into segments with dividers between // them. The active segment is filled gray with white text; inactive segments // are white and recede. The group is left-aligned (flush with the file-folder // tabs) and a full-width baseline separates the bar from the content below. const CRM_SUBTAB_WRAP = "flex pb-3 border-b border-line-strong overflow-x-auto"; const CRM_SUBTAB_GROUP = "inline-flex items-stretch rounded-md border border-line-strong overflow-hidden text-sm select-none"; const CRM_SUBTAB_BASE = "flex items-center gap-1.5 py-1 px-3 cursor-pointer font-medium whitespace-nowrap transition-colors"; const CRM_SUBTAB_DIVIDER = "border-l border-line-strong"; const CRM_SUBTAB_ACTIVE = "bg-neutral-500 text-white"; const CRM_SUBTAB_INACTIVE = "bg-surface text-ink-soft hover:bg-surface-raised hover:text-ink"; const CRM_SUBTAB_BADGE = "inline-flex items-center justify-center min-w-5 h-5 px-1 text-xs font-semibold bg-black/10 text-current rounded-full"; export function CrmSubTabGroup(props: CrmTabGroupProps) { const { activeIndex, setActiveIndex } = createCrmTabState(props); return
{(item: CrmTabItem, index: () => number) => ( )}
{crmTabContent(props.items, activeIndex)}
; }