diff --git a/CLAUDE.md b/CLAUDE.md index 76ad2851..d3e00fcc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,6 +43,13 @@ writes CSS, and the text is just as likely to be Go (the gowasm kit writes its m 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 @@ -125,8 +132,8 @@ 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 `jsruntime/styles/theme.css` defines them and the -bundler prepends it, so the app's `style.css` carries brand only. +(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. @@ -145,10 +152,6 @@ get a white flash until the bundle loads. It is the only hand-written JavaScript 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`. -- `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.) diff --git a/go/cmd/kjol-website/css/app.css b/go/cmd/kjol-website/css/app.css index 1d663b7c..661ed955 100644 --- a/go/cmd/kjol-website/css/app.css +++ b/go/cmd/kjol-website/css/app.css @@ -1,17 +1,17 @@ -@import "tailwindcss"; - /* --------------------------------------------------------------------------- - Dark mode: `dark:` as a CLASS, not a media query. + kjol-website — brand stylesheet for the Kjol Go/WASM section (/wasm/*). --------------------------------------------------------------------------- - Tailwind's built-in dark variant follows the operating system. A site with its own - theme switch cannot use it: the OS says one thing, the switch says another, and the - media query wins — so the switch appears to do nothing. + There is deliberately no `@import "tailwindcss"` and no `@custom-variant dark` + here. twcss compiles this file via tw.CompileAppFiles, which prepends kjol's + shared extension layer (tw/kjol_theme.css) — the import, the class-based dark + variant, the semantic tokens (surface/line/ink/…), and the chart palette. This + file carries only what is genuinely this app's: the brand. - This redefines it against a class on , which webui.Theme toggles. The OS is - still respected: it is the DEFAULT (see the boot script in server/main.go), just no - longer the last word. + The values below match the /js side's frontend/css/style.css on purpose — same + Open Sans, same navy accent — so that crossing between /wasm and /js reads as two + parts of ONE site rather than two demos that happen to share a domain. The two + sections are built by completely different pipelines; they should not look like it. --------------------------------------------------------------------------- */ -@custom-variant dark (&:where(.dark, .dark *)); /* --------------------------------------------------------------------------- Open Sans — self-hosted (files in wwwroot/fonts). One variable file per subset @@ -53,13 +53,9 @@ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; } -/* App-side design tokens the webui kit references (Tailwind v4 @theme). Brand - values live with the app; the kit stays generic. - - The surface/line/ink tokens are the kit's THEME CONTRACT (see webui.ThemeTokens): - components say bg-surface / border-line / text-ink and never name a colour, so the - whole kit changes theme by changing these ten values rather than by carrying a dark: - variant on four hundred class strings. */ +/* The brand — the values the shared kit's tokens are re-pointed to. Everything the + kit already defines (surfaces, lines, ink, the chart palette, the state colours) + comes from tw/kjol_theme.css; this block sets only what is this site's own. */ @theme { --radius-default: 0.375rem; @@ -71,8 +67,7 @@ /* Navy and red — the flag, muted. kjøl is a Norwegian word and the palette says so. Both are dark and low-key: the page is mostly prose, code and tables, and the brand's - job is to mark the few things you can act on, not to compete with them for attention. - (The previous sky blue did the same job, but said nothing.) */ + job is to mark the few things you can act on, not to compete with them for attention. */ --color-primary: #1e3a63; /* muted navy — FILLS; they carry white text */ --color-primary-hover: #16294a; --color-primary-subtle: #eef2f8; /* a navy wash — tinted panels, badges, callouts */ @@ -82,44 +77,12 @@ separate token from primary, because the two have opposite constraints: a fill must be dark enough for white text on TOP of it, and accent text must be readable ON the surface. Here they are the same hue — navy — but not the same value: the accent is a - touch deeper so a navy link on white is unmistakably a link. (The red is gone; the - brand is navy throughout now. Only the flag keeps its red field.) */ + touch deeper so a navy link on white is unmistakably a link. */ --color-accent: #1c3a66; /* navy — accent TEXT */ - /* Surfaces, lines, ink — the kit's theme contract. */ - --color-surface: #ffffff; - --color-surface-muted: #fafafa; - --color-surface-raised: #f5f5f5; - --color-surface-strong: #e5e5e5; - --color-line: #e5e5e5; - --color-line-strong: #d4d4d4; - --color-ink: #171717; - --color-ink-soft: #525252; - --color-ink-muted: #737373; - --color-ink-faint: #a3a3a3; - + /* Heading ink — a touch stronger than body ink. Not part of the shared contract, so + the site names it here and re-points it in the .dark block below. */ --color-text-heading: #111827; - --color-text-on-dark: #f9fafb; - --color-text-on-dark-muted: #9ca3af; - - /* The categorical CHART palette (webui/chart.go) and the sequential choropleth ramp - (webui/usheatmap.go) — the SAME tokens the Solid kit names in jsruntime/styles/ - theme.css, so a chart looks identical on both front-ends. Marks name these as raw - CSS variables in the SVG they draw. Dark values in the .dark block below. */ - --color-chart-1: #2a78d6; - --color-chart-2: #1baf7a; - --color-chart-3: #eda100; - --color-chart-4: #008300; - --color-chart-5: #4a3aa7; - --color-chart-6: #e34948; - --color-chart-7: #e87ba4; - --color-chart-8: #eb6834; - --color-choropleth-1: #dbe9fb; - --color-choropleth-2: #b3d0f6; - --color-choropleth-3: #85b3ee; - --color-choropleth-4: #5591e4; - --color-choropleth-5: #2f6fca; - --color-choropleth-6: #124f8f; } /* --------------------------------------------------------------------------- @@ -139,37 +102,15 @@ --grid-line: rgba(30, 58, 99, 0.06); /* the navy, at the edge of visible */ } -/* --------------------------------------------------------------------------- - The dark theme. +/* The dark values for the brand. --------------------------------------------------------------------------- - Only the token VALUES change. Not one component knows this block exists — they ask - for bg-surface and text-ink, and here is where those come to mean something else. - - This is a plain rule, not another @theme block: @theme generates utilities, and these - are overrides of utilities that already exist. - - The surfaces are not pure black. Black gives a dark UI a hard, glaring edge against - white text and makes every border invisible; a very dark grey leaves room for the - raised surfaces and lines above it to actually be seen. --------------------------- */ + The shared surfaces/lines/ink re-point themselves in tw/kjol_theme.css's own .dark + block; this one moves only the site's brand. Navy is too dark to read on a near-black + page, so both brand tokens climb to a lighter blue. The accent (TEXT) climbs furthest, + to a soft sky a link stays legible in; the fill climbs less, to a steel blue that still + looks like a button and still carries white text. The tinted panel inverts outright, + because a pale wash on #101013 is not a tint, it is a white box. */ .dark { - --color-surface: #101013; - --color-surface-muted: #17171b; - --color-surface-raised: #1f1f24; - --color-surface-strong: #2c2c33; - --color-line: #2a2a30; - --color-line-strong: #3d3d45; - --color-ink: #f2f2f3; - --color-ink-soft: #c6c6cc; - --color-ink-muted: #9a9aa3; - --color-ink-faint: #71717a; - - /* Both brand tokens move in the dark, and both for the same reason now: navy is too dark - to read on a near-black page, so each climbs to a lighter blue. - - The accent (TEXT) climbs furthest — a link has to be legible at body-text weight, so it - goes to a soft sky. The fill climbs less: it only has to look like a button and still - carry white text (~7:1), so it lifts to a steel blue and stops there, well below where - the accent lands. */ --color-accent: #9fc1ec; --color-primary: #2b4f80; --color-primary-hover: #37619b; @@ -180,30 +121,13 @@ /* The grid is drawn in ink, not in shadow, once the page is dark. */ --grid-line: rgba(226, 232, 240, 0.05); - - /* Chart palette re-stepped for the dark surface; choropleth inverts its lightness - direction so a high value reads as brighter. Mirrors the Solid scaffold's .dark. */ - --color-chart-1: #3987e5; - --color-chart-2: #199e70; - --color-chart-3: #c98500; - --color-chart-4: #008300; - --color-chart-5: #9085e9; - --color-chart-6: #e66767; - --color-chart-7: #d55181; - --color-chart-8: #d95926; - --color-choropleth-1: #1b2a44; - --color-choropleth-2: #21406c; - --color-choropleth-3: #2c5f97; - --color-choropleth-4: #3f80c8; - --color-choropleth-5: #649de8; - --color-choropleth-6: #93c2f7; } -/* The page's own background — painted before the app mounts, and behind it afterwards. - Without this, a dark app sits in a white window. */ +/* Only the font. The page's background and text colour come from the shared layer's + `html` rule, which paints them from --color-surface / --color-ink — the tokens the + .dark block re-points. Setting them here would pin the page to white and leave a dark + app sitting in a white window. */ html { - background-color: var(--color-surface); - color: var(--color-ink); font-family: var(--font-sans); } @@ -258,7 +182,3 @@ html { -webkit-mask-image: linear-gradient(to bottom, #000, #000 35%, transparent 100%); mask-image: linear-gradient(to bottom, #000, #000 35%, transparent 100%); } - -/* (The hero glow that used to live here went with the hero. A coloured wash behind an - oversized headline is the most recognisable gesture in framework marketing, and this - page is not making that argument any more.) */ diff --git a/go/cmd/kjol-website/frontend/css/style.css b/go/cmd/kjol-website/frontend/css/style.css index b4eb4561..e2591010 100644 --- a/go/cmd/kjol-website/frontend/css/style.css +++ b/go/cmd/kjol-website/frontend/css/style.css @@ -1,10 +1,10 @@ /* --------------------------------------------------------------------------- kjol-website — brand stylesheet for the Kjol JS Web section (/js/*). --------------------------------------------------------------------------- - There is deliberately no `@import "tailwindcss"` here. The bundler PREPENDS - kjol's shared scaffold (go/jsruntime/styles/theme.css) to this file, and that - scaffold does the import — an @import has to come first, and this file no - longer is. See jsbundler/css.go and the header of theme.css. + There is deliberately no `@import "tailwindcss"` here. The bundler compiles this + file via tw.CompileApp, which PREPENDS kjol's shared extension layer + (tw/kjol_theme.css); that layer does the import — an @import has to come first, + and this file no longer is. See jsbundler/css.go and the header of kjol_theme.css. What is left is only what is genuinely this app's: the brand. diff --git a/go/cmd/kjol-website/server/main.go b/go/cmd/kjol-website/server/main.go index 0b7d0e59..5f92087f 100644 --- a/go/cmd/kjol-website/server/main.go +++ b/go/cmd/kjol-website/server/main.go @@ -44,8 +44,9 @@ func main() { "app", "wasm", "css", // the Go/WASM app "frontend", // the Solid app — a .tsx save rebuilds the JS bundle "../../webui", "../../wasmruntime", // the wasm engine (vdom + rsc now live UNDER wasmruntime, and the watcher walks subdirs) - "../../lexer", // the code-block highlighter - "../../jsruntime/uikit", "../../jsruntime/styles", // the Solid kit + the shared theme + "../../lexer", // the code-block highlighter + "../../jsruntime/uikit", // the Solid kit + "../../tw", // the Tailwind engine + the shared kjol extension layer (kjol_theme.css) }, Build: build.All, BuildCSS: build.Tailwind, // a .css save skips codegen+wasm and hot-swaps the stylesheet diff --git a/go/cmd/twcss/main.go b/go/cmd/twcss/main.go index c409675a..4b6aa5e0 100644 --- a/go/cmd/twcss/main.go +++ b/go/cmd/twcss/main.go @@ -1,8 +1,12 @@ -// Command twcss compiles a Tailwind v4 stylesheet with kjol's native engine, -// scanning explicit content globs for utility candidates. Unlike the app bundler -// (which is wired to the frontend tree) it takes the entry, output, and content -// globs as flags/args, so it works for markup authored in any language — used by -// the kjol-website site, whose Go/WASM half writes its UI in Go. +// Command twcss compiles a kjol app's Tailwind stylesheet with kjol's native +// engine, scanning explicit content globs for utility candidates. Unlike the app +// bundler (which is wired to the frontend tree) it takes the entry, output, and +// content globs as flags/args, so it works for markup authored in any language — +// used by the kjol-website site, whose Go/WASM half writes its UI in Go. +// +// It compiles via tw.CompileAppFiles, so the -entry stylesheet is layered onto +// kjol's shared extension layer: base Tailwind → kjol extensions → this stylesheet. +// The entry therefore carries only brand and names none of the shared tokens. // // Usage (globs are relative to -base; pass "**" for a recursive walk): // @@ -32,7 +36,7 @@ func main() { fmt.Fprintln(os.Stderr, "twcss:", err) os.Exit(1) } - css, err := tw.CompileFiles(string(src), *base, flag.Args()) + css, err := tw.CompileAppFiles(string(src), *base, flag.Args()) if err != nil { fmt.Fprintln(os.Stderr, "twcss:", err) os.Exit(1) diff --git a/go/jsbundler/config.go b/go/jsbundler/config.go index 9ce0bcb5..6774d5ee 100644 --- a/go/jsbundler/config.go +++ b/go/jsbundler/config.go @@ -94,14 +94,5 @@ func vendorDirs() []string { return []string{filepath.Join(frontendDir, "vendor")} } -// themeCSSPath is the shared Tailwind @theme scaffold prepended to the app's -// brand style.css, or "" in single-tree mode (the app's style.css is complete). -func themeCSSPath() string { - if webDir != "" { - return filepath.Join(webDir, "styles", "theme.css") - } - return "" -} - // faOutPath is where the generated FA registry is written (app-owned). func faOutPath() string { return filepath.Join(genTSDir, "faIcons.ts") } diff --git a/go/jsbundler/css.go b/go/jsbundler/css.go index 78599423..a4f984f1 100644 --- a/go/jsbundler/css.go +++ b/go/jsbundler/css.go @@ -78,17 +78,10 @@ func compileCSSBundle(label string, twSources []string, outName string) (bundleS kitCands := tw.Scan(uikitDir(), []string{"**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"}) candidates = dedupStrings(append(candidates, kitCands...)) - // Prepend the shared @theme scaffold (kjol jsruntime/styles/theme.css) ahead of the - // app's brand style.css so its tokens/vars are in scope. Absent in single-tree - // mode (the app's style.css is already complete). - input := string(src) - if tp := themeCSSPath(); tp != "" { - if theme, e := os.ReadFile(tp); e == nil { - input = string(theme) + "\n" + input - } - } - - compiled, count, err := tw.Compile(input, cssDir, candidates) + // CompileApp prepends kjol's shared extension layer (tokens, the class-based dark + // variant, the chart palette) ahead of the app's brand style.css, so the app + // stylesheet carries only brand: base Tailwind → kjol extensions → app brand. + compiled, count, err := tw.CompileApp(string(src), cssDir, candidates) if err != nil { return bundleStats{}, fmt.Errorf("tailwind compile (%s): %w", label, err) } diff --git a/go/jsruntime/doc.go b/go/jsruntime/doc.go index 340b6d25..40dd6427 100644 --- a/go/jsruntime/doc.go +++ b/go/jsruntime/doc.go @@ -1,6 +1,10 @@ // Package jsruntime is the JS/TS tree of kjol: the Solid component kit, the -// vendored Solid runtime, the FontAwesome SVG source kit, the shared Tailwind -// @theme scaffold, and the generic TS scaffolding (auth, hooks, ssr, utils). +// vendored Solid runtime, the FontAwesome SVG source kit, and the generic TS +// scaffolding (auth, hooks, ssr, utils). +// +// The shared Tailwind design tokens are NOT here — they live in kjol/tw as the +// extension layer (tw/kjol_theme.css), which the Tailwind engine layers between +// its own defaults and an app's brand. See tw.CompileApp. // // It holds no Go beyond this file. The package exists so the tree has a stable // home inside the Go module rather than a sibling directory the build has to go @@ -26,9 +30,6 @@ // per-app registry (@appgen/faIcons); the registry is app-owned and is // not committed here. 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 + :root fa vars, prepended to the -// app's brand style.css by the bundler. Brand color/font tokens stay -// app-side. // // The rest (auth/ hooks/ ssr/ utils/ env.ts basic.ts finance.ts superfun.ts) is // generic TS scaffolding; apps import it as @kjol/*. Concrete permission diff --git a/go/jsruntime/styles/theme.css b/go/tw/kjol_theme.css similarity index 88% rename from go/jsruntime/styles/theme.css rename to go/tw/kjol_theme.css index d90978d1..5569eb60 100644 --- a/go/jsruntime/styles/theme.css +++ b/go/tw/kjol_theme.css @@ -1,16 +1,22 @@ /* --------------------------------------------------------------------------- - kjol — shared Tailwind v4 scaffold for the JS (Solid) kit. + kjol — the shared Tailwind extension layer. --------------------------------------------------------------------------- - The bundler PREPENDS this file to the app's own css/style.css (see - jsbundler/css.go), so it is the first thing the Tailwind engine sees. That is - why the `@import "tailwindcss"` lives here rather than in the app's stylesheet: - an @import has to come first, and the app's file no longer is. + Three layers stack to make a kjol app's stylesheet, in this order: - An app adopting the shared tree therefore DROPS `@import "tailwindcss"` from - its own style.css and keeps only what is genuinely its own — its brand palette, - its fonts, its one-off rules. In single-tree mode (WebDir unset) this file is - not prepended at all and the app's style.css stays complete, which is why - cdrateline and Hotlap still import Tailwind themselves. + base Tailwind (tw_theme.css + tw_preflight.css) the framework defaults + kjol extensions (THIS FILE) the shared design system + app brand (the app's own style.css) colours, fonts, one-offs + + tw.CompileApp / tw.CompileAppFiles prepend this file to the app's entry + stylesheet, so the Tailwind engine sees it before the app's brand. That is why + `@import "tailwindcss"` lives here rather than in the app's stylesheet: an + @import has to come first, and the app's file no longer is. A kjol app therefore + carries only brand — its palette, its fonts, its own rules — and names none of + the tokens below; it inherits them. + + The vocabulary here is shared with the Go/WASM kit (webui.ThemeTokens) on + purpose: two kits, one contract. A designer changes `surface` once and both + layers of a site move together. --------------------------------------------------------------------------- */ @import "tailwindcss"; @@ -23,8 +29,9 @@ the media query wins, and the button appears to do nothing. This redefines the variant against a class on , which the Theme controller - (uikit/Theme.tsx) toggles. The OS is still respected — it is the DEFAULT, applied - by the boot script before first paint — just no longer the last word. + (uikit/Theme.tsx, webui.Theme) toggles. The OS is still respected — it is the + DEFAULT, applied by the boot script before first paint — just no longer the last + word. --------------------------------------------------------------------------- */ @custom-variant dark (&:where(.dark, .dark *)); @@ -118,9 +125,9 @@ they sit on a dark bar or panel in an otherwise light page — so these stay dark in BOTH themes and are not overridden below. - They were named by the components (bg-dark, text-text-on-dark, …) and - defined by nobody, so until now every one of them compiled to no colour at - all and the onDark paths silently rendered unstyled. */ + They are named by the components (bg-dark, text-text-on-dark, …); defining + them here is what makes those onDark paths render styled rather than with no + colour at all. */ --color-dark: #101013; --color-dark-raised: #1f1f24; --color-text-on-dark: #f9fafb; diff --git a/go/tw/tailwind.go b/go/tw/tailwind.go index 66d844b6..e52bd3d8 100644 --- a/go/tw/tailwind.go +++ b/go/tw/tailwind.go @@ -1269,6 +1269,14 @@ var defaultThemeCSS string //go:embed tw_preflight.css var defaultPreflightCSS string +// kjolThemeCSS is the shared kjol extension layer — the semantic design tokens +// (surface/line/ink/...), the class-based dark variant, the chart palette and the +// :root icon vars. It sits between Tailwind's defaults and an app's brand, and is +// prepended to the app entry stylesheet by CompileApp / CompileAppFiles. +// +//go:embed kjol_theme.css +var kjolThemeCSS string + func parseThemeOptions(params string) ThemeOptions { o := themeNone for _, f := range strings.Fields(params) { diff --git a/go/tw/tw.go b/go/tw/tw.go index 6f2de312..584aeb57 100644 --- a/go/tw/tw.go +++ b/go/tw/tw.go @@ -55,5 +55,28 @@ func CompileFiles(entryCSS, baseDir string, sourceGlobs []string) (string, error return Minify(compiled) } +// CompileApp is Compile with kjol's shared extension layer prepended to entryCSS — +// the layering every kjol front-end (Solid and gowasm) shares: +// +// base Tailwind → kjol extensions (kjol_theme.css) → the app's brand entryCSS +// +// The kjol layer does the `@import "tailwindcss"` and defines the semantic tokens, +// the class-based dark variant and the chart palette, so entryCSS carries only +// brand. Compile itself stays a plain Tailwind engine (no kjol tokens) for callers +// that want exactly that. +func CompileApp(entryCSS, baseDir string, candidates []string) (css string, utilities int, err error) { + return twCompile(kjolThemeCSS+"\n"+entryCSS, baseDir, candidates) +} + +// CompileAppFiles is Scan + CompileApp + minify: CompileFiles with the kjol +// extension layer prepended. +func CompileAppFiles(entryCSS, baseDir string, sourceGlobs []string) (string, error) { + compiled, _, err := CompileApp(entryCSS, baseDir, scanSources(baseDir, sourceGlobs)) + if err != nil { + return "", err + } + return Minify(compiled) +} + // Minify shrinks compiled CSS. func Minify(css string) (string, error) { return minifier.String("text/css", css) }