Update kjol website with C documentation
This commit is contained in:
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user