Files
kjol/CLAUDE.md

6.8 KiB

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 <app>/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 "<app>/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 language

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/

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.

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 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).

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.

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) 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).

web/

  • kit/ — 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.
  • 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 @imports this.
  • 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).

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

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.