package app import ( . "kjol/vdom" ui "kjol/webui" ) // Documentation chrome. // // The app routes are the framework's documentation, so they are built from one small // vocabulary rather than each page inventing its own headings and spacing: a page has a // title and a lede, then sections; a section explains something in prose, shows the Go // that does it, and then RUNS that Go on the page you are reading. The last part is the // point — a docs page for a UI framework that only shows screenshots of its components // is a docs page that cannot tell you when it has gone stale. // docsNav is the sidebar: the sections of the documentation, in reading order. // // It is data, not markup, because it is consumed twice — once by the sidebar and once // by the /docs index, which lists the same pages as cards. Two hand-written copies of a // nav is two copies to forget to update. type docsGroup struct { Title string Items []docsItem } type docsItem struct { Path string Label string Blurb string // shown on the /docs index; too long for the sidebar Icon string } func docsNav() []docsGroup { return []docsGroup{{ Title: "Introduction", Items: []docsItem{ {Path: "/docs", Label: "Overview", Icon: "book-open", Blurb: "What Kjol Web is, how a page becomes a WebAssembly binary, and what runs where."}, }, }, { Title: "Rendering", Items: []docsItem{ {Path: "/chart", Label: "SSR & hydration", Icon: "chart-column", Blurb: "The same Go renders HTML on the server and takes over in the browser. Charts, server-drawn as SVG."}, {Path: "/server", Label: "Server components", Icon: "server", Blurb: "Components whose state and code stay on the server. Calling one looks like calling any other."}, {Path: "/data", Label: "Data fetching", Icon: "cloud-arrow-down", Blurb: "gob to your own server (Go types end to end, no JSON) and JSON to a third-party API."}, }, }, { Title: "Components", Items: []docsItem{ {Path: "/kit", Label: "UI kit", Icon: "squares", Blurb: "Buttons, forms, tabs, alerts, cards — the kjol/webui components, written in Go."}, {Path: "/overlays", Label: "Overlays", Icon: "layers", Blurb: "Tooltips, popovers, menus, modals: measured against the real viewport, flipped and shifted to fit."}, {Path: "/table", Label: "AutoTable", Icon: "table", Blurb: "Filtering, sorting, column management, calculated columns, CSV and PDF export."}, }, }} } // ---- page scaffolding --------------------------------------------------- // docPage is the frame every documentation page shares: an eyebrow, a title, a lede, // and then its sections. func docPage(eyebrow, title, lede string, sections ...*VNode) *VNode { mods := []Mod{Attr("class", "pb-16")} mods = append(mods, Div(Attr("class", "border-b border-line pb-6"), P(Attr("class", "text-xs font-semibold uppercase tracking-widest text-accent"), Text(eyebrow)), H1(Attr("class", "mt-2 text-3xl font-semibold tracking-tight text-text-heading"), Text(title)), P(Attr("class", "mt-3 max-w-3xl text-ink-muted leading-relaxed"), Text(lede)), ), ) for _, s := range sections { mods = append(mods, s) } return Div(mods...) } // docSection is a titled slab of the page. The id is what the "on this page" links and // the tour steps anchor to. func docSection(id, title string, body ...*VNode) *VNode { mods := []Mod{Attr("id", id), Attr("class", "mt-12 scroll-mt-24")} mods = append(mods, H2(Attr("class", "text-xl font-semibold tracking-tight text-text-heading"), Text(title)), ) for _, b := range body { mods = append(mods, b) } return El("section", mods...) } // prose is a paragraph of explanation. Constrained to a reading measure: a line of body // text that runs the full width of a wide screen is genuinely harder to read, and the // demos beside it are allowed to be as wide as they like. func prose(text string) *VNode { return P(Attr("class", "mt-3 max-w-3xl text-ink-soft leading-relaxed"), Text(text)) } // ---- code --------------------------------------------------------------- // code is a Go snippet, captioned with where it comes from. // // The caption is a real file path in this example, not a decoration: every snippet on // these pages is copied from code that actually runs, and saying where from is what // lets you go and check. func code(caption, src string) *VNode { return codeLang(caption, "Go", src) } // codeLang is code() for a block that is not Go — a shell session, a formula. The label // in the corner says what you are looking at, and a shell command labelled "Go" is worse // than no label at all. // // Go blocks are syntax-highlighted (webui.HighlightGo); the others are shown verbatim. // A shell transcript put through a Go lexer comes out with `serving` painted as an // identifier and quotes as string literals — highlighting the wrong language is more // distracting than not highlighting at all. func codeLang(caption, lang, src string) *VNode { var body *VNode if lang == "Go" { // Raw, not Text: HighlightGo returns HTML. It escapes every run of source on the // way out, so the snippets that contain markup stay inert. body = El("code", Raw(ui.HighlightGo(src))) } else { body = El("code", Text(src)) } return Div(Attr("class", "mt-4 overflow-hidden rounded-default border border-neutral-800 bg-neutral-900"), Div(Attr("class", "flex items-center gap-2 border-b border-neutral-800 px-4 py-2"), Span(Attr("class", "text-xs font-medium text-ink-faint font-mono"), Text(caption)), Span(Attr("class", "ml-auto rounded-full bg-white/5 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-ink-faint"), Text(lang)), ), Pre(Attr("class", "overflow-x-auto px-4 py-3 text-[13px] leading-relaxed text-neutral-100 font-mono"), body), ) } // ---- demos -------------------------------------------------------------- // demo is the panel a section's example sits in, captioned with what it is showing. func demo(title string, body ...*VNode) *VNode { mods := []Mod{Attr("class", "mt-4 rounded-default border border-line bg-surface shadow-xs")} mods = append(mods, Div(Attr("class", "border-b border-line px-4 py-2"), Span(Attr("class", "text-xs text-ink-muted"), Text(title)), ), ) inner := []Mod{Attr("class", "p-4")} for _, b := range body { inner = append(inner, b) } mods = append(mods, Div(inner...)) return Div(mods...) } // note is an aside — a caveat, a gotcha, the reason something is the way it is. func note(title, body string) *VNode { return Div(Attr("class", "mt-4 max-w-3xl rounded-default border border-primary-border bg-primary-subtle px-4 py-3"), P(Attr("class", "text-sm font-semibold text-text-heading"), Text(title)), P(Attr("class", "mt-1 text-sm text-ink-soft leading-relaxed"), Text(body)), ) } // ---- reference tables --------------------------------------------------- type apiRow struct{ Name, Desc string } // apiTable is the reference half of a page: the names, and what each one does. func apiTable(rows ...apiRow) *VNode { body := make([]*VNode, 0, len(rows)) for _, r := range rows { body = append(body, El("tr", Attr("class", "border-t border-line"), El("td", Attr("class", "py-2 pr-4 align-top whitespace-nowrap"), El("code", Attr("class", "rounded bg-surface-raised px-1.5 py-0.5 text-[13px] font-mono text-ink"), Text(r.Name))), El("td", Attr("class", "py-2 text-sm text-ink-soft leading-relaxed"), Text(r.Desc)), )) } rowMods := []Mod{} for _, b := range body { rowMods = append(rowMods, b) } return Div(Attr("class", "mt-4 max-w-5xl overflow-x-auto"), El("table", Attr("class", "w-full border-collapse text-left"), Tbody(rowMods...), ), ) } // ---- the docs index ----------------------------------------------------- //gowasm:page /docs static layout=app func DocsPage(d Deps) func() *VNode { return func() *VNode { var groups []*VNode for _, g := range docsNav() { grid := []Mod{Attr("class", "mt-3 grid gap-3 sm:grid-cols-2")} for _, it := range g.Items { if it.Path == "/docs" { continue // don't list this page on itself } grid = append(grid, docsCard(d, it)) } if len(grid) == 1 { continue // the group held nothing but this page } groups = append(groups, Div(Attr("class", "mt-10"), H2(Attr("class", "text-sm font-semibold uppercase tracking-widest text-ink-faint"), Text(g.Title)), Div(grid...), ), ) } return docPage("Introduction", "Overview", "Kjol Web is kjol's Go→WebAssembly UI engine. You write components as ordinary Go functions "+ "returning a virtual DOM; the server renders them to HTML and the same code hydrates them "+ "in the browser. There is no JavaScript build step, and the engine depends on nothing "+ "outside the standard library.", docSection("what-runs-where", "What runs where", prose("A page is Go, compiled twice. On the server it renders to an HTML string, so the first "+ "paint needs no WebAssembly at all. In the browser the same functions run again, adopt the "+ "markup that is already there, and from then on a signal write re-renders and reconciles into "+ "the live DOM."), code("app/pages.go", ssrSnippet), note("The host API is dual-build", "Components measure the DOM — a tooltip has to know where its trigger is. Those calls are "+ "real under js/wasm and no-ops natively, which is what lets one component both SSR and "+ "position itself, without a branch in the component."), ), appendNodes(Div(Attr("class", "mt-14 border-t border-line pt-2")), groups...), ) } } func docsCard(d Deps, it docsItem) *VNode { return A( Attr("class", "group block rounded-default border border-line bg-surface p-4 no-underline shadow-xs transition hover:border-primary-border hover:shadow-sm"), Attr("href", it.Path), navigate(d, it.Path), Div(Attr("class", "flex items-center gap-2"), Span(Attr("class", "inline-flex h-7 w-7 items-center justify-center rounded-default bg-primary-subtle text-accent"), ui.IconInline(it.Icon, 14, "")), Span(Attr("class", "font-semibold text-text-heading"), Text(it.Label)), Span(Attr("class", "ml-auto text-ink-faint transition group-hover:text-accent"), ui.IconInline("arrow-right", 12, "")), ), P(Attr("class", "mt-2 text-sm text-ink-muted leading-relaxed"), Text(it.Blurb)), ) } // appendNodes adds children to a node after the fact — the shape a few of these pages // need, where the section list is computed rather than written out. func appendNodes(parent *VNode, children ...*VNode) *VNode { parent.Children = append(parent.Children, children...) return parent } const ssrSnippet = `//gowasm:page /docs static layout=app func DocsPage(d Deps) func() *VNode { count := NewSignal(0) // state lives in the closure return func() *VNode { // the render: pure, called again on every change return Div(Attr("class", "space-y-2"), H1(Text("Overview")), Button( Attr("class", "btn"), On(EVENT_CLICK, func() { count.Set(count.Get() + 1) }), Text("clicked "+itoa(count.Get())+" times"), ), ) } } // static => the server pre-renders this route to HTML. // The same function then hydrates it in the browser.`