initial port of the UI kit
This commit is contained in:
186
go/cmd/examples/go-wasm-web/app/kit.go
Normal file
186
go/cmd/examples/go-wasm-web/app/kit.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
. "kjol/vdom"
|
||||
ui "kjol/webui"
|
||||
)
|
||||
|
||||
// row is a flex/grid container helper (appends *VNode children as Mods).
|
||||
func row(class string, children ...*VNode) *VNode {
|
||||
mods := []Mod{Attr("class", class)}
|
||||
for _, c := range children {
|
||||
mods = append(mods, c)
|
||||
}
|
||||
return Div(mods...)
|
||||
}
|
||||
|
||||
// kitSection wraps a labeled demo block in a card.
|
||||
func kitSection(title string, body ...*VNode) *VNode {
|
||||
return ui.Card("",
|
||||
ui.CardHeader("", Text(title)),
|
||||
row("flex flex-col gap-4", body...),
|
||||
)
|
||||
}
|
||||
|
||||
func ptRow(name, plan string, status *VNode) *VNode {
|
||||
td := func(cls string, c *VNode) *VNode { return El("td", Attr("class", "px-3 py-2 text-sm "+cls), c) }
|
||||
return El("tr",
|
||||
td("text-neutral-800", Text(name)),
|
||||
td("text-neutral-600", Text(plan)),
|
||||
El("td", Attr("class", "px-3 py-2 text-sm text-right"), status),
|
||||
)
|
||||
}
|
||||
|
||||
//gowasm:page /kit layout=app
|
||||
func KitPage(d Deps) func() *VNode {
|
||||
// Interactive demos own their state via signals (a write re-renders).
|
||||
tab := NewSignal(0)
|
||||
acc := NewSignal(0)
|
||||
notify := NewSignal(true)
|
||||
span := NewSignal("week")
|
||||
modalOpen := NewSignal(false)
|
||||
menuOpen := NewSignal(false)
|
||||
name := NewSignal("")
|
||||
email := NewSignal("")
|
||||
plan := NewSignal("pro")
|
||||
|
||||
return func() *VNode {
|
||||
return Div(Attr("class", "space-y-8"),
|
||||
Div(
|
||||
H2(Attr("class", "text-2xl font-semibold tracking-tight text-text-heading"), Text("UI Kit")),
|
||||
P(Attr("class", "mt-1 text-neutral-500"),
|
||||
Text("The kjol/webui components, ported from the Solid.js kit and styled with Tailwind. "+
|
||||
"Interactive components are driven by signals; overlays and floating elements render "+
|
||||
"in their static form (see the note at the bottom).")),
|
||||
),
|
||||
|
||||
kitSection("Buttons",
|
||||
row("flex flex-wrap items-center gap-2",
|
||||
ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Text: "Primary"}),
|
||||
ui.Button(ui.ButtonProps{Color: ui.ButtonGreen, Text: "Green"}),
|
||||
ui.Button(ui.ButtonProps{Color: ui.ButtonRed, Text: "Red"}),
|
||||
ui.Button(ui.ButtonProps{Color: ui.ButtonBlue, Text: "Blue"}),
|
||||
ui.Button(ui.ButtonProps{Color: ui.ButtonNeutral, Text: "Neutral"}),
|
||||
),
|
||||
row("flex flex-wrap items-center gap-2",
|
||||
ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Outline: true, Text: "Outline"}),
|
||||
ui.Button(ui.ButtonProps{Color: ui.ButtonRed, Outline: true, Text: "Danger"}),
|
||||
ui.Button(ui.ButtonProps{Color: ui.ButtonGreen, Small: true, Icon: "check", Text: "Small + icon"}),
|
||||
ui.Button(ui.ButtonProps{Color: ui.ButtonNeutral, Icon: "plus"}),
|
||||
ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Text: "Disabled", Disabled: true}),
|
||||
),
|
||||
),
|
||||
|
||||
kitSection("Badges",
|
||||
row("flex flex-wrap items-center gap-2",
|
||||
ui.Badge(ui.BadgeProps{Color: ui.BadgeGreen}, Text("active")),
|
||||
ui.Badge(ui.BadgeProps{Color: ui.BadgeRed}, Text("failed")),
|
||||
ui.Badge(ui.BadgeProps{Color: ui.BadgeBlue}, Text("info")),
|
||||
ui.Badge(ui.BadgeProps{Color: ui.BadgeAmber, Pill: true}, Text("pending")),
|
||||
ui.Badge(ui.BadgeProps{Color: ui.BadgeNeutral}, Text("default")),
|
||||
ui.Badge(ui.BadgeProps{Color: ui.BadgeMuted}, Text("muted")),
|
||||
),
|
||||
),
|
||||
|
||||
kitSection("Alerts",
|
||||
ui.Alert(ui.AlertBlue, "Heads up", Text("An informational message with a header.")),
|
||||
ui.Alert(ui.AlertGreen, "", Text("A success alert without a header.")),
|
||||
ui.Alert(ui.AlertYellow, "Warning", Text("Something needs your attention.")),
|
||||
ui.Alert(ui.AlertRed, "Error", Text("Something went wrong.")),
|
||||
),
|
||||
|
||||
kitSection("Toggles & segmented control",
|
||||
ui.ToggleSwitch(notify.Get(), func(v bool) { notify.Set(v) }, "Email notifications", "Send me product updates", false, ""),
|
||||
ui.SegmentedButtons([]ui.SegmentedButtonOption{
|
||||
{Value: "day", Label: "Day"},
|
||||
{Value: "week", Label: "Week"},
|
||||
{Value: "month", Label: "Month"},
|
||||
}, span.Get(), func(v string) { span.Set(v) }, false, "max-w-xs"),
|
||||
),
|
||||
|
||||
kitSection("Tabs",
|
||||
ui.TabGroup(ui.TabGroupProps{
|
||||
Items: []ui.TabItem{
|
||||
{Title: "Overview", Content: P(Attr("class", "pt-3 text-sm text-neutral-600"), Text("The overview panel."))},
|
||||
{Title: "Details", Content: P(Attr("class", "pt-3 text-sm text-neutral-600"), Text("The details panel."))},
|
||||
{Title: "Activity", Badge: 3, Content: P(Attr("class", "pt-3 text-sm text-neutral-600"), Text("The activity panel (3 new)."))},
|
||||
},
|
||||
ActiveIndex: tab.Get(),
|
||||
OnTabChange: func(i int) { tab.Set(i) },
|
||||
}),
|
||||
),
|
||||
|
||||
kitSection("Accordion",
|
||||
ui.SingleAccordion([]ui.AccordionItemData{
|
||||
{Title: "What is gowasm?", Content: P(Attr("class", "text-sm text-neutral-600"), Text("A tiny Go→WebAssembly UI engine."))},
|
||||
{Title: "Is it isomorphic?", Content: P(Attr("class", "text-sm text-neutral-600"), Text("Yes — the same Go renders on the server (SSR) and hydrates on the client."))},
|
||||
{Title: "How is it styled?", Content: P(Attr("class", "text-sm text-neutral-600"), Text("Tailwind utility classes, compiled by kjol's native Tailwind engine."))},
|
||||
}, acc.Get(), func(i int) { acc.Set(i) }),
|
||||
),
|
||||
|
||||
kitSection("Forms",
|
||||
row("grid gap-4 sm:grid-cols-3",
|
||||
row("flex flex-col gap-1", ui.FormLabel(ui.FormLabelProps{}, Text("Name")),
|
||||
ui.FormInput(ui.FormInputProps{Value: name.Get(), Placeholder: "Ada Lovelace", OnInput: func(v string) { name.Set(v) }})),
|
||||
row("flex flex-col gap-1", ui.FormLabel(ui.FormLabelProps{}, Text("Email")),
|
||||
ui.FormEmailInput(ui.FormInputProps{Value: email.Get(), Placeholder: "ada@example.com", OnInput: func(v string) { email.Set(v) }}, true)),
|
||||
row("flex flex-col gap-1", ui.FormLabel(ui.FormLabelProps{}, Text("Plan")),
|
||||
ui.FormSelect(ui.FormSelectProps{Value: plan.Get(), OnChange: func(v string) { plan.Set(v) }},
|
||||
ui.FormOption("free", "Free", false),
|
||||
ui.FormOption("pro", "Pro", false),
|
||||
ui.FormOption("enterprise", "Enterprise", false))),
|
||||
),
|
||||
P(Attr("class", "text-xs text-neutral-500"),
|
||||
Text("Live: name=\""+name.Get()+"\" email=\""+email.Get()+"\" plan=\""+plan.Get()+"\"")),
|
||||
),
|
||||
|
||||
kitSection("Table",
|
||||
ui.PrettyTable(
|
||||
[]ui.PrettyTableColumn{
|
||||
{DisplayName: "Name"},
|
||||
{DisplayName: "Plan"},
|
||||
{DisplayName: "Status", DisplayPosition: ui.PrettyTableColRight},
|
||||
},
|
||||
ui.PrettyTableOptions{Hover: true, Alternate: true, SurroundingBorder: true, HeaderBorderY: true},
|
||||
ptRow("Ada Lovelace", "Pro", ui.Badge(ui.BadgeProps{Color: ui.BadgeGreen}, Text("active"))),
|
||||
ptRow("Alan Turing", "Free", ui.Badge(ui.BadgeProps{Color: ui.BadgeNeutral}, Text("trial"))),
|
||||
ptRow("Grace Hopper", "Enterprise", ui.Badge(ui.BadgeProps{Color: ui.BadgeBlue}, Text("invited"))),
|
||||
),
|
||||
),
|
||||
|
||||
kitSection("Overlays (interactive)",
|
||||
row("flex flex-wrap items-center gap-4",
|
||||
// Modal, toggled by a signal.
|
||||
ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Text: "Open modal", OnClick: func() { modalOpen.Set(true) }}),
|
||||
// Menu, toggled by a signal.
|
||||
ui.Menu("relative inline-block",
|
||||
ui.MenuTrigger(func() { menuOpen.Set(!menuOpen.Get()) }, "",
|
||||
ui.Button(ui.ButtonProps{Color: ui.ButtonWhite, Text: "Menu ▾"})),
|
||||
ui.MenuContent(menuOpen.Get(), ui.MenuPlacementBottomStart, "",
|
||||
ui.MenuItem(ui.MenuItemProps{Icon: "check", OnClick: func() { menuOpen.Set(false) }}, Text("Profile")),
|
||||
ui.MenuItem(ui.MenuItemProps{OnClick: func() { menuOpen.Set(false) }}, Text("Settings")),
|
||||
ui.MenuDivider(""),
|
||||
ui.MenuItem(ui.MenuItemProps{OnClick: func() { menuOpen.Set(false) }}, Text("Sign out")),
|
||||
),
|
||||
),
|
||||
// Tooltip (pure CSS hover).
|
||||
ui.HoverTooltip(Span(Text("A CSS-only tooltip")), "top", "",
|
||||
ui.Button(ui.ButtonProps{Color: ui.ButtonNeutral, Outline: true, Text: "Hover me"})),
|
||||
),
|
||||
ui.Modal(ui.ModalProps{
|
||||
IsOpen: modalOpen.Get(),
|
||||
OnClose: func() { modalOpen.Set(false) },
|
||||
Size: ui.ModalMedium,
|
||||
Header: H3(Attr("class", "text-lg font-semibold text-text-heading"), Text("Example modal")),
|
||||
},
|
||||
P(Attr("class", "text-neutral-600"), Text("This modal is toggled by a signal; the backdrop and close button round-trip through OnClose.")),
|
||||
),
|
||||
),
|
||||
|
||||
ui.Alert(ui.AlertGray, "About this page",
|
||||
Text("Floating/positioned components (menus, tooltips, modals, dropdowns) are ported as "+
|
||||
"structure + Tailwind + signal/event wiring — the neutral runtime has no floating-ui, "+
|
||||
"portals, or element measurement, so positioning is approximated with static classes.")),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user