Add landing page for kjol, documentation
This commit is contained in:
276
go/cmd/examples/go-wasm-web/app/docs.go
Normal file
276
go/cmd/examples/go-wasm-web/app/docs.go
Normal file
@@ -0,0 +1,276 @@
|
||||
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.`
|
||||
Reference in New Issue
Block a user