package webui import "kjol/wasmruntime/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-line rounded-default overflow-hidden" const accordionItemCls = "ui-accordion-item border-b border-line 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-ink bg-surface-muted cursor-pointer border-none transition-colors hover:bg-surface-raised active:bg-surface-strong focus:outline-hidden disabled:text-ink-faint disabled:cursor-not-allowed disabled:bg-surface-muted" const accordionTitleCls = "ui-accordion-title flex-1" const accordionContentCls = "ui-accordion-content px-4 pb-4 text-ink-soft" func accordionIconCls(open bool) string { c := "ui-accordion-icon text-ink-muted 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.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.Span(vdom.Attr("class", accordionIconCls(p.IsOpen)), Icon(chev, 18, ""), )) } mods := []vdom.Mod{vdom.Attr("class", accordionItemCls), vdom.Button(trigger...)} if p.IsOpen && !p.Disabled { mods = append(mods, vdom.Div(kids([]vdom.Mod{vdom.Attr("class", accordionContentCls)}, children)...)) } return vdom.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.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.Div(mods...) }