//go:build !(js && wasm) package app import ( "strconv" "time" . "kjol/vdom" ui "kjol/webui" ) // ServerCounter is a SERVER component — note it's written exactly like a client // component (same builders, signals, On handlers). The //gowasm:server directive // makes the build generate a client stub so calling ServerCounter() on the // frontend is identical to calling any component; the state and this render run // on the server (its chart SVG is drawn there by webui.ChartSVG), and clicks // round-trip over /rsc. // // The chart plots the counter value against the wall-clock time of each click // (milliseconds since the first click), so spacing clicks out spreads the data // points along the x-axis. Because the component is stateless on the server, the // click points live in a signal that round-trips with the rest of its state // (a plain slice would reset on every request). // //gowasm:server func ServerCounter() func() *VNode { count := NewSignal(0) points := NewSignal([]clickPoint{}) bump := func(delta int) { count.Set(count.Get() + delta) points.Set(append(points.Get(), clickPoint{T: time.Now().UnixMilli(), V: count.Get()})) } // No card of its own: the component draws bare content and lets the caller frame it. // The docs page already puts it in a demo panel, and a card inside a card gives you // two borders and two shadows around the same thing. return func() *VNode { return Div( Div(Attr("class", "flex items-center gap-2 mb-3"), Span(Attr("class", "text-ink-soft"), Text("Server counter: ")), Strong(Attr("class", "badge inline-flex items-center rounded-full bg-green-700 px-2.5 py-0.5 text-sm font-semibold text-white"), Text(strconv.Itoa(count.Get()))), Div(Attr("class", "ml-auto flex gap-1"), ui.Button(ui.ButtonProps{Color: ui.ButtonSecondary, Small: true, Text: "−", OnClick: func() { bump(-1) }}), ui.Button(ui.ButtonProps{Color: ui.ButtonGreen, Small: true, Text: "+", OnClick: func() { bump(1) }}), ), ), Div(Attr("class", "rounded-default border border-line bg-surface p-2 overflow-auto"), Raw(clickChartSVG(points.Get()))), ) } } // clickPoint records one click: its wall-clock time and the resulting counter // value. Exported fields so the signal's JSON snapshot round-trips it. type clickPoint struct { T int64 // click time, Unix milliseconds V int // counter value after the click } // clickChartSVG plots the counter value at each click as a line, drawn on the server by // webui.ChartSVG — the same chart geometry the client kit uses, rendered to a static SVG // string (no controller, no hover) because a server component's output is HTML. func clickChartSVG(points []clickPoint) string { if len(points) == 0 { return `Click + / − to plot the counter over each click.` } vals := make([]float64, len(points)) labels := make([]string, len(points)) for i, p := range points { vals[i] = float64(p.V) labels[i] = strconv.Itoa(i + 1) } return ui.ChartSVG(ui.ChartProps{ Kind: ui.ChartLine, Labels: labels, Series: []ui.ChartSeries{{Name: "Counter", Data: vals, Color: "var(--color-chart-4)"}}, Height: 240, NoTooltip: true, }) }