# @wrnexus/styles ## Reusable layers and presets Compose local or package foundations in order; later layers override earlier ones and the application has final base-config precedence: ```ts export default defineConfig({ extends: ["@workroot/wrnexus-enterprise", "./layers/company"], profiles: { production: { port: 8080 } }, }); ``` A directory layer exports `wrnexus.layer.ts` (JavaScript/MJS are supported). A package can provide that conventional file or declare `wrnexus.layer` in its `package.json`. Layers may extend other layers and carry the complete app configuration, including plugins that contribute layouts, components, routes, middleware, and migrations. `plugins` and `head` compose; other arrays intentionally replace earlier values. Cycles and missing/invalid entries fail with stable `WRN-CONFIG-LAYER-*` diagnostics. `wrnexus config --explain` lists every resolved layer source. > Global CSS bundling, the `--wire-*` design-token theme system, and the `wrnexus.config.ts` app-config loader for WrNexus apps. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. ## Overview This package owns three server-side concerns that shape every page a WrNexus app renders: 1. **Global stylesheet pipeline** — finds `app/styles/global.css` (or aggregates `app/styles/*.css`), bundles it with Bun's CSS bundler (which resolves `@import`, including from `node_modules`), and produces one stylesheet that is ``ed into every page's ``. Because it is a plain global sheet, it styles server-rendered markup and hydrated client islands identically. A custom `process` hook lets you swap in Tailwind / PostCSS / Sass. 2. **Theme system** — design tokens exposed as CSS custom properties (`--wire-`), with built-in `light`/`dark` sets, deep-merged user overrides, an SSR `` render (no flash), and a tiny client runtime to toggle/persist the choice. 3. **App config** — loads `wrnexus.config.ts` (the `AppConfig` type), applies named profile overrides, and loads the `.env` cascade. It runs server-side / at build time. Reach for it when configuring an app, defining themes, or customising how global CSS is produced. ## Installation ```bash bun add @wrnexus/styles ``` > Private package — the machine must be authenticated to the `wrnexus` npm org > (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported). ## API Everything is exported from the package root (`@wrnexus/styles`). ### Config loading | Export | Signature | Purpose | | ---------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `loadAppConfig` | `(appRoot: string, profile?: string) => Promise` | Load `wrnexus.config.*` with the active profile deep-merged in (`profiles` stripped from the result). | | `loadRawConfig` | `(appRoot: string) => Promise` | Load the raw config with the `profiles` map intact; returns `{}` if no config file exists. | | `resolveProfile` | `(options?: { explicit?; mode? }) => string` | Resolve the active profile: explicit arg > `WRNEXUS_PROFILE` env var > mode-based default (`production` in prod, else `development`). | | `loadEnv` | `(appRoot: string, profile: string) => Record` | Load the `.env` cascade for a profile into `process.env` without clobbering real env vars. Returns what it loaded. | | `headToString` | `(head?: string \| string[]) => string` | Flatten `AppConfig.head` into a single HTML string. | Config file names probed, in order: `wrnexus.config.ts`, `wrnexus.config.js`, `wrnexus.config.mjs`. `.env` cascade precedence (low → high): `.env` < `.env.` < `.env.local` < `.env..local`. Variables already present in the real environment always win. ### `AppConfig` The type of the object your `wrnexus.config.ts` default-exports. Every field is optional. | Field | Type | Description | | ----------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `head` | `string \| string[]` | Raw HTML appended to every page's `` (e.g. CDN stylesheet/script links). | | `seo` | `SeoConfig` | Global SEO defaults, merged with each page's exported `meta`. (from `@wrnexus/core`) | | `security` | `SecurityConfig` | Framework security headers and optional CORS policy. (from `@wrnexus/core`) | | `styles` | `StylesConfig` | Global stylesheet pipeline config (see below). | | `theme` | `ThemeConfig` | Design-token themes, deep-merged over the built-in light/dark. | | `i18n` | `{ default?: string; locales?: string[] }` | Default language + supported locales (strings live in `app/locales/*.json`). | | `db` | `{ driver: "sqlite" \| "postgres" \| "mysql" \| "mongo"; url: string }` | Default database connection; reached with `getDb()`. | | `databases` | `Record` | Additional named databases, reached with `getDb("")`; each has its own `app/db//` migrations/queries. | | `realtime` | `{ scale?: boolean; redisUrl?: string }` | When `scale` is true (or `redisUrl` is set), room broadcasts bridge over Redis pub/sub so they reach clients on every app process. | | `port` | `number` | Default server port. | | `profiles` | `Record>>` | Named profiles (dev, prod, uat, test, …). The active profile's overrides are deep-merged over the base config. Selected via `--profile=` or `WRNEXUS_PROFILE`. | ### Styles pipeline | Export | Signature | Purpose | | ---------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `findStyleEntry` | `(appDir, appRoot, override?) => string \| null` | Resolve the CSS entry: `override` (relative to `appRoot`) → `app/styles/global.css` → an aggregate of all `app/styles/*.css` (written to `app/.wrnexus/styles-entry.css`). `null` if the app has no styles. | | `bundleCss` | `(entryPath: string, mode: Mode) => Promise` | Bundle an entry with `Bun.build` (CSS bundler). Resolves `@import` (local + node_modules), handles nesting, minifies when `mode === "production"`. | | `renderStyles` | `(ctx: StyleProcessContext, styles?: StylesConfig) => Promise` | Produce final CSS: runs `styles.process(ctx)` if provided, else `bundleCss`. Returns `""` when `ctx.entryPath` is null. | `StylesConfig`: ```ts interface StylesConfig { /** CSS entry path relative to the app root. Default: app/styles/global.css */ entry?: string; /** Custom processor — return the final CSS string (Tailwind/PostCSS/Sass). */ process?: (ctx: StyleProcessContext) => string | Promise; } interface StyleProcessContext { entryPath: string | null; // resolved absolute CSS entry, or null appDir: string; appRoot: string; mode: Mode; // "development" | "production" } ``` ### Theme system | Export | Type / Signature | Purpose | | -------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `DEFAULT_THEMES` | `Record` | Built-in `light` and `dark` token maps. | | `THEME_COOKIE` | `"wire-theme"` | Cookie the resolved theme is read from / persisted to. | | `THEME_CSS_HREF` | `"/__wrnexus/theme.css"` | URL the generated theme stylesheet is served at. | | `THEME_JS_HREF` | `"/__wrnexus/theme.js"` | URL the client theme runtime is served at. | | `resolveThemeConfig` | `(config?: ThemeConfig) => ResolvedTheme` | Deep-merge the user's `theme` config over the defaults; pick the default theme (config's `default` if valid, else `dark`, else the first). | | `resolveThemeName` | `(cookieValue: string \| undefined, theme: ResolvedTheme) => string` | Pick a valid theme name from a cookie, falling back to `theme.default`. | | `renderThemeCss` | `(theme: ResolvedTheme) => string` | Generate the theme stylesheet: a `:root{…}` default plus one `[data-theme=""]{…}` block per theme. | | `renderThemeRuntime` | `(theme: ResolvedTheme) => string` | Generate the client runtime (see below). | Tokens are emitted as `--wire-` custom properties, **except** the reserved key `color-scheme`, which is emitted as the native `color-scheme` CSS property so form controls and scrollbars match the theme. `ThemeConfig` / `ThemeTokens` / `ResolvedTheme`: ```ts type ThemeTokens = Record; interface ThemeConfig { palette?: ThemePaletteName | CustomThemePalette; default?: string; // theme used when no cookie is present themes?: Record; // deep-merged over built-in light/dark } interface ResolvedTheme { default: string; names: string[]; themes: Record; } ``` Built-in palettes are `blue`, `indigo`, `violet`, `emerald`, `cyan`, `rose`, `amber`, and `slate`. Each supplies primary, secondary, info, success, warning, danger/error, hover, and contrast colors to every light/dark theme. Built-in token keys also include surfaces, text, borders, radii, fonts, and shadows. A custom palette is intentionally complete, so components never fall back to an unrelated blue status or action color: ```ts theme: { palette: { primary: "#7c3aed", primaryHover: "#6d28d9", primaryContrast: "#ffffff", secondary: "#db2777", secondaryHover: "#be185d", secondaryContrast: "#ffffff", info: "#2563eb", success: "#059669", warning: "#d97706", danger: "#dc2626", }, } ``` The client runtime (`renderThemeRuntime`) exposes `window.wireTheme` with `{ get, set, toggle, bind, themes }`, wires up any `[data-wire-theme-toggle]` and `[data-wire-theme-set]` elements on load, and persists the choice to the `wire-theme` cookie (`max-age` 1 year, `samesite=lax`). `toggle()` cycles through the configured theme names in order. ## Usage ### `wrnexus.config.ts` ```ts import type { AppConfig } from "@wrnexus/styles"; export default { head: [ '', ], port: 3000, db: { driver: "sqlite", url: "app.db" }, theme: { palette: "violet", default: "dark", themes: { light: { "color-primary": "#7c3aed" }, // override one token; rest inherited brand: { // add a whole new theme "color-scheme": "dark", "color-bg": "#0a0a0a", "color-primary": "#22d3ee", }, }, }, styles: { entry: "app/styles/main.css", }, profiles: { production: { db: { driver: "postgres", url: process.env.DATABASE_URL! }, }, }, } satisfies AppConfig; ``` ### Loading config + producing CSS ```ts import { loadAppConfig, resolveProfile, loadEnv, findStyleEntry, renderStyles, } from "@wrnexus/styles"; const appRoot = process.cwd(); const mode = "production" as const; const profile = resolveProfile({ mode }); loadEnv(appRoot, profile); const config = await loadAppConfig(appRoot, profile); const appDir = `${appRoot}/app`; const entryPath = findStyleEntry(appDir, appRoot, config.styles?.entry); const css = await renderStyles({ entryPath, appDir, appRoot, mode }, config.styles); ``` ### Rendering the theme ```ts import { resolveThemeConfig, resolveThemeName, renderThemeCss, renderThemeRuntime, THEME_COOKIE, } from "@wrnexus/styles"; const theme = resolveThemeConfig(config.theme); // Server: pick the active theme from the request cookie (no flash). const active = resolveThemeName(cookies[THEME_COOKIE], theme); // → render const themeCss = renderThemeCss(theme); // served at THEME_CSS_HREF const themeJs = renderThemeRuntime(theme); // served at THEME_JS_HREF ``` In templates, consume tokens via the custom properties: ```css .card { background: var(--wire-color-surface); color: var(--wire-color-text); border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius); box-shadow: var(--wire-shadow-1); } ``` ```html ``` ## Requirements / Notes - **Bun-only.** `bundleCss` uses `Bun.build`'s CSS bundler for `@import` resolution, nesting, and minification. Node is not supported. - Config and env loading use `node:fs` / `node:path` / `node:url` and read from `process.env`. - Peer package: `@wrnexus/core` supplies the `SeoConfig` and `SecurityConfig` types referenced by `AppConfig`. - The bundled global stylesheet, the theme stylesheet (`THEME_CSS_HREF`), and the theme runtime (`THEME_JS_HREF`) are wired into pages by the framework's server; this package only produces their contents.