Update kjol website with C documentation

This commit is contained in:
2026-07-14 13:05:12 -04:00
parent 02a6dc6c48
commit 7d7b7354df
66 changed files with 23884 additions and 2551 deletions

View File

@@ -17,10 +17,6 @@ import (
. "kjol/vdom"
// The host API is dual-build — real under js/wasm, no-ops natively — so neutral page
// code can measure the browser and still server-render. The landing page uses it for
// exactly one thing: reading the clock when hydration commits.
"kjol/wasmruntime"
ui "kjol/webui"
)
@@ -67,16 +63,18 @@ func notFound(path string) *VNode {
// wordmark is the brand lockup, shared by both layouts so they cannot drift.
//
// The boat is the point of the name: kjol is Norwegian for KEEL — the spine of a hull,
// The boat is the point of the name: kjøl is Norwegian for KEEL — the spine of a hull,
// the thing every other part is built onto. Which is what this library is meant to be
// for the applications that share it.
func wordmark(d Deps, href string) *VNode {
// The lockup names the LAYER you are standing in, not the site. On the front page
// that is kjol itself; inside /wasm it is Kjol Wasm Web. A wordmark that says the
// same thing everywhere is one more thing the reader has to keep track of himself.
name, sub := "kjol", "a shared base layer"
// that is Kjøl itself; inside /wasm it is Kjøl Wasm Web; inside /c it is Kjøl C —
// Wordmark, not Name, because up here "C" alone names a language rather than the thing
// you are reading. A wordmark that says the same thing everywhere is one more thing the
// reader has to keep track of himself.
name, sub := "Kjøl", "a shared base layer"
if l := CurrentLayer(d.Path()); l != nil {
name, sub = l.Name, "Go + WebAssembly"
name, sub = l.Wordmark(), l.Sub
}
return A(Attr("class", "flex items-center gap-2.5 no-underline"), Attr("href", href), navigate(d, href),
@@ -103,22 +101,39 @@ func PublicLayout(d Deps, content *VNode) *VNode {
// read aloud.
Div(Attr("class", "pointer-events-none fixed inset-0 -z-10 bg-grid grid-fade"), Attr("aria-hidden", "true")),
// The nav, the content and the footer are ONE column, and the way to get that is for
// all three to be built the same way: gutter on the outside, measure on the inside.
//
// <div class="px-4"> <div class="mx-auto max-w-3xl"> …
//
// This used to be `mx-auto max-w-3xl px-4` on the nav's inner div — measure and gutter
// on the SAME element. On a wide screen the gutter has nothing to do (the centring has
// already pushed the box in much further), so all it did was inset the nav's contents
// by another 16px: the wordmark sat a finger's width to the right of the headline
// underneath it. Close enough to look like a mistake, far enough to see.
//
// The footer was worse — it was max-w-2xl, a different measure entirely.
Nav(Attr("class", "site-nav border-b border-line"),
Div(Attr("class", "mx-auto flex max-w-3xl items-center gap-2 px-4 py-4"),
wordmark(d, "/"),
Div(Attr("class", "ml-auto flex items-center gap-1"),
layersMenu(d),
Ul(Attr("class", "flex items-center gap-1"),
navItem(d, "/about", "About", false),
Li(Attr("class", "ml-1"), Theme.ThemeToggle(ui.ThemeToggleProps{Small: true})),
),
))),
Div(Attr("class", "px-4"),
Div(Attr("class", "mx-auto flex max-w-3xl items-center gap-2 py-4"),
wordmark(d, "/"),
Div(Attr("class", "ml-auto flex items-center gap-1"),
layersMenu(d),
compositionsMenu(d),
Ul(Attr("class", "flex items-center gap-1"),
navItem(d, "/about", "About", false),
Li(Attr("class", "ml-1"), Theme.ThemeToggle(ui.ThemeToggleProps{Small: true})),
),
)))),
Main(Attr("class", "px-4 py-14"), content),
Footer(Attr("class", "mx-auto max-w-2xl px-4 pb-14"),
P(Attr("class", "text-sm text-ink-faint"),
Text("kjol is a shared base layer, factored out of several applications so they stay in sync. It is Norwegian for keel.")),
Footer(Attr("class", "px-4 pb-14"),
Div(Attr("class", "mx-auto max-w-3xl"),
P(Attr("class", "text-sm text-ink-faint"),
Text("Kjøl is a shared base layer, factored out of several applications so they stay in "+
"sync. It is Norwegian for keel.")),
),
),
ui.ModalHost(),
)
@@ -127,7 +142,7 @@ func PublicLayout(d Deps, content *VNode) *VNode {
// wideRoutes get a roomier container. A table with a dozen columns, a drag handle
// and three calculated columns has no business being squeezed into a reading-width
// column; prose pages still are.
var wideRoutes = map[string]bool{"/wasm/table": true}
var wideRoutes = map[string]bool{"/wasm/components": true}
// AppLayout is the DOCUMENTATION shell: a sidebar of sections on the left, the page on
// the right. The app routes are the framework's docs — each one explains a capability,
@@ -155,6 +170,7 @@ func AppLayout(d Deps, content *VNode) *VNode {
Span(Attr("class", "rounded-full border border-line px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wider text-ink-faint"), Text("Docs")),
Div(Attr("class", "ml-auto flex items-center gap-2"),
layersMenu(d),
compositionsMenu(d),
Ul(Attr("class", "flex items-center gap-2"),
navItem(d, "/", "Home", false),
Li(Theme.ThemeToggle(ui.ThemeToggleProps{Small: true})),
@@ -179,9 +195,22 @@ func AppLayout(d Deps, content *VNode) *VNode {
// docsSidebar is the section list. Sticky, so it stays put while a long page scrolls —
// on a documentation site the nav is how you know where you are, and a nav that scrolls
// away leaves you nowhere.
// sidebarNav is the sidebar's contents, which depend on WHICH LAYER you are reading.
//
// AppLayout is shared by every documentation page in this binary, and those pages are no
// longer all about the same thing: /wasm/* documents the Go→WebAssembly engine, /c
// documents the C base layer. A sidebar listing the engine's chapters while you are
// reading about arenas would be worse than no sidebar at all.
func sidebarNav(path string) []docsGroup {
if path == "/c" || strings.HasPrefix(path, "/c/") {
return cNav()
}
return docsNav()
}
func docsSidebar(d Deps) *VNode {
mods := []Mod{Attr("class", "sticky top-[3.75rem] hidden h-[calc(100vh-3.75rem)] w-56 shrink-0 overflow-y-auto py-10 lg:block")}
for _, g := range docsNav() {
for _, g := range sidebarNav(d.Path()) {
items := []Mod{Attr("class", "mt-2 space-y-0.5")}
for _, it := range g.Items {
items = append(items, Li(sidebarLink(d, it)))
@@ -197,13 +226,30 @@ func docsSidebar(d Deps) *VNode {
}
func sidebarLink(d Deps, it docsItem) *VNode {
base, frag, isAnchor := strings.Cut(it.Path, "#")
cls := "flex items-center gap-2 rounded-default px-2 py-1.5 text-sm no-underline text-ink-soft hover:bg-surface-raised hover:text-ink"
iconCls := "text-ink-faint"
if d.Path() == it.Path {
// A section link is NEVER "active", and that is deliberate. It cannot be: it would
// have to know which section you had scrolled to, which means measuring all fifteen of
// them on every scroll frame, and the only way to act on the answer is a signal write
// — which re-renders this entire page. Sixty times a second, to move a highlight.
//
// (Marking them active by PAGE instead lights up all fifteen at once, which is worse
// than no highlight: it tells you nothing and looks broken.)
active := !isAnchor && d.Path() == it.Path
if active {
cls = "active flex items-center gap-2 rounded-default px-2 py-1.5 text-sm no-underline bg-primary-subtle font-medium text-accent"
iconCls = "text-accent"
}
return A(Attr("class", cls), Attr("href", it.Path), navigate(d, it.Path),
click := navigate(d, it.Path)
if isAnchor {
click = navigateAnchor(d, base, frag)
}
return A(Attr("class", cls), Attr("href", it.Path), click,
ui.IconInline(it.Icon, 14, iconCls),
Text(it.Label),
)
@@ -251,122 +297,56 @@ func Counter(label string, count *Signal[int]) *VNode {
// ---- landing ------------------------------------------------------------
// The landing page is one narrow column of plain text, a demo, and a list.
// The landing page is a column of plain text and two lists.
//
// It used to be a framework marketing page: an oversized headline, a hero glow, feature
// cards in a grid, numbered chapters, a call to action repeated at both ends. All of it
// was arguing. None of it was showing. A library this small does not need to argue — it
// needs to say what it is, show that it works, and get out of the way, and a reader who
// wants to be convinced can click into the docs and find every page running the code it
// documents.
// It used to carry the Wasm Web engine's own highlights: the two-runtime demo, a list of
// SSR/hydration/server-component features, the build transcript. All of it was true, and
// none of it belonged HERE — the front page is kjøl's, and kjøl is not the Go/WebAssembly
// engine any more than it is the C arena allocator. A reader landing on it should learn
// what the thing IS, not be pitched one of its five parts.
//
// What survives is the part that could not be faked: the same Go function rendered twice
// at once, as live DOM and as the HTML string the server sends.
// So the demo moved to /wasm, where it is the first thing that section shows, and the
// front page says what is actually true of the whole: here are the languages, here are the
// frameworks assembled out of them, go and read one.
//
//gowasm:page / static layout=public
func HomePage(d Deps) func() *VNode {
clicks := NewSignal(0)
// The one measurement on the page: performance.now() when the client's first render
// commits. Zero until then — which is what the SERVER renders, and what the client
// renders on its first pass, so the two agree and hydration stays clean.
hydratedAt := NewSignal(0.0)
wasmruntime.AfterRender(func() {
if hydratedAt.Get() == 0 {
hydratedAt.Set(wasmruntime.Now())
}
})
// demoTree is called TWICE per render below — once for the DOM, once for the HTML.
// That is the point: the two panes cannot drift, because there is only one of them.
demoTree := func() *VNode {
return Div(Attr("class", "flex items-center gap-3"),
ui.Button(ui.ButtonProps{
Color: ui.ButtonPrimary, Text: "Click me",
OnClick: func() { clicks.Set(clicks.Get() + 1) },
}),
Span(Attr("class", "text-ink-soft"), Text("clicked "+itoa(clicks.Get())+" times")),
)
}
return func() *VNode {
markup := RenderHTML(demoTree())
return Div(Attr("class", "mx-auto max-w-3xl"),
H1(Attr("class", "text-3xl font-semibold tracking-tight text-text-heading"),
Text("kjol")),
Text("Kjøl")),
P(Attr("class", "mt-3 leading-relaxed text-ink-soft"),
Text("A shared base layer, factored out of several applications so they stay in sync. "+
"Kjol is Norwegian for KEEL: the spine of a hull, the thing every other part is built onto.")),
"Kjøl is Norwegian for KEEL: the spine of a hull, the thing every other part is built onto.")),
P(Attr("class", "mt-3 leading-relaxed text-ink-soft"),
Text("It is not one library. It is a stack of them, in several languages, and each one is "+
"documented here.")),
Text("It is not one library. It is a set of them, in several languages, and a couple of "+
"frameworks assembled out of those. Each one is documented here, and every page of that "+
"documentation runs the code it documents.")),
// ---- the layers ----
//
// The layers are the site. Everything else on this page is evidence that they
// work; this is the part you are meant to click.
H2(Attr("class", "mt-12 text-lg font-semibold text-text-heading"), Text("The layers")),
layersGrid(d),
// ---- the demonstration ----
//
// This survives from the old landing page because it is the one thing on the site
// that cannot be faked: the same Go function, rendered twice at once, as live DOM
// and as the HTML string the server sent.
H2(Attr("class", "mt-12 text-lg font-semibold text-text-heading"), Text("One function, two runtimes")),
// ---- layers: the languages ----
H2(Attr("class", "mt-12 text-lg font-semibold text-text-heading"), Text("Layers")),
P(Attr("class", "mt-2 leading-relaxed text-ink-soft"),
Text("Below is a single Go function, shown twice. On the left it has been reconciled into "+
"the DOM and you can use it. On the right is the HTML the same function produces when "+
"the server renders it — the markup that reached your browser before any WebAssembly "+
"had loaded. Click the button; both move.")),
Text("What Kjøl is written in, and what it gives you in each. A layer is a directory of "+
"code you can use on its own — the Go base does not know the C one exists.")),
layerGrid(Languages()),
Div(Attr("class", "mt-5 border border-line sm:grid sm:grid-cols-2"),
Div(Attr("class", "border-b border-line sm:border-b-0 sm:border-r"),
paneLabel("in your browser"),
Div(Attr("class", "px-4 py-8"), demoTree()),
),
Div(
paneLabel(itoa(len(markup))+" bytes of HTML"),
Pre(Attr("class", "whitespace-pre-wrap px-4 py-4 font-mono text-[12px] leading-relaxed text-ink-muted"),
El("code", Text(prettyHTML(markup))),
),
),
),
P(Attr("class", "mt-3 text-sm leading-relaxed text-ink-muted"),
Text("The right pane is not a picture of the source. It is vdom.RenderHTML, called on the "+
"very tree the left pane is showing, recomputed on every click.")),
// ---- what is in it ----
H2(Attr("class", "mt-12 text-lg font-semibold text-text-heading"), Text("What is in it")),
Ul(Attr("class", "mt-3 space-y-1.5 leading-relaxed text-ink-soft"),
item("Server-side rendering and client hydration, from one codebase."),
item("Server components: mark a function and its code and state stay on the server."),
item("A component kit — forms, tabs, modals, tooltips, toasts — with dark mode."),
item("A table with filtering, sorting, column management, formulas, and CSV and PDF export."),
item("Tailwind, compiled by a Go program that reads your Go."),
),
// ---- building ----
H2(Attr("class", "mt-12 text-lg font-semibold text-text-heading"), Text("Building it")),
// ---- compositions: the frameworks ----
H2(Attr("class", "mt-12 text-lg font-semibold text-text-heading"), Text("Compositions")),
P(Attr("class", "mt-2 leading-relaxed text-ink-soft"),
Text("Two commands. The first produced the page you are reading; the second serves it and "+
"rebuilds on save.")),
codeLang("terminal", "sh", buildTranscript),
Text("What the layers become when they are assembled into something that does a job. A "+
"composition is not another language: Kjøl Wasm Web is Go all the way down, and Kjøl JS "+
"Web is TypeScript compiled by a Go toolchain. These are the two you can click into.")),
layerGrid(Compositions()),
// ---- close ----
P(Attr("class", "mt-12 border-t border-line pt-6 leading-relaxed text-ink-soft"),
Text("Every page of the documentation runs the code it documents — there are no screenshots "+
"of components anywhere on this site. "),
Text("There is not a screenshot of a component anywhere on this site. Every example is the "+
"real thing, running — which is the only way a documentation page can tell you when it "+
"has gone stale. "),
A(Attr("class", "text-accent underline underline-offset-4"),
Attr("href", "/wasm"), navigate(d, "/wasm"), Text("Read the docs")),
Text(", or "),
A(Attr("class", "text-accent underline underline-offset-4"),
Attr("href", "/wasm/kit"), navigate(d, "/wasm/kit"), Text("look at the components")),
Attr("href", "/about"), navigate(d, "/about"), Text("Why this exists")),
Text("."),
),
P(Attr("class", "mt-4 text-sm text-ink-muted"),
Text(hydrationNote(hydratedAt.Get()))),
)
}
}
@@ -404,10 +384,14 @@ func prettyHTML(s string) string {
return strings.ReplaceAll(s, "><", ">\n<")
}
const buildTranscript = `$ go run ./build
// The real transcript. It is on the front page, so it is the first thing anybody copies —
// which makes it the first thing to notice when it goes stale.
const buildTranscript = `$ go run ./server -build
==> generating directive glue (//gowasm:page, //gowasm:layout, //gowasm:server)
==> compiling Tailwind CSS -> wwwroot/app.css
==> compiling ./wasm -> wwwroot/app.wasm (GOOS=js GOARCH=wasm)
==> bundling the Solid app -> wwwroot/bundle.min.{js,css} (TSX -> Solid -> esbuild)
==> copying Go's wasm_exec.js shim into wwwroot/
$ go run ./server
serving "./wwwroot" on http://localhost:8085`
@@ -422,16 +406,22 @@ func AboutPage(d Deps) func() *VNode {
H1(Attr("class", "mt-2 text-4xl font-semibold tracking-tight text-text-heading"), Text("Why this exists")),
P(Attr("class", "mt-6 text-lg leading-relaxed text-ink-soft"),
Text("Kjol Web is one part of kjol — a shared base layer factored out of several applications "+
"so they stay in sync. (Kjol is Norwegian for KEEL: the spine of a hull, the thing every "+
"other part is built onto.) The applications had drifted: the same table, the same forms, "+
"the same charts, each subtly different in each app, each fixed twice.")),
Text("Kjøl is a shared base layer, factored out of several applications so they stay in sync. "+
"(Kjøl is Norwegian for KEEL: the spine of a hull, the thing every other part is built "+
"onto.) The applications had drifted: the same table, the same forms, the same charts, "+
"each subtly different in each app, each fixed twice.")),
P(Attr("class", "mt-4 leading-relaxed text-ink-soft"),
Text("The UI kit began as Solid.js components. Kjol Web is the same kit, written in Go and "+
"compiled to WebAssembly — the same components, the same Tailwind, no JavaScript build. "+
"That means one language across the server and the browser, and a table you can share "+
"between a web app and a native one because it is a Go function, not a JSX file.")),
Text("The UI kit began as Solid.js components, and it still is — that is Kjøl JS Web, and it "+
"is what those applications run today. Kjøl Wasm Web is the same kit written a second time "+
"in Go and compiled to WebAssembly: the same components, the same Tailwind, no JavaScript "+
"build at all. One language across the server and the browser, and a table you could share "+
"with a native app, because it is a Go function rather than a JSX file.")),
P(Attr("class", "mt-4 leading-relaxed text-ink-soft"),
Text("Neither of them is Kjøl. They are two compositions of it — two uses of the layers "+
"underneath, which are just directories of Go, TypeScript, C and Jai. The front page lists "+
"both, and does not argue for either.")),
H2(Attr("class", "mt-12 text-2xl font-semibold tracking-tight text-text-heading"), Text("The rules it keeps")),
Div(Attr("class", "mt-6 space-y-4"),