# @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 `--wrn-*` 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 ### Production CSS inlining `styles.inlineMaxBytes` controls the maximum uncompressed size of the combined production stylesheet to embed in the document. The default is 4096 bytes; `0` disables inlining. A measured first-visit optimization can use, for example, `styles: { inlineMaxBytes: 128 * 1024 }`. Larger limits remove a blocking stylesheet request but repeat the CSS in each HTML response. Stylesheets containing relative asset URLs stay external so their URL base does not change. Theme switching and the separately served stylesheet remain available. 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 (`--wrn-`), 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` | `"wrn-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 `--wrn-` 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.wrnTheme` with `{ get, set, toggle, bind, themes }`, connects up any `[data-wrn-theme-toggle]` and `[data-wrn-theme-set]` elements on load, and persists the choice to the `wrn-theme` cookie (`max-age` 1 year, `samesite=lax`). `toggle()` cycles through the configured theme names in order. ### Shared browser cookies Set one cookie policy for theme, accent, language, and application cookies: ```ts export default { cookies: { defaults: { domain: "example.com", path: "/", maxAge: 31_536_000, sameSite: "Lax", secure: true, }, language: { maxAge: 2_592_000 }, }, } satisfies AppConfig; ``` The theme, accent, and language switchers use this policy automatically. Client functions can use the same resolved policy for other values: ```ts window.wrnCookies.set("dashboard-view", "compact"); window.wrnCookies.set("locale-preview", "hi", "language"); const value = window.wrnCookies.get("dashboard-view"); window.wrnCookies.remove("dashboard-view"); ``` ## 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(--wrn-color-surface); color: var(--wrn-color-text); border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius); box-shadow: var(--wrn-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 connected into pages by the framework's server; this package only produces their contents.