208 lines
12 KiB
Markdown
208 lines
12 KiB
Markdown
# 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 build root
|
|
|
|
Each top-level directory is one language / build root:
|
|
|
|
```
|
|
kjol/
|
|
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/
|
|
```
|
|
|
|
**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/<name>`: `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 `<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 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`.
|