# @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 `<link>`ed into every page's `<head>`. 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-<key>`), with built-in `light`/`dark` sets, deep-merged user overrides, an SSR `<html data-theme>` 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<AppConfig>`    | Load `wrnexus.config.*` with the active profile deep-merged in (`profiles` stripped from the result).                                 |
| `loadRawConfig`  | `(appRoot: string) => Promise<AppConfig>`                      | 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<string, string>` | 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.<profile>` < `.env.local` < `.env.<profile>.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 `<head>` (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<string, { driver; url }>`                                       | Additional named databases, reached with `getDb("<name>")`; each has its own `app/db/<name>/` 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<string, Partial<Omit<AppConfig, "profiles">>>`                  | Named profiles (dev, prod, uat, test, …). The active profile's overrides are deep-merged over the base config. Selected via `--profile=<name>` 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<string>`                   | 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<string>` | 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<string>;
}

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<string, ThemeTokens>`                                        | 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="<name>"]{…}` block per theme.                                   |
| `renderThemeRuntime` | `(theme: ResolvedTheme) => string`                                   | Generate the client runtime (see below).                                                                                                   |

Tokens are emitted as `--wire-<key>` 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<string, string>;

interface ThemeConfig {
  palette?: ThemePaletteName | CustomThemePalette;
  default?: string; // theme used when no cookie is present
  themes?: Record<string, ThemeTokens>; // deep-merged over built-in light/dark
}

interface ResolvedTheme {
  default: string;
  names: string[];
  themes: Record<string, ThemeTokens>;
}
```

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: [
    '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css">',
  ],
  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 <html data-theme={active}>

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
<button data-wire-theme-toggle>Toggle theme</button>
<button data-wire-theme-set="brand">Brand theme</button>
```

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