// Port of web/kit/Sidebar.tsx. package webui import "kjol/vdom" const sidebarNavRoot = "bg-surface rounded-default shadow-sm border border-line py-2" const sidebarNavList = "list-none m-0 p-0" const sidebarNavBtn = "w-full text-left py-2 pr-3 pl-4 text-sm cursor-pointer bg-transparent text-ink-soft border-0 hover:text-ink hover:bg-surface-muted" const sidebarNavSubBtn = "w-full text-left py-1.5 pr-3 pl-8 text-xs cursor-pointer bg-transparent text-ink-muted border-0 hover:text-ink hover:bg-surface-muted" const sidebarNavIcon = "mr-2" const sidebarLayoutRoot = "grid grid-cols-1 gap-8 min-h-screen items-start lg:grid-cols-12" const sidebarLayoutSidebar = "hidden lg:block lg:col-span-2 lg:self-start lg:h-full" const sidebarLayoutSticky = "sticky top-20 max-h-[calc(100vh_-_7rem)] overflow-y-auto" const sidebarLayoutStickyFull = "sticky top-0 h-screen overflow-y-auto" // sidebarLayoutMain: lg:pr-8 mirrors the grid's gap-8 so the content has matching // breathing room on the right instead of hugging the viewport edge. const sidebarLayoutMain = "col-span-1 min-w-0 lg:col-span-10 lg:pr-8" // SidebarNavItem is one entry in a SidebarNav. Icon is an optional leading node // (e.g. webui.Icon(...)). Children are optional second-level items that jump to // sub-sections within this item. type SidebarNavItem struct { ID string Label string Icon *vdom.VNode Children []SidebarNavItem } // SidebarNav renders a vertical nav of jump links. onItemClick receives the id // of the clicked item (top-level or sub-item). // // NOTE: the TSX handler also does document.getElementById(id).scrollIntoView({ // behavior: "smooth" }) to smooth-scroll to the target section. That is // browser-only (no document in the neutral runtime) and is dropped here; perform // the scroll from within the app's onItemClick if needed. func SidebarNav(items []SidebarNavItem, onItemClick func(string), class string) *vdom.VNode { handleClick := func(id string) { if onItemClick != nil { onItemClick(id) } } listMods := []vdom.Mod{vdom.Attr("class", sidebarNavList)} for _, item := range items { id := item.ID btnMods := []vdom.Mod{ vdom.Attr("class", sidebarNavBtn), vdom.On(vdom.EVENT_CLICK, func() { handleClick(id) }), } if item.Icon != nil { btnMods = append(btnMods, vdom.Span(vdom.Attr("class", sidebarNavIcon), item.Icon)) } btnMods = append(btnMods, vdom.Text(item.Label)) liMods := []vdom.Mod{vdom.Button(btnMods...)} if len(item.Children) > 0 { subListMods := []vdom.Mod{vdom.Attr("class", sidebarNavList)} for _, sub := range item.Children { sid := sub.ID subBtnMods := []vdom.Mod{ vdom.Attr("class", sidebarNavSubBtn), vdom.On(vdom.EVENT_CLICK, func() { handleClick(sid) }), } if sub.Icon != nil { subBtnMods = append(subBtnMods, vdom.Span(vdom.Attr("class", sidebarNavIcon), sub.Icon)) } subBtnMods = append(subBtnMods, vdom.Text(sub.Label)) subListMods = append(subListMods, vdom.Li(vdom.Button(subBtnMods...))) } liMods = append(liMods, vdom.Ul(subListMods...)) } listMods = append(listMods, vdom.Li(liMods...)) } return vdom.Nav(vdom.Attr("class", cx(sidebarNavRoot, class)), vdom.Ul(listMods...), ) } // SidebarLayoutProps configures SidebarLayout. Sidebar is the aside content; // children (variadic on SidebarLayout) are the main content. type SidebarLayoutProps struct { Sidebar *vdom.VNode Class string FullHeight bool Collapsible bool OnToggleCollapse func() } // SidebarLayout is a two-column responsive grid: a sticky sidebar (aside) and a // main content column. Children are the main content. // // NOTE: the TSX keeps an internal `collapsed` signal toggled by the collapse // button, but nothing in its markup reads it — so collapsing has no visual // effect there. This port renders the toggle button when Collapsible is set and // wires OnToggleCollapse; the app owns any collapsed state/behavior. func SidebarLayout(p SidebarLayoutProps, children ...*vdom.VNode) *vdom.VNode { sticky := sidebarLayoutSticky if p.FullHeight { sticky = sidebarLayoutStickyFull } asideMods := []vdom.Mod{ vdom.Attr("class", sidebarLayoutSidebar), vdom.Div(kids([]vdom.Mod{vdom.Attr("class", sticky)}, []*vdom.VNode{p.Sidebar})...), } if p.Collapsible { btnMods := []vdom.Mod{ vdom.Attr("type", "button"), vdom.Attr("aria-label", "Toggle sidebar"), } if p.OnToggleCollapse != nil { btnMods = append(btnMods, vdom.On(vdom.EVENT_CLICK, p.OnToggleCollapse)) } asideMods = append(asideMods, vdom.Button(btnMods...)) } main := vdom.Main(kids([]vdom.Mod{vdom.Attr("class", sidebarLayoutMain)}, children)...) return vdom.Div(vdom.Attr("class", cx(sidebarLayoutRoot, p.Class)), vdom.Aside(asideMods...), main, ) }