restructure theme css

This commit is contained in:
2026-07-17 16:58:16 -04:00
parent 35f0ea6db3
commit 03b5bea72d
11 changed files with 117 additions and 166 deletions

View File

@@ -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 <html>, 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.) */

View File

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

View File

@@ -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

View File

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

View File

@@ -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") }

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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 <html>, 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;

View File

@@ -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) {

View File

@@ -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) }