initial port of the UI kit
This commit is contained in:
131
go/webui/sidebar.go
Normal file
131
go/webui/sidebar.go
Normal file
@@ -0,0 +1,131 @@
|
||||
// Port of web/kit/Sidebar.tsx.
|
||||
|
||||
package webui
|
||||
|
||||
import "kjol/vdom"
|
||||
|
||||
const sidebarNavRoot = "bg-white rounded-default shadow-sm border border-neutral-200 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-neutral-600 border-0 hover:text-neutral-900 hover:bg-neutral-50"
|
||||
const sidebarNavSubBtn = "w-full text-left py-1.5 pr-3 pl-8 text-xs cursor-pointer bg-transparent text-neutral-500 border-0 hover:text-neutral-900 hover:bg-neutral-50"
|
||||
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.El("span", vdom.Attr("class", sidebarNavIcon), item.Icon))
|
||||
}
|
||||
btnMods = append(btnMods, vdom.Text(item.Label))
|
||||
|
||||
liMods := []vdom.Mod{vdom.El("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.El("span", vdom.Attr("class", sidebarNavIcon), sub.Icon))
|
||||
}
|
||||
subBtnMods = append(subBtnMods, vdom.Text(sub.Label))
|
||||
subListMods = append(subListMods, vdom.El("li", vdom.El("button", subBtnMods...)))
|
||||
}
|
||||
liMods = append(liMods, vdom.El("ul", subListMods...))
|
||||
}
|
||||
|
||||
listMods = append(listMods, vdom.El("li", liMods...))
|
||||
}
|
||||
|
||||
return vdom.El("nav",
|
||||
vdom.Attr("class", cx(sidebarNavRoot, class)),
|
||||
vdom.El("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.El("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.El("button", btnMods...))
|
||||
}
|
||||
|
||||
main := vdom.El("main", kids([]vdom.Mod{vdom.Attr("class", sidebarLayoutMain)}, children)...)
|
||||
|
||||
return vdom.El("div",
|
||||
vdom.Attr("class", cx(sidebarLayoutRoot, p.Class)),
|
||||
vdom.El("aside", asideMods...),
|
||||
main,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user