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)), ) }