Files
kjol/go/webui/crmtabs.go

136 lines
4.7 KiB
Go

package webui
import (
"strconv"
"kjol/vdom"
)
// Port of web/kit/CrmTabs.tsx.
//
// CrmTabGroup / CrmSubTabGroup are behaviourally-identical siblings of TabGroup
// with a different look: CrmTabGroup is boxed tabs with a sky-blue top accent;
// CrmSubTabGroup is a segmented control. Content sits flat beneath the tab row.
//
// NOTE: As in Tabs.tsx, the TSX localStorage persistence (storageKey) and
// cross-component "storage"-event syncing are browser-only and dropped;
// selection collapses to a plain ActiveIndex value + OnTabChange callback (the
// caller owns state). The TSX defaultIndex prop collapses into ActiveIndex.
// CrmTabItem is one CRM tab: title, optional numeric badge (shown when > 0), and
// inline panel content.
type CrmTabItem struct {
Title string
Badge int
Content *vdom.VNode
}
// CrmTabGroupProps configures CrmTabGroup / CrmSubTabGroup. ActiveIndex is the
// selected tab; clicking a tab calls OnTabChange with its index.
type CrmTabGroupProps struct {
Items []CrmTabItem
ActiveIndex int
OnTabChange func(int)
}
// crmTabsPanels renders one wrapper div per item; only the active one is shown.
func crmTabsPanels(p CrmTabGroupProps) []*vdom.VNode {
out := make([]*vdom.VNode, 0, len(p.Items))
for i, item := range p.Items {
cls := "hidden"
if i == p.ActiveIndex {
cls = ""
}
out = append(out, vdom.Div(vdom.Attr("class", cls), item.Content))
}
return out
}
// --- CrmTabGroup: boxed top-accent tabs -------------------------------------
const crmTabRow = "flex w-full overflow-x-auto text-sm"
const crmTabBase = "flex items-center gap-1.5 cursor-pointer p-4 font-medium border-neutral-300 transition-colors"
const crmTabInactive = "border-b text-neutral-500 hover:text-neutral-800"
const crmTabActive = "border-x border-t-2 border-t-sky-700 text-primary"
const crmTabBadge = "inline-flex items-center justify-center min-w-5 h-5 px-1 text-xs font-semibold bg-primary text-white rounded-full"
// CrmTabGroup renders boxed tabs with a sky-blue top accent; content sits flat
// beneath the row (no body panel), with a trailing filler extending the baseline.
func CrmTabGroup(p CrmTabGroupProps) *vdom.VNode {
row := []vdom.Mod{vdom.Attr("class", crmTabRow)}
for i, item := range p.Items {
idx := i
state := crmTabInactive
if i == p.ActiveIndex {
state = crmTabActive
}
btn := []vdom.Mod{
vdom.Attr("type", "button"),
vdom.Attr("class", cx(crmTabBase, 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", crmTabBadge), vdom.Text(strconv.Itoa(item.Badge))))
}
row = append(row, vdom.Button(btn...))
}
row = append(row, vdom.Div(vdom.Attr("class", "flex-1 border-b border-neutral-300")))
return vdom.Div(vdom.Attr("class", "w-full"),
vdom.Div(row...),
vdom.Div(kids(nil, crmTabsPanels(p))...),
)
}
// --- CrmSubTabGroup: segmented control --------------------------------------
const crmSubTabWrap = "flex pb-3 border-b border-neutral-300 overflow-x-auto"
const crmSubTabGroup = "inline-flex items-stretch rounded-md border border-neutral-300 overflow-hidden text-sm select-none"
const crmSubTabBase = "flex items-center gap-1.5 py-1 px-3 cursor-pointer font-medium whitespace-nowrap transition-colors"
const crmSubTabDivider = "border-l border-neutral-300"
const crmSubTabActive = "bg-neutral-500 text-white"
const crmSubTabInactive = "bg-white text-neutral-600 hover:bg-neutral-100 hover:text-neutral-900"
const crmSubTabBadge = "inline-flex items-center justify-center min-w-5 h-5 px-1 text-xs font-semibold bg-black/10 text-current rounded-full"
// CrmSubTabGroup renders a left-aligned segmented control (interlocking
// segments) with a full-width baseline separating the bar from the content.
func CrmSubTabGroup(p CrmTabGroupProps) *vdom.VNode {
group := []vdom.Mod{vdom.Attr("class", crmSubTabGroup)}
for i, item := range p.Items {
idx := i
divider := ""
if i > 0 {
divider = crmSubTabDivider
}
state := crmSubTabInactive
if i == p.ActiveIndex {
state = crmSubTabActive
}
btn := []vdom.Mod{
vdom.Attr("type", "button"),
vdom.Attr("class", cx(crmSubTabBase, divider, 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", crmSubTabBadge), vdom.Text(strconv.Itoa(item.Badge))))
}
group = append(group, vdom.Button(btn...))
}
return vdom.Div(vdom.Attr("class", "w-full pt-3"),
vdom.Div(vdom.Attr("class", crmSubTabWrap),
vdom.Div(group...),
),
vdom.Div(kids([]vdom.Mod{vdom.Attr("class", "pt-3")}, crmTabsPanels(p))...),
)
}