146 lines
4.3 KiB
Go
146 lines
4.3 KiB
Go
package webui
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"kjol/wasmruntime/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-ss 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...)
|
|
}
|