Files
kjol/go/webui/sidebar.go

130 lines
4.7 KiB
Go

// Port of web/kit/Sidebar.tsx.
package webui
import "kjol/wasmruntime/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,
)
}