Begin go documentation (ai slop for now)
This commit is contained in:
303
go/cmd/kjol-web/app/golayer.go
Normal file
303
go/cmd/kjol-web/app/golayer.go
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "kjol/vdom"
|
||||||
|
ui "kjol/webui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The Go layer's documentation.
|
||||||
|
//
|
||||||
|
// One page, like /c and the component pages. The Go module is the biggest layer by far —
|
||||||
|
// a dozen small packages plus both web engines — so the job here is a MAP, not a manual:
|
||||||
|
// say what each subsystem is for and name the handful of identifiers you would reach for,
|
||||||
|
// and leave the exhaustive reference to go doc.
|
||||||
|
//
|
||||||
|
// The web engines get a deliberately short section. They are the two compositions, and
|
||||||
|
// each already has a whole documentation section of its own (/wasm, /js) — repeating it
|
||||||
|
// here would be two maps of the same ground, kept in sync by hand. So this page points at
|
||||||
|
// them and moves on.
|
||||||
|
//
|
||||||
|
// The organising idea is the same as the C page: every section is a SUBSYSTEM, and the
|
||||||
|
// sidebar lists them. A subsystem here is a small group of packages that answer one
|
||||||
|
// question — "how does it talk to a database", "how does it not trust its input" — rather
|
||||||
|
// than one package per section, which for thirteen packages would be a wall.
|
||||||
|
|
||||||
|
// goSubsystems is the single source for the page's sections AND the sidebar that jumps to
|
||||||
|
// them, so the sidebar cannot offer a jump to a section that does not exist.
|
||||||
|
func goSubsystems() []subsystem {
|
||||||
|
return []subsystem{
|
||||||
|
{ID: "config", Label: "Configuration", Icon: "bolt",
|
||||||
|
Blurb: "Where the binary learns its world: the environment baked in at compile time, and the config struct read at startup."},
|
||||||
|
{ID: "data", Label: "Data", Icon: "table",
|
||||||
|
Blurb: "A PostgreSQL query builder and row-to-struct automapper, and CSV in and out."},
|
||||||
|
{ID: "http", Label: "HTTP & email", Icon: "globe",
|
||||||
|
Blurb: "Dependency-free HTTP glue — CORS, responses, a typed client — and pluggable mail."},
|
||||||
|
{ID: "values", Label: "Values", Icon: "calculator",
|
||||||
|
Blurb: "The small stuff done once: generic helpers, UTC-first time, and money as integer cents."},
|
||||||
|
{ID: "trust", Label: "Trust & logging", Icon: "shield-check",
|
||||||
|
Blurb: "Crypto, input validation, and logging — the three that decide what the program will believe and remember."},
|
||||||
|
{ID: "text", Label: "Text", Icon: "code",
|
||||||
|
Blurb: "Syntax highlighting: source in, coloured HTML out. A lexer, not a parser."},
|
||||||
|
{ID: "engines", Label: "Web engines", Icon: "layers",
|
||||||
|
Blurb: "The two front-end frameworks are Go too — but documented on their own pages. This is only the pointer."},
|
||||||
|
{ID: "tooling", Label: "Tooling", Icon: "cube",
|
||||||
|
Blurb: "The command-line tools, most of them run by the build rather than by hand."},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// goNav is the sidebar while you are reading /go. The group is called Subsystems, as on /c.
|
||||||
|
func goNav() []docsGroup {
|
||||||
|
items := make([]docsItem, 0, len(goSubsystems()))
|
||||||
|
for _, s := range goSubsystems() {
|
||||||
|
items = append(items, docsItem{
|
||||||
|
Path: "/go#" + s.ID,
|
||||||
|
Label: s.Label,
|
||||||
|
Icon: s.Icon,
|
||||||
|
Blurb: s.Blurb,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return []docsGroup{
|
||||||
|
{
|
||||||
|
Title: "Introduction",
|
||||||
|
Items: []docsItem{
|
||||||
|
{Path: "/go", Label: "Overview", Icon: "book-open",
|
||||||
|
Blurb: "What the Go layer is, and the one rule that shapes all of it."},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{Title: "Subsystems", Items: items},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//gowasm:page /go static layout=app
|
||||||
|
func GoPage(d Deps) func() *VNode {
|
||||||
|
return func() *VNode {
|
||||||
|
return docPage("Layers", "Kjøl Go",
|
||||||
|
"The oldest and largest layer: configuration, a database toolkit, logging, HTTP helpers, "+
|
||||||
|
"mail, money math, validation — the parts an application needs that are not the application. "+
|
||||||
|
"Both web engines live here too, but those have their own pages; this is the base beneath them.",
|
||||||
|
|
||||||
|
docSection("what-this-is", "What this is",
|
||||||
|
prose("A single Go module, imported package by package as kjol/<name>. Each package is small, "+
|
||||||
|
"stdlib-first, and does one thing — there is no framework object to construct and no "+
|
||||||
|
"lifecycle to learn. You import config, or dbutil, or chrono, and call it."),
|
||||||
|
prose("One rule shapes the whole layer, and it is worth stating before the parts: the framework "+
|
||||||
|
"NEVER imports application code. Where a package needs something only the app knows — the "+
|
||||||
|
"names of its tables, where to write a log, its mail credentials, the shape of its config — "+
|
||||||
|
"the app hands that in, and the package is written against the gap. That is why dbutil has a "+
|
||||||
|
"Register, l4g a SetDatabaseWriter, snailmail a Configure, and config a generic Load[T]. The "+
|
||||||
|
"inversions are not decoration; they are the reason one base layer can sit under several "+
|
||||||
|
"different applications without knowing anything about any of them."),
|
||||||
|
|
||||||
|
note("This layer is consumed in place, not published",
|
||||||
|
"kjøl is a git submodule inside each app, wired up with a go.work file — so editing a file "+
|
||||||
|
"here takes effect in the consuming app immediately, with no version to bump and no go get. "+
|
||||||
|
"There is no ABI to keep stable because there is nothing to keep stable between: the layer "+
|
||||||
|
"and its consumer are built together."),
|
||||||
|
),
|
||||||
|
|
||||||
|
goConfig(),
|
||||||
|
goData(),
|
||||||
|
goHTTP(),
|
||||||
|
goValues(),
|
||||||
|
goTrust(),
|
||||||
|
goText(),
|
||||||
|
goEngines(),
|
||||||
|
goTooling(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- configuration -------------------------------------------------------
|
||||||
|
|
||||||
|
func goConfig() *VNode {
|
||||||
|
return docSection("config", "Configuration",
|
||||||
|
prose("Two packages, and they answer the same question — what world is this binary running in — at "+
|
||||||
|
"two different times. appenv answers it at COMPILE time: Environment is a const chosen by a build "+
|
||||||
|
"tag, so a production binary cannot be talked into thinking it is staging by a stray environment "+
|
||||||
|
"variable. The value is fixed the moment go build runs, and the bundler reads it to define the "+
|
||||||
|
"same constant for the JavaScript side."),
|
||||||
|
prose("config answers it at RUNTIME. Load[T] fills the app's own config struct from environment "+
|
||||||
|
"variables and an optional .env file, with real environment variables always winning over the "+
|
||||||
|
"file. The app defines the struct — its fields, its env tags, its defaults — and the package only "+
|
||||||
|
"provides the generic loading, so no two apps have to agree on what configuration means."),
|
||||||
|
apiTable(
|
||||||
|
apiRow{"appenv.Environment", "The deployment environment, a const fixed by build tag (-tags staging / production). No runtime path can change it."},
|
||||||
|
apiRow{"config.Load[T](file, *T)", "Fill your config struct from env + .env. Generic over the struct; environment variables beat the file."},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- data ----------------------------------------------------------------
|
||||||
|
|
||||||
|
func goData() *VNode {
|
||||||
|
return docSection("data", "Data",
|
||||||
|
prose("dbutil is the largest package in the module, and it is two things: a PostgreSQL query builder "+
|
||||||
|
"and a reflection-based row-to-struct automapper. It is deliberately NOT an ORM — there are no "+
|
||||||
|
"migrations here (that is a separate tool) and no magic persistence. You bind a model type to a "+
|
||||||
|
"table, build parameterized SQL through chainable Select / InsertInto / Update / DeleteFrom, and "+
|
||||||
|
"scan the result straight into your structs by their db tags — including LEFT JOINs, which map a "+
|
||||||
|
"missing joined row to a nil pointer rather than a lie."),
|
||||||
|
prose("The table names come from a registry the app fills at startup (RegisterAll), which is the "+
|
||||||
|
"inversion at work: the builder resolves a Go type to a table without ever importing the app's "+
|
||||||
|
"models. Field references are type-safe — you pass a pointer to a struct field and the builder "+
|
||||||
|
"turns it into a column — so a renamed field is a compile error, not a wrong query at runtime."),
|
||||||
|
prose("csv is the small sibling: build CSV text from headers and rows, or straight from a slice of "+
|
||||||
|
"structs, and stream it to the browser as a download."),
|
||||||
|
apiTable(
|
||||||
|
apiRow{"dbutil.Init / ConnConfig", "Open the pooled *sql.DB and pin the session to UTC. Credentials are injected, never read from app config."},
|
||||||
|
apiRow{"dbutil.Register / RegisterAll", "Map a model type to its table name. The inversion: dbutil never imports your models."},
|
||||||
|
apiRow{"dbutil.Select / InsertInto / Update / DeleteFrom", "Chainable builders — Where, joins, order, paging — that emit $1,$2 parameterized SQL and its args."},
|
||||||
|
apiRow{"dbutil.ScanAll / QueryOne / QueryScalar[T]", "Result rows into your structs by db tag; joined rows that are all-NULL become nil pointers."},
|
||||||
|
apiRow{"dbutil.ParseFilterFromRequest / ApplyPagination", "Turn a request's query-string filters and paging into WHERE and LIMIT."},
|
||||||
|
apiRow{"csv.MakeCSV / StructToCSV / WriteCSVtoHTTP", "CSV from rows or from a slice of structs, and the headers to send it as an attachment."},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- http & mail ---------------------------------------------------------
|
||||||
|
|
||||||
|
func goHTTP() *VNode {
|
||||||
|
return docSection("http", "HTTP & email",
|
||||||
|
prose("httputil is the HTTP glue, and it imports nothing of the app's. CorsMiddleware is "+
|
||||||
|
"constructor-style — you hand it the allowed domains and a function that reports the current "+
|
||||||
|
"bundle version, and it never reads those from config itself. Alongside it are the response "+
|
||||||
|
"writers (JSON, gob, error) and their client-side mirrors: FetchGob[T] and FetchJSON[T] do a typed "+
|
||||||
|
"GET-and-decode, and are a deliberate no-op during server rendering, so an SSR pass keeps its "+
|
||||||
|
"loading state instead of blocking on a network call."),
|
||||||
|
prose("snailmail sends mail through a provider chosen at startup — SMTP or Cloudflare — with the "+
|
||||||
|
"credentials injected via Configure and the actual branded message composed on the app side. The "+
|
||||||
|
"package takes an already-rendered Email and a type (text or HTML) and sends it; it does not know "+
|
||||||
|
"or care what the mail says."),
|
||||||
|
apiTable(
|
||||||
|
apiRow{"httputil.CorsMiddleware(CorsConfig)", "CORS as constructor-style middleware; allowed domains and the bundle-version source are injected."},
|
||||||
|
apiRow{"httputil.RespondJSON / RespondGob / RespondError", "Encode and write a response."},
|
||||||
|
apiRow{"httputil.FetchGob[T] / FetchJSON[T]", "Client-side typed GET+decode. A no-op during SSR, so the server keeps a loading state."},
|
||||||
|
apiRow{"snailmail.Configure(Settings)", "Pick SMTP or Cloudflare and hand it credentials, once at startup."},
|
||||||
|
apiRow{"snailmail.SendMail(Email, type)", "Send an already-rendered message. TYPE_TEXT or TYPE_HTML."},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- values --------------------------------------------------------------
|
||||||
|
|
||||||
|
func goValues() *VNode {
|
||||||
|
return docSection("values", "Values",
|
||||||
|
prose("Three packages of the small stuff, done once so the apps do not each do it slightly "+
|
||||||
|
"differently. basic is the personal standard library: generic slice, map and pointer helpers, "+
|
||||||
|
"name capitalization that knows about Mc and O', number-to-string with commas, a reflection-based "+
|
||||||
|
"struct diff. chrono treats every stored time as UTC and only localizes at the edge — format a "+
|
||||||
|
"time in a timezone for display, parse an HTML date input back to UTC, render \"3 days ago\". "+
|
||||||
|
"finance keeps money as integer cents, never a float, and formats it back out with a symbol and "+
|
||||||
|
"grouping."),
|
||||||
|
apiTable(
|
||||||
|
apiRow{"basic.Reverse / IndexOf / RemoveDuplicates / MapMerge", "Generic slice and map helpers, stdlib-only."},
|
||||||
|
apiRow{"basic.NormalizeName / Int64ToStringWithCommas / CompareStructs", "Name casing, grouped numbers, and a field-by-field struct diff."},
|
||||||
|
apiRow{"chrono.FormatWithTz / DateToHTMLString", "A UTC time localized for display, or fed into an HTML date field."},
|
||||||
|
apiRow{"chrono.HTMLDateToTime / TimeSinceToString", "An HTML input parsed back to UTC; and \"Just now\" / \"3 days ago\"."},
|
||||||
|
apiRow{"finance.Int64ToMoneyWithCommas / MoneyToInt64", "Cents-as-int64 to a dollar string and back — no float ever touches the money."},
|
||||||
|
apiRow{"finance.MultiplyByPercentage / DaysToRateTerm", "Percentage math on cents, and a day count as a best-fit term string."},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- trust & logging -----------------------------------------------------
|
||||||
|
|
||||||
|
func goTrust() *VNode {
|
||||||
|
return docSection("trust", "Trust & logging",
|
||||||
|
prose("Three packages that decide what the program will believe and what it will remember. security "+
|
||||||
|
"is the crypto: bcrypt for passwords, AES-256-GCM for secrets (with generic EncryptData[T] that "+
|
||||||
|
"gob-serializes then encrypts), hashing, base58/64, random keys, and a bluemonday HTML "+
|
||||||
|
"sanitization policy you initialize at startup. validation cleans and checks input — email, "+
|
||||||
|
"phone, US state, tax id, ZIP — and its validators return descriptive errors rather than a bare "+
|
||||||
|
"false, so the caller can say what was wrong."),
|
||||||
|
prose("l4g is logging, and it carries the same inversion as dbutil. It owns the Entry type — the "+
|
||||||
|
"framework's mirror of the app's log-entry model — and persists to the database through a function "+
|
||||||
|
"the app registers with SetDatabaseWriter. If none is registered it falls back to the terminal, so "+
|
||||||
|
"a line is never silently dropped. The main logger is terminal, file, or database, chosen by an "+
|
||||||
|
"environment variable."),
|
||||||
|
apiTable(
|
||||||
|
apiRow{"security.HashPassword / ComparePasswords", "bcrypt."},
|
||||||
|
apiRow{"security.EncryptData[T] / DecryptData[T]", "gob-serialize then AES-256-GCM, generic over the value."},
|
||||||
|
apiRow{"security.Init / SanitizationPolicy", "The bluemonday UGC policy (extended to allow svg/path). Initialize it before use."},
|
||||||
|
apiRow{"validation.SanitizeEmail / ValidatePhoneNumber / ValidateStateCode", "Clean and check US-centric input; validators return an error, not a bool."},
|
||||||
|
apiRow{"l4g.Init / Write / Fatal", "Terminal, file, or database logging, selected by LOGGER_TYPE."},
|
||||||
|
apiRow{"l4g.SetDatabaseWriter(func(Entry) error)", "The inversion: l4g owns Entry, the app owns the table it lands in."},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- text ----------------------------------------------------------------
|
||||||
|
|
||||||
|
func goText() *VNode {
|
||||||
|
return docSection("text", "Text",
|
||||||
|
prose("lexer is syntax highlighting: source code in, HTML with coloured spans out. It is a lexer and "+
|
||||||
|
"not a parser on purpose — it degrades to escaped plain text on anything it does not understand "+
|
||||||
|
"rather than failing, so an unknown language is not an error and a half-written snippet still "+
|
||||||
|
"renders. It lives beside webui rather than inside it because it touches no DOM; it is a string in "+
|
||||||
|
"and a string out. It is what colours the C snippets over on the C page."),
|
||||||
|
apiTable(
|
||||||
|
apiRow{"lexer.Highlight(lang, src)", "Dispatch by language name. An unknown language comes back escaped and unpainted, not wrong."},
|
||||||
|
apiRow{"lexer.HighlightGo / HighlightC", "The two languages implemented so far."},
|
||||||
|
),
|
||||||
|
note("Its output is class names, so the stylesheet has to know it exists",
|
||||||
|
"The spans lexer emits carry Tailwind classes (text-emerald-300 and the like), so any stylesheet "+
|
||||||
|
"that renders a code block has to scan lexer/**/*.go for them. A build that forgets still "+
|
||||||
|
"compiles and just renders the snippet unstyled — which is exactly how it is wired into this "+
|
||||||
|
"site's Tailwind step."),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- web engines ---------------------------------------------------------
|
||||||
|
|
||||||
|
// The short section, on purpose: these are the two compositions, and each is documented in
|
||||||
|
// full elsewhere. All this page owes them is a sentence and a door.
|
||||||
|
func goEngines() *VNode {
|
||||||
|
return docSection("engines", "Web engines",
|
||||||
|
prose("Both of kjøl's web frameworks are assembled out of this layer — and both have their own "+
|
||||||
|
"documentation, so this is only the map. The gowasm engine (the packages vdom, wasmruntime, rsc "+
|
||||||
|
"and wasmdevserver, plus the webui component kit) lets you write user interfaces as ordinary Go "+
|
||||||
|
"compiled to WebAssembly, server-rendered and then hydrated, with no JavaScript build at all. "+
|
||||||
|
"jsbundler and tw are the other road: the JavaScript build — TSX to Solid to esbuild — and a "+
|
||||||
|
"Tailwind v4 compiler written in Go, which is what styles both engines."),
|
||||||
|
P(Attr("class", "mt-4 flex flex-wrap gap-3"),
|
||||||
|
engineLink("/wasm", "code", "Kjøl Wasm Web"),
|
||||||
|
engineLink("/js", "table", "Kjøl JS Web"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// engineLink is a pill that crosses into a composition's documentation. A plain href, not a
|
||||||
|
// client-side route: /js is a different binary's SPA, and even /wasm is reached most simply
|
||||||
|
// by letting the browser navigate rather than asking this page to swap itself out. No Deps,
|
||||||
|
// therefore — there is no navigate() to intercept.
|
||||||
|
func engineLink(href, icon, label string) *VNode {
|
||||||
|
return A(Attr("class", "inline-flex items-center gap-2 rounded-default border border-line px-3 py-1.5 text-sm font-medium text-ink-soft no-underline hover:border-primary-border hover:bg-primary-subtle hover:text-accent"),
|
||||||
|
Attr("href", href),
|
||||||
|
ui.IconInline(icon, 14, "text-ink-faint"),
|
||||||
|
Text(label),
|
||||||
|
ui.IconInline("arrow-right", 12, "text-ink-faint"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- tooling -------------------------------------------------------------
|
||||||
|
|
||||||
|
func goTooling() *VNode {
|
||||||
|
return docSection("tooling", "Tooling",
|
||||||
|
prose("The module ships a handful of command-line programs under cmd/. Most of them are run by the "+
|
||||||
|
"build rather than typed by hand: wasmgen reads the //gowasm: directives and writes the route and "+
|
||||||
|
"layout glue, twcss compiles the Tailwind stylesheet, bundle drives the JavaScript build, and "+
|
||||||
|
"typecheck runs the TypeScript checker. The rest are operational: migrate applies database "+
|
||||||
|
"migrations, loc reports the lines of code across the repository, and passgen bcrypt-hashes a "+
|
||||||
|
"password from the command line."),
|
||||||
|
apiTable(
|
||||||
|
apiRow{"cmd/wasmgen", "Preprocesses the //gowasm: directives into glue: the route map, the layouts, the server-component calls."},
|
||||||
|
apiRow{"cmd/twcss", "The Tailwind v4 compiler as a CLI — scan the sources, write the stylesheet."},
|
||||||
|
apiRow{"cmd/bundle", "A thin CLI over jsbundler: TSX → Solid → esbuild, plus the SSR bake."},
|
||||||
|
apiRow{"cmd/typecheck", "Runs the frontend TypeScript checker (tsgo, the native-Go TypeScript compiler)."},
|
||||||
|
apiRow{"cmd/migrate", "PostgreSQL migrations — up and down, behind an advisory lock."},
|
||||||
|
apiRow{"cmd/loc", "A lines-of-code report over git-tracked files (gocloc), vendored code excluded."},
|
||||||
|
apiRow{"cmd/passgen", "bcrypt-hash a password given on the command line."},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -72,6 +72,8 @@ func Languages() []Layer {
|
|||||||
Name: "Go",
|
Name: "Go",
|
||||||
Href: "/go",
|
Href: "/go",
|
||||||
Tagline: "The base: config, database, logging, HTTP, mail, validation — and both web engines.",
|
Tagline: "The base: config, database, logging, HTTP, mail, validation — and both web engines.",
|
||||||
|
Sub: "the base layer",
|
||||||
|
Live: true,
|
||||||
Icon: "server",
|
Icon: "server",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -196,9 +198,12 @@ func layerGrid(rows []Layer) *VNode {
|
|||||||
return Div(mods...)
|
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 {
|
func layerRow(l Layer) *VNode {
|
||||||
head := Span(Attr("class", "flex items-center gap-2"),
|
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)),
|
Span(Attr("class", "font-medium text-ink"), Text(l.Name)),
|
||||||
iff2(l.Live,
|
iff2(l.Live,
|
||||||
func() *VNode { return nil },
|
func() *VNode { return nil },
|
||||||
@@ -207,7 +212,7 @@ func layerRow(l Layer) *VNode {
|
|||||||
Text("reference"))
|
Text("reference"))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
body := P(Attr("class", "mt-1 pl-[23px] text-sm leading-relaxed text-ink-muted"), Text(l.Tagline))
|
body := P(Attr("class", "mt-1 text-sm leading-relaxed text-ink-muted"), Text(l.Tagline))
|
||||||
|
|
||||||
if !l.Live {
|
if !l.Live {
|
||||||
return Div(Attr("class", "px-5 py-4 opacity-75"), head, body)
|
return Div(Attr("class", "px-5 py-4 opacity-75"), head, body)
|
||||||
@@ -215,22 +220,13 @@ func layerRow(l Layer) *VNode {
|
|||||||
// A real navigation: the next layer is a different binary.
|
// 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),
|
return A(Attr("class", "block px-5 py-4 no-underline hover:bg-surface-muted"), Attr("href", l.Href),
|
||||||
head, body,
|
head, body,
|
||||||
Span(Attr("class", "mt-2 inline-flex items-center gap-1.5 pl-[23px] text-sm text-accent"),
|
Span(Attr("class", "mt-2 inline-block text-sm font-medium text-accent"),
|
||||||
Text("Read the docs"),
|
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
|
// iff2 picks a node. Go has no ternary, and a four-line if statement inside a tree literal
|
||||||
// statement inside a tree literal breaks the shape of the markup worse than these do.
|
// breaks the shape of the markup worse than this does.
|
||||||
func iff(cond bool, a, b string) string {
|
|
||||||
if cond {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func iff2(cond bool, a, b func() *VNode) *VNode {
|
func iff2(cond bool, a, b func() *VNode) *VNode {
|
||||||
if cond {
|
if cond {
|
||||||
return a()
|
return a()
|
||||||
@@ -238,18 +234,20 @@ func iff2(cond bool, a, b func() *VNode) *VNode {
|
|||||||
return b()
|
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 {
|
func layerItem(d Deps, l Layer) *VNode {
|
||||||
active := CurrentLayer(d.Path()) != nil && CurrentLayer(d.Path()).Href == l.Href
|
active := CurrentLayer(d.Path()) != nil && CurrentLayer(d.Path()).Href == l.Href
|
||||||
|
|
||||||
if !l.Live {
|
if !l.Live {
|
||||||
return Div(Attr("class", "flex cursor-default flex-col gap-0.5 px-3 py-2 opacity-55"),
|
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"),
|
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),
|
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"),
|
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")),
|
Text("reference")),
|
||||||
),
|
),
|
||||||
Span(Attr("class", "pl-6 text-xs text-ink-muted"), Text(l.Tagline)),
|
Span(Attr("class", "text-xs text-ink-muted"), Text(l.Tagline)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,10 +256,7 @@ func layerItem(d Deps, l Layer) *VNode {
|
|||||||
cls += " bg-primary-subtle"
|
cls += " bg-primary-subtle"
|
||||||
}
|
}
|
||||||
return A(Attr("class", cls), Attr("href", l.Href),
|
return A(Attr("class", cls), Attr("href", l.Href),
|
||||||
Span(Attr("class", "flex items-center gap-2 text-sm font-medium text-ink"),
|
Span(Attr("class", "text-sm font-medium text-ink"), Text(l.Name)),
|
||||||
ui.IconInline(l.Icon, 14, "text-accent"),
|
Span(Attr("class", "text-xs text-ink-muted"), Text(l.Tagline)),
|
||||||
Text(l.Name),
|
|
||||||
),
|
|
||||||
Span(Attr("class", "pl-6 text-xs text-ink-muted"), Text(l.Tagline)),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,11 @@ func wordmark(d Deps, href string) *VNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return A(Attr("class", "flex items-center gap-2.5 no-underline"), Attr("href", href), navigate(d, href),
|
return A(Attr("class", "flex items-center gap-2.5 no-underline"), Attr("href", href), navigate(d, href),
|
||||||
Span(Attr("class", "inline-flex h-8 w-8 items-center justify-center rounded-default bg-ink text-surface"),
|
// text-white, not text-surface: the flag is the same in both themes, so the boat on
|
||||||
|
// top of it has to be too. text-surface inverts to near-black in dark mode, which
|
||||||
|
// would hide the boat against the navy cross. The flag itself carries a dark scrim
|
||||||
|
// (see .flag-no) so this plain white boat reads without a shadow of its own.
|
||||||
|
Span(Attr("class", "inline-flex h-8 w-8 items-center justify-center rounded-default flag-no text-white"),
|
||||||
ui.IconInline("sailboat", 17, "")),
|
ui.IconInline("sailboat", 17, "")),
|
||||||
Span(Attr("class", "flex items-baseline gap-1.5"),
|
Span(Attr("class", "flex items-baseline gap-1.5"),
|
||||||
Span(Attr("class", "text-lg font-semibold tracking-tight text-text-heading"), Text(name)),
|
Span(Attr("class", "text-lg font-semibold tracking-tight text-text-heading"), Text(name)),
|
||||||
@@ -202,10 +206,14 @@ func AppLayout(d Deps, content *VNode) *VNode {
|
|||||||
// documents the C base layer. A sidebar listing the engine's chapters while you are
|
// 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.
|
// reading about arenas would be worse than no sidebar at all.
|
||||||
func sidebarNav(path string) []docsGroup {
|
func sidebarNav(path string) []docsGroup {
|
||||||
if path == "/c" || strings.HasPrefix(path, "/c/") {
|
switch {
|
||||||
|
case path == "/c" || strings.HasPrefix(path, "/c/"):
|
||||||
return cNav()
|
return cNav()
|
||||||
|
case path == "/go" || strings.HasPrefix(path, "/go/"):
|
||||||
|
return goNav()
|
||||||
|
default:
|
||||||
|
return docsNav()
|
||||||
}
|
}
|
||||||
return docsNav()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func docsSidebar(d Deps) *VNode {
|
func docsSidebar(d Deps) *VNode {
|
||||||
@@ -225,11 +233,13 @@ func docsSidebar(d Deps) *VNode {
|
|||||||
return El("aside", mods...)
|
return El("aside", mods...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No icon: the sidebar is a list of words, and a glyph on every row is noise the reader has
|
||||||
|
// to look past to read the label. The label is the navigation. (docsItem still carries an
|
||||||
|
// Icon — it is used on the /docs index cards, where a larger tile earns one.)
|
||||||
func sidebarLink(d Deps, it docsItem) *VNode {
|
func sidebarLink(d Deps, it docsItem) *VNode {
|
||||||
base, frag, isAnchor := strings.Cut(it.Path, "#")
|
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"
|
cls := "block 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"
|
|
||||||
|
|
||||||
// A section link is NEVER "active", and that is deliberate. It cannot be: it would
|
// 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
|
// have to know which section you had scrolled to, which means measuring all fifteen of
|
||||||
@@ -240,8 +250,7 @@ func sidebarLink(d Deps, it docsItem) *VNode {
|
|||||||
// than no highlight: it tells you nothing and looks broken.)
|
// than no highlight: it tells you nothing and looks broken.)
|
||||||
active := !isAnchor && d.Path() == it.Path
|
active := !isAnchor && d.Path() == it.Path
|
||||||
if active {
|
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"
|
cls = "active block rounded-default px-2 py-1.5 text-sm no-underline bg-primary-subtle font-medium text-accent"
|
||||||
iconCls = "text-accent"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
click := navigate(d, it.Path)
|
click := navigate(d, it.Path)
|
||||||
@@ -250,7 +259,6 @@ func sidebarLink(d Deps, it docsItem) *VNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return A(Attr("class", cls), Attr("href", it.Path), click,
|
return A(Attr("class", cls), Attr("href", it.Path), click,
|
||||||
ui.IconInline(it.Icon, 14, iconCls),
|
|
||||||
Text(it.Label),
|
Text(it.Label),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ func Routes(d Deps) map[string]func() *vdom.VNode {
|
|||||||
"/": HomePage(d),
|
"/": HomePage(d),
|
||||||
"/about": AboutPage(d),
|
"/about": AboutPage(d),
|
||||||
"/c": CPage(d),
|
"/c": CPage(d),
|
||||||
|
"/go": GoPage(d),
|
||||||
"/wasm": DocsPage(d),
|
"/wasm": DocsPage(d),
|
||||||
"/wasm/chart": ChartPage(d),
|
"/wasm/chart": ChartPage(d),
|
||||||
"/wasm/components": ComponentsPage(d),
|
"/wasm/components": ComponentsPage(d),
|
||||||
@@ -22,6 +23,7 @@ var StaticPaths = map[string]bool{
|
|||||||
"/": true,
|
"/": true,
|
||||||
"/about": true,
|
"/about": true,
|
||||||
"/c": true,
|
"/c": true,
|
||||||
|
"/go": true,
|
||||||
"/wasm": true,
|
"/wasm": true,
|
||||||
"/wasm/chart": true,
|
"/wasm/chart": true,
|
||||||
"/wasm/data": true,
|
"/wasm/data": true,
|
||||||
@@ -32,6 +34,7 @@ var RouteLayout = map[string]string{
|
|||||||
"/": "public",
|
"/": "public",
|
||||||
"/about": "public",
|
"/about": "public",
|
||||||
"/c": "app",
|
"/c": "app",
|
||||||
|
"/go": "app",
|
||||||
"/wasm": "app",
|
"/wasm": "app",
|
||||||
"/wasm/chart": "app",
|
"/wasm/chart": "app",
|
||||||
"/wasm/components": "app",
|
"/wasm/components": "app",
|
||||||
|
|||||||
@@ -86,11 +86,13 @@
|
|||||||
--color-primary-subtle: #eef2f8; /* a navy wash — tinted panels, badges, callouts */
|
--color-primary-subtle: #eef2f8; /* a navy wash — tinted panels, badges, callouts */
|
||||||
--color-primary-border: #c5d1e2;
|
--color-primary-border: #c5d1e2;
|
||||||
|
|
||||||
/* accent is for TEXT and icons, and it is the RED — which is why it is a separate token
|
/* accent is for TEXT and icons — links, the eyebrow, an active sidebar row. It is still a
|
||||||
from primary rather than a lighter shade of it. The two have opposite constraints: a
|
separate token from primary, because the two have opposite constraints: a fill must be
|
||||||
fill must be dark enough for white text on top of it, and accent text must be readable
|
dark enough for white text on TOP of it, and accent text must be readable ON the
|
||||||
ON the surface. Here they are not even the same hue: navy fills, red links. */
|
surface. Here they are the same hue — navy — but not the same value: the accent is a
|
||||||
--color-accent: #9e1b32; /* dark red */
|
touch deeper so a navy link on white is unmistakably a link. (The red is gone; the
|
||||||
|
brand is navy throughout now. Only the flag keeps its red field.) */
|
||||||
|
--color-accent: #1c3a66; /* navy — accent TEXT */
|
||||||
|
|
||||||
/* Surfaces, lines, ink — the kit's theme contract. */
|
/* Surfaces, lines, ink — the kit's theme contract. */
|
||||||
--color-surface: #ffffff;
|
--color-surface: #ffffff;
|
||||||
@@ -157,17 +159,14 @@
|
|||||||
--color-ink-muted: #9a9aa3;
|
--color-ink-muted: #9a9aa3;
|
||||||
--color-ink-faint: #71717a;
|
--color-ink-faint: #71717a;
|
||||||
|
|
||||||
/* Both brand tokens move in the dark, and for different reasons.
|
/* Both brand tokens move in the dark, and both for the same reason now: navy is too dark
|
||||||
|
to read on a near-black page, so each climbs to a lighter blue.
|
||||||
|
|
||||||
The accent has to CLIMB: a dark red on a near-black surface is unreadable, so it goes
|
The accent (TEXT) climbs furthest — a link has to be legible at body-text weight, so it
|
||||||
up to a light rose. It is still the flag's red, just the only version of it you can
|
goes to a soft sky. The fill climbs less: it only has to look like a button and still
|
||||||
read here.
|
carry white text (~7:1), so it lifts to a steel blue and stops there, well below where
|
||||||
|
the accent lands. */
|
||||||
The fill has to climb too — which the sky blue it replaced did not. Navy is dark
|
--color-accent: #9fc1ec;
|
||||||
enough that on a #101013 page a navy button loses its edges and reads as a hole in the
|
|
||||||
surface. So it lifts to a steel blue that still carries white text (~7:1) and still
|
|
||||||
looks like a button. */
|
|
||||||
--color-accent: #f0a3ad;
|
|
||||||
--color-primary: #2b4f80;
|
--color-primary: #2b4f80;
|
||||||
--color-primary-hover: #37619b;
|
--color-primary-hover: #37619b;
|
||||||
--color-primary-subtle: #182234;
|
--color-primary-subtle: #182234;
|
||||||
@@ -186,6 +185,40 @@ html {
|
|||||||
color: var(--color-ink);
|
color: var(--color-ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------------
|
||||||
|
The wordmark's logo tile: a muted Norwegian flag.
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
kjøl is a Norwegian word — the mark says so. It is the flag's Scandinavian cross: a
|
||||||
|
navy cross with an off-white outline on a red field, offset LEFT as the real flag is
|
||||||
|
(the vertical bar sits at ~38% rather than centre). Muted, not the flag's full
|
||||||
|
saturation — it is a 32px tile next to body text, not a banner.
|
||||||
|
|
||||||
|
Drawn in layered gradients over a red base rather than as an <img>, so it inherits the
|
||||||
|
tile's rounded corners and border, needs no asset request, and cannot 404. Layers paint
|
||||||
|
TOP-first, so the reading is: dim scrim, then navy cross, off-white cross, red field. Each
|
||||||
|
band is transparent outside its stripe (hard stops via doubled positions) so the layer
|
||||||
|
beneath shows through. The bands are constant across light and dark — a flag does not
|
||||||
|
change with the page theme.
|
||||||
|
|
||||||
|
The topmost layer is a flat dark scrim, and it is there so the WHITE sailboat on top of
|
||||||
|
the tile reads on its own — no outline, no shadow on the glyph. The boat is a thin white
|
||||||
|
stroke and the off-white cross is exactly the band it would vanish into; rather than trace
|
||||||
|
the boat in shadow, the whole flag is dimmed until white stands out against every band of
|
||||||
|
it, the pale cross included. A dimmer, moodier flag with a crisp white boat, on purpose.
|
||||||
|
|
||||||
|
The cross geometry, as percentages of the tile:
|
||||||
|
vertical (offset left, centre 38%): off-white 27–49%, navy 32.5–43.5%
|
||||||
|
horizontal (centred, centre 50%): off-white 39–61%, navy 44.5–55.5% */
|
||||||
|
.flag-no {
|
||||||
|
background-color: #a83f4c; /* the red field, muted (then dimmed by the scrim above) */
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(0, 0, 0, 0.42), rgba(0, 0, 0, 0.42)), /* the dim scrim, on top */
|
||||||
|
linear-gradient(180deg, transparent 44.5%, #294c76 44.5%, #294c76 55.5%, transparent 55.5%),
|
||||||
|
linear-gradient(90deg, transparent 32.5%, #294c76 32.5%, #294c76 43.5%, transparent 43.5%),
|
||||||
|
linear-gradient(180deg, transparent 39%, #ece6da 39%, #ece6da 61%, transparent 61%),
|
||||||
|
linear-gradient(90deg, transparent 27%, #ece6da 27%, #ece6da 49%, transparent 49%);
|
||||||
|
}
|
||||||
|
|
||||||
.bg-grid {
|
.bg-grid {
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(to right, var(--grid-line) 1px, transparent 1px),
|
linear-gradient(to right, var(--grid-line) 1px, transparent 1px),
|
||||||
|
|||||||
@@ -16,19 +16,19 @@
|
|||||||
--------------------------------------------------------------------------- */
|
--------------------------------------------------------------------------- */
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
/* The brand: navy and red, the Norwegian flag muted down. These are the SAME values
|
/* The brand: navy throughout, a muted flag-navy. These are the SAME values the Go/WASM
|
||||||
the Go/WASM section's css/app.css sets, and under the same names — so the Layers
|
section's css/app.css sets, and under the same names — so the Layers menu, the
|
||||||
menu, the sidebar highlight and the callouts are the same navy on both sides of the
|
sidebar highlight and the callouts are the same navy on both sides of the site rather
|
||||||
site rather than two navies that happen to be close.
|
than two navies that happen to be close.
|
||||||
|
|
||||||
primary FILLS (white text sits on it) and is the NAVY; accent is TEXT and icons and
|
primary FILLS (white text sits on it); accent is TEXT and icons (it has to be
|
||||||
is the RED (it has to be readable on the surface); primary-subtle/-border are the
|
readable on the surface) and is a slightly deeper navy so a link reads as one;
|
||||||
tinted panel. */
|
primary-subtle/-border are the tinted panel. Only the flag tile keeps a red. */
|
||||||
--color-primary: #1e3a63; /* muted navy — fills; they carry white text */
|
--color-primary: #1e3a63; /* muted navy — fills; they carry white text */
|
||||||
--color-primary-hover: #16294a;
|
--color-primary-hover: #16294a;
|
||||||
--color-primary-subtle: #eef2f8; /* a navy wash — tinted panels, callouts */
|
--color-primary-subtle: #eef2f8; /* a navy wash — tinted panels, callouts */
|
||||||
--color-primary-border: #c5d1e2;
|
--color-primary-border: #c5d1e2;
|
||||||
--color-accent: #9e1b32; /* dark red — accent TEXT */
|
--color-accent: #1c3a66; /* navy — accent TEXT (links, eyebrow, active rows) */
|
||||||
|
|
||||||
/* Lora, the same body face the Go/WASM section vendors. The woff2 files are
|
/* Lora, the same body face the Go/WASM section vendors. The woff2 files are
|
||||||
served out of wwwroot/fonts by the same server, so this section pays no
|
served out of wwwroot/fonts by the same server, so this section pays no
|
||||||
@@ -85,18 +85,32 @@ html {
|
|||||||
|
|
||||||
/* The dark values for the brand.
|
/* The dark values for the brand.
|
||||||
---------------------------------------------------------------------------
|
---------------------------------------------------------------------------
|
||||||
Both tokens move, for different reasons. The accent CLIMBS: a dark red on a near-black
|
Navy is too dark to read on a near-black page, so both brand tokens climb to a lighter
|
||||||
surface is unreadable, so it goes up to a light rose — still the flag's red, just the
|
blue. The accent (TEXT) climbs furthest, to a soft sky a link stays legible in; the fill
|
||||||
only version of it you can read here. The FILL climbs too, which the sky blue it
|
climbs less, to a steel blue that still looks like a button and still carries white text.
|
||||||
replaced did not have to: navy is dark enough that a navy button on a #101013 page loses
|
And the tinted panel inverts outright, because a pale wash on #101013 is not a tint, it
|
||||||
its edges and reads as a hole. And the tinted panel inverts outright, because a pale
|
is a white box.
|
||||||
wash on #101013 is not a tint, it is a white box.
|
|
||||||
|
|
||||||
Same values, same names, as the Go/WASM section's css/app.css. */
|
Same values, same names, as the Go/WASM section's css/app.css. */
|
||||||
.dark {
|
.dark {
|
||||||
--color-accent: #f0a3ad;
|
--color-accent: #9fc1ec;
|
||||||
--color-primary: #2b4f80;
|
--color-primary: #2b4f80;
|
||||||
--color-primary-hover: #37619b;
|
--color-primary-hover: #37619b;
|
||||||
--color-primary-subtle: #182234;
|
--color-primary-subtle: #182234;
|
||||||
--color-primary-border: #2c3e5c;
|
--color-primary-border: #2c3e5c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The wordmark's logo tile: a muted Norwegian flag. Identical to the Go/WASM section's
|
||||||
|
css/app.css — the two front-ends share one mark. See there for the full note; in short
|
||||||
|
it is the flag's Scandinavian cross (navy cross, off-white outline, red field, offset
|
||||||
|
left) drawn in layered gradients over a red base, constant across themes, with a flat
|
||||||
|
dark scrim on top so the plain white sailboat reads on it without an outline. */
|
||||||
|
.flag-no {
|
||||||
|
background-color: #a83f4c;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(0, 0, 0, 0.42), rgba(0, 0, 0, 0.42)),
|
||||||
|
linear-gradient(180deg, transparent 44.5%, #294c76 44.5%, #294c76 55.5%, transparent 55.5%),
|
||||||
|
linear-gradient(90deg, transparent 32.5%, #294c76 32.5%, #294c76 43.5%, transparent 43.5%),
|
||||||
|
linear-gradient(180deg, transparent 39%, #ece6da 39%, #ece6da 61%, transparent 61%),
|
||||||
|
linear-gradient(90deg, transparent 27%, #ece6da 27%, #ece6da 49%, transparent 49%);
|
||||||
|
}
|
||||||
|
|||||||
@@ -93,22 +93,18 @@ function LayerItem(props: { layer: Layer; current?: Layer }) {
|
|||||||
fallback={
|
fallback={
|
||||||
<div class="flex cursor-default flex-col gap-0.5 px-3 py-2 opacity-55">
|
<div class="flex cursor-default flex-col gap-0.5 px-3 py-2 opacity-55">
|
||||||
<span class="flex items-center gap-2 text-sm font-medium text-ink-muted">
|
<span class="flex items-center gap-2 text-sm font-medium text-ink-muted">
|
||||||
<Icon icon={props.layer.icon} size={14} class="shrink-0 text-ink-faint" />
|
|
||||||
{props.layer.name}
|
{props.layer.name}
|
||||||
<span class="rounded-full bg-surface-raised px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-ink-muted">
|
<span class="rounded-full bg-surface-raised px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-ink-muted">
|
||||||
reference
|
reference
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="pl-6 text-xs text-ink-muted">{props.layer.tagline}</span>
|
<span class="text-xs text-ink-muted">{props.layer.tagline}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* A plain <a href>, not MenuLink and not the router's <A>. Both of the
|
{/* A plain <a href>, not the router's <A>: the other side is served by a
|
||||||
alternatives are wrong here: MenuLink lays its icon out BESIDE the whole
|
different binary, so crossing to it has to be a real navigation, not a
|
||||||
two-line block (so the tagline never lines up under the name, which is what
|
client-side route the router would try to handle itself. */}
|
||||||
the Go menu does), and the router would try to handle the jump itself — but
|
|
||||||
the other side is served by a different binary, so it has to be a real
|
|
||||||
navigation. */}
|
|
||||||
<a
|
<a
|
||||||
href={props.layer.href}
|
href={props.layer.href}
|
||||||
class={
|
class={
|
||||||
@@ -116,11 +112,8 @@ function LayerItem(props: { layer: Layer; current?: Layer }) {
|
|||||||
(props.current?.href === props.layer.href ? "bg-primary-subtle" : "")
|
(props.current?.href === props.layer.href ? "bg-primary-subtle" : "")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span class="flex items-center gap-2 text-sm font-medium text-ink">
|
<span class="text-sm font-medium text-ink">{props.layer.name}</span>
|
||||||
<Icon icon={props.layer.icon} size={14} class="shrink-0 text-accent" />
|
<span class="text-xs text-ink-muted">{props.layer.tagline}</span>
|
||||||
{props.layer.name}
|
|
||||||
</span>
|
|
||||||
<span class="pl-6 text-xs text-ink-muted">{props.layer.tagline}</span>
|
|
||||||
</a>
|
</a>
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
@@ -132,7 +125,10 @@ function Wordmark() {
|
|||||||
// back where you started.
|
// back where you started.
|
||||||
return (
|
return (
|
||||||
<a href="/" class="flex items-center gap-2.5 no-underline">
|
<a href="/" class="flex items-center gap-2.5 no-underline">
|
||||||
<span class="inline-flex h-8 w-8 items-center justify-center rounded-default bg-fill-neutral text-on-fill-neutral">
|
{/* text-white, not a theme token: the flag tile is the same in both themes, so
|
||||||
|
the boat on top of it has to be too. The flag carries a dark scrim (.flag-no)
|
||||||
|
so the plain white boat reads without a shadow of its own. */}
|
||||||
|
<span class="inline-flex h-8 w-8 items-center justify-center rounded-default flag-no text-white">
|
||||||
<Icon icon="sailboat" size={17} />
|
<Icon icon="sailboat" size={17} />
|
||||||
</span>
|
</span>
|
||||||
<span class="flex items-baseline gap-1.5">
|
<span class="flex items-baseline gap-1.5">
|
||||||
@@ -154,10 +150,13 @@ function Sidebar() {
|
|||||||
// The same active treatment the Go sidebar uses (app/pages.go: sidebarLink) — a
|
// The same active treatment the Go sidebar uses (app/pages.go: sidebarLink) — a
|
||||||
// tinted panel and accent text, not a grey fill. Now that the Solid theme carries the
|
// tinted panel and accent text, not a grey fill. Now that the Solid theme carries the
|
||||||
// primary-subtle / accent tokens, the two sidebars are the same sidebar.
|
// primary-subtle / accent tokens, the two sidebars are the same sidebar.
|
||||||
|
//
|
||||||
|
// No icons, also matching the Go sidebar: the sidebar is a list of words, and a glyph on
|
||||||
|
// every row is noise to read past. So `block`, not `flex items-center gap-2`.
|
||||||
const linkCls = (on: boolean) =>
|
const linkCls = (on: boolean) =>
|
||||||
on
|
on
|
||||||
? "flex items-center gap-2 rounded-default bg-primary-subtle px-2 py-1.5 text-sm font-medium text-accent no-underline"
|
? "block rounded-default bg-primary-subtle px-2 py-1.5 text-sm font-medium text-accent no-underline"
|
||||||
: "flex items-center gap-2 rounded-default px-2 py-1.5 text-sm text-ink-soft no-underline hover:bg-surface-raised hover:text-ink";
|
: "block rounded-default px-2 py-1.5 text-sm text-ink-soft no-underline hover:bg-surface-raised hover:text-ink";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside class="sticky top-[3.75rem] hidden h-[calc(100vh-3.75rem)] w-56 shrink-0 overflow-y-auto py-10 lg:block">
|
<aside class="sticky top-[3.75rem] hidden h-[calc(100vh-3.75rem)] w-56 shrink-0 overflow-y-auto py-10 lg:block">
|
||||||
@@ -167,11 +166,6 @@ function Sidebar() {
|
|||||||
{(item) => (
|
{(item) => (
|
||||||
<li>
|
<li>
|
||||||
<A href={item.path} end={item.path === "/"} class={linkCls(active(item.path))}>
|
<A href={item.path} end={item.path === "/"} class={linkCls(active(item.path))}>
|
||||||
<Icon
|
|
||||||
icon={item.icon}
|
|
||||||
size={14}
|
|
||||||
class={active(item.path) ? "text-accent" : "text-ink-faint"}
|
|
||||||
/>
|
|
||||||
{item.label}
|
{item.label}
|
||||||
</A>
|
</A>
|
||||||
</li>
|
</li>
|
||||||
@@ -201,7 +195,6 @@ function Sidebar() {
|
|||||||
jumpTo(navigate, onComponents(), g.id);
|
jumpTo(navigate, onComponents(), g.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon={g.icon} size={14} class="text-ink-faint" />
|
|
||||||
{g.label}
|
{g.label}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
Reference in New Issue
Block a user