# kjol `kjol` ("keel" in Norwegian) is a **shared, multilingual base layer** factored out of the user's applications so they stay in sync. It is consumed as a **git submodule** inside each app (at `/kjol`). Current consumers: `cdrateline.com_2.0` and `Hotlap` — near-identical forks it was extracted from. Scope will grow to more projects and languages. ## Golden rules 1. **The framework NEVER imports application code.** Wherever kjol needs app-specific behavior, the app injects it (interfaces, registration functions, config structs, callbacks). See **Coupling inversions** below. If you find yourself wanting to `import "/internal/..."` from kjol, invert it instead. 2. **Never run git operations here on the user's behalf.** The user creates the submodule and makes all commits. You may edit files, build, and test. 3. kjol is edited **in place** via the submodule + `go.work` — there is no publish / `go get` 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 build root Each top-level directory is one language / build root: ``` kjol/ go/ the Go module `kjol` (go.mod lives here). Imports are `kjol/`. 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/ ``` **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. `tw` also owns the **shared design system** as CSS, in three files: `tw_theme.css` and `tw_preflight.css` are Tailwind's own defaults, and `tw/kjol_theme.css` is the kjol extension layer — the semantic tokens (`surface`/`line`/`ink`/…), the class-based `dark` variant and the chart palette that BOTH kits name. `tw.CompileApp` stacks them: base Tailwind → kjol extensions → the app's brand `style.css`. So neither kit ships the tokens in its own tree; both inherit them from the compiler, and an app carries only brand. ### go/ — module `kjol` Packages, imported as `kjol/`: `appenv basic chrono config csv dbutil finance httputil l4g lexer security snailmail validation jsbundler tw`, plus the **gowasm** web-UI engine (`wasmruntime` and its sub-packages `wasmruntime/vdom` + `wasmruntime/rsc`, `wasmdevserver`, and `webui` — a Tailwind-styled component kit ported from `jsruntime/uikit`; author components in pure Go compiled to WebAssembly; all stdlib-only), and `cmd/{bundle,twcss,migrate,loc,passgen,typecheck,wasmgen}`. `vdom` and `rsc` are **sub-packages of `wasmruntime`** — imported as `kjol/wasmruntime/vdom` and `kjol/wasmruntime/rsc` (the package names stay `vdom` / `rsc`, so call sites are still `vdom.VNode`, `rsc.Mount`). They nest cleanly because `vdom` imports nothing and both `rsc` and `wasmruntime` import only `vdom` — no child imports the parent, so there is no cycle. `wasmgen` emits these import paths in the `*.gen.go` glue, so if they ever move again, its templates move with them. `jsbundler` is the **JS** build (TSX → Solid → esbuild + the goja SSR bake). It was called `webbundler`. `lexer` is syntax highlighting: source in, HTML with coloured spans out, `Highlight(lang, src)` plus `HighlightGo` / `HighlightC`. It is a lexer and not a parser on purpose — it degrades to escaped plain text rather than failing, and an unknown language is not an error. It is outside `webui` because it touches no DOM. **Its output is Tailwind class names**, so any stylesheet that has to render a code block must scan `lexer/**/*.go` — a build that forgets to still compiles and just renders the snippet unstyled (see `cmd/kjol-website/build.Tailwind`). The C tables deliberately mirror `c/lexer/lexer_c.c`; add a type to one, add it to the other. **`cmd/kjol-website` 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. The kit is documented on ONE page per layer (`/wasm/components`, `/js/components`), not one page per source file — a reader hunting for a date picker should not have to guess whether it was filed under forms or overlays. `componentGroups()` / `COMPONENT_GROUPS` is the single list that drives the sections, the sidebar that jumps to them, and the index. Build it with `go run ./server -build`; the steps live in the `build` package (a library, because the server imports it). Build / test (run from repo root): ``` go -C go build ./... go -C go vet ./... go -C go test ./... ``` **The wasm host API** (`wasmruntime/host.go` + `host_wasm.go` / `host_native.go`) is how neutral component code reaches the browser: element measurement (`Measure`, `Viewport`), imperative style writes (`SetStyle` — positioning must NOT go through signals, which re-render the whole tree), the post-commit hook (`AfterRender`, the only point at which a just-rendered element can be measured), document/window listeners, timers, localStorage, and file download. It is **dual-build**: real under `js && wasm`, no-op stubs natively — which is what lets `webui` call it unconditionally and still SSR. Refs come from `vdom.Ref` + `vdom.WithRef`; `vdom.Portal` mounts children at `document.body` (needed to escape `overflow:hidden` / `transform` ancestors). The reconciler is wasm-only and needs a DOM, so `go test ./...` cannot reach it. It has its own harness — a minimal DOM under node: ``` GOOS=js GOARCH=wasm go -C go test -exec="node testdata/domexec.js" ./wasmruntime ``` `webui` components that measure the page (Tooltip, Popover, Menu, Modal, DatePicker, Tutorial, AutoTable, SignaturePad, AsyncCombobox) are **controllers**: create them once alongside your signals, never inside a render closure. Floating panels share one positioning engine (`webui/position.go`, pure math, unit-tested natively) driven by the `Floating` controller (`webui/floating.go`). **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 `` 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 layers of a site move. 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). `@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 `tw/kjol_theme.css` defines them and the Tailwind engine prepends it (`tw.CompileApp`), 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` 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). kjol ships only the SUBSET its own kit + `kjol-website` reference. An app's own `frontend/icons` is searched FIRST, so an app with a fuller kit keeps it — see `jsbundler.iconsDirs`. - `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/*` → `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) - `go.work` at the app root: ``` use ( . ./kjol/go ) ``` then vendor with `go work vendor` (not `go mod vendor`). - Startup wiring the app performs (this is how the inversions get their app-side halves): `dbutil.RegisterAll(models.Tables)`, `l4g.SetDatabaseWriter(...)`, `dbutil.Init(dbutil.ConnConfig{...})`, `snailmail.Configure(snailmail.Settings{...})`, a thin `config.Load` wrapper over `config.Load[T]`, and an app-side `internal/httpauth` for the session/authn middleware. ## Coupling inversions (how the framework stays app-agnostic) | Package | Inversion | |---|---| | `dbutil` | table names via `Register`/`RegisterAll` (not the app's `models.Tables`) | | `l4g` | owns the `Entry` type; DB persistence via `SetDatabaseWriter(func(Entry) error)` | | `config` | generic `Load[T](file, *T) error`; each app defines its own config struct | | `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-website/internal/handlers`. | ## Stays app-side (never moves into kjol) Domain models/repository/handlers-api, migrations, pages/routes/layouts, brand UI (`TopBar`/`AppSidebar`/`TransitionOverlay`), concrete permission constants, the app config struct, `embed.go` + `wwwroot/`, and generated artifacts (`faIcons` registry, `routes.gen.ts`, `public_pages.gen.go`). ## Provenance Big-bang extraction from cdrateline + Hotlap. The detailed migration plan lives on the author's machine at `~/.claude/plans/foamy-humming-tulip.md`.