initial port of the UI kit

This commit is contained in:
2026-07-13 01:58:29 -04:00
parent 261a7f2b4d
commit b02df48a66
51 changed files with 7507 additions and 154 deletions

92
go/webui/general.go Normal file
View File

@@ -0,0 +1,92 @@
package webui
import "kjol/vdom"
// Port of web/kit/General.tsx. Anchor components render plain <a href>; the app
// wires SPA navigation on top (e.g. its layout nav uses Button/OnClick).
// PageContainer wraps a page body (marker class kept for app CSS).
func PageContainer(children ...*vdom.VNode) *vdom.VNode {
return vdom.El("div", kids([]vdom.Mod{vdom.Attr("class", "admin-page-container")}, children)...)
}
// Divider is a thin horizontal rule.
func Divider() *vdom.VNode { return vdom.El("hr", vdom.Attr("class", "text-neutral-200 mt-1 mb-3")) }
// CodeBox renders a dark monospace code block.
func CodeBox(code, class string) *vdom.VNode {
return vdom.El("div",
vdom.Attr("class", cx("text-xs p-3 bg-neutral-800 text-neutral-100 border border-neutral-700 rounded-default", class)),
vdom.El("pre", vdom.El("code", vdom.Text(code))),
)
}
// PageHeader is a centered page title with an underline.
func PageHeader(text, class string) *vdom.VNode {
return vdom.El("header", vdom.Attr("class", class),
vdom.El("div", vdom.Attr("class", "mt-1"),
vdom.El("h1", vdom.Attr("class", "text-center text-2xl font-light text-neutral-800 mb-2"), vdom.Text(text)),
vdom.El("hr", vdom.Attr("class", "text-neutral-200 mb-2")),
),
)
}
// PageLink is an inline text link (blue). newTab opens in a new tab.
func PageLink(href string, newTab bool, class string, children ...*vdom.VNode) *vdom.VNode {
mods := []vdom.Mod{
vdom.Attr("href", href),
vdom.Attr("class", cx("text-sky-700 hover:text-sky-800 hover:underline hover:decoration-1", class)),
}
if newTab {
mods = append(mods, vdom.Attr("target", "_blank"), vdom.Attr("rel", "noopener noreferrer"))
}
return vdom.El("a", kids(mods, children)...)
}
// Loader is a centered spinning ring.
func Loader() *vdom.VNode {
return vdom.El("div", vdom.Attr("class", "flex items-center justify-center p-8"),
vdom.El("div", vdom.Attr("class", "h-8 w-8 border-4 border-sky-700 border-t-transparent rounded-full animate-spin")),
)
}
// BreadcrumbItem is one crumb (URL + display text).
type BreadcrumbItem struct {
URL string
DisplayText string
}
// Breadcrumbs renders a chevron-separated crumb trail; the last item is bold and
// not linked.
func Breadcrumbs(items []BreadcrumbItem) *vdom.VNode {
mods := []vdom.Mod{vdom.Attr("class", "flex flex-row items-center text-neutral-400 text-xs")}
for i, crumb := range items {
if i != len(items)-1 {
mods = append(mods, vdom.El("span", vdom.Attr("class", "flex items-center"),
vdom.El("a", vdom.Attr("href", crumb.URL),
vdom.Attr("class", "text-neutral-500 cursor-pointer no-underline hover:text-neutral-700 hover:underline"),
vdom.Text(crumb.DisplayText)),
Icon("chevron-right", 12, "mx-[0.15rem] opacity-50"),
))
} else {
mods = append(mods, vdom.El("span", vdom.Attr("class", "text-neutral-700 font-medium"), vdom.Text(crumb.DisplayText)))
}
}
return vdom.El("div", mods...)
}
// ManagerPageHeader is a title/description row with an optional action slot
// (marker classes kept for app CSS).
func ManagerPageHeader(title, description string, action *vdom.VNode) *vdom.VNode {
inner := vdom.El("div",
vdom.El("h2", vdom.Attr("class", "page-title"), vdom.Text(title)),
)
if description != "" {
inner.Children = append(inner.Children, vdom.El("p", vdom.Attr("class", "page-desc"), vdom.Text(description)))
}
mods := []vdom.Mod{vdom.Attr("class", "page-header"), inner}
if action != nil {
mods = append(mods, action)
}
return vdom.El("div", mods...)
}