136 lines
4.7 KiB
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))...),
|
|
)
|
|
}
|