package app import ( "strings" . "kjol/wasmruntime/vdom" ui "kjol/webui" ) // What kjøl is made of, as data. // // There are two kinds of thing here, and conflating them was the mistake this file used // to make — one flat list called "the layers", holding both. // // LAYERS are LANGUAGES. What kjøl is written in, and what it gives you in each: // the Go base, the TypeScript kit, the C base, the Jai modules. A layer is // a directory of code you can use on its own. // // COMPOSITIONS are FRAMEWORKS. What you get when the layers are assembled into // something that does a job — the two web engines. A composition is not // another language; it is a use of them. // // Kjøl Wasm Web is Go, all the way down. Kjøl JS Web is TypeScript compiled by a Go // toolchain — two layers, one framework. Listing that beside "C" as though they were the // same kind of noun told the reader nothing about either. // // This is the Go mirror of frontend/src/layers.ts. The site has two front-ends built by // two completely different pipelines, and these menus have to be identical in both — so // each is a LIST, not markup, and the two lists are the only thing that has to be kept in // step. // // (A shared source would be better than a mirrored one. There isn't one: this side // compiles to WebAssembly and the other is bundled by esbuild, and nothing is upstream of // both. Keeping each to a flat slice of plain data is what makes the duplication // survivable — you can diff them by eye.) type Layer struct { Name string Href string Tagline string // Sub is the half-line beside the wordmark while you are inside this layer. It says // what you are standing in — "Go + WebAssembly", "arenas, strings, a lexer" — and a // wordmark that says the same thing everywhere is one more thing the reader has to // keep track of himself. Sub string // Live means you can click into worked examples. The others are documented but // have no demo — they still appear, because a menu that silently omits half the // library teaches the reader that the library is half the size it is. Live bool Icon string } // Wordmark is what the CHROME calls this layer — the top bar, and the page's own title. // The menu calls it Name. // // They differ, and only for the languages. In a menu headed "Layers" the row says "C", // because the row is answering "which language"; up in the top bar, alone, "C" is the name // of a language rather than the name of the thing you are reading, and it has to say whose // C this is. The compositions are already named "Kjøl Wasm Web" — the product's name is // part of what they ARE, not a prefix bolted on — so they are returned unchanged. func (l Layer) Wordmark() string { if strings.HasPrefix(l.Name, "Kjøl") { return l.Name } return "Kjøl " + l.Name } // Languages: what kjøl is written in. func Languages() []Layer { return []Layer{ { Name: "Go", Href: "/go", Tagline: "The base: config, database, logging, HTTP, mail, validation — and both web engines.", Sub: "the base layer", Live: true, Icon: "server", }, { Name: "TypeScript", Href: "/ts", Tagline: "The Solid component kit, the vendored runtime, and the generic scaffolding apps import as @kjol/*.", Icon: "squares", }, { Name: "C", Href: "/c", Tagline: "Arena allocator, counted strings, math, a lexer, a platform layer — and a build system that is a C file.", Sub: "a base layer in C", Live: true, Icon: "bolt", }, { Name: "Jai", Href: "/jai", Tagline: "Console rendering. Early.", Icon: "cube", }, } } // Compositions: what the languages are assembled into. func Compositions() []Layer { return []Layer{ { Name: "Kjøl Wasm Web", Href: "/wasm", Tagline: "Web interfaces written in Go, compiled to WebAssembly. SSR + hydration, and no JavaScript build at all.", Sub: "Go + WebAssembly", Live: true, Icon: "code", }, { Name: "Kjøl JS Web", Href: "/js", Tagline: "The Solid component kit, bundled by a Go toolchain: TSX → Solid → esbuild, Tailwind in Go.", Sub: "Solid + Go toolchain", Live: true, Icon: "table", }, } } // CurrentLayer is the layer or composition the given path belongs to, or nil on the front // page. The wordmark uses it to name where you are standing. func CurrentLayer(path string) *Layer { all := append(Compositions(), Languages()...) for i, l := range all { if path == l.Href || strings.HasPrefix(path, l.Href+"/") { return &all[i] } } return nil } // The two menus' controllers. // // They are created ONCE, here, at package level — not inside the functions below, which a // layout calls on every single render. A floating component is a controller: it owns an // open signal, a positioning engine and document listeners, and building a fresh one per // render would leak all three and give you a menu that never opens. Same rule as Theme, a // few lines up in pages.go. // // TWO menus, not one with two headings inside it. They are different questions — "what is // this written in" and "what can I read" — and a reader who wants the second should not // have to scroll past the first to find it. The kit's single-open manager means opening // one closes the other, so they behave like one control with two halves. var ( LayersMenuCtl = ui.NewMenu(ui.MenuOptions{Placement: ui.PlacementBottomEnd}) CompositionsMenuCtl = ui.NewMenu(ui.MenuOptions{Placement: ui.PlacementBottomEnd}) ) // layersMenu lists the LANGUAGES. compositionsMenu, below, lists the frameworks. // // An entry that is Live is a link. One that is not is inert and dimmed, with the word // "reference" on it — it exists, it is documented in the repository, there is simply // nothing here to click. // // Crossing into another layer is a REAL navigation, not a client-side route: /js is a // different binary's SPA and /wasm is this one. Hence a plain href and no navigate() // interception — an intercepted click would ask this WebAssembly to render a page it // does not have. func layersMenu(d Deps) *VNode { return dropdown(d, LayersMenuCtl, "Layers", Languages()) } // compositionsMenu lists the FRAMEWORKS — the two things assembled out of the layers, and // the two a reader can actually click into. func compositionsMenu(d Deps) *VNode { return dropdown(d, CompositionsMenuCtl, "Compositions", Compositions()) } func dropdown(d Deps, ctl *ui.Menu, label string, rows []Layer) *VNode { content := make([]*VNode, 0, len(rows)) for _, l := range rows { content = append(content, layerItem(d, l)) } return Div(Attr("class", "relative"), ctl.Trigger(ui.MenuTriggerProps{ Class: "inline-flex items-center gap-1.5 rounded-default px-3 py-1.5 text-sm font-medium text-ink-soft hover:bg-surface-raised hover:text-ink", }, Text(label), ui.IconInline("chevron-down", 11, "text-ink-faint"), ), ctl.Content("w-96", content...), ) } // layerGrid is the front page's list — the same data as the menu, laid out to be read // rather than navigated. A layer with no examples still gets a row: the point of the page // is what kjøl IS, and half of it having no demo yet does not make that half not exist. func layerGrid(rows []Layer) *VNode { mods := []Mod{Attr("class", "mt-5 divide-y divide-line rounded-default border border-line")} for _, l := range rows { mods = append(mods, layerRow(l)) } return Div(mods...) } // No icon beside the name, and none on the "Read the docs" link. The front page reads as a // short list of what kjøl is, and a glyph next to every row — a boat, a table, a globe — // asks to be decoded before the word beside it is read. The words are the point; they carry // themselves. (The reference badge stays: it says something the name does not.) func layerRow(l Layer) *VNode { head := Span(Attr("class", "flex items-center gap-2"), Span(Attr("class", "font-medium text-ink"), Text(l.Name)), iff2(l.Live, func() *VNode { return nil }, func() *VNode { return Span(Attr("class", "rounded-full border border-line px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-ink-faint"), Text("reference")) }), ) body := P(Attr("class", "mt-1 text-sm leading-relaxed text-ink-muted"), Text(l.Tagline)) if !l.Live { return Div(Attr("class", "px-5 py-4 opacity-75"), head, body) } // A real navigation: the next layer is a different binary. return A(Attr("class", "block px-5 py-4 no-underline hover:bg-surface-muted"), Attr("href", l.Href), head, body, Span(Attr("class", "mt-2 inline-block text-sm font-medium text-accent"), Text("Read the docs")), ) } // iff2 picks a node. Go has no ternary, and a four-line if statement inside a tree literal // breaks the shape of the markup worse than this does. func iff2(cond bool, a, b func() *VNode) *VNode { if cond { return a() } return b() } // No icon on the menu rows either — the name and its one-line tagline are the whole item, // same as the front-page list and the sidebar. (The chevron on the menu TRIGGER stays: it // is not a layer's glyph, it is the cue that the thing opens.) func layerItem(d Deps, l Layer) *VNode { active := CurrentLayer(d.Path()) != nil && CurrentLayer(d.Path()).Href == l.Href if !l.Live { return Div(Attr("class", "flex cursor-default flex-col gap-0.5 px-3 py-2 opacity-55"), Span(Attr("class", "flex items-center gap-2 text-sm font-medium text-ink-muted"), Text(l.Name), Span(Attr("class", "rounded-full bg-surface-raised px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-ink-muted"), Text("reference")), ), Span(Attr("class", "text-ss text-ink-muted"), Text(l.Tagline)), ) } cls := "flex flex-col gap-0.5 px-3 py-2 no-underline hover:bg-surface-raised" if active { cls += " bg-primary-subtle" } return A(Attr("class", cls), Attr("href", l.Href), Span(Attr("class", "text-sm font-medium text-ink"), Text(l.Name)), Span(Attr("class", "text-ss text-ink-muted"), Text(l.Tagline)), ) }