@wrnexus/styles
CSS pipeline, themes, fonts, profiles, and application config.
bun add @wrnexus/styles@0.2.15Global CSS bundling, the--wire-*design-token theme system, and thewrnexus.config.tsapp-config loader for WRNexusJS apps.
Part of the WRNexusJS framework — an SSR-first, Bun-native full-stack web framework.
Overview
This package owns three server-side concerns that shape every page a WRNexusJS 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
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:
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:
type ThemeTokens = Record<string, string>;
interface ThemeConfig {
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 token keys (both light and dark): color-scheme, color-bg, color-surface, color-surface-2, color-text, color-muted, color-border, color-primary, color-primary-hover, color-primary-contrast, color-danger, color-success, color-warning, radius, radius-sm, font-sans, shadow-1.
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
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: {
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
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
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:
.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);
}
<button data-wire-theme-toggle>Toggle theme</button>
<button data-wire-theme-set="brand">Brand theme</button>
Requirements / Notes
- Bun-only.
bundleCssusesBun.build's CSS bundler for@importresolution, nesting, and minification. Node is not supported. - Config and env loading use
node:fs/node:path/node:urland read fromprocess.env. - Peer package:
@wrnexus/coresupplies theSeoConfigandSecurityConfigtypes referenced byAppConfig. - 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.
Complete TypeScript API
This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.
import { SeoConfig, SecurityConfig } from '@wrnexus/core';
import { StorageConfig } from '@wrnexus/uploader';
/**
* Theme system — design tokens that work SSR and client-side.
*
* Tokens are plain CSS custom properties (`--wire-<key>`) so they cascade and
* can be overridden by user CSS. Each theme is a flat token map; the framework
* ships default `light`/`dark` sets and the user's config deep-merges over them.
*
* The server renders `<html data-theme="…">` from the `wire-theme` cookie (no
* flash), and a tiny client runtime toggles/persists it. The reserved token key
* `color-scheme` is emitted as the native CSS property (not a variable) so form
* controls and scrollbars match the theme.
*/
type ThemeTokens = Record<string, string>;
interface ThemeConfig {
/** Name of the theme used when no `wire-theme` cookie is present. */
default?: string;
/** Named token maps. Deep-merged over the framework's built-in light/dark. */
themes?: Record<string, ThemeTokens>;
}
interface ResolvedTheme {
default: string;
names: string[];
themes: Record<string, ThemeTokens>;
}
/** Cookie the resolved theme is read from / persisted to. */
declare const THEME_COOKIE = "wire-theme";
declare const THEME_CSS_HREF = "/__wrnexus/theme.css";
declare const THEME_JS_HREF = "/__wrnexus/theme.js";
/** Built-in themes so components have tokens out of the box. */
declare const DEFAULT_THEMES: Record<string, ThemeTokens>;
/** Merge the user's theme config over the built-in defaults. */
declare function resolveThemeConfig(config?: ThemeConfig): ResolvedTheme;
/** Pick a valid theme name from a cookie value, falling back to the default. */
declare function resolveThemeName(cookieValue: string | undefined, theme: ResolvedTheme): string;
/** Generate the theme stylesheet: a `:root` default plus one block per theme. */
declare function renderThemeCss(theme: ResolvedTheme): string;
/**
* Generate the client theme runtime. It exposes `window.wireTheme` and binds
* `[data-wire-theme-toggle]` / `[data-wire-theme-set]` elements. The configured
* theme names are baked in so `toggle()` cycles through them in order.
*/
declare function renderThemeRuntime(theme: ResolvedTheme): string;
/**
* Font configuration.
*
* Declare fonts in `wrnexus.config.ts` under `fonts` and the framework emits
* optimized `<head>` markup for you:
* - Google Fonts: `preconnect` hints + a single subsetted stylesheet request
* (only the weights you list) with `font-display`. The CSP is auto-extended
* so the fonts load under the default security policy (see loadAppConfig).
* - Self-hosted fonts: generated `@font-face` rules + optional `<link rel=preload>`
* for above-the-fold text (the fastest, no-third-party option).
* - Family stacks: `sans`/`mono`/`serif` become `--wrn-font-*` CSS variables,
* and `sans` is applied to `body`.
*/
type FontDisplay = "auto" | "block" | "swap" | "fallback" | "optional";
interface GoogleFont {
/** Family name as it appears on fonts.google.com, e.g. "Inter". */
family: string;
/** Weights to load — ONLY these are fetched. Default: [400]. */
weights?: (number | string)[];
/** Also load italic styles for each weight. */
italic?: boolean;
/** Per-font `font-display` override (else the config default). */
display?: FontDisplay;
}
interface LocalFontFace {
/** `font-family` name this face defines. */
family: string;
/** URL to the font file, typically served from `public/` (e.g. "/fonts/inter.woff2"). */
src: string;
/** e.g. 400, "700", or "100 900" for a variable font. Default: 400. */
weight?: number | string;
style?: "normal" | "italic";
/** CSS `src` format; inferred from the file extension when omitted. */
format?: string;
display?: FontDisplay;
/** Emit `<link rel="preload" as="font">` — use for the primary above-the-fold face. */
preload?: boolean;
/** Optional `unicode-range` subset. */
unicodeRange?: string;
}
interface FontConfig {
/** Google Fonts, loaded with preconnect + weight subsetting + `font-display`. */
google?: GoogleFont[];
/** Self-hosted `@font-face` definitions (files served from `public/`). */
local?: LocalFontFace[];
/** Default `font-display` for faces that don't set their own. Default: "swap". */
display?: FontDisplay;
/** Body / default family stack → `--wrn-font-sans` + `body { font-family }`. */
sans?: string;
/** Monospace family stack → `--wrn-font-mono`. */
mono?: string;
/** Serif family stack → `--wrn-font-serif`. */
serif?: string;
}
/**
* Render all `<head>` markup for a font config. Returns "" when nothing is
* configured. The output is trusted, framework-controlled HTML.
*/
declare function renderFontHead(fonts?: FontConfig): string;
/**
* Production variant that inlines the small Google Fonts stylesheet at build
* time. This removes a render-blocking CSS round trip while retaining the same
* font files, `font-display`, CSP sources, and offline-safe fallback markup.
*/
declare function renderProductionFontHead(fonts?: FontConfig, fetcher?: (input: string, init?: RequestInit) => Promise<Response>): Promise<string>;
/**
* CSP source hosts required by the configured fonts, so the policy can be
* auto-extended (Google Fonts need their CSS + static hosts allow-listed).
*/
declare function fontCspSources(fonts?: FontConfig): {
style: string[];
font: string[];
};
/**
* App configuration loader (`wrnexus.config.ts`).
*
* The config is optional. It lets an app inject arbitrary `<head>` HTML (ideal
* for CDN-delivered CSS frameworks like Bootstrap or the Tailwind Play CDN) and
* customise the global stylesheet pipeline (entry file or a custom processor for
* Tailwind / PostCSS / Sass).
*/
type Mode = "development" | "production";
interface StyleProcessContext {
/** Resolved absolute path to the CSS entry, or null if there is none. */
entryPath: string | null;
appDir: string;
appRoot: string;
mode: Mode;
}
interface StylesConfig {
/** Path to the CSS entry, relative to the app root. Default: app/styles/global.css */
entry?: string;
/**
* Optional custom processor. Return the final CSS string. Use this to run
* Tailwind, PostCSS, Sass, etc. When omitted, the built-in Bun CSS bundler is
* used (which already resolves `@import`, including from node_modules).
*/
process?: (ctx: StyleProcessContext) => string | Promise<string>;
}
interface MobileConfig {
enabled?: boolean;
/** Mobile renderer. `webview` uses Capacitor; `native` scaffolds an Expo/React Native app. */
mode?: "webview" | "native";
appId?: string;
appName?: string;
serverUrl?: string;
userAgent?: string;
layout?: string;
backgroundColor?: string;
icon?: string;
errorTitle?: string;
errorMessage?: string;
/** Base URL used by a fully native client for WRNexusJS API and realtime requests. */
apiUrl?: string;
/** URL scheme used for native deep links (defaults to a slug of appName). */
scheme?: string;
/** Advanced Expo app config fields merged into generated app.config.ts. */
expo?: Record<string, unknown>;
/** Advanced CapacitorConfig fields merged into generated capacitor.config.ts. */
capacitor?: Record<string, unknown>;
}
interface PwaScreenshot {
src: string;
sizes: string;
type?: string;
formFactor?: "wide" | "narrow";
label?: string;
}
interface PwaShortcut {
name: string;
shortName?: string;
description?: string;
url: string;
icons?: Array<{
src: string;
sizes: string;
type?: string;
purpose?: string;
}>;
}
interface PwaConfig {
enabled?: boolean;
id?: string;
name?: string;
shortName?: string;
description?: string;
startUrl?: string;
scope?: string;
lang?: string;
display?: "standalone" | "fullscreen" | "minimal-ui" | "browser";
orientation?: "any" | "natural" | "landscape" | "landscape-primary" | "landscape-secondary" | "portrait" | "portrait-primary" | "portrait-secondary";
themeColor?: string;
backgroundColor?: string;
icons?: Array<{
src: string;
sizes: string;
type?: string;
purpose?: string;
}>;
categories?: string[];
screenshots?: PwaScreenshot[];
shortcuts?: PwaShortcut[];
/** Disable service-worker registration while keeping the web manifest. */
serviceWorker?: boolean;
/** Navigation shown when both the network and requested page cache are unavailable. */
offlineUrl?: string;
/** Additional same-origin URLs precached during service-worker installation. */
cacheUrls?: string[];
/** Service-worker cache key. Change it to invalidate existing PWA caches. */
cacheName?: string;
}
interface AppConfig {
/** Raw HTML appended to every page's `<head>` (e.g. CDN stylesheet links). */
head?: string | string[];
/** Global SEO defaults merged with every page's exported `meta`. */
seo?: SeoConfig;
/** Framework security headers and optional CORS policy. */
security?: SecurityConfig;
styles?: StylesConfig;
/** Capacitor/native shell defaults and mobile-only page rendering. */
mobile?: MobileConfig;
/** Progressive Web App metadata. Enabled by default unless set to false. */
pwa?: PwaConfig | false;
/**
* Fonts. Declare Google Fonts (subsetted + preconnect + `font-display`) and/or
* self-hosted `@font-face` (with preload), and set `sans`/`mono`/`serif` family
* stacks. Google Fonts auto-extend the CSP so they load under the default policy.
*/
fonts?: FontConfig;
/** Design-token themes (deep-merged over the built-in light/dark). */
theme?: ThemeConfig;
/** i18n: default language + supported locales (strings live in app/locales/*.json). */
i18n?: {
default?: string;
locales?: string[];
};
/** Default database connection (driver + url); reached with `getDb()`. */
db?: {
driver: "sqlite" | "postgres" | "mysql" | "mongo";
url: string;
};
/**
* File-upload storage. Declare named stores (local dir or S3-compatible),
* upload with `handleUpload`/`upload` from `@wrnexus/uploader`, and serve
* files back. Each store is `access: "public" | "private"`.
*/
storage?: StorageConfig;
/**
* Additional named databases, reached with `getDb("<name>")`. Each has its own
* migrations/queries under `app/db/<name>/`. Connect to as many as you like and
* read/write to any of them per request.
*
* databases: { analytics: { driver: "postgres", url: "…" } }
*/
databases?: Record<string, {
driver: "sqlite" | "postgres" | "mysql" | "mongo";
url: string;
}>;
/**
* Realtime scaling. When `scale` is true (or `redisUrl` is set), room
* broadcasts are bridged over Redis pub/sub so they reach clients on **every**
* app process/instance — realtime that works with multiple running apps.
*/
realtime?: {
scale?: boolean;
redisUrl?: string;
};
/** Default server port. */
port?: number;
/**
* Named config profiles (dev, prod, uat, test, …). When a profile is active
* its overrides are DEEP-MERGED over the base config. Select with
* `--profile=<name>` or the `WRNEXUS_PROFILE` env var.
*/
profiles?: Record<string, Partial<Omit<AppConfig, "profiles">>>;
}
/**
* Resolve the active profile name: explicit argument > `WRNEXUS_PROFILE` env var
* > a mode-based default ("production" in prod, else "development").
*/
declare function resolveProfile(options?: {
explicit?: string;
mode?: Mode;
}): string;
/** Load the raw `wrnexus.config.*` (with the `profiles` map intact), or `{}`. */
declare function loadRawConfig(appRoot: string): Promise<AppConfig>;
/** Load `wrnexus.config.*`, applying the active profile's overrides. */
declare function loadAppConfig(appRoot: string, profile?: string): Promise<AppConfig>;
/**
* Load the `.env` cascade for a profile into `process.env`, WITHOUT clobbering
* variables already set in the real environment (which always win). Order, low
* → high precedence: `.env` < `.env.<profile>` < `.env.local` < `.env.<profile>.local`.
* Returns the variables it loaded.
*/
declare function loadEnv(appRoot: string, profile: string): Record<string, string>;
/** Flatten a head config into a single HTML string. */
declare function headToString(head?: string | string[]): string;
/**
* Global stylesheet pipeline.
*
* Convention: `app/styles/global.css` is the entry. If it is absent but other
* `app/styles/*.css` files exist, they are aggregated into one entry. The entry
* is bundled by Bun's CSS bundler, which resolves `@import` — including from
* node_modules — so any npm CSS framework (Bootstrap, etc.) works by importing
* it. A custom `process` hook can replace the bundler for Tailwind/PostCSS/Sass.
*/
/**
* Resolve the CSS entry for an app.
* - `override` (from config.styles.entry) is resolved relative to `appRoot`.
* - otherwise prefer `app/styles/global.css`.
* - otherwise aggregate all `app/styles/*.css` into a generated entry.
* Returns null when the app has no styles.
*/
declare function findStyleEntry(appDir: string, appRoot: string, override?: string): string | null;
/**
* Bundle a CSS entry into a single stylesheet string using Bun's CSS bundler.
* Resolves `@import` (local and node_modules), handles nesting, minifies in prod.
*/
declare function bundleCss(entryPath: string, mode: Mode): Promise<string>;
/**
* @wrnexus/styles — global stylesheet pipeline + app config.
*
* Works for SSR and CSR: the bundled stylesheet is `<link>`ed into every page's
* `<head>`, so it styles server-rendered markup and hydrated client islands
* alike. Use any CSS framework via `@import` in global.css (npm) or via a CDN
* link in `wrnexus.config.ts`'s `head` field.
*/
/**
* Produce the final CSS for an entry: run the config's custom processor if one
* is provided (Tailwind/PostCSS/Sass), otherwise use the built-in Bun bundler.
*
* If a custom processor throws (e.g. Tailwind can't resolve `tailwindcss`
* because deps aren't installed), we DON'T crash every request — we log a clear,
* actionable message and fall back to best-effort CSS so the app keeps serving.
*/
declare function renderStyles(ctx: StyleProcessContext, styles?: StylesConfig): Promise<string>;
export { type AppConfig, DEFAULT_THEMES, type FontConfig, type FontDisplay, type GoogleFont, type LocalFontFace, type MobileConfig, type Mode, type PwaConfig, type ResolvedTheme, type StyleProcessContext, type StylesConfig, type Mode as StylesMode, THEME_COOKIE, THEME_CSS_HREF, THEME_JS_HREF, type ThemeConfig, type ThemeTokens, bundleCss, findStyleEntry, fontCspSources, headToString, loadAppConfig, loadEnv, loadRawConfig, renderFontHead, renderProductionFontHead, renderStyles, renderThemeCss, renderThemeRuntime, resolveProfile, resolveThemeConfig, resolveThemeName };
Examples
Copy-ready examples taken from this package's published documentation.
Example 1
bun add @wrnexus/stylesExample 2
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"
}Example 3
type ThemeTokens = Record<string, string>;
interface ThemeConfig {
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>;
}Example 4
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: {
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;