package webui import ( "strconv" "strings" "kjol/vdom" ) // Port of web/kit/Tabs.tsx. // // NOTE: The TSX persisted the active index in localStorage (storageKey) and // synced sibling tab groups via synthetic "storage" events on mount. Those are // browser-only side effects with no neutral-runtime equivalent, so they are // dropped: selection collapses to a plain ActiveIndex value plus an OnTabChange // callback (the caller owns the state, per the porting guide). The TSX // defaultIndex prop likewise collapses into the caller-supplied ActiveIndex. const tabsBase = "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 tabsInactive = "text-text-muted border-line hover:text-text-body" const tabsActive = "text-accent border-primary" // TabItem is one tab: a title, an optional numeric badge (shown only when > 0), // and optional inline panel content. type TabItem struct { Title string Badge int Content *vdom.VNode } // TabGroupProps configures TabGroup. ActiveIndex is the selected tab; clicking a // tab calls OnTabChange with its index (the caller updates ActiveIndex). type TabGroupProps struct { Items []TabItem ActiveIndex int OnTabChange func(int) // Actions is optional content rendered on the right side of the tab bar. Actions *vdom.VNode // Class adds extra classes on the root (e.g. "ui-tabs" for structured panel // CSS, "page-tabs" for tighter spacing). Class string // Fill makes tabs fill available height with internally-scrolling panels. Fill bool // Stretch is tri-state (nil = unset): when Actions is nil it defaults to // true, otherwise it defaults to false — matching the TSX prop semantics. Stretch *bool PageTabs bool } // TabGroup renders a horizontal tab bar with optional inline panels. func TabGroup(p TabGroupProps) *vdom.VNode { structured := p.Fill || strings.Contains(p.Class, "ui-tabs") pageTabs := p.PageTabs || strings.Contains(p.Class, "page-tabs") var rootCls string switch { case p.Fill: rootCls = cx("ui-tabs flex w-full min-h-0 flex-1 flex-col overflow-hidden", p.Class) case pageTabs: rootCls = cx("w-full page-tabs", p.Class) default: rootCls = cx("w-full pb-4", p.Class) } headerCls := "overflow-x-auto flex flex-row w-full text-sm" if structured { headerCls = "header overflow-x-auto flex flex-row w-full shrink-0 text-sm" } stretchTabs := false if p.Actions != nil { stretchTabs = p.Stretch != nil && *p.Stretch } else { stretchTabs = p.Stretch == nil || *p.Stretch } stretchCls := "" if stretchTabs { stretchCls = "flex-1 justify-center md:flex-initial md:justify-start" } panelCls := func(index int) string { active := index == p.ActiveIndex if !structured { if active { return "" } return "hidden" } if !active { return "panel hidden" } if p.Fill { return "panel flex min-h-0 flex-1 flex-col overflow-hidden" } return "panel" } header := []vdom.Mod{vdom.Attr("class", headerCls)} for i, item := range p.Items { idx := i state := tabsInactive if i == p.ActiveIndex { state = tabsActive } btn := []vdom.Mod{ vdom.Attr("type", "button"), vdom.Attr("class", cx(tabsBase, stretchCls, state)), vdom.On(vdom.EVENT_CLICK, func() { if p.OnTabChange != nil { p.OnTabChange(idx) } }), vdom.Text(item.Title), } if item.Badge > 0 { btn = append(btn, vdom.Span(vdom.Attr("class", "inline-flex items-center justify-center min-w-5 h-5 px-1 text-xs font-semibold bg-primary text-white rounded-full"), vdom.Text(strconv.Itoa(item.Badge)))) } header = append(header, vdom.Button(btn...)) } if p.Actions != nil { header = append(header, vdom.Div(vdom.Attr("class", "tab-actions flex-1 self-end border-b-2 border-line flex items-center justify-end pb-1"), vdom.Div(vdom.Attr("class", "flex items-center min-w-0"), p.Actions), )) } root := []vdom.Mod{vdom.Attr("class", rootCls), vdom.Div(header...)} hasInlinePanels := false for _, item := range p.Items { if item.Content != nil { hasInlinePanels = true break } } if hasInlinePanels { for i, item := range p.Items { if item.Content == nil { continue } root = append(root, vdom.Div(vdom.Attr("class", panelCls(i)), item.Content)) } } return vdom.Div(root...) }