Add js web stuff to landing page + documentation

This commit is contained in:
2026-07-14 10:33:12 -04:00
parent fec8ef4a3e
commit 02a6dc6c48
435 changed files with 69567 additions and 1522 deletions

110
CLAUDE.md
View File

@@ -17,35 +17,49 @@ forks it was extracted from. Scope will grow to more projects and languages.
step. Editing a kjol file takes effect in the consuming app immediately.
4. When a file exists in both apps and has drifted, reconcile by **merging best-of-both**.
## Organization — by language
## Organization — by build root
Each top-level directory is one language / build root:
```
kjol/
go/ all Go. Module `kjol` (go.mod lives in go/). Imports are `kjol/<pkg>`.
web/ all JS/TS (browser + SSR). No build system of its own; built by go/webbundler.
# future: cpp/ kotlin/ swift/
go/ the Go module `kjol` (go.mod lives here). Imports are `kjol/<pkg>`.
ALSO holds jsruntime/ — all the JS/TS. See below.
c/ C base layer (arena, strings, math, lexer, platform).
jai/ Jai modules. Early.
# future: kotlin/ swift/
```
Language-first, **not** feature-first. Consequence: the **web bundler is Go** and lives in
`go/webbundler` even though it builds `web/`. Don't "fix" this by splitting it.
**The JS tree lives inside `go/`, at `go/jsruntime`.** It has no Go in it beyond a doc
file — it is the Solid kit, the vendored Solid runtime, the FontAwesome SVGs and the
Tailwind `@theme` scaffold. It sits there because the thing that BUILDS it is Go
(`go/jsbundler`), the thing that styles it is Go (`go/tw`), and a sibling `web/` at the
repo root was one more directory the build had to go hunting for. `go build ./...`
ignores it; nothing imports it as a package.
Consequence, and don't "fix" it: the **web bundler is Go**. `tw` is the **Tailwind v4
compiler** and is deliberately NOT inside the bundler — Tailwind only reads text and
writes CSS, and the text is just as likely to be Go (the gowasm kit writes its markup in
Go and has no JS build at all). Keeping it in the bundler made every Go-only consumer
drag a JavaScript bundler along for a CSS file.
### go/ — module `kjol`
Packages, imported as `kjol/<name>`: `appenv basic chrono config csv dbutil finance httputil
l4g security snailmail validation webbundler tw`, plus the **gowasm** web-UI engine (`vdom`
l4g security snailmail validation jsbundler tw`, plus the **gowasm** web-UI engine (`vdom`
`wasmruntime` `rsc` `wasmdevserver`, and `webui` — a Tailwind-styled component kit ported
from `web/kit`; author components in pure Go compiled to WebAssembly; all stdlib-only), and
`cmd/{bundle,twcss,migrate,loc,passgen,typecheck,wasmgen}`. A runnable
example lives in `cmd/examples/go-wasm-web` (its own nested module so its go-chart dep stays
out of kjol).
from `jsruntime/uikit`; author components in pure Go compiled to WebAssembly; all
stdlib-only), and `cmd/{bundle,twcss,migrate,loc,passgen,typecheck,wasmgen}`.
`webbundler` is the **JS** build (TSX → Solid → esbuild). `tw` is the **Tailwind v4
compiler**, and it is deliberately NOT inside it: Tailwind only reads text and writes CSS,
and the text is just as likely to be Go — the gowasm kit writes its markup in Go and has no
JS build at all. Keeping it in the bundler made every Go-only consumer drag a JavaScript
bundler along for a CSS file.
`jsbundler` is the **JS** build (TSX → Solid → esbuild + the goja SSR bake). It was called
`webbundler`.
**`cmd/kjol-web` is the website**: the landing page and documentation for the whole
codebase, and the runnable example of both web layers. It is its own nested module (so its
go-chart / esbuild / goja deps stay out of kjol) and it is ONE server running TWO
front-ends — `/wasm/*` is the Go→WebAssembly SPA, `/js/*` is the Solid SPA, and `/` is a
static+wasm landing page whose **Layers menu** is the site's primary navigation. Its
README is the map. Anything user-visible you add to kjol should show up there, running.
Build / test (run from repo root):
```
@@ -75,34 +89,56 @@ signals, never inside a render closure. Floating panels share one positioning en
(`webui/position.go`, pure math, unit-tested natively) driven by the `Floating` controller
(`webui/floating.go`).
**Theming / dark mode.** The kit is themed by **semantic tokens**, not by a `dark:` variant on
every class: components say `bg-surface` / `border-line` / `text-ink` / `text-accent` and never
name a colour, so a theme is ten CSS variables rather than four hundred class strings. The app
must define them (see `webui.ThemeTokens` for the required set, and the example's `css/app.css`
for a working pair) plus `@custom-variant dark (&:where(.dark, .dark *));` — the built-in `dark`
variant is a `prefers-color-scheme` media query, which a site with its own switch cannot use.
Only genuinely *coloured* things (an alert's red tint) carry `dark:` variants. `webui.Theme` is
the controller (`Toggle`, `ThemeToggle`, `Init`); `webui.ThemeBootScript` goes in the document
head **before** the stylesheet, or dark-mode users get a white flash until the wasm loads. It is
the only JavaScript in a gowasm app.
**Theming / dark mode — BOTH kits, one vocabulary.** Neither kit names a colour: components say
`bg-surface` / `border-line` / `text-ink` / `text-accent`, and a `.dark` class on `<html>`
re-points what those mean. A theme is a dozen CSS variables rather than four hundred class
strings, and `dark:` on every component is exactly the thing to avoid. The Go and Solid kits use
the **same token names on purpose** — change `surface` once and both halves of a site move.
### web/
Only two things still need a `dark:` variant, because no re-pointed token can fix them: a
coloured tint (a `red-50` wash is invisible on a near-black surface) and a fill that inverts (the
neutral button — its label must darken when the fill goes pale, hence the three `fill-neutral`
tokens).
- `kit/` — Solid.js `.tsx` component kit. Apps import components as `@ui/*`.
`@custom-variant dark (&:where(.dark, .dark *));` is required — the built-in `dark` variant is a
`prefers-color-scheme` media query, which a site with its own switch cannot use (the OS says one
thing, the switch says another, and the media query wins). For gowasm the app defines the tokens
(see `webui.ThemeTokens`); for the Solid kit `jsruntime/styles/theme.css` defines them and the
bundler prepends it, so the app's `style.css` carries brand only.
Controllers: `webui.Theme` (Go) and `jsruntime/uikit/Theme.tsx` (Solid) — both read the same
`kjol-theme` localStorage key, so a preference survives crossing between two front-ends.
`webui.ThemeBootScript` goes in the document head **before** the stylesheet, or dark-mode users
get a white flash until the bundle loads. It is the only hand-written JavaScript in a gowasm app.
### go/jsruntime — the JS/TS tree
- `uikit/` — Solid.js `.tsx` component kit. Apps import components as `@ui/*`.
- `runtime/` — vendored Solid runtime + `vendor.json` (base entrypoints). The app merges its
own `vendor.json` (chart.js, pdf-lib, ...) on top; **kjol's solid-js must resolve first** so
there is a single reactive instance.
own `vendor.json` on top; **kjol's solid-js must resolve first** so there is a single reactive
instance (a split one does not error — it silently stops flushing effects).
`uikit/AutoTable.tsx` imports `pdf-lib` and `pdfjs-dist` at the TOP LEVEL, so any app using
AutoTable must vendor them or the bundle fails to evaluate at all.
- `icons/` — FontAwesome SVG source kit (the bundler scans usage and generates a per-app
registry; the generated file is app-owned, not committed here).
- `styles/``theme.css` (`@theme` scaffold + `:root` fa vars). Brand color/font tokens stay
app-side; the app's `style.css` `@import`s this.
registry; the generated file is app-owned, not committed here). kjol ships only the SUBSET its
own kit + `kjol-web` reference. An app's own `frontend/icons` is searched FIRST, so an app with
a fuller kit keeps it — see `jsbundler.iconsDirs`.
- `styles/theme.css` — the `@theme` scaffold, the semantic tokens, the `.dark` overrides, and the
`:root` fa vars. **It does the `@import "tailwindcss"`**, because the bundler PREPENDS it to the
app's `style.css` and an `@import` has to come first. An app adopting the shared tree therefore
drops that import from its own stylesheet and keeps only brand.
- `auth/ utils/ hooks/ ssr/ env.ts basic.ts finance.ts superfun.ts types.d.ts` — generic TS
scaffolding. Apps import as `@kjol/*`. (Concrete permission constants stay app-side.)
**Frontend import aliases** (resolved by the bundler and mirrored in each app's tsconfig
`paths`): `@ui/*``web/uikit`, `@kjol/*``web/`, `@appgen/*` → the app's generated dir
(e.g. the FA `faIcons` registry — app-owned, gitignored, regenerated each build). The kit's
own imports of sibling components stay relative (`./Buttons.tsx`).
`paths`): `@ui/*``go/jsruntime/uikit`, `@kjol/*``go/jsruntime/`, `@appgen/*` → the app's
generated dir (e.g. the FA `faIcons` registry — app-owned, gitignored, regenerated each build).
The kit's own imports of sibling components stay relative (`./Buttons.tsx`).
**Solid gotchas** (they bite every time): DOM handlers keep their DOM names — `onclick`,
`oninput`, `onchange`, *not* `onClick`. Everything is a named export. And Tailwind finds classes
by **scanning source for literal strings**, so `"bg-" + name` compiles to nothing — write the
class out in full.
## Consumption (per app)
@@ -130,6 +166,8 @@ own imports of sibling components stay relative (`./Buttons.tsx`).
| `dbutil.ConnConfig`, `snailmail.Settings` | DB / mail credentials injected, never read from app config |
| `appenv` | compile-time environment via build tags (`-tags staging` / `-tags production`); the bundler reads `appenv.Environment` for the JS `__ENV_TYPE__` define |
| `httputil.CorsMiddleware(CorsConfig{...})` | allowed domains + bundle-version source injected |
| `wasmdevserver` | the app injects `Build` / `Render` / `Document` / `Handle` via `Config` |
| `jsbundler` public pages | kjol generates the registry (`public_pages.gen.go`); the APP owns the `publicPage` type it is written against, and the document shell. See `cmd/kjol-web/internal/handlers`. |
## Stays app-side (never moves into kjol)