initial port of the UI kit
This commit is contained in:
131
go/webui/accordion.go
Normal file
131
go/webui/accordion.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package webui
|
||||
|
||||
import "kjol/vdom"
|
||||
|
||||
// Port of web/kit/Accordion.tsx.
|
||||
//
|
||||
// NOTE: The TSX AccordionItem kept its open state in an internal signal (seeded
|
||||
// by startOpen, synced from an optional isOpen prop via createEffect). Per the
|
||||
// porting guide, selection state collapses to a plain value + callback: IsOpen
|
||||
// is a plain bool and OnToggle fires on click (the caller flips its own state).
|
||||
// The TSX startOpen prop collapses into the caller-supplied initial IsOpen /
|
||||
// open[] / openIndex.
|
||||
|
||||
const accordionRootCls = "ui-accordion border border-neutral-200 rounded-default overflow-hidden"
|
||||
const accordionItemCls = "ui-accordion-item border-b border-neutral-200 last:border-b-0"
|
||||
const accordionTriggerCls = "ui-accordion-trigger flex items-center justify-between w-full py-3 px-4 text-left font-medium text-neutral-900 bg-neutral-50 cursor-pointer border-none transition-colors hover:bg-neutral-100 active:bg-neutral-200 focus:outline-hidden disabled:text-neutral-400 disabled:cursor-not-allowed disabled:bg-neutral-50"
|
||||
const accordionTitleCls = "ui-accordion-title flex-1"
|
||||
const accordionContentCls = "ui-accordion-content px-4 pb-4 text-neutral-700"
|
||||
|
||||
func accordionIconCls(open bool) string {
|
||||
c := "ui-accordion-icon text-neutral-500 leading-none transition-transform duration-200"
|
||||
if open {
|
||||
c += " rotate-180"
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// AccordionItemProps configures a single AccordionItem. IsOpen is the current
|
||||
// open state; OnToggle fires when the (enabled) trigger is clicked.
|
||||
type AccordionItemProps struct {
|
||||
IsOpen bool
|
||||
Disabled bool
|
||||
Title string
|
||||
OnToggle func()
|
||||
}
|
||||
|
||||
// AccordionItem is one collapsible row: a trigger button plus its content
|
||||
// (children), shown when open and not disabled.
|
||||
func AccordionItem(p AccordionItemProps, children ...*vdom.VNode) *vdom.VNode {
|
||||
ariaExpanded := "false"
|
||||
if p.IsOpen {
|
||||
ariaExpanded = "true"
|
||||
}
|
||||
trigger := []vdom.Mod{
|
||||
vdom.Attr("type", "button"),
|
||||
vdom.Attr("class", accordionTriggerCls),
|
||||
vdom.Attr("aria-expanded", ariaExpanded),
|
||||
vdom.On(vdom.EVENT_CLICK, func() {
|
||||
if p.Disabled {
|
||||
return
|
||||
}
|
||||
if p.OnToggle != nil {
|
||||
p.OnToggle()
|
||||
}
|
||||
}),
|
||||
vdom.El("span", vdom.Attr("class", accordionTitleCls), vdom.Text(p.Title)),
|
||||
}
|
||||
if p.Disabled {
|
||||
trigger = append(trigger, vdom.Attr("disabled", "disabled"))
|
||||
} else {
|
||||
chev := "chevron-down"
|
||||
if p.IsOpen {
|
||||
chev = "chevron-up"
|
||||
}
|
||||
trigger = append(trigger, vdom.El("span",
|
||||
vdom.Attr("class", accordionIconCls(p.IsOpen)),
|
||||
Icon(chev, 18, ""),
|
||||
))
|
||||
}
|
||||
|
||||
mods := []vdom.Mod{vdom.Attr("class", accordionItemCls), vdom.El("button", trigger...)}
|
||||
if p.IsOpen && !p.Disabled {
|
||||
mods = append(mods, vdom.El("div", kids([]vdom.Mod{vdom.Attr("class", accordionContentCls)}, children)...))
|
||||
}
|
||||
return vdom.El("div", mods...)
|
||||
}
|
||||
|
||||
// AccordionItemData is one entry for Accordion / SingleAccordion.
|
||||
type AccordionItemData struct {
|
||||
Title string
|
||||
Content *vdom.VNode
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
// Accordion renders independently-collapsible items. open[i] is item i's open
|
||||
// state (missing entries are closed); clicking item i calls onToggle(i) — the
|
||||
// caller flips open[i].
|
||||
func Accordion(items []AccordionItemData, open []bool, onToggle func(int)) *vdom.VNode {
|
||||
mods := []vdom.Mod{vdom.Attr("class", accordionRootCls)}
|
||||
for i, item := range items {
|
||||
idx := i
|
||||
isOpen := i < len(open) && open[i]
|
||||
mods = append(mods, AccordionItem(AccordionItemProps{
|
||||
IsOpen: isOpen,
|
||||
Disabled: item.Disabled,
|
||||
Title: item.Title,
|
||||
OnToggle: func() {
|
||||
if onToggle != nil {
|
||||
onToggle(idx)
|
||||
}
|
||||
},
|
||||
}, item.Content))
|
||||
}
|
||||
return vdom.El("div", mods...)
|
||||
}
|
||||
|
||||
// SingleAccordion renders items where at most one is open. openIndex is the open
|
||||
// item (-1 = none); clicking an item calls onChange with the new open index —
|
||||
// the clicked index, or -1 if that item was already open.
|
||||
func SingleAccordion(items []AccordionItemData, openIndex int, onChange func(int)) *vdom.VNode {
|
||||
mods := []vdom.Mod{vdom.Attr("class", accordionRootCls)}
|
||||
for i, item := range items {
|
||||
idx := i
|
||||
mods = append(mods, AccordionItem(AccordionItemProps{
|
||||
IsOpen: openIndex == i,
|
||||
Disabled: item.Disabled,
|
||||
Title: item.Title,
|
||||
OnToggle: func() {
|
||||
if onChange == nil {
|
||||
return
|
||||
}
|
||||
if openIndex == idx {
|
||||
onChange(-1)
|
||||
} else {
|
||||
onChange(idx)
|
||||
}
|
||||
},
|
||||
}, item.Content))
|
||||
}
|
||||
return vdom.El("div", mods...)
|
||||
}
|
||||
Reference in New Issue
Block a user