DataTable replaces the 20-line Table scaffold entirely: columns, sorting, filtering, pagination, selection, bulk actions, comparison layout, sticky first column, custom HTML cells, and a remote source driven by a `request` output rather than a function prop (props travel as HTML attributes, so a function arrives as its own source text). Toaster replaces the hand-rolled status div: tone icons, actions, hover pause/resume and a progress bar. Overlays audit -- Modal and Drawer declared aria-modal="true" but nothing ever moved focus into the panel, so the @keydown handler on their root never ran and closeOnEscape did nothing. Focus, focus restore, a Tab trap and a body scroll lock now live in the reactive runtime, shared by both. ContextMenu placed pointer menus by subtracting a guessed 340x420 from the viewport, which pushed every menu that was not that size away from the pointer; it now positions at the pointer and lets the anchored clamp pull it back once it can be measured. The reactive runtime size budget moves 150k -> 175k to cover anchored overlays, dialog behaviour, the toaster and the DataTable client half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@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:
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 thewrnexus.config.tsapp-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:
- Global stylesheet pipeline — finds
app/styles/global.css(or aggregatesapp/styles/*.css), bundles it with Bun's CSS bundler (which resolves@import, including fromnode_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 customprocesshook lets you swap in Tailwind / PostCSS / Sass. - Theme system — design tokens exposed as CSS custom properties (
--wire-<key>), with built-inlight/darksets, deep-merged user overrides, an SSR<html data-theme>render (no flash), and a tiny client runtime to toggle/persist the choice. - App config — loads
wrnexus.config.ts(theAppConfigtype), applies named profile overrides, and loads the.envcascade.
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
wrnexusnpm 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 {
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:
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
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
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.