Add landing page for kjol, documentation

This commit is contained in:
2026-07-13 16:51:21 -04:00
parent 5230bd6702
commit fec8ef4a3e
54 changed files with 3529 additions and 547 deletions

View File

@@ -13,6 +13,20 @@ func Measure(*vdom.Ref) Rect { return Rect{} }
func Viewport() Size { return Size{} }
func CSSVarPx(string) float64 { return 0 }
// Now is 0 on the server: there is no page load to measure from. A component that
// reports a timing therefore SSRs an empty reading and fills it in once the client has
// taken over — which is the honest answer, since the number IS a property of the client.
func Now() float64 { return 0 }
// The theme is a property of the BROWSER — the OS setting and a localStorage choice,
// neither of which the server can see. So the server always renders the light theme, and
// the document's boot script (webui.ThemeBootScript) applies the dark class before the
// first paint. Nothing here can help with that, and pretending otherwise would just move
// the flash somewhere harder to find.
func PrefersDark() bool { return false }
func OnMediaChange(string, func(bool)) Unsub { return func() {} }
func SetRootClass(string, bool) {}
func RAF(func()) int { return 0 }
func CancelRAF(int) {}
func AfterRender(func()) {}

View File

@@ -67,8 +67,63 @@ func CSSVarPx(name string) float64 {
return parseCSSLength(raw, style.Get("fontSize").String())
}
// ---- theme ----
// PrefersDark reports whether the OS asks for a dark colour scheme.
func PrefersDark() bool {
mq := window.Call("matchMedia", "(prefers-color-scheme: dark)")
return mq.Truthy() && mq.Get("matches").Bool()
}
// OnMediaChange watches a media query. The theme controller uses it to follow the OS
// while the user has not overridden it — a preference changed in the system settings
// should reach an open tab, not wait for a reload.
func OnMediaChange(query string, fn func(matches bool)) Unsub {
mq := window.Call("matchMedia", query)
if !mq.Truthy() {
return func() {}
}
cb := js.FuncOf(func(_ js.Value, args []js.Value) any {
matches := false
if len(args) > 0 {
matches = args[0].Get("matches").Bool()
}
fn(matches)
return nil
})
mq.Call("addEventListener", "change", cb)
return func() {
mq.Call("removeEventListener", "change", cb)
cb.Release()
}
}
// SetRootClass adds or removes a class on <html>.
//
// The THEME lives there rather than on the app's root element because the page's own
// background — the thing behind everything, painted before the app mounts — is styled
// from <html>. A dark class on #app leaves a white margin around a dark page.
func SetRootClass(class string, on bool) {
list := document.Get("documentElement").Get("classList")
if on {
list.Call("add", class)
return
}
list.Call("remove", class)
}
// ---- frame timing + the post-render hook ----
// Now is performance.now(): milliseconds since the page began navigating, with
// sub-millisecond resolution.
//
// It is measured from the same origin the browser uses for its own timings, so a
// reading taken when the first render commits IS the time it took this app to become
// interactive — not an interval the app started and stopped itself.
func Now() float64 {
return window.Get("performance").Call("now").Float()
}
func RAF(fn func()) int {
var cb js.Func
cb = js.FuncOf(func(js.Value, []js.Value) any {