// Package app holds the go-wasm-web example's pages and components as // standalone, platform-neutral functions (SSR on the server, hydrate on the // client). UI is built from the kjol webui kit + Tailwind utility classes. // // Directives (processed by kjol/cmd/wasmgen at build time): // // //gowasm:page [static] [layout=] a route (static => SSR'd) // //gowasm:layout a func(Deps, *VNode) *VNode wrapper // //gowasm:server (see server_counter.go) a server component package app //go:generate go run kjol/cmd/wasmgen . import ( "strconv" . "kjol/vdom" ui "kjol/webui" ) // Deps are the client-only capabilities, injected so pages stay neutral. type Deps struct { Path func() string Navigate func(string) } func itoa(n int) string { return strconv.Itoa(n) } // Layout wraps a page's content with shared chrome (declared with //gowasm:layout, // selected per route via `layout=`; the generated LayoutFor dispatches by name). type Layout func(d Deps, content *VNode) *VNode // Shell renders the current route's page inside its declared layout. func Shell(d Deps, routes map[string]func() *VNode) *VNode { path := d.Path() var content *VNode if page := routes[path]; page != nil { content = page() } else { content = notFound(path) } return LayoutFor(d, path, content) } func notFound(path string) *VNode { return Div(Attr("class", "py-10"), H2(Attr("class", "text-xl font-semibold text-neutral-800 mb-2"), Text("Page not found")), P(Attr("class", "text-neutral-500"), Text("No route matches "+path+".")), ) } // --- layouts (Tailwind chrome) ------------------------------------------- //gowasm:layout public func PublicLayout(d Deps, content *VNode) *VNode { return Div( Nav(Attr("class", "site-nav sticky top-0 z-10 border-b border-neutral-200 bg-white"), Div(Attr("class", "mx-auto flex max-w-5xl items-center gap-2 px-4 py-3"), A(Attr("class", "text-lg font-semibold tracking-tight text-text-heading no-underline"), Attr("href", "/"), navigate(d, "/"), Text("gowasm")), Ul(Attr("class", "ml-auto flex items-center gap-1"), navItem(d, "/", "Home", false), navItem(d, "/about", "About", false), Li(Attr("class", "ml-2"), A(Attr("class", "inline-flex items-center gap-1 rounded-default bg-primary px-3 py-1.5 text-sm font-medium text-white no-underline hover:bg-primary-hover"), Attr("href", "/chart"), navigate(d, "/chart"), Text("Open app →"))), ))), Main(Attr("class", "mx-auto max-w-5xl px-4 py-8"), content, Footer(Attr("class", "mt-12 border-t border-neutral-200 pt-4 text-sm text-neutral-400"), Text("gowasm — pure-Go components compiled to WebAssembly.")), ), ) } //gowasm:layout app func AppLayout(d Deps, content *VNode) *VNode { return Div(Attr("class", "min-h-screen"), Nav(Attr("class", "app-nav border-b border-neutral-800 bg-neutral-900"), Div(Attr("class", "mx-auto flex max-w-5xl items-center gap-2 px-4 py-3"), A(Attr("class", "text-lg font-semibold tracking-tight text-text-on-dark no-underline"), Attr("href", "/chart"), navigate(d, "/chart"), Text("gowasm · app")), Ul(Attr("class", "ml-4 flex items-center gap-1"), navItem(d, "/chart", "Chart", true), navItem(d, "/server", "Server", true), navItem(d, "/kit", "UI Kit", true)), Ul(Attr("class", "ml-auto flex items-center"), navItem(d, "/", "Home", true)), )), Main(Attr("class", "mx-auto max-w-5xl px-4 py-8"), content), ) } // navItem is a nav link with an active state; dark switches to on-dark colors. func navItem(d Deps, path, label string, dark bool) *VNode { active := d.Path() == path var cls string switch { case dark && active: cls = "active rounded-default px-3 py-1.5 text-sm font-medium bg-white/10 text-white" case dark: cls = "rounded-default px-3 py-1.5 text-sm font-medium text-neutral-300 hover:bg-white/5 hover:text-white" case active: cls = "active rounded-default px-3 py-1.5 text-sm font-medium bg-neutral-100 text-neutral-900" default: cls = "rounded-default px-3 py-1.5 text-sm font-medium text-neutral-600 hover:bg-neutral-100 hover:text-neutral-900" } return Li(A(Attr("class", cls+" no-underline"), Attr("href", path), navigate(d, path), Text(label))) } // navigate intercepts a link click for client-side SPA navigation (Navigate is // nil on the server, so the anchor falls back to a normal navigation). func navigate(d Deps, path string) Mod { return OnEvent(EVENT_CLICK, func(e Event) { if d.Navigate != nil { e.PreventDefault() d.Navigate(path) } }) } // Counter is a presentational client component; state is owned by the caller. func Counter(label string, count *Signal[int]) *VNode { return Div(Attr("class", "counter flex items-center gap-3 rounded-default border border-neutral-200 bg-white px-4 py-3 shadow-xs"), Span(Attr("class", "font-medium text-neutral-700"), Text(label+": ")), Strong(Attr("class", "badge inline-flex min-w-8 items-center justify-center rounded-full bg-primary px-2.5 py-0.5 text-sm font-semibold text-white"), Text(itoa(count.Get()))), Div(Attr("class", "ml-auto flex gap-1"), ui.Button(ui.ButtonProps{Color: ui.ButtonSecondary, Small: true, Text: "−", OnClick: func() { count.Update(func(v int) int { return v - 1 }) }}), ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Small: true, Text: "+", OnClick: func() { count.Update(func(v int) int { return v + 1 }) }}), ), ) } //gowasm:page / static layout=public func HomePage(d Deps) func() *VNode { a := NewSignal(0) b := NewSignal(0) dark := NewSignal(false) return func() *VNode { return Div(Attr("class", "space-y-8"), Div( H2(Attr("class", "text-2xl font-semibold tracking-tight text-text-heading"), Text("Home — component composition")), P(Attr("class", "mt-1 text-neutral-500"), Text("Two counters; the total is derived across them. Server-rendered, then hydrated.")), ), Div(Attr("class", "grid gap-3 sm:grid-cols-2"), Counter("Apples", a), Counter("Bananas", b), ), ui.Alert(ui.AlertBlue, "", Span(Text("Combined total: ")), Strong(Attr("class", "font-semibold"), Text(itoa(a.Get()+b.Get()))), ), ui.Card("", ui.CardHeader("", Text("webui kit")), Div(Attr("class", "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, Outline: true, Text: "Danger"}), ui.Button(ui.ButtonProps{Color: ui.ButtonNeutral, Small: true, Icon: "check", Text: "Small"}), ui.Badge(ui.BadgeProps{Color: ui.BadgeGreen}, Text("active")), ui.Badge(ui.BadgeProps{Color: ui.BadgeAmber, Pill: true}, Text("pending")), ), Div(Attr("class", "mt-4"), ui.ToggleSwitch(dark.Get(), func(v bool) { dark.Set(v) }, "Dark mode", "Just a demo toggle", false, "")), ), ) } } //gowasm:page /about static layout=public func AboutPage(d Deps) func() *VNode { return func() *VNode { return Div(Attr("class", "space-y-6"), H2(Attr("class", "text-2xl font-semibold tracking-tight text-text-heading"), Text("About")), ui.Card("", P(Attr("class", "text-neutral-600 leading-relaxed"), Text("Components are standalone Go functions; calling a server component looks "+ "identical to calling a client one — the //gowasm:server directive and the "+ "build-time codegen wire up the round-trip. Static routes are SSR'd; the rest "+ "render on the client. The UI kit (kjol/webui) is a Go port of the Solid.js "+ "component kit, styled with Tailwind.")), ), ui.Alert(ui.AlertGreen, "Neutral + isomorphic", Text("This page's markup runs on the server (SSR) and hydrates on the client from the same Go code.")), ) } } //gowasm:page /server layout=app func ServerPage(d Deps) func() *VNode { // ServerCounter is a server component — calling it is just like calling any // component. On the client this resolves to a generated stub that mounts it // over /rsc; on the server it's the real function. counter := ServerCounter() return func() *VNode { return Div(Attr("class", "space-y-6"), H2(Attr("class", "text-2xl font-semibold tracking-tight text-text-heading"), Text("Server component")), P(Attr("class", "text-neutral-500"), Text("This counter runs on the server. Its state lives there; clicks round-trip "+ "and the returned render merges into the DOM.")), counter(), ) } }