130 lines
4.7 KiB
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-ss 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,
|
|
)
|
|
}
|