201 lines
6.7 KiB
Go
201 lines
6.7 KiB
Go
package app
|
|
|
|
import (
|
|
"strings"
|
|
|
|
. "kjol/vdom"
|
|
ui "kjol/webui"
|
|
)
|
|
|
|
// The layers of kjol, as data.
|
|
//
|
|
// This is the Go mirror of frontend/src/layers.ts. The site has two front-ends built
|
|
// by two completely different pipelines, and the Layers menu has to be identical in
|
|
// both — so it is a LIST in each, 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 half
|
|
// compiles to WebAssembly and the other is bundled by esbuild, and nothing is upstream
|
|
// of both. Keeping it to a flat slice of plain data is what makes the duplication
|
|
// survivable — you can diff the two by eye.)
|
|
|
|
type Layer struct {
|
|
Name string
|
|
Href string
|
|
Tagline 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
|
|
}
|
|
|
|
func Layers() []Layer {
|
|
return []Layer{
|
|
{
|
|
Name: "Kjol Go",
|
|
Href: "/go",
|
|
Tagline: "The server base: config, database, logging, HTTP, mail, validation.",
|
|
Icon: "server",
|
|
},
|
|
{
|
|
Name: "Kjol Wasm Web",
|
|
Href: "/wasm",
|
|
Tagline: "Web interfaces written in Go, compiled to WebAssembly. SSR + hydration, no JS build.",
|
|
Live: true,
|
|
Icon: "code",
|
|
},
|
|
{
|
|
Name: "Kjol JS Web",
|
|
Href: "/js",
|
|
Tagline: "The Solid component kit, bundled by a Go toolchain: TSX → Solid → esbuild, Tailwind in Go.",
|
|
Live: true,
|
|
Icon: "squares",
|
|
},
|
|
{
|
|
Name: "Kjol C",
|
|
Href: "/c",
|
|
Tagline: "Arena allocator, strings, math, lexer, platform layer.",
|
|
Icon: "bolt",
|
|
},
|
|
{
|
|
Name: "Kjol Jai",
|
|
Href: "/jai",
|
|
Tagline: "Console rendering module. Early.",
|
|
Icon: "cube",
|
|
},
|
|
}
|
|
}
|
|
|
|
// CurrentLayer is the layer the given path belongs to, or nil on the front page.
|
|
func CurrentLayer(path string) *Layer {
|
|
for i, l := range Layers() {
|
|
if path == l.Href || strings.HasPrefix(path, l.Href+"/") {
|
|
return &Layers()[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LayersMenuCtl is the Layers menu's controller.
|
|
//
|
|
// It is created ONCE, here, at package level — not inside layersMenu, which is called
|
|
// from a layout 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.
|
|
var LayersMenuCtl = ui.NewMenu(ui.MenuOptions{Placement: ui.PlacementBottomEnd})
|
|
|
|
// layersMenu is the site's primary navigation: kjol is a stack of layers, and this is
|
|
// how you get from any one of them to any other.
|
|
//
|
|
// A layer 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 {
|
|
content := []*VNode{
|
|
P(Attr("class", "px-3 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-widest text-ink-faint"),
|
|
Text("The layers of kjol")),
|
|
}
|
|
for _, l := range Layers() {
|
|
content = append(content, layerItem(d, l))
|
|
}
|
|
|
|
return Div(Attr("class", "relative"),
|
|
LayersMenuCtl.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("Layers"),
|
|
ui.IconInline("chevron-down", 11, "text-ink-faint"),
|
|
),
|
|
LayersMenuCtl.Content("w-96", content...),
|
|
)
|
|
}
|
|
|
|
// layersGrid is the front page's list of layers — 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 kjol IS, and half of it having no demo yet does not make
|
|
// that half not exist.
|
|
func layersGrid(d Deps) *VNode {
|
|
rows := []Mod{Attr("class", "mt-5 divide-y divide-line rounded-default border border-line")}
|
|
for _, l := range Layers() {
|
|
rows = append(rows, layerRow(l))
|
|
}
|
|
return Div(rows...)
|
|
}
|
|
|
|
func layerRow(l Layer) *VNode {
|
|
head := Span(Attr("class", "flex items-center gap-2"),
|
|
ui.IconInline(l.Icon, 15, iff(l.Live, "text-accent", "text-ink-muted")),
|
|
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 pl-[23px] 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-flex items-center gap-1.5 pl-[23px] text-sm text-accent"),
|
|
Text("Read the docs"),
|
|
ui.IconInline("arrow-right", 12, ""),
|
|
),
|
|
)
|
|
}
|
|
|
|
// iff picks a string; 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 these do.
|
|
func iff(cond bool, a, b string) string {
|
|
if cond {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func iff2(cond bool, a, b func() *VNode) *VNode {
|
|
if cond {
|
|
return a()
|
|
}
|
|
return b()
|
|
}
|
|
|
|
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"),
|
|
ui.IconInline(l.Icon, 14, "text-ink-faint"),
|
|
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", "pl-6 text-xs 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", "flex items-center gap-2 text-sm font-medium text-ink"),
|
|
ui.IconInline(l.Icon, 14, "text-accent"),
|
|
Text(l.Name),
|
|
),
|
|
Span(Attr("class", "pl-6 text-xs text-ink-muted"), Text(l.Tagline)),
|
|
)
|
|
}
|