# WRNexusJS documentation 0.8.7

Status: Private Developer Preview. This site documents 47 release-aligned packages.

# WrNexus

> WrNexus is an SSR-first, **Bun-native** full-stack web framework. UI is written in
> `.wrn` files (its own component language — NOT React/JSX/Vue). Routing is file-based.
> This document teaches an AI how to write correct WrNexus code. It is private and
> post-dates model training data, so rely on THIS document, not prior web-framework
> assumptions.

## Golden rules

- **Pages, components, and layouts are `.wrn` files.** Do NOT write `.tsx`/`.jsx`/React
  for UI. Do NOT use `useState`, hooks, JSX, or a client bundler.
- **Routing is file-based** under `app/`. The filename is the route. No router config.
- **Interactivity** lives in `state` + `{expr}` + `@event` inside `.wrn`. Components render
  on the server and hydrate automatically — you never write client-side JS islands.
- **Runtime is Bun only** (uses `Bun.serve`, `bun:sqlite`, `Bun.password`, …). Node is not supported.
- To add files, prefer the CLI: `wrnexus generate page <Name>` / `component <name>` / `api <path>` / `schema <name>`.

## Project layout

```
app/
  pages/       *.wrn  → routes: index.wrn = "/", about.wrn = "/about", blog/[slug].wrn = "/blog/:slug"
  components/  *.wrn  → reusable UI, mounted in a page/component via <div data-component="name" ...props>
  layouts/     *.wrn  → named layouts; a page opts in with  layout = "name"
  api/         *.ts   → HTTP handlers: export const GET/POST/PUT/PATCH/DELETE = async (ctx) => Response
  middleware/  *.ts   → export default async (ctx, next) => next()
  realtime/    *.ts   → export default defineRoom({ ... }) from "@wrnexus/core"  (ws://host/realtime/<name>)
  schemas/     *.ts   → validation schemas (the `v` builder), used by forms + parseBody
  locales/     *.json → i18n messages per language
  db/          schema.ts, queries/*.sql, migrations/*.sql
  styles/      global.css  → Tailwind (default) or plain CSS
wrnexus.config.ts    → app config (AppConfig from "@wrnexus/styles")
public/              → static assets served at /
```

## `.wrn` page

```wrn
page Home {
  layout = "public"          // optional: a component in app/layouts/<name>.wrn ("none" to skip)

  state count = 0            // optional: seeds client-reactive state (omit for pure SSR)

  seo {
    title = "Home"
    description = "..."
    canonical = "/"
  }

  view {
    <h1>Hello</h1>
    <p>Count is {count}, doubled is {count * 2}.</p>
    <button @click="count++">Increment</button>
    <div data-component="counter" start="5" label="Clicks"></div>
  }

  style {
    h1 { color: var(--wire-color-text); }
  }
}
```

## `.wrn` component

```wrn
component Counter {
  props {                    // props come from mount attributes; each is coerced to the
    start = 0                // TYPE of its default (so start="5" arrives as the number 5)
    label = "Count"
  }
  state count = start        // state may reference props
  view {
    <button @click="count++">{label}: {count}</button>
  }
}
```

Mount it from any page/component: `<div data-component="counter" start="0" label="Clicks"></div>`.
Components render on the server with their props, then hydrate — no per-component JS.

## The `view { }` block (plain HTML + a few directives)

- `{expr}` — interpolate a JS expression. Reactive if it references `state`: `{count}`, `{count * 2}`, `{user.name}`.
- `@event="expr"` — bind a DOM event; the expression runs in the reactive scope: `@click="count++"`, `@input="name = event.target.value"`.
- `<div data-component="name" prop="v">` — mount a component (attrs become string props, coerced).
- `<slot></slot>` / `<slot name="x"></slot>` — component/layout slots; fill with `<div data-slot="x">…</div>`.
- **Server loop (DB/list/table):** `{#each <list> as <item>[, <i>]} …rows… {:empty} …fallback… {/each}` — iterates SSR data on the server and renders markup per item. `{item.field}` interpolates (HTML-escaped, XSS-safe). `<list>` is a JS expression, usually an `ssr` data binding (see "Data-driven tables" below). This is how you render a database table in `.wrn`.
- **Server conditional:** `{#if <expr>} … {:else if <expr>} … {:else} … {/if}` — renders the first truthy branch on the server. `<expr>` can reference `ssr` data, or the `item`/`index` of an enclosing `{#each}`. Works at page level and inside loops (e.g. `{#if r.active}<span>●</span>{:else}<span>○</span>{/if}` per row). For client-side show/hide based on reactive `state`, use `data-show="expr"` instead.
- i18n: `{t:home.title}` in text, `t:placeholder="form.name"` on attributes — resolved per request from `app/locales/`.
- Theme: any element with `data-wire-theme-toggle` toggles light/dark; `data-wire-theme-set="dark"` sets it.
- Void/self-closing tags are fine: `<br />`, `<img src="..." />`.
- Only `{` and `}` are special (interpolation). Don't use a bare `}` in view text.

## Data-driven tables / lists (server-rendered `.wrn`)

Use an `ssr` data binding to fetch rows on the server, then `{#each}` to render them.
This renders on the **server** (SSR-first) and is HTML-escaped by default.

```wrn
page Admin {
  layout = "dashboard"

  // Fetch on the server. The api handler at /api/contacts returns { contacts: [...] };
  // this block's `return contacts` exposes that array (via `$data`) as the binding `rows`.
  ssr {
    api rows GET /api/contacts { return contacts }
  }

  view {
    <table>
      <tbody>
        {#each rows as r, i}
          <tr>
            <td>#{i}</td>
            <td>{r.name}</td>
            <td><a href="mailto:{r.email}">{r.email}</a></td>
          </tr>
        {:empty}
          <tr><td colspan="3">No submissions yet.</td></tr>
        {/each}
      </tbody>
    </table>
  }
}
```

The matching API returns the array under a key the `ssr` block reads:

```ts
// app/api/contacts.ts  → GET /api/contacts
import { getDb } from "@wrnexus/db";
export const GET = async () => {
  const contacts = await getDb().all("SELECT id, name, email FROM contacts ORDER BY id DESC");
  return Response.json({ contacts });   // ssr block does `return contacts`
};
```

**Prefer this `.wrn` + `{#each}` approach for DB-backed tables and lists.** (`.ts`/`.tsx`
pages returning an HTML string are also supported for fully-custom programmatic rendering,
but a `.wrn` page with `ssr` data + `{#each}` is the idiomatic, SSR-first way.)

## API routes (`app/api/*.ts`)

```ts
// app/api/users/list.ts  → GET /api/users/list
import { getDb } from "@wrnexus/db";

export const GET = async (ctx) => {
  return Response.json({ users: await ListUsers(getDb()) });
};

export const POST = async (ctx) => {
  const body = await ctx.req.json();
  return Response.json({ ok: true, body }, { status: 201 });
};
```

`ctx` (the `Context` from `@wrnexus/core`) has:
`req: Request`, `url: URL`, `params: Record<string,string>` (dynamic route params, e.g. `/users/[id]` → `ctx.params.id`),
`lang: string`, `t(key, params?)` (i18n), `cookies` (get/set), `session` (get/set). Auth: `getUser(ctx)` after `sessionAuth`/`logIn`.

When an SSO forward-auth verifier needs the URL that originally reached the gateway, use
`@wrnexus/helpers` instead of constructing it from untrusted headers:

```ts
import { redirectToLogin } from "@wrnexus/helpers";

return redirectToLogin(ctx, "/login", {
  allowedHosts: ["admin.localhost:3000", "reports.localhost:3000"],
});
```

The package also exports `getOriginalRequestUrl`, `getOriginalRequestOrigin`,
`getOriginalRequestPath`, and `getOriginalRequestMethod`. Always pass `allowedHosts` when
using forwarded gateway URLs; the helper rejects untrusted redirect destinations.

## Middleware & realtime

```ts
// app/middleware/logger.ts
export default async function logger(ctx, next) {
  console.log(ctx.req.method, ctx.url.pathname);
  return next();               // return a Response WITHOUT calling next() to short-circuit
}
```

```ts
// app/realtime/chat.ts  → ws://host/realtime/chat
import { defineRoom } from "@wrnexus/core";
export default defineRoom({
  onConnect(client) { client.send({ type: "system", text: "connected" }); },
  onMessage(client, msg) { client.room.broadcast({ type: "message", data: msg }); },
});
```
Client side: a page opts in with `data-room="chat"` (handled by the realtime runtime).

## Config (`wrnexus.config.ts`)

```ts
import type { AppConfig } from "@wrnexus/styles";
const config: AppConfig = {
  seo: { title: "App", titleTemplate: "%s | App", description: "..." },
  styles: { entry: "app/styles/global.css", process: async ({ entryPath, mode }) => /* Tailwind */ "" },
  fonts: { sans: '"Inter", system-ui, sans-serif', google: [{ family: "Inter", weights: [400, 600] }] },
  theme: { default: "dark", themes: { light: { "color-primary": "#2563eb" } } },
  i18n: { default: "en", locales: ["en", "es"] },
  db: { driver: "sqlite", url: "file:./dev.db" },
  security: { cors: { enabled: true, origin: ["http://localhost:5173"] } },
  // profiles: { production: { db: { driver: "postgres", url: process.env.DATABASE_URL } } },
};
export default config;
```

## Database (`@wrnexus/db`)

```ts
// app/db/schema.ts
import { v, table } from "@wrnexus/db";
export const users = table("users", {
  id: v.id(),
  name: v.string(),
  email: v.string().unique(),
  createdAt: v.timestamp(),
});
```
- Queries: write `app/db/queries/*.sql` with `-- name: ListUsers :many` blocks; `wrnexus db generate` emits typed functions.
- Access at runtime: `import { getDb } from "@wrnexus/db"; const rows = await ListUsers(getDb());`
- Migrations in `app/db/migrations/`; run `wrnexus db migrate` (dev auto-migrates sqlite).

## Validation (`@wrnexus/validation`)

```ts
// app/schemas/login.ts
import { v } from "@wrnexus/validation";
export default v.object({
  email: v.string().email(),
  password: v.string().min(8),
});
```
In an API route: `import s from "../schemas/login"; import { parseBody } from "@wrnexus/validation"; const r = await parseBody(s, ctx.req);` → `r.ok ? r.value : r.response`.
In a form: `<form data-schema="login" action="/api/login" method="post">` + `<span data-error="email"></span>` (client + server validation wired automatically).

## AI / LLM (`@wrnexus/ai`)

```ts
// app/api/ai.ts
import { createAI } from "@wrnexus/ai";
const ai = createAI();                     // reads ANTHROPIC_API_KEY; default model claude-opus-4-8
export const POST = async (ctx) => {
  const { prompt } = await ctx.req.json();
  return ai.streamResponse(prompt);        // or: return Response.json({ text: await ai.generate(prompt) })
};
```

## CLI

See the generated **Complete CLI command reference** below. It is sourced from the installed 0.8.7 executable so command names and options cannot drift.

## When asked to "create a page/component/feature"

1. Create the `.wrn` file under `app/pages/` (or `app/components/`) with a `page`/`component` block — or run `wrnexus generate page <Name>`.
2. Put markup in `view { }`, interactive bits in `state` + `{expr}` + `@event`, reusable UI as components mounted via `data-component`.
3. For data, add an `app/api/*.ts` route and `getDb()`; for forms, add an `app/schemas/*.ts` and `data-schema`.
4. Style with Tailwind utility classes in the view, or theme tokens (`var(--wire-*)`), or `style { }`.
5. Never emit React/JSX, a manual router, or client-side island JS — the framework handles hydration.

# Complete CLI command reference

This section is generated from the installed `@wrnexus/cli@0.8.7` executable. It is the canonical command inventory for this release.

```text
wrnexus — WrNexus CLI

Usage:
  wrnexus dev [app-dir] [--port=3000] [--host=::]
                                             Start the development server (live reload)
  wrnexus dev [app-dir] --services        Start local production-service simulators with the app
  wrnexus dev [app-dir] --production-runtime
  wrnexus dev [app-dir] --services [--services-port=3099] [--services-http]
                                             Rebuild and reload the exact production artifact
  wrnexus build [app-dir]               Build a production server bundle + assets
  wrnexus preview [app-dir]             Serve the existing exact production output
  wrnexus create <app-name>             Scaffold a new app
  wrnexus workspace <name>              Scaffold a monorepo (apps/* + shared packages/*)
  wrnexus workspace add <name> [--domain=name.localhost]
                                             Add an app to the current workspace
  wrnexus gateway [--port=3000]         Serve every workspace app behind one port, routed by domain
  wrnexus production [workspace-dir]    Build, migrate, and serve the complete production workspace
  wrnexus <environment> [workspace-dir] Run a configured workspace environment (development/staging/custom)
  wrnexus generate <type> <name>        Scaffold a page | component | api | schema
  wrnexus generate routes | docker | mobile
                                             Generate routes or scaffold deployment targets
  wrnexus generate types [app-dir]       Generate application-wide route/component/key types
  wrnexus routes [app-dir]               Generate typed named routes
  wrnexus typecheck [app-dir]            Generate types and check TypeScript plus every .wrn file
  wrnexus mobile add <package...>       Install Capacitor or Expo native packages
  wrnexus mobile compile                Compile .wrn pages into native Expo routes
  wrnexus native list                   List cross-platform native capabilities
  wrnexus native add <capability...>    Install capability packages for the configured mobile mode
  wrnexus eject <name...>               Copy a Wire UI component into app/components
  wrnexus update [dir] [--latest]       Upgrade deps, migrate project files, and verify the app
  wrnexus db <cmd>                      Migrations: migrate | rollback | status | seed | generate | new
  wrnexus authz <cmd>                    Authorization: list | generate | init [--dialect=sqlite|postgres|mysql]
  wrnexus test [level] [app-dir] [--watch]
                                             Run unit | component | api | browser | visual | accessibility | performance
  wrnexus profiles [app-dir]            List config profiles (dev/prod/uat/…) and their env files
  wrnexus doctor [app-dir] [--fix]      Check project health; optionally apply safe repairs
  wrnexus compatibility <check|explain|upgrade> [app-dir]
                                             Inspect or explicitly upgrade behavior defaults
  wrnexus contracts <check|snapshot> [app-dir]
                                             Detect breaking boundary contract changes
  wrnexus security <audit|headers|test> [app-dir]
                                             Audit ASVS controls, inspect headers, or run abuse tests
  wrnexus api <generate|docs> [app-dir] Generate OpenAPI, docs, Postman, examples and SDKs
  wrnexus sdk generate <language> [app-dir]
                                             Generate TypeScript, JavaScript, Java, Go, or Python SDK
  wrnexus deploy <target> [app-dir]       Generate docker | kubernetes | systemd | railway | render | fly
  wrnexus mcp [app-dir]                   Start the WRNexus MCP server over stdio
  wrnexus i18n <extract|validate> [app-dir]
                                             Extract and audit translation keys
  wrnexus report [app-dir] [--file=app/pages/page.wrn]
                                             Create a sanitized reproduction bundle
  wrnexus playground [--port=4173]         Start the shareable WRN compiler playground
  wrnexus config [app-dir] --explain    Print the fully resolved profile configuration
  wrnexus analyze [app-dir]             Inspect dist/build-report.json and performance budgets
  wrnexus explain <route|build|hydration|bundle> [subject] [app-dir]
                                             Explain compiler and production build decisions
  wrnexus explain <cache|permission> <subject> [app-dir]
                                             Explain route caching or permission enforcement
  wrnexus inspect <target> [app-dir]    Inspect packages | plugins | routes | assets | runtimes | styles | migrations | bundle
  wrnexus inspect component <name> [app-dir]
                                             Inspect a component's typed public contract
  wrnexus generate system <name>        Scaffold a complete framework-native package

Profiles: pass --profile=<name> to dev/build/db (or set WRNEXUS_PROFILE) to load
that profile's .env cascade and config overrides. e.g. wrnexus dev --profile=uat

Workspace environments: configure protocol, rootDomain, port, hostname, runtime
(development|production), hmr, build, migrate, and profile in wrnexus.workspace.ts.
CLI overrides: --port, --host, --build/--no-build, --migrate/--no-migrate,
--profile, and --prepare-only.

Update options: --dry-run previews changes; --no-verify skips post-update check/build.
```

## CLI workflows with expected output

Commands below are copy-ready. Paths, ports, counts, and timings vary by project; the shown output identifies the success condition to check.

### Create, check, build, and preview an application

```bash
bunx @wrnexus/cli@0.8.7 create my-app
cd my-app
bun install
bunx wrnexus typecheck .
bunx wrnexus build .
bunx wrnexus preview . --port=3000
```

Expected output:

```text
✓ Application types are valid
✓ Runtime: .../dist/reactive.js
✓ Styles:  .../dist/styles.css
✓ Server:  .../dist/server.js
Run it:  bun .../dist/server.js
```

### Generate framework files and committed application types

```bash
bunx wrnexus generate page Dashboard
bunx wrnexus generate component status-card
bunx wrnexus generate api health
bunx wrnexus generate schema account
bunx wrnexus generate routes
bunx wrnexus generate types .
```

Expected output includes created source paths followed by:

```text
✓ Generated app/routes.gen.ts (... routes)
✓ Generated .../app/types/wrnexus.generated.d.ts (... routes, ... components)
```

### Run development and production-runtime modes

```bash
bunx wrnexus dev . --port=3000
bunx wrnexus dev . --services --services-port=3099
bunx wrnexus dev . --production-runtime --port=3000
```

Expected output reports the active profile, discovered plugins/routes, generated cache path, and listening URL. Production-runtime mode rebuilds and reloads the exact production artifact.

### Database lifecycle

```bash
bunx wrnexus db status
bunx wrnexus db new create_accounts --from-models
bunx wrnexus db migrate
bunx wrnexus db generate
bunx wrnexus db seed
```

Expected output identifies the selected profile/dialect, migration state, applied migration names, and generated typed query files. Run `db rollback` only when intentionally reverting the latest migration.

### Diagnose, inspect, and enforce contracts

```bash
bunx wrnexus doctor .
bunx wrnexus typecheck .
bunx wrnexus compatibility check .
bunx wrnexus contracts check .
bunx wrnexus security audit .
bunx wrnexus inspect packages .
bunx wrnexus inspect routes .
bunx wrnexus inspect component Navbar .
bunx wrnexus analyze .
```

Successful checks exit with status 0. Inspect commands print the resolved package, route, component, asset, runtime, style, migration, or bundle contract without changing the application.

### Tests, API artifacts, SDKs, and deployment manifests

```bash
bunx wrnexus test unit .
bunx wrnexus test component .
bunx wrnexus test api .
bunx wrnexus test browser .
bunx wrnexus api generate .
bunx wrnexus api docs .
bunx wrnexus sdk generate typescript .
bunx wrnexus deploy docker .
```

Test commands return the underlying suite summary and a nonzero status on failure. API/SDK/deploy commands report each generated artifact; review and commit generated files after validation.

### Workspaces and environments

```bash
bunx wrnexus workspace company-platform
cd company-platform
bunx wrnexus workspace add reports --domain=reports.localhost
bunx wrnexus gateway --port=3000
bunx wrnexus production . --prepare-only
bunx wrnexus staging .
```

Expected output lists created apps, domain mappings, selected environment/profile, build and migration decisions, and the gateway address.

### Internationalization, native targets, MCP, and support bundles

```bash
bunx wrnexus i18n extract .
bunx wrnexus i18n validate .
bunx wrnexus generate mobile
bunx wrnexus mobile compile
bunx wrnexus native list
bunx wrnexus mcp .
bunx wrnexus report . --file=app/pages/index.wrn
```

Expected output identifies extracted/validated translation keys, generated native routes, available capabilities, MCP stdio startup, or the sanitized reproduction archive. MCP uses stdio, so protocol messages—not a web URL—are its normal runtime output.

### Safe upgrades

```bash
bunx wrnexus update . --latest --dry-run
bunx wrnexus update . --latest
```

The dry run previews dependency and migration changes. A real update prints the source and target versions, backup directory, each migration action, installation result, and verification result. Commit or back up the application before upgrading.

# Complete CLI command reference

This section is generated from the installed `@wrnexus/cli@0.8.7` executable. It is the canonical command inventory for this release.

```text
wrnexus — WrNexus CLI

Usage:
  wrnexus dev [app-dir] [--port=3000] [--host=::]
                                             Start the development server (live reload)
  wrnexus dev [app-dir] --services        Start local production-service simulators with the app
  wrnexus dev [app-dir] --production-runtime
  wrnexus dev [app-dir] --services [--services-port=3099] [--services-http]
                                             Rebuild and reload the exact production artifact
  wrnexus build [app-dir]               Build a production server bundle + assets
  wrnexus preview [app-dir]             Serve the existing exact production output
  wrnexus create <app-name>             Scaffold a new app
  wrnexus workspace <name>              Scaffold a monorepo (apps/* + shared packages/*)
  wrnexus workspace add <name> [--domain=name.localhost]
                                             Add an app to the current workspace
  wrnexus gateway [--port=3000]         Serve every workspace app behind one port, routed by domain
  wrnexus production [workspace-dir]    Build, migrate, and serve the complete production workspace
  wrnexus <environment> [workspace-dir] Run a configured workspace environment (development/staging/custom)
  wrnexus generate <type> <name>        Scaffold a page | component | api | schema
  wrnexus generate routes | docker | mobile
                                             Generate routes or scaffold deployment targets
  wrnexus generate types [app-dir]       Generate application-wide route/component/key types
  wrnexus routes [app-dir]               Generate typed named routes
  wrnexus typecheck [app-dir]            Generate types and check TypeScript plus every .wrn file
  wrnexus mobile add <package...>       Install Capacitor or Expo native packages
  wrnexus mobile compile                Compile .wrn pages into native Expo routes
  wrnexus native list                   List cross-platform native capabilities
  wrnexus native add <capability...>    Install capability packages for the configured mobile mode
  wrnexus eject <name...>               Copy a Wire UI component into app/components
  wrnexus update [dir] [--latest]       Upgrade deps, migrate project files, and verify the app
  wrnexus db <cmd>                      Migrations: migrate | rollback | status | seed | generate | new
  wrnexus authz <cmd>                    Authorization: list | generate | init [--dialect=sqlite|postgres|mysql]
  wrnexus test [level] [app-dir] [--watch]
                                             Run unit | component | api | browser | visual | accessibility | performance
  wrnexus profiles [app-dir]            List config profiles (dev/prod/uat/…) and their env files
  wrnexus doctor [app-dir] [--fix]      Check project health; optionally apply safe repairs
  wrnexus compatibility <check|explain|upgrade> [app-dir]
                                             Inspect or explicitly upgrade behavior defaults
  wrnexus contracts <check|snapshot> [app-dir]
                                             Detect breaking boundary contract changes
  wrnexus security <audit|headers|test> [app-dir]
                                             Audit ASVS controls, inspect headers, or run abuse tests
  wrnexus api <generate|docs> [app-dir] Generate OpenAPI, docs, Postman, examples and SDKs
  wrnexus sdk generate <language> [app-dir]
                                             Generate TypeScript, JavaScript, Java, Go, or Python SDK
  wrnexus deploy <target> [app-dir]       Generate docker | kubernetes | systemd | railway | render | fly
  wrnexus mcp [app-dir]                   Start the WRNexus MCP server over stdio
  wrnexus i18n <extract|validate> [app-dir]
                                             Extract and audit translation keys
  wrnexus report [app-dir] [--file=app/pages/page.wrn]
                                             Create a sanitized reproduction bundle
  wrnexus playground [--port=4173]         Start the shareable WRN compiler playground
  wrnexus config [app-dir] --explain    Print the fully resolved profile configuration
  wrnexus analyze [app-dir]             Inspect dist/build-report.json and performance budgets
  wrnexus explain <route|build|hydration|bundle> [subject] [app-dir]
                                             Explain compiler and production build decisions
  wrnexus explain <cache|permission> <subject> [app-dir]
                                             Explain route caching or permission enforcement
  wrnexus inspect <target> [app-dir]    Inspect packages | plugins | routes | assets | runtimes | styles | migrations | bundle
  wrnexus inspect component <name> [app-dir]
                                             Inspect a component's typed public contract
  wrnexus generate system <name>        Scaffold a complete framework-native package

Profiles: pass --profile=<name> to dev/build/db (or set WRNEXUS_PROFILE) to load
that profile's .env cascade and config overrides. e.g. wrnexus dev --profile=uat

Workspace environments: configure protocol, rootDomain, port, hostname, runtime
(development|production), hmr, build, migrate, and profile in wrnexus.workspace.ts.
CLI overrides: --port, --host, --build/--no-build, --migrate/--no-migrate,
--profile, and --prepare-only.

Update options: --dry-run previews changes; --no-verify skips post-update check/build.
```

## CLI workflows with expected output

Commands below are copy-ready. Paths, ports, counts, and timings vary by project; the shown output identifies the success condition to check.

### Create, check, build, and preview an application

```bash
bunx @wrnexus/cli@0.8.7 create my-app
cd my-app
bun install
bunx wrnexus typecheck .
bunx wrnexus build .
bunx wrnexus preview . --port=3000
```

Expected output:

```text
✓ Application types are valid
✓ Runtime: .../dist/reactive.js
✓ Styles:  .../dist/styles.css
✓ Server:  .../dist/server.js
Run it:  bun .../dist/server.js
```

### Generate framework files and committed application types

```bash
bunx wrnexus generate page Dashboard
bunx wrnexus generate component status-card
bunx wrnexus generate api health
bunx wrnexus generate schema account
bunx wrnexus generate routes
bunx wrnexus generate types .
```

Expected output includes created source paths followed by:

```text
✓ Generated app/routes.gen.ts (... routes)
✓ Generated .../app/types/wrnexus.generated.d.ts (... routes, ... components)
```

### Run development and production-runtime modes

```bash
bunx wrnexus dev . --port=3000
bunx wrnexus dev . --services --services-port=3099
bunx wrnexus dev . --production-runtime --port=3000
```

Expected output reports the active profile, discovered plugins/routes, generated cache path, and listening URL. Production-runtime mode rebuilds and reloads the exact production artifact.

### Database lifecycle

```bash
bunx wrnexus db status
bunx wrnexus db new create_accounts --from-models
bunx wrnexus db migrate
bunx wrnexus db generate
bunx wrnexus db seed
```

Expected output identifies the selected profile/dialect, migration state, applied migration names, and generated typed query files. Run `db rollback` only when intentionally reverting the latest migration.

### Diagnose, inspect, and enforce contracts

```bash
bunx wrnexus doctor .
bunx wrnexus typecheck .
bunx wrnexus compatibility check .
bunx wrnexus contracts check .
bunx wrnexus security audit .
bunx wrnexus inspect packages .
bunx wrnexus inspect routes .
bunx wrnexus inspect component Navbar .
bunx wrnexus analyze .
```

Successful checks exit with status 0. Inspect commands print the resolved package, route, component, asset, runtime, style, migration, or bundle contract without changing the application.

### Tests, API artifacts, SDKs, and deployment manifests

```bash
bunx wrnexus test unit .
bunx wrnexus test component .
bunx wrnexus test api .
bunx wrnexus test browser .
bunx wrnexus api generate .
bunx wrnexus api docs .
bunx wrnexus sdk generate typescript .
bunx wrnexus deploy docker .
```

Test commands return the underlying suite summary and a nonzero status on failure. API/SDK/deploy commands report each generated artifact; review and commit generated files after validation.

### Workspaces and environments

```bash
bunx wrnexus workspace company-platform
cd company-platform
bunx wrnexus workspace add reports --domain=reports.localhost
bunx wrnexus gateway --port=3000
bunx wrnexus production . --prepare-only
bunx wrnexus staging .
```

Expected output lists created apps, domain mappings, selected environment/profile, build and migration decisions, and the gateway address.

### Internationalization, native targets, MCP, and support bundles

```bash
bunx wrnexus i18n extract .
bunx wrnexus i18n validate .
bunx wrnexus generate mobile
bunx wrnexus mobile compile
bunx wrnexus native list
bunx wrnexus mcp .
bunx wrnexus report . --file=app/pages/index.wrn
```

Expected output identifies extracted/validated translation keys, generated native routes, available capabilities, MCP stdio startup, or the sanitized reproduction archive. MCP uses stdio, so protocol messages—not a web URL—are its normal runtime output.

### Safe upgrades

```bash
bunx wrnexus update . --latest --dry-run
bunx wrnexus update . --latest
```

The dry run previews dependency and migration changes. A real update prints the source and target versions, backup directory, each migration action, installation result, and verification result. Commit or back up the application before upgrading.

# Complete CLI command reference

This section is generated from the installed `@wrnexus/cli@0.8.7` executable. It is the canonical command inventory for this release.

```text
wrnexus — WrNexus CLI

Usage:
  wrnexus dev [app-dir] [--port=3000] [--host=::]
                                             Start the development server (live reload)
  wrnexus dev [app-dir] --services        Start local production-service simulators with the app
  wrnexus dev [app-dir] --production-runtime
  wrnexus dev [app-dir] --services [--services-port=3099] [--services-http]
                                             Rebuild and reload the exact production artifact
  wrnexus build [app-dir]               Build a production server bundle + assets
  wrnexus preview [app-dir]             Serve the existing exact production output
  wrnexus create <app-name>             Scaffold a new app
  wrnexus workspace <name>              Scaffold a monorepo (apps/* + shared packages/*)
  wrnexus workspace add <name> [--domain=name.localhost]
                                             Add an app to the current workspace
  wrnexus gateway [--port=3000]         Serve every workspace app behind one port, routed by domain
  wrnexus production [workspace-dir]    Build, migrate, and serve the complete production workspace
  wrnexus <environment> [workspace-dir] Run a configured workspace environment (development/staging/custom)
  wrnexus generate <type> <name>        Scaffold a page | component | api | schema
  wrnexus generate routes | docker | mobile
                                             Generate routes or scaffold deployment targets
  wrnexus generate types [app-dir]       Generate application-wide route/component/key types
  wrnexus routes [app-dir]               Generate typed named routes
  wrnexus typecheck [app-dir]            Generate types and check TypeScript plus every .wrn file
  wrnexus mobile add <package...>       Install Capacitor or Expo native packages
  wrnexus mobile compile                Compile .wrn pages into native Expo routes
  wrnexus native list                   List cross-platform native capabilities
  wrnexus native add <capability...>    Install capability packages for the configured mobile mode
  wrnexus eject <name...>               Copy a Wire UI component into app/components
  wrnexus update [dir] [--latest]       Upgrade deps, migrate project files, and verify the app
  wrnexus db <cmd>                      Migrations: migrate | rollback | status | seed | generate | new
  wrnexus authz <cmd>                    Authorization: list | generate | init [--dialect=sqlite|postgres|mysql]
  wrnexus test [level] [app-dir] [--watch]
                                             Run unit | component | api | browser | visual | accessibility | performance
  wrnexus profiles [app-dir]            List config profiles (dev/prod/uat/…) and their env files
  wrnexus doctor [app-dir] [--fix]      Check project health; optionally apply safe repairs
  wrnexus compatibility <check|explain|upgrade> [app-dir]
                                             Inspect or explicitly upgrade behavior defaults
  wrnexus contracts <check|snapshot> [app-dir]
                                             Detect breaking boundary contract changes
  wrnexus security <audit|headers|test> [app-dir]
                                             Audit ASVS controls, inspect headers, or run abuse tests
  wrnexus api <generate|docs> [app-dir] Generate OpenAPI, docs, Postman, examples and SDKs
  wrnexus sdk generate <language> [app-dir]
                                             Generate TypeScript, JavaScript, Java, Go, or Python SDK
  wrnexus deploy <target> [app-dir]       Generate docker | kubernetes | systemd | railway | render | fly
  wrnexus mcp [app-dir]                   Start the WRNexus MCP server over stdio
  wrnexus i18n <extract|validate> [app-dir]
                                             Extract and audit translation keys
  wrnexus report [app-dir] [--file=app/pages/page.wrn]
                                             Create a sanitized reproduction bundle
  wrnexus playground [--port=4173]         Start the shareable WRN compiler playground
  wrnexus config [app-dir] --explain    Print the fully resolved profile configuration
  wrnexus analyze [app-dir]             Inspect dist/build-report.json and performance budgets
  wrnexus explain <route|build|hydration|bundle> [subject] [app-dir]
                                             Explain compiler and production build decisions
  wrnexus explain <cache|permission> <subject> [app-dir]
                                             Explain route caching or permission enforcement
  wrnexus inspect <target> [app-dir]    Inspect packages | plugins | routes | assets | runtimes | styles | migrations | bundle
  wrnexus inspect component <name> [app-dir]
                                             Inspect a component's typed public contract
  wrnexus generate system <name>        Scaffold a complete framework-native package

Profiles: pass --profile=<name> to dev/build/db (or set WRNEXUS_PROFILE) to load
that profile's .env cascade and config overrides. e.g. wrnexus dev --profile=uat

Workspace environments: configure protocol, rootDomain, port, hostname, runtime
(development|production), hmr, build, migrate, and profile in wrnexus.workspace.ts.
CLI overrides: --port, --host, --build/--no-build, --migrate/--no-migrate,
--profile, and --prepare-only.

Update options: --dry-run previews changes; --no-verify skips post-update check/build.
```

## CLI workflows with expected output

Commands below are copy-ready. Paths, ports, counts, and timings vary by project; the shown output identifies the success condition to check.

### Create, check, build, and preview an application

```bash
bunx @wrnexus/cli@0.8.7 create my-app
cd my-app
bun install
bunx wrnexus typecheck .
bunx wrnexus build .
bunx wrnexus preview . --port=3000
```

Expected output:

```text
✓ Application types are valid
✓ Runtime: .../dist/reactive.js
✓ Styles:  .../dist/styles.css
✓ Server:  .../dist/server.js
Run it:  bun .../dist/server.js
```

### Generate framework files and committed application types

```bash
bunx wrnexus generate page Dashboard
bunx wrnexus generate component status-card
bunx wrnexus generate api health
bunx wrnexus generate schema account
bunx wrnexus generate routes
bunx wrnexus generate types .
```

Expected output includes created source paths followed by:

```text
✓ Generated app/routes.gen.ts (... routes)
✓ Generated .../app/types/wrnexus.generated.d.ts (... routes, ... components)
```

### Run development and production-runtime modes

```bash
bunx wrnexus dev . --port=3000
bunx wrnexus dev . --services --services-port=3099
bunx wrnexus dev . --production-runtime --port=3000
```

Expected output reports the active profile, discovered plugins/routes, generated cache path, and listening URL. Production-runtime mode rebuilds and reloads the exact production artifact.

### Database lifecycle

```bash
bunx wrnexus db status
bunx wrnexus db new create_accounts --from-models
bunx wrnexus db migrate
bunx wrnexus db generate
bunx wrnexus db seed
```

Expected output identifies the selected profile/dialect, migration state, applied migration names, and generated typed query files. Run `db rollback` only when intentionally reverting the latest migration.

### Diagnose, inspect, and enforce contracts

```bash
bunx wrnexus doctor .
bunx wrnexus typecheck .
bunx wrnexus compatibility check .
bunx wrnexus contracts check .
bunx wrnexus security audit .
bunx wrnexus inspect packages .
bunx wrnexus inspect routes .
bunx wrnexus inspect component Navbar .
bunx wrnexus analyze .
```

Successful checks exit with status 0. Inspect commands print the resolved package, route, component, asset, runtime, style, migration, or bundle contract without changing the application.

### Tests, API artifacts, SDKs, and deployment manifests

```bash
bunx wrnexus test unit .
bunx wrnexus test component .
bunx wrnexus test api .
bunx wrnexus test browser .
bunx wrnexus api generate .
bunx wrnexus api docs .
bunx wrnexus sdk generate typescript .
bunx wrnexus deploy docker .
```

Test commands return the underlying suite summary and a nonzero status on failure. API/SDK/deploy commands report each generated artifact; review and commit generated files after validation.

### Workspaces and environments

```bash
bunx wrnexus workspace company-platform
cd company-platform
bunx wrnexus workspace add reports --domain=reports.localhost
bunx wrnexus gateway --port=3000
bunx wrnexus production . --prepare-only
bunx wrnexus staging .
```

Expected output lists created apps, domain mappings, selected environment/profile, build and migration decisions, and the gateway address.

### Internationalization, native targets, MCP, and support bundles

```bash
bunx wrnexus i18n extract .
bunx wrnexus i18n validate .
bunx wrnexus generate mobile
bunx wrnexus mobile compile
bunx wrnexus native list
bunx wrnexus mcp .
bunx wrnexus report . --file=app/pages/index.wrn
```

Expected output identifies extracted/validated translation keys, generated native routes, available capabilities, MCP stdio startup, or the sanitized reproduction archive. MCP uses stdio, so protocol messages—not a web URL—are its normal runtime output.

### Safe upgrades

```bash
bunx wrnexus update . --latest --dry-run
bunx wrnexus update . --latest
```

The dry run previews dependency and migration changes. A real update prints the source and target versions, backup directory, each migration action, installation result, and verification result. Commit or back up the application before upgrading.

# Complete CLI command reference

This section is generated from the installed `@wrnexus/cli@0.8.7` executable. It is the canonical command inventory for this release.

```text
wrnexus — WrNexus CLI

Usage:
  wrnexus dev [app-dir] [--port=3000] [--host=::]
                                             Start the development server (live reload)
  wrnexus dev [app-dir] --services        Start local production-service simulators with the app
  wrnexus dev [app-dir] --production-runtime
  wrnexus dev [app-dir] --services [--services-port=3099] [--services-http]
                                             Rebuild and reload the exact production artifact
  wrnexus build [app-dir]               Build a production server bundle + assets
  wrnexus preview [app-dir]             Serve the existing exact production output
  wrnexus create <app-name>             Scaffold a new app
  wrnexus workspace <name>              Scaffold a monorepo (apps/* + shared packages/*)
  wrnexus workspace add <name> [--domain=name.localhost]
                                             Add an app to the current workspace
  wrnexus gateway [--port=3000]         Serve every workspace app behind one port, routed by domain
  wrnexus production [workspace-dir]    Build, migrate, and serve the complete production workspace
  wrnexus <environment> [workspace-dir] Run a configured workspace environment (development/staging/custom)
  wrnexus generate <type> <name>        Scaffold a page | component | api | schema
  wrnexus generate routes | docker | mobile
                                             Generate routes or scaffold deployment targets
  wrnexus generate types [app-dir]       Generate application-wide route/component/key types
  wrnexus routes [app-dir]               Generate typed named routes
  wrnexus typecheck [app-dir]            Generate types and check TypeScript plus every .wrn file
  wrnexus mobile add <package...>       Install Capacitor or Expo native packages
  wrnexus mobile compile                Compile .wrn pages into native Expo routes
  wrnexus native list                   List cross-platform native capabilities
  wrnexus native add <capability...>    Install capability packages for the configured mobile mode
  wrnexus eject <name...>               Copy a Wire UI component into app/components
  wrnexus update [dir] [--latest]       Upgrade deps, migrate project files, and verify the app
  wrnexus db <cmd>                      Migrations: migrate | rollback | status | seed | generate | new
  wrnexus authz <cmd>                    Authorization: list | generate | init [--dialect=sqlite|postgres|mysql]
  wrnexus test [level] [app-dir] [--watch]
                                             Run unit | component | api | browser | visual | accessibility | performance
  wrnexus profiles [app-dir]            List config profiles (dev/prod/uat/…) and their env files
  wrnexus doctor [app-dir] [--fix]      Check project health; optionally apply safe repairs
  wrnexus compatibility <check|explain|upgrade> [app-dir]
                                             Inspect or explicitly upgrade behavior defaults
  wrnexus contracts <check|snapshot> [app-dir]
                                             Detect breaking boundary contract changes
  wrnexus security <audit|headers|test> [app-dir]
                                             Audit ASVS controls, inspect headers, or run abuse tests
  wrnexus api <generate|docs> [app-dir] Generate OpenAPI, docs, Postman, examples and SDKs
  wrnexus sdk generate <language> [app-dir]
                                             Generate TypeScript, JavaScript, Java, Go, or Python SDK
  wrnexus deploy <target> [app-dir]       Generate docker | kubernetes | systemd | railway | render | fly
  wrnexus mcp [app-dir]                   Start the WRNexus MCP server over stdio
  wrnexus i18n <extract|validate> [app-dir]
                                             Extract and audit translation keys
  wrnexus report [app-dir] [--file=app/pages/page.wrn]
                                             Create a sanitized reproduction bundle
  wrnexus playground [--port=4173]         Start the shareable WRN compiler playground
  wrnexus config [app-dir] --explain    Print the fully resolved profile configuration
  wrnexus analyze [app-dir]             Inspect dist/build-report.json and performance budgets
  wrnexus explain <route|build|hydration|bundle> [subject] [app-dir]
                                             Explain compiler and production build decisions
  wrnexus explain <cache|permission> <subject> [app-dir]
                                             Explain route caching or permission enforcement
  wrnexus inspect <target> [app-dir]    Inspect packages | plugins | routes | assets | runtimes | styles | migrations | bundle
  wrnexus inspect component <name> [app-dir]
                                             Inspect a component's typed public contract
  wrnexus generate system <name>        Scaffold a complete framework-native package

Profiles: pass --profile=<name> to dev/build/db (or set WRNEXUS_PROFILE) to load
that profile's .env cascade and config overrides. e.g. wrnexus dev --profile=uat

Workspace environments: configure protocol, rootDomain, port, hostname, runtime
(development|production), hmr, build, migrate, and profile in wrnexus.workspace.ts.
CLI overrides: --port, --host, --build/--no-build, --migrate/--no-migrate,
--profile, and --prepare-only.

Update options: --dry-run previews changes; --no-verify skips post-update check/build.
```

## CLI workflows with expected output

Commands below are copy-ready. Paths, ports, counts, and timings vary by project; the shown output identifies the success condition to check.

### Create, check, build, and preview an application

```bash
bunx @wrnexus/cli@0.8.7 create my-app
cd my-app
bun install
bunx wrnexus typecheck .
bunx wrnexus build .
bunx wrnexus preview . --port=3000
```

Expected output:

```text
✓ Application types are valid
✓ Runtime: .../dist/reactive.js
✓ Styles:  .../dist/styles.css
✓ Server:  .../dist/server.js
Run it:  bun .../dist/server.js
```

### Generate framework files and committed application types

```bash
bunx wrnexus generate page Dashboard
bunx wrnexus generate component status-card
bunx wrnexus generate api health
bunx wrnexus generate schema account
bunx wrnexus generate routes
bunx wrnexus generate types .
```

Expected output includes created source paths followed by:

```text
✓ Generated app/routes.gen.ts (... routes)
✓ Generated .../app/types/wrnexus.generated.d.ts (... routes, ... components)
```

### Run development and production-runtime modes

```bash
bunx wrnexus dev . --port=3000
bunx wrnexus dev . --services --services-port=3099
bunx wrnexus dev . --production-runtime --port=3000
```

Expected output reports the active profile, discovered plugins/routes, generated cache path, and listening URL. Production-runtime mode rebuilds and reloads the exact production artifact.

### Database lifecycle

```bash
bunx wrnexus db status
bunx wrnexus db new create_accounts --from-models
bunx wrnexus db migrate
bunx wrnexus db generate
bunx wrnexus db seed
```

Expected output identifies the selected profile/dialect, migration state, applied migration names, and generated typed query files. Run `db rollback` only when intentionally reverting the latest migration.

### Diagnose, inspect, and enforce contracts

```bash
bunx wrnexus doctor .
bunx wrnexus typecheck .
bunx wrnexus compatibility check .
bunx wrnexus contracts check .
bunx wrnexus security audit .
bunx wrnexus inspect packages .
bunx wrnexus inspect routes .
bunx wrnexus inspect component Navbar .
bunx wrnexus analyze .
```

Successful checks exit with status 0. Inspect commands print the resolved package, route, component, asset, runtime, style, migration, or bundle contract without changing the application.

### Tests, API artifacts, SDKs, and deployment manifests

```bash
bunx wrnexus test unit .
bunx wrnexus test component .
bunx wrnexus test api .
bunx wrnexus test browser .
bunx wrnexus api generate .
bunx wrnexus api docs .
bunx wrnexus sdk generate typescript .
bunx wrnexus deploy docker .
```

Test commands return the underlying suite summary and a nonzero status on failure. API/SDK/deploy commands report each generated artifact; review and commit generated files after validation.

### Workspaces and environments

```bash
bunx wrnexus workspace company-platform
cd company-platform
bunx wrnexus workspace add reports --domain=reports.localhost
bunx wrnexus gateway --port=3000
bunx wrnexus production . --prepare-only
bunx wrnexus staging .
```

Expected output lists created apps, domain mappings, selected environment/profile, build and migration decisions, and the gateway address.

### Internationalization, native targets, MCP, and support bundles

```bash
bunx wrnexus i18n extract .
bunx wrnexus i18n validate .
bunx wrnexus generate mobile
bunx wrnexus mobile compile
bunx wrnexus native list
bunx wrnexus mcp .
bunx wrnexus report . --file=app/pages/index.wrn
```

Expected output identifies extracted/validated translation keys, generated native routes, available capabilities, MCP stdio startup, or the sanitized reproduction archive. MCP uses stdio, so protocol messages—not a web URL—are its normal runtime output.

### Safe upgrades

```bash
bunx wrnexus update . --latest --dry-run
bunx wrnexus update . --latest
```

The dry run previews dependency and migration changes. A real update prints the source and target versions, backup directory, each migration action, installation result, and verification result. Commit or back up the application before upgrading.

# Complete CLI command reference

This section is generated from the installed `@wrnexus/cli@0.8.7` executable. It is the canonical command inventory for this release.

```text
wrnexus — WrNexus CLI

Usage:
  wrnexus dev [app-dir] [--port=3000] [--host=::]
                                             Start the development server (live reload)
  wrnexus dev [app-dir] --services        Start local production-service simulators with the app
  wrnexus dev [app-dir] --production-runtime
  wrnexus dev [app-dir] --services [--services-port=3099] [--services-http]
                                             Rebuild and reload the exact production artifact
  wrnexus build [app-dir]               Build a production server bundle + assets
  wrnexus preview [app-dir]             Serve the existing exact production output
  wrnexus create <app-name>             Scaffold a new app
  wrnexus workspace <name>              Scaffold a monorepo (apps/* + shared packages/*)
  wrnexus workspace add <name> [--domain=name.localhost]
                                             Add an app to the current workspace
  wrnexus gateway [--port=3000]         Serve every workspace app behind one port, routed by domain
  wrnexus production [workspace-dir]    Build, migrate, and serve the complete production workspace
  wrnexus <environment> [workspace-dir] Run a configured workspace environment (development/staging/custom)
  wrnexus generate <type> <name>        Scaffold a page | component | api | schema
  wrnexus generate routes | docker | mobile
                                             Generate routes or scaffold deployment targets
  wrnexus generate types [app-dir]       Generate application-wide route/component/key types
  wrnexus routes [app-dir]               Generate typed named routes
  wrnexus typecheck [app-dir]            Generate types and check TypeScript plus every .wrn file
  wrnexus mobile add <package...>       Install Capacitor or Expo native packages
  wrnexus mobile compile                Compile .wrn pages into native Expo routes
  wrnexus native list                   List cross-platform native capabilities
  wrnexus native add <capability...>    Install capability packages for the configured mobile mode
  wrnexus eject <name...>               Copy a Wire UI component into app/components
  wrnexus update [dir] [--latest]       Upgrade deps, migrate project files, and verify the app
  wrnexus db <cmd>                      Migrations: migrate | rollback | status | seed | generate | new
  wrnexus authz <cmd>                    Authorization: list | generate | init [--dialect=sqlite|postgres|mysql]
  wrnexus test [level] [app-dir] [--watch]
                                             Run unit | component | api | browser | visual | accessibility | performance
  wrnexus profiles [app-dir]            List config profiles (dev/prod/uat/…) and their env files
  wrnexus doctor [app-dir] [--fix]      Check project health; optionally apply safe repairs
  wrnexus compatibility <check|explain|upgrade> [app-dir]
                                             Inspect or explicitly upgrade behavior defaults
  wrnexus contracts <check|snapshot> [app-dir]
                                             Detect breaking boundary contract changes
  wrnexus security <audit|headers|test> [app-dir]
                                             Audit ASVS controls, inspect headers, or run abuse tests
  wrnexus api <generate|docs> [app-dir] Generate OpenAPI, docs, Postman, examples and SDKs
  wrnexus sdk generate <language> [app-dir]
                                             Generate TypeScript, JavaScript, Java, Go, or Python SDK
  wrnexus deploy <target> [app-dir]       Generate docker | kubernetes | systemd | railway | render | fly
  wrnexus mcp [app-dir]                   Start the WRNexus MCP server over stdio
  wrnexus i18n <extract|validate> [app-dir]
                                             Extract and audit translation keys
  wrnexus report [app-dir] [--file=app/pages/page.wrn]
                                             Create a sanitized reproduction bundle
  wrnexus playground [--port=4173]         Start the shareable WRN compiler playground
  wrnexus config [app-dir] --explain    Print the fully resolved profile configuration
  wrnexus analyze [app-dir]             Inspect dist/build-report.json and performance budgets
  wrnexus explain <route|build|hydration|bundle> [subject] [app-dir]
                                             Explain compiler and production build decisions
  wrnexus explain <cache|permission> <subject> [app-dir]
                                             Explain route caching or permission enforcement
  wrnexus inspect <target> [app-dir]    Inspect packages | plugins | routes | assets | runtimes | styles | migrations | bundle
  wrnexus inspect component <name> [app-dir]
                                             Inspect a component's typed public contract
  wrnexus generate system <name>        Scaffold a complete framework-native package

Profiles: pass --profile=<name> to dev/build/db (or set WRNEXUS_PROFILE) to load
that profile's .env cascade and config overrides. e.g. wrnexus dev --profile=uat

Workspace environments: configure protocol, rootDomain, port, hostname, runtime
(development|production), hmr, build, migrate, and profile in wrnexus.workspace.ts.
CLI overrides: --port, --host, --build/--no-build, --migrate/--no-migrate,
--profile, and --prepare-only.

Update options: --dry-run previews changes; --no-verify skips post-update check/build.
```

## CLI workflows with expected output

Commands below are copy-ready. Paths, ports, counts, and timings vary by project; the shown output identifies the success condition to check.

### Create, check, build, and preview an application

```bash
bunx @wrnexus/cli@0.8.7 create my-app
cd my-app
bun install
bunx wrnexus typecheck .
bunx wrnexus build .
bunx wrnexus preview . --port=3000
```

Expected output:

```text
✓ Application types are valid
✓ Runtime: .../dist/reactive.js
✓ Styles:  .../dist/styles.css
✓ Server:  .../dist/server.js
Run it:  bun .../dist/server.js
```

### Generate framework files and committed application types

```bash
bunx wrnexus generate page Dashboard
bunx wrnexus generate component status-card
bunx wrnexus generate api health
bunx wrnexus generate schema account
bunx wrnexus generate routes
bunx wrnexus generate types .
```

Expected output includes created source paths followed by:

```text
✓ Generated app/routes.gen.ts (... routes)
✓ Generated .../app/types/wrnexus.generated.d.ts (... routes, ... components)
```

### Run development and production-runtime modes

```bash
bunx wrnexus dev . --port=3000
bunx wrnexus dev . --services --services-port=3099
bunx wrnexus dev . --production-runtime --port=3000
```

Expected output reports the active profile, discovered plugins/routes, generated cache path, and listening URL. Production-runtime mode rebuilds and reloads the exact production artifact.

### Database lifecycle

```bash
bunx wrnexus db status
bunx wrnexus db new create_accounts --from-models
bunx wrnexus db migrate
bunx wrnexus db generate
bunx wrnexus db seed
```

Expected output identifies the selected profile/dialect, migration state, applied migration names, and generated typed query files. Run `db rollback` only when intentionally reverting the latest migration.

### Diagnose, inspect, and enforce contracts

```bash
bunx wrnexus doctor .
bunx wrnexus typecheck .
bunx wrnexus compatibility check .
bunx wrnexus contracts check .
bunx wrnexus security audit .
bunx wrnexus inspect packages .
bunx wrnexus inspect routes .
bunx wrnexus inspect component Navbar .
bunx wrnexus analyze .
```

Successful checks exit with status 0. Inspect commands print the resolved package, route, component, asset, runtime, style, migration, or bundle contract without changing the application.

### Tests, API artifacts, SDKs, and deployment manifests

```bash
bunx wrnexus test unit .
bunx wrnexus test component .
bunx wrnexus test api .
bunx wrnexus test browser .
bunx wrnexus api generate .
bunx wrnexus api docs .
bunx wrnexus sdk generate typescript .
bunx wrnexus deploy docker .
```

Test commands return the underlying suite summary and a nonzero status on failure. API/SDK/deploy commands report each generated artifact; review and commit generated files after validation.

### Workspaces and environments

```bash
bunx wrnexus workspace company-platform
cd company-platform
bunx wrnexus workspace add reports --domain=reports.localhost
bunx wrnexus gateway --port=3000
bunx wrnexus production . --prepare-only
bunx wrnexus staging .
```

Expected output lists created apps, domain mappings, selected environment/profile, build and migration decisions, and the gateway address.

### Internationalization, native targets, MCP, and support bundles

```bash
bunx wrnexus i18n extract .
bunx wrnexus i18n validate .
bunx wrnexus generate mobile
bunx wrnexus mobile compile
bunx wrnexus native list
bunx wrnexus mcp .
bunx wrnexus report . --file=app/pages/index.wrn
```

Expected output identifies extracted/validated translation keys, generated native routes, available capabilities, MCP stdio startup, or the sanitized reproduction archive. MCP uses stdio, so protocol messages—not a web URL—are its normal runtime output.

### Safe upgrades

```bash
bunx wrnexus update . --latest --dry-run
bunx wrnexus update . --latest
```

The dry run previews dependency and migration changes. A real update prints the source and target versions, backup directory, each migration action, installation result, and verification result. Commit or back up the application before upgrading.

Release: WRNexusJS 0.8.7

# Complete CLI command reference

This section is generated from the installed `@wrnexus/cli@0.8.7` executable. It is the canonical command inventory for this release.

```text
wrnexus — WrNexus CLI

Usage:
  wrnexus dev [app-dir] [--port=3000] [--host=::]
                                             Start the development server (live reload)
  wrnexus dev [app-dir] --services        Start local production-service simulators with the app
  wrnexus dev [app-dir] --production-runtime
  wrnexus dev [app-dir] --services [--services-port=3099] [--services-http]
                                             Rebuild and reload the exact production artifact
  wrnexus build [app-dir]               Build a production server bundle + assets
  wrnexus preview [app-dir]             Serve the existing exact production output
  wrnexus create <app-name>             Scaffold a new app
  wrnexus workspace <name>              Scaffold a monorepo (apps/* + shared packages/*)
  wrnexus workspace add <name> [--domain=name.localhost]
                                             Add an app to the current workspace
  wrnexus gateway [--port=3000]         Serve every workspace app behind one port, routed by domain
  wrnexus production [workspace-dir]    Build, migrate, and serve the complete production workspace
  wrnexus <environment> [workspace-dir] Run a configured workspace environment (development/staging/custom)
  wrnexus generate <type> <name>        Scaffold a page | component | api | schema
  wrnexus generate routes | docker | mobile
                                             Generate routes or scaffold deployment targets
  wrnexus generate types [app-dir]       Generate application-wide route/component/key types
  wrnexus routes [app-dir]               Generate typed named routes
  wrnexus typecheck [app-dir]            Generate types and check TypeScript plus every .wrn file
  wrnexus mobile add <package...>       Install Capacitor or Expo native packages
  wrnexus mobile compile                Compile .wrn pages into native Expo routes
  wrnexus native list                   List cross-platform native capabilities
  wrnexus native add <capability...>    Install capability packages for the configured mobile mode
  wrnexus eject <name...>               Copy a Wire UI component into app/components
  wrnexus update [dir] [--latest]       Upgrade deps, migrate project files, and verify the app
  wrnexus db <cmd>                      Migrations: migrate | rollback | status | seed | generate | new
  wrnexus authz <cmd>                    Authorization: list | generate | init [--dialect=sqlite|postgres|mysql]
  wrnexus test [level] [app-dir] [--watch]
                                             Run unit | component | api | browser | visual | accessibility | performance
  wrnexus profiles [app-dir]            List config profiles (dev/prod/uat/…) and their env files
  wrnexus doctor [app-dir] [--fix]      Check project health; optionally apply safe repairs
  wrnexus compatibility <check|explain|upgrade> [app-dir]
                                             Inspect or explicitly upgrade behavior defaults
  wrnexus contracts <check|snapshot> [app-dir]
                                             Detect breaking boundary contract changes
  wrnexus security <audit|headers|test> [app-dir]
                                             Audit ASVS controls, inspect headers, or run abuse tests
  wrnexus api <generate|docs> [app-dir] Generate OpenAPI, docs, Postman, examples and SDKs
  wrnexus sdk generate <language> [app-dir]
                                             Generate TypeScript, JavaScript, Java, Go, or Python SDK
  wrnexus deploy <target> [app-dir]       Generate docker | kubernetes | systemd | railway | render | fly
  wrnexus mcp [app-dir]                   Start the WRNexus MCP server over stdio
  wrnexus i18n <extract|validate> [app-dir]
                                             Extract and audit translation keys
  wrnexus report [app-dir] [--file=app/pages/page.wrn]
                                             Create a sanitized reproduction bundle
  wrnexus playground [--port=4173]         Start the shareable WRN compiler playground
  wrnexus config [app-dir] --explain    Print the fully resolved profile configuration
  wrnexus analyze [app-dir]             Inspect dist/build-report.json and performance budgets
  wrnexus explain <route|build|hydration|bundle> [subject] [app-dir]
                                             Explain compiler and production build decisions
  wrnexus explain <cache|permission> <subject> [app-dir]
                                             Explain route caching or permission enforcement
  wrnexus inspect <target> [app-dir]    Inspect packages | plugins | routes | assets | runtimes | styles | migrations | bundle
  wrnexus inspect component <name> [app-dir]
                                             Inspect a component's typed public contract
  wrnexus generate system <name>        Scaffold a complete framework-native package

Profiles: pass --profile=<name> to dev/build/db (or set WRNEXUS_PROFILE) to load
that profile's .env cascade and config overrides. e.g. wrnexus dev --profile=uat

Workspace environments: configure protocol, rootDomain, port, hostname, runtime
(development|production), hmr, build, migrate, and profile in wrnexus.workspace.ts.
CLI overrides: --port, --host, --build/--no-build, --migrate/--no-migrate,
--profile, and --prepare-only.

Update options: --dry-run previews changes; --no-verify skips post-update check/build.
```

## CLI workflows with expected output

Commands below are copy-ready. Paths, ports, counts, and timings vary by project; the shown output identifies the success condition to check.

### Create, check, build, and preview an application

```bash
bunx @wrnexus/cli@0.8.7 create my-app
cd my-app
bun install
bunx wrnexus typecheck .
bunx wrnexus build .
bunx wrnexus preview . --port=3000
```

Expected output:

```text
✓ Application types are valid
✓ Runtime: .../dist/reactive.js
✓ Styles:  .../dist/styles.css
✓ Server:  .../dist/server.js
Run it:  bun .../dist/server.js
```

### Generate framework files and committed application types

```bash
bunx wrnexus generate page Dashboard
bunx wrnexus generate component status-card
bunx wrnexus generate api health
bunx wrnexus generate schema account
bunx wrnexus generate routes
bunx wrnexus generate types .
```

Expected output includes created source paths followed by:

```text
✓ Generated app/routes.gen.ts (... routes)
✓ Generated .../app/types/wrnexus.generated.d.ts (... routes, ... components)
```

### Run development and production-runtime modes

```bash
bunx wrnexus dev . --port=3000
bunx wrnexus dev . --services --services-port=3099
bunx wrnexus dev . --production-runtime --port=3000
```

Expected output reports the active profile, discovered plugins/routes, generated cache path, and listening URL. Production-runtime mode rebuilds and reloads the exact production artifact.

### Database lifecycle

```bash
bunx wrnexus db status
bunx wrnexus db new create_accounts --from-models
bunx wrnexus db migrate
bunx wrnexus db generate
bunx wrnexus db seed
```

Expected output identifies the selected profile/dialect, migration state, applied migration names, and generated typed query files. Run `db rollback` only when intentionally reverting the latest migration.

### Diagnose, inspect, and enforce contracts

```bash
bunx wrnexus doctor .
bunx wrnexus typecheck .
bunx wrnexus compatibility check .
bunx wrnexus contracts check .
bunx wrnexus security audit .
bunx wrnexus inspect packages .
bunx wrnexus inspect routes .
bunx wrnexus inspect component Navbar .
bunx wrnexus analyze .
```

Successful checks exit with status 0. Inspect commands print the resolved package, route, component, asset, runtime, style, migration, or bundle contract without changing the application.

### Tests, API artifacts, SDKs, and deployment manifests

```bash
bunx wrnexus test unit .
bunx wrnexus test component .
bunx wrnexus test api .
bunx wrnexus test browser .
bunx wrnexus api generate .
bunx wrnexus api docs .
bunx wrnexus sdk generate typescript .
bunx wrnexus deploy docker .
```

Test commands return the underlying suite summary and a nonzero status on failure. API/SDK/deploy commands report each generated artifact; review and commit generated files after validation.

### Workspaces and environments

```bash
bunx wrnexus workspace company-platform
cd company-platform
bunx wrnexus workspace add reports --domain=reports.localhost
bunx wrnexus gateway --port=3000
bunx wrnexus production . --prepare-only
bunx wrnexus staging .
```

Expected output lists created apps, domain mappings, selected environment/profile, build and migration decisions, and the gateway address.

### Internationalization, native targets, MCP, and support bundles

```bash
bunx wrnexus i18n extract .
bunx wrnexus i18n validate .
bunx wrnexus generate mobile
bunx wrnexus mobile compile
bunx wrnexus native list
bunx wrnexus mcp .
bunx wrnexus report . --file=app/pages/index.wrn
```

Expected output identifies extracted/validated translation keys, generated native routes, available capabilities, MCP stdio startup, or the sanitized reproduction archive. MCP uses stdio, so protocol messages—not a web URL—are its normal runtime output.

### Safe upgrades

```bash
bunx wrnexus update . --latest --dry-run
bunx wrnexus update . --latest
```

The dry run previews dependency and migration changes. A real update prints the source and target versions, backup directory, each migration action, installation result, and verification result. Commit or back up the application before upgrading.

# Canonical documentation locations

- Framework and package documentation: https://wrnexusjs.dev/
- Interactive UI component showcase and examples: https://component.wrnexusjs.dev/

# Installed package documentation

The following README files and declarations come from the installed private 0.8.7 release.

## @wrnexus/ai

Documentation URL: https://wrnexusjs.dev/packages/ai

# @wrnexus/ai

Provider-neutral AI orchestration for OpenAI, Anthropic, Google and local OpenAI-compatible models,
with streaming, structured output, tools, embeddings, vector search/RAG, conversation persistence,
templates, guardrails, usage events, fallback, rate limits and evaluation reports.

> A tiny, zero-dependency Claude (Anthropic) client for WrNexus apps — generate and stream text with Claude from any server-side code.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

`@wrnexus/ai` is a thin, dependency-free wrapper over the Anthropic **Messages API**,
built on `fetch` (Bun-native, no SDK). Use it in API routes, jobs, or middleware to
call Claude. It defaults to the most capable model, **`claude-opus-4-8`**, reads your
key from `ANTHROPIC_API_KEY`, and supports both one-shot generation and streaming.

## Installation

```bash
bun add @wrnexus/ai
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

Set your key in the environment (e.g. `.env`):

```
ANTHROPIC_API_KEY=sk-ant-...
```

## API

### `createAI(config?)`

Creates a client. The key is read at call time, so it's safe to create at import.

```ts
import { createAI } from "@wrnexus/ai";
const ai = createAI(); // or createAI({ apiKey, model, maxTokens, baseURL, version })
```

`AIConfig` fields (all optional):

| Field       | Default                     | Description                |
| ----------- | --------------------------- | -------------------------- |
| `apiKey`    | `ANTHROPIC_API_KEY`         | Anthropic API key          |
| `model`     | `"claude-opus-4-8"`         | Model id                   |
| `maxTokens` | `4096`                      | Default max output tokens  |
| `baseURL`   | `https://api.anthropic.com` | API base URL               |
| `version`   | `"2023-06-01"`              | `anthropic-version` header |

### `ai.generate(prompt, opts?): Promise<string>`

One-shot text generation. `prompt` is a string or a `Message[]` history.

```ts
const text = await ai.generate("Write a haiku about Bun.");

const reply = await ai.generate(
  [
    { role: "user", content: "My name is Ada." },
    { role: "assistant", content: "Hi Ada!" },
    { role: "user", content: "What's my name?" },
  ],
  { system: "You are concise." },
);
```

### `ai.stream(prompt, opts?): AsyncGenerator<string>`

Yields text deltas as they arrive.

```ts
for await (const chunk of ai.stream("Tell me a story.")) {
  process.stdout.write(chunk);
}
```

### `ai.streamResponse(prompt, opts?): Response`

Returns a streaming `text/plain` `Response` — drop it straight into an API route.

```ts
// app/api/chat.ts
import { createAI } from "@wrnexus/ai";
const ai = createAI();

export const POST = async (ctx) => {
  const { prompt } = await ctx.req.json();
  return ai.streamResponse(prompt);
};
```

### `GenerateOptions`

| Option      | Type                                              | Description                                          |
| ----------- | ------------------------------------------------- | ---------------------------------------------------- |
| `system`    | `string`                                          | System prompt                                        |
| `model`     | `string`                                          | Override the model for this call                     |
| `maxTokens` | `number`                                          | Override max output tokens                           |
| `thinking`  | `boolean`                                         | Enable adaptive extended thinking (deeper reasoning) |
| `effort`    | `"low" \| "medium" \| "high" \| "xhigh" \| "max"` | Reasoning effort / token spend                       |
| `messages`  | `Message[]`                                       | Full history — supersedes `prompt`                   |
| `signal`    | `AbortSignal`                                     | Cancel the request                                   |

> `temperature` / `top_p` are intentionally **not** exposed — the current Claude
> models reject them (400). Steer output with prompting instead.

### `AIError`

Thrown on non-2xx responses or a model refusal. Carries `.status` and `.type`
(e.g. `"authentication_error"`, `"rate_limit_error"`, `"refusal"`).

```ts
import { AIError } from "@wrnexus/ai";
try {
  await ai.generate("...");
} catch (e) {
  if (e instanceof AIError && e.type === "rate_limit_error") {
    /* back off */
  }
}
```

### Multi-provider client

`createAIClient` adds named-provider selection and fallback, capability discovery,
validated JSON output, validated tool execution, abort-aware exponential retries,
and per-provider circuit breakers. Attempt events intentionally contain metadata
only: prompts, credentials, and raw model responses are never passed to telemetry.

```ts
import { anthropicProvider, createAIClient } from "@wrnexus/ai";

const ai = createAIClient({
  providers: [anthropicProvider()],
  retry: { attempts: 3, baseDelayMs: 100, maxDelayMs: 2_000 },
  circuitBreaker: { failureThreshold: 5, resetAfterMs: 30_000 },
});

const result = await ai.generateObject<{ title: string }>("Return a JSON title", {
  validate: (value): value is { title: string } =>
    typeof value === "object" && value !== null && "title" in value,
});
```

Providers can return normalized `usage` (`inputTokens`, `outputTokens`,
`totalTokens`, and `costUsd`) and `toolCalls`. Use `executeTools` with a named,
validated tool registry; unknown tools and invalid arguments are rejected before
application code runs. `deterministicAIProvider` supplies ordered or computed
offline responses for tests and examples without API keys or network calls.

## Usage

### Return generated JSON from an API route

```ts
// app/api/summarize.ts — summarize posted text
import { createAI } from "@wrnexus/ai";
const ai = createAI();

export const POST = async (ctx) => {
  const { text } = await ctx.req.json().catch(() => ({}));
  if (!text) return Response.json({ error: "Provide 'text'." }, { status: 400 });
  const summary = await ai.generate(`Summarize in one sentence:\n\n${text}`, {
    system: "You are a precise summarizer.",
  });
  return Response.json({ summary });
};
```

### Stream a chat response to the browser

```ts
// app/api/chat.ts
import { createAI } from "@wrnexus/ai";

const ai = createAI({ model: "claude-sonnet-5" });

export const POST = async (ctx) => {
  const { messages } = await ctx.req.json();
  return ai.streamResponse(messages, {
    system: "Answer using concise Markdown.",
    maxTokens: 1_500,
  });
};
```

## Requirements / Notes

- **Bun-only.** Uses `fetch`, `ReadableStream`, `TextDecoder`/`TextEncoder`, and
  reads `ANTHROPIC_API_KEY` from `Bun.env` (falls back to `process.env`).
- **Zero dependencies** — no `@anthropic-ai/sdk`; talks to the Messages API directly.
- Defaults to `claude-opus-4-8`. Pass `{ model }` for a different model (e.g.
  `"claude-sonnet-5"` for speed/cost, `"claude-haiku-4-5"` for the fastest).

### Exported TypeScript declarations

```ts
export { A as AI, a as AIAttemptEvent, b as AICircuitBreakerOptions, c as AIClient, d as AIClientOptions, e as AIConfig, f as AIError, AIGuardrail, g as AIProvider, h as AIProviderCapabilities, i as AIResult, j as AIRetryOptions, k as AITool, l as AIToolCall, m as AIUsage, ConversationStore, D as DeterministicAIProviderOptions, E as Effort, EmbeddingProvider, G as GenerateOptions, HttpAIProviderOptions, M as Message, R as Role, VectorMatch, VectorRecord, VectorStore, n as aiProvider, aiRateLimiter, o as anthropicProvider, p as createAI, q as createAIClient, createRagPipeline, r as deterministicAIProvider, evaluateAI, googleAIProvider, guardedProvider, localAIProvider, maxPromptLength, memoryConversationStore, memoryVectorStore, openAIEmbeddings, openAIProvider, promptTemplate } from './platform.js';
```

---

## @wrnexus/auth

Documentation URL: https://wrnexusjs.dev/packages/auth

# @wrnexus/auth

Framework-native authentication, identity, account-security, and session management for WRNexusJS.

## Capabilities

- Password registration, login, recovery, reset, and authenticated password changes
- Email, phone, and username identities with verification and generic resend responses
- Magic links and passwordless email/SMS OTP login
- MFA transactions using verified email OTP, verified SMS OTP, TOTP, or recovery codes
- RFC 6238 TOTP with counter replay protection
- One-use recovery codes; regeneration invalidates previous unused codes
- Passkey/WebAuthn registration and strong passwordless sign-in through a provider contract
- OAuth account linking and provider sign-in
- Invitations, session rotation, idle and absolute expiry, revocation, and trusted devices
- Deny-by-default audited support impersonation
- Adaptive risk scoring, CAPTCHA escalation, temporary lockout, and optional login alerts
- Memory and SQL stores
- Optional encryption-keyring protection for TOTP and OAuth secrets
- Automatic API routes, middleware, browser schemas, components, runtime, migrations, and DevToolbar checks

Passkeys are a strong sign-in method. They are not currently exposed as a selectable second step in `TwoFactorChallenge`; the implemented MFA methods are email OTP, SMS OTP, TOTP, and recovery codes.

## Install

```bash
bun add @wrnexus/auth
```

WRNexusJS discovers the package automatically. Do not copy package components, client scripts, schemas, or standard `/api/auth/*` route files into the application.

## Default configuration

Create the engine:

```ts
// app/lib/auth.ts
import { createAuthEngine, MemoryAuthStore } from "@wrnexus/auth";

export const auth = createAuthEngine({
  store: new MemoryAuthStore(),
  secret: process.env.AUTH_SECRET!,
  issuer: "My application",
  onSignedIn(ctx, returnTo) {
    const safe = returnTo?.startsWith("/") && !returnTo.startsWith("//") ? returnTo : "/account";
    return Response.redirect(new URL(safe, ctx.url), 303);
  },
  onSignedOut(ctx) {
    return Response.redirect(new URL("/sign-in", ctx.url), 303);
  },
  onSuccessfulSignUp() {
    return {
      autoSignIn: true,
      redirectTo: "/account",
    };
  },
  delivery: {
    async send(message) {
      // Queue email/SMS through your provider. Never log message.code or message.token.
    },
  },
});
```

Authentication behavior belongs in this engine definition: delivery, token URL
mapping, successful sign-in/sign-out responses, password policy, risk thresholds,
MFA, passkeys, and auditing can all be configured in one server-only location.
The older `config.auth.onSignedIn` and `config.auth.onSignedOut` fields remain
supported as compatibility overrides, but new applications should configure
these hooks on `createAuthEngine`.

### Successful signup behavior

Without `onSuccessfulSignUp`, a successful package registration redirects to
`/sign-in`.

To sign in immediately after registration:

```ts
onSuccessfulSignUp(ctx, user) {
  return {
    autoSignIn: true,
    redirectTo: "/account",
  };
}
```

Automatic sign-in runs the normal login policy. It does not bypass required
email or phone verification, CAPTCHA, MFA, account status, or risk checks. The
hook may also return a `Response` for a completely custom HTTP result, or return
`{ redirectTo: "/welcome" }` to redirect without creating a session.

Register it through application configuration:

```ts
// wrnexus.config.ts
import type { AuthConfig } from "@wrnexus/auth";
import type { AppConfig } from "@wrnexus/styles";
import { auth } from "./app/lib/auth.ts";

const config = {
  auth: {
    engine: auth,
    routes: true,
    middleware: true,
    migrations: false,
  },
} satisfies AppConfig & { auth: AuthConfig };

export default config;
```

That configuration automatically activates package routes, auth-session middleware, components, browser validation schemas, and the auth client runtime. `setDefaultAuthEngine()` remains available only for advanced manual integrations and tests.

## SQL production configuration

```ts
import { createAuthEngine, SqlAuthStore } from "@wrnexus/auth";
import { getDb } from "@wrnexus/db";

export const auth = createAuthEngine({
  store: new SqlAuthStore(getDb()),
  secret: process.env.AUTH_SECRET!,
});
```

```ts
export default {
  db: {
    // Application database configuration.
  },
  auth: {
    engine: auth,
    routes: true,
    middleware: true,
    migrations: true,
  },
};
```

The package contributes both ordered migrations:

```text
001_auth.sql
002_auth_otp_purpose.sql
```

Migrations are enabled automatically only when `auth.engine` and a default `config.db` are present. Set `auth.migrations` explicitly when an application needs different behavior.

## Delivered action URLs

By default, the engine builds links from the supplied `baseUrl` and token purpose. Applications can map those links to their own page structure without replacing package APIs:

```ts
const auth = createAuthEngine({
  store: new SqlAuthStore(getDb()),
  secret: process.env.AUTH_SECRET!,
  tokenUrl({ purpose, token, baseUrl }) {
    if (!baseUrl) return undefined;

    const paths = {
      "verify-email": `/verify-email?token=${encodeURIComponent(token)}`,
      "verify-phone": `/verify-phone?token=${encodeURIComponent(token)}`,
      "password-reset": `/recover/reset?token=${encodeURIComponent(token)}`,
      "magic-link": `/magic-link?token=${encodeURIComponent(token)}`,
      invite: `/invitation?token=${encodeURIComponent(token)}`,
    };

    const path = paths[purpose as keyof typeof paths];
    return path ? new URL(path, baseUrl).toString() : undefined;
  },
});
```

Returning `undefined` intentionally omits the URL while still delivering the raw token. The callback runs only in trusted server code.

## Built-in validation

Every packaged auth form has a built-in `@wrnexus/validation` schema. The same resolved schema is used by the browser and the package API handler.

Default use requires no `app/schemas` files:

```wrn
<SignUp />
<SignIn />
<ForgotPassword />
<ResetPassword token='{token}' />
<TwoFactorChallenge />
```

To customize one schema, extend the package default and register only that override:

```ts
// app/schemas/custom-password-request.ts
import { authSchemas } from "@wrnexus/auth";
import { v } from "@wrnexus/validation";

export default authSchemas.passwordResetRequest.extend({
  identifier: v
    .string()
    .trim()
    .required("Enter your registered email address")
    .email("Enter a valid registered email address"),
});
```

```ts
import customPasswordRequest from "./app/schemas/custom-password-request.ts";

export default {
  auth: {
    engine: auth,
    schemas: {
      passwordResetRequest: customPasswordRequest,
    },
  },
};
```

`<ForgotPassword />` can keep its default `schema="auth-password-request"`. The plugin automatically publishes the overridden browser descriptor under that same built-in schema ID. All other forms continue using package defaults.

## Route controls

Use a boolean to enable or disable all package routes:

```ts
auth: {
  engine: auth,
  routes: true,
}
```

Or control feature groups:

```ts
routes: {
  enabled: true,
  registration: true,
  login: true,
  verification: true,
  password: true,
  invitations: true,
  magicLink: true,
  otp: true,
  mfa: true,
  sessions: true,
  impersonation: false,
  passkeys: true,
}
```

Application routes have normal framework precedence. Disable a package group only when the application intentionally owns every endpoint in that group; no `excludeRoutes` list is required.

## Package endpoints

```text
POST     /api/auth/register
POST     /api/auth/login
POST     /api/auth/logout
POST     /api/auth/verification/request
GET|POST /api/auth/verify/email
POST     /api/auth/verify/phone
POST     /api/auth/password/request
POST     /api/auth/password/reset
POST     /api/auth/password/change
POST     /api/auth/invitations/accept
POST     /api/auth/magic-link/request
GET|POST /api/auth/magic-link
POST     /api/auth/otp/login/request
POST     /api/auth/otp/login/complete
POST     /api/auth/otp
POST     /api/auth/otp/verify
POST     /api/auth/totp/setup
POST     /api/auth/totp/confirm
POST     /api/auth/totp/disable
POST     /api/auth/recovery-codes
POST     /api/auth/mfa/otp
POST     /api/auth/mfa/complete
GET      /api/auth/sessions
POST     /api/auth/sessions/revoke
POST     /api/auth/impersonation/start
POST     /api/auth/impersonation/stop
POST     /api/auth/passkeys/register/options
POST     /api/auth/passkeys/register/verify
POST     /api/auth/passkeys/login/options
POST     /api/auth/passkeys/login/verify
```

Each URL uses a route-specific module, so rewritten framework request URLs cannot make the handler fall through to a shared-dispatcher `404`.

Unsafe package routes validate the framework CSRF token by default. Set `auth.csrf: false` only when an external API gateway provides an equivalent protection model.

## Components

```wrn
<SignIn />
<SignUp />
<ForgotPassword />
<ResetPassword token='{token}' />
<OtpSignIn method="email-otp" />
<MagicLinkSignIn />
<PasskeyButton mode="authenticate" />
<TwoFactorChallenge mfaToken='{mfaToken}' challengeId='{challengeId}' />
<AuthenticatorSetup credentialId='{credentialId}' secret='{secret}' uri='{uri}' />
<RecoveryCodes codes='{codes}' />
<DeviceSessions sessions='{sessions}' currentSessionId='{currentSessionId}' />
<VerifyEmail token='{token}' identifier='{identifier}' />
<VerifyPhone token='{token}' identifier='{identifier}' />
<InvitationAccept token='{token}' />
<ImpersonationBanner targetName='{targetName}' />
<AccountStatus status='{account.status}' />
```

`identifier` is optional on verification components. Supply it when an unauthenticated verification page should support resending a token. The response remains generic whether the account exists or not.

## CAPTCHA and risk

The HTTP handlers never trust a browser `captchaVerified` field. CAPTCHA completion is accepted only from server-populated `ctx.locals.captcha.success` or `ctx.locals.captchaVerified === true`.

Rate limiting remains an application or gateway responsibility. Apply it to registration, login, reset, magic-link, OTP, verification, passkey, invitation, and impersonation endpoints.

## MFA

1. Password, OAuth, magic-link, or OTP login may return `code: "mfa-required"` with a short-lived `mfaToken`.
2. The response lists only methods actually available to that user.
3. Email/SMS MFA is offered only for verified linked identities.
4. `beginMfaOtp()` issues an MFA-bound OTP when needed.
5. `completeMfa()` consumes the one-time transaction and creates the session.

## Passkeys

The browser runtime coordinates `navigator.credentials.create()` and `navigator.credentials.get()`. A configured server-side `PasskeyProvider` must verify the challenge, RP ID, origin, signature, user presence or verification, counter, and credential ownership.

Multi-process deployments must provide a shared `PasskeyChallengeStore`; the default memory implementation is process-local. Missing passkey providers return a controlled `503` response rather than crashing the route.

## Protect long-lived secrets

```ts
import { createAuthSecretProtector } from "@wrnexus/auth";
import { createKeyring } from "@wrnexus/encryption";

const keyring = createKeyring([
  {
    id: "auth-2026-01",
    secret: process.env.AUTH_ENCRYPTION_KEY!,
    active: true,
  },
]);

const auth = createAuthEngine({
  store: new SqlAuthStore(getDb()),
  secret: process.env.AUTH_SECRET!,
  secretProtector: createAuthSecretProtector(keyring),
});
```

TOTP seeds and OAuth access/refresh tokens are protected before persistence. Keep old keys available during rotation.

## Custom HTTP integration

`createAuthHttpHandlers()` remains available for custom route paths or response behavior. Prefer package routes for standard flows; copied application API files are unnecessary.

## Development

```bash
bun run auth:dev
bun run validate:auth
```

Read [SECURITY.md](./SECURITY.md) before production deployment.

## Package-owned UI blocks and route helpers

Authentication forms continue to compose `@wrnexus/ui` inputs, buttons, cards, alerts, badges, avatars, and PIN controls. The package also provides:

- `<AuthShell />`
- `<AuthProviderButtons />`
- `<AuthSecurityNotice />`
- complete sign-in, sign-up, MFA, passkey, recovery, account-status, session, and impersonation blocks

Server helpers include `authRoute`, `authSuccess`, `authFailure`, `requireAuthUser`, `optionalAuthUser`, `currentAuthSession`, and `authComponentProps`.

### Exported TypeScript declarations

```ts
import { A as AuthEngine } from './engine-BDu0-aZp.js';
export { c as createAuthEngine, i as inferIdentityType, n as normalizeEmail, a as normalizeIdentity, b as normalizePhone, d as normalizeUsername, p as publicUser, s as safeAuthReturnTo } from './engine-BDu0-aZp.js';
import { o as AuthRiskSignals, m as AuthRiskDecision, s as AuthSession, B as AuthUser } from './types-BhwPi3qr.js';
export { A as AUTH_SECURITY_EVENT_TYPES, a as AuthAccountStatus, b as AuthClock, c as AuthDeliveryMessage, d as AuthDeliveryProvider, e as AuthEngineOptions, f as AuthIdentity, g as AuthIdentityType, h as AuthImpersonationDecision, i as AuthMfaMethod, j as AuthPublicUser, k as AuthRandom, l as AuthResult, n as AuthRiskLevel, p as AuthSecretProtector, q as AuthSecurityEvent, r as AuthSecurityEventType, t as AuthSessionVerificationHandler, u as AuthSignedInHandler, v as AuthSignedOutHandler, U as AuthStore, w as AuthSuccessfulSignUpAction, x as AuthSuccessfulSignUpHandler, y as AuthTokenPurpose, z as AuthTokenUrlInput, C as AuthenticatedContext, K as KnownAuthSecurityEventType, L as LoginAttempt, D as LoginInput, V as MemoryPasskeyChallengeStore, O as OAuthAccount, E as OneTimeToken, F as OtpChallenge, P as PasskeyAuthenticationOptions, W as PasskeyChallengeKind, X as PasskeyChallengeRecord, Y as PasskeyChallengeStore, G as PasskeyCredential, H as PasskeyProvider, I as PasskeyRegistrationOptions, J as PasskeyVerificationResult, M as PasswordBreachProvider, N as PasswordCredential, R as RecoveryCodeRecord, Q as RegisterInput, T as TotpCredential, S as TrustedDevice, Z as assertPasskeyProvider } from './types-BhwPi3qr.js';
export { MemoryAuthStore } from './stores/memory.js';
export { SqlAuthStore } from './stores/sql.js';
export { AUTH_SESSION_KEY, authSession, clearAuthSession, establishAuthSession, getAuthSession, getAuthUser, isAuthenticatedContext, requireAuth } from './middleware.js';
export { A as AuthHttpOptions, a as AuthPasskeyHttpOptions, b as AuthSchemaOverrides, c as AuthSchemaSet, d as authBrowserSchemaDescriptors, e as authBrowserSchemaMap, f as authSchemas, g as authenticatorConfirmSchema, h as authenticatorDisableSchema, i as authenticatorSetupSchema, j as changePasswordSchema, k as createAuthHttpHandlers, l as emptyActionSchema, m as impersonationStartSchema, n as invitationAcceptSchema, o as loginSchema, p as magicLinkConsumeSchema, q as magicLinkRequestSchema, r as mfaOtpRequestSchema, s as mfaSchema, t as otpIssueSchema, u as otpLoginCompleteSchema, v as otpLoginRequestSchema, w as otpSchema, x as passkeyAuthenticationOptionsSchema, y as passkeyAuthenticationVerifySchema, z as passkeyRegistrationOptionsSchema, B as passkeyRegistrationVerifySchema, C as passwordResetRequestSchema, D as passwordResetSchema, E as recoveryCodesSchema, F as registerSchema, G as resolveAuthSchemas, H as sessionRevokeSchema, I as signUpSchema, J as verificationRequestSchema, K as verificationTokenSchema } from './index-CSXo5T-F.js';
export { AuthAuditIssue, AuthConfig, AuthPluginOptions, AuthRoutesConfig, authComponentsDir, authPlugin } from './plugin.js';
export { DefaultAuthRouteOptions, clearDefaultAuthEngine, getDefaultAuthEngine, getDefaultAuthRouteOptions, getDefaultAuthSchemas, hasDefaultAuthEngine, setDefaultAuthEngine, setDefaultAuthRouteOptions, setDefaultAuthSchemas, tryGetDefaultAuthEngine } from './runtime.js';
export { createAuthSecretProtector } from './protector.js';
export { decodeBase32, encodeBase32, generateTotp, generateTotpSecret, totpUri, verifyTotp } from './totp/index.js';
import { Context } from '@wrnexus/core';
import '@wrnexus/oauth';
import '@wrnexus/db';
import '@wrnexus/validation';
import '@wrnexus/plugin';
import '@wrnexus/encryption';

interface RiskPolicy {
    captchaThreshold: number;
    mfaThreshold: number;
    blockThreshold: number;
}
declare function evaluateAuthRisk(signals?: AuthRiskSignals, policy?: RiskPolicy): AuthRiskDecision;

type AuthRouteName = "signIn" | "signUp" | "signOut" | "forgotPassword" | "resetPassword" | "verifyEmail" | "verifyPhone" | "twoFactor" | "sessions" | "passkeys";
declare function authRoute(name: AuthRouteName, options?: {
    basePath?: string;
    overrides?: Partial<Record<AuthRouteName, string>>;
}): string;
declare function authSuccess<T extends Record<string, unknown>>(data: T, init?: ResponseInit): Response;
declare function authFailure(code: string, message: string, status?: number, details?: Record<string, unknown>): Response;
declare function requireAuthUser(ctx: Context): AuthUser;
declare function optionalAuthUser(ctx: Context): AuthUser | null;
declare function currentAuthSession(engine: AuthEngine, sessionId: string | undefined): Promise<AuthSession | null>;
declare function authComponentProps(input: Record<string, unknown>, defaults?: {
    color?: string;
    size?: string;
    class?: string;
}): Record<string, unknown>;

export { AuthEngine, AuthRiskDecision, AuthRiskSignals, type AuthRouteName, AuthSession, AuthUser, type RiskPolicy, authComponentProps, authFailure, authRoute, authSuccess, currentAuthSession, evaluateAuthRisk, optionalAuthUser, requireAuthUser };
```

---

## @wrnexus/authz

Documentation URL: https://wrnexusjs.dev/packages/authz

# @wrnexus/authz

> Composable authorization for WrNexus — role-based (RBAC), policy-based (PBAC), and attribute-based (ABAC) access control that reduces to a boolean check plus an `authorize()` guard.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

`@wrnexus/authz` is a small, server-side authorization toolkit. It gives you three
interchangeable models — RBAC (roles → permissions), PBAC (policy predicates), and
ABAC (attribute matchers) — that all collapse to a `boolean | Promise<boolean>` decision.
Wrap any decision in a `Middleware` guard (`authorize`, `requireRole`, `requirePermission`)
to protect WrNexus routes. Reach for it whenever a route or action needs to be gated on who
the user is, what roles they hold, or attributes of the user and the resource. It plugs into
`@wrnexus/core` by reading `ctx.user` as the authorization subject.

## Installation

```bash
bun add @wrnexus/authz
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

## API

The package has a single entry point (`@wrnexus/authz`) exporting the following.

### Types

| Symbol                             | Description                                                                                   |
| ---------------------------------- | --------------------------------------------------------------------------------------------- |
| `Subject`                          | The authorized principal: `{ id?: string; roles?: string[]; [attribute: string]: unknown }`.  |
| `Rbac`                             | An RBAC checker: `{ can(subject, permission): boolean; permissionsFor(roles): Set<string> }`. |
| `Policy<S = Subject, R = unknown>` | A predicate `(subject: S, resource?: R) => boolean \| Promise<boolean>`.                      |

### RBAC

#### `defineRbac(roles: Record<string, string[]>): Rbac`

Builds an RBAC checker from a role → permissions map. Supported permission forms:

- `"*"` — grants every permission.
- `"ns:*"` — namespace wildcard (e.g. `"post:*"` grants `"post:write"`).
- `"role:<name>"` — inherits all permissions of another role (resolved recursively, cycle-safe).

The returned `Rbac` provides:

- `can(subject, permission)` — `true` if any of `subject.roles` grants `permission` (honouring `*` and namespace wildcards). Returns `false` when the subject has no roles.
- `permissionsFor(roles)` — the resolved `Set<string>` of all permissions granted to a set of roles.

#### `hasRole(subject: Subject | undefined, ...required: string[]): boolean`

`true` if the subject holds **all** of the given roles.

### PBAC / ABAC combinators

- `any<S, R>(...policies: Policy<S, R>[]): Policy<S, R>` — allow if **any** policy passes (OR); awaits async policies.
- `all<S, R>(...policies: Policy<S, R>[]): Policy<S, R>` — allow only if **all** policies pass (AND); awaits async policies.
- `attr<S extends Subject>(name: string, match: unknown | ((value: unknown) => boolean)): Policy<S>` — ABAC helper that allows when `subject[name]` equals `match`, or when `match` is a function, when `match(value)` is truthy.

### Guards (middleware)

Each guard returns a `@wrnexus/core` `Middleware`. A denied request short-circuits with
`Response.json({ ok: false, error: "Forbidden" }, { status: 403 })`.

- `authorize(policy: (ctx: Context) => boolean | Promise<boolean>): Middleware` — runs `policy` against the request `Context`; calls `next()` when it resolves truthy, otherwise returns 403.
- `requireRole(...roles: string[]): Middleware` — allows when `ctx.user` holds **any** of the listed roles.
- `requirePermission(rbac: Rbac, permission: string): Middleware` — allows when `rbac.can(ctx.user, permission)` is `true`.

## Usage

### RBAC

```ts
import { defineRbac, hasRole } from "@wrnexus/authz";

const rbac = defineRbac({
  admin: ["*"],
  editor: ["post:read", "post:write"],
  viewer: ["post:read"],
  // role inheritance: lead gets everything an editor has, plus post:publish
  lead: ["role:editor", "post:publish"],
});

const user = { id: "u1", roles: ["editor"] };

rbac.can(user, "post:write"); // true
rbac.can(user, "post:delete"); // false
rbac.permissionsFor(["lead"]); // Set { "post:read", "post:write", "post:publish" }
hasRole(user, "editor"); // true
```

### Guarding routes

```ts
import { authorize, requireRole, requirePermission, defineRbac } from "@wrnexus/authz";

const rbac = defineRbac({ admin: ["*"], editor: ["post:read", "post:write"] });

// Only admins or editors
app.get("/dashboard", requireRole("admin", "editor"), handler);

// Requires a specific permission
app.post("/posts", requirePermission(rbac, "post:write"), handler);

// Arbitrary policy over the request context
app.delete(
  "/posts/:id",
  authorize((ctx) => hasRole(ctx.user, "admin")),
  handler,
);
```

### PBAC / ABAC policies

```ts
import { any, all, attr, authorize, type Policy } from "@wrnexus/authz";

interface User {
  id: string;
  department?: string;
  roles?: string[];
}
interface Post {
  authorId: string;
}

// Ownership policy (subject + resource)
const ownsPost: Policy<User, Post> = (u, post) => u.id === post?.authorId;

// ABAC: attribute equality, or a predicate
const inEngineering = attr<User>("department", "engineering");
const isVerified = attr<User>("verified", (v) => v === true);

// Compose: allow if the user owns the post OR is in engineering AND verified
const canEdit = any(ownsPost, all(inEngineering, isVerified));

app.put(
  "/posts/:id",
  authorize((ctx) => canEdit(ctx.user as User, loadPost(ctx))),
  handler,
);
```

## Requirements / Notes

- **Bun-only** — like the rest of WrNexus, this package targets the Bun runtime; Node is not supported.
- Works with [`@wrnexus/core`](../core) — the guards return `Middleware` and read the subject from `ctx.user` on the request `Context`. Both types are imported from `@wrnexus/core`.
- Policy combinators (`any`, `all`) and `authorize` are async-aware, so policies may return a `Promise<boolean>` (e.g. for a database ownership check).

## Declaring permissions

The RBAC/PBAC/ABAC surface above is the low-level toolkit. On top of it sits a
declarative **registry + catalog + store + engine**: permissions, roles, and
policies are declared once in code, merged into a frozen catalog at boot, and
resolved per-request against a pluggable `PermissionStore` that holds who has
what.

Put declarations in `app/authz/<name>.ts`; they are discovered automatically
and merged (conflicting declarations of the same permission/role/policy across
files fail the boot loudly, naming both source files).

```ts
import { defineAuthz, owner } from "@wrnexus/authz";

export default defineAuthz({
  permissions: {
    "post:read": { title: "View posts", public: true },
    "post:delete": { title: "Delete posts", risk: "high" },
  },
  // "post:*" is a namespace wildcard grant, valid inside a role's list — it is
  // not itself a registered permission, so it can only ever grant permissions
  // that ARE declared above (e.g. "post:read", "post:delete").
  roles: { editor: ["post:*"], admin: ["role:editor"] },
  policies: { ownsPost: owner("id", "authorId") },
  bindings: { "post:delete": ["ownsPost"] },
});
```

`public: true` means anonymous callers may hold the permission — but any
policy bound to it still runs, and can still veto the anonymous caller (e.g. a
`notBanned` policy on a public `post:preview` permission).

## Checking permissions

Register `authzMiddleware` once, in `app/middleware/`, with the merged
catalog and a `PermissionStore`. Like every other `app/middleware/*.ts` file,
the registration is an eager, module-scope call — the same shape as
`authzMiddleware({ catalog, store })` requires — so it must run after the
catalog has been populated. Both the dev server and `wrnexus build`'s
generated production entry guarantee `getAuthzCatalog()` is populated before
any app middleware module evaluates. Name the file so it sorts after whatever
middleware sets `ctx.user` (middleware runs in alphabetical filename order —
`authz.ts` after `auth.ts`, for instance).

```ts
// app/middleware/authz.ts
import { authzMiddleware, getAuthzCatalog } from "@wrnexus/authz";
import { dbPermissionStore } from "@wrnexus/authz/db";
import { getDb } from "@wrnexus/db";

export default authzMiddleware({ catalog: getAuthzCatalog(), store: dbPermissionStore(getDb()) });
```

> **`subject.id` must be a non-empty string.** The engine denies (and logs to
> stderr) whenever `ctx.user.id` is present but not a non-empty string — this
> includes the common case of an integer primary key. Coerce it before it
> reaches `ctx.user`, e.g. `user.id = String(row.id)`, or every request for
> that user denies with "Invalid subject" instead of resolving normally.
> `owner()` (the built-in ownership policy) compares subject and resource ids
> with `Object.is`, so both sides must be the same type too — `owner()` on a
> numeric `resource.authorId` against a stringified `subject.id` never
> matches even when they represent "the same" id.

There is no per-route `middleware` export — `app/middleware/*.ts` is the only
place middleware is registered. To gate part of the app, branch on the
request the same way any other conditional middleware does (compare
`app/middleware/captcha-login.ts` in the auth showcase, which branches on
method + path the same way):

```ts
// app/middleware/protect-posts.ts
import type { Context, Next } from "@wrnexus/core";
import { guardPermission } from "@wrnexus/authz";

const guardPostWrite = guardPermission("post:write");

export default function protectPosts(ctx: Context, next: Next) {
  return ctx.url.pathname.startsWith("/api/posts") && ctx.req.method !== "GET"
    ? guardPostWrite(ctx, next)
    : next();
}
```

Or check inline inside a route handler with the free function `can()`:

```ts
// app/api/posts/[id].ts
import type { Context } from "@wrnexus/core";
import { can } from "@wrnexus/authz";

export const DELETE = async (ctx: Context) => {
  const post = { id: "1", authorId: "alice" }; // load your own resource here
  if (!(await can(ctx, "post:delete", post))) {
    return Response.json({ ok: false, error: "Forbidden" }, { status: 403 });
  }
  return Response.json({ ok: true });
};
```

`can()` is a free function taking `ctx`, not `ctx.can` — `@wrnexus/core` must
not depend on `@wrnexus/authz`, so the per-request resolver lives in
`ctx.locals` instead, reached through `can()` / `decideFor()` /
`guardPermission()` / `filterCan()`. Calling any of them before
`authzMiddleware` has run for that request throws a `WRN-AUTHZ-SETUP` error
naming the missing registration, rather than silently denying.

See `examples/auth-showcase/app/authz/showcase.ts` and
`examples/auth-showcase/app/middleware/authz.ts` for a complete, runnable
version of this wiring.

## Precedence

1. An explicit deny wins over everything, including `*` — and honours the
   same namespace-wildcard matching as grants (denying `post:*` blocks
   `post:comment:delete`, not just `post:*` itself).
2. A bound policy can veto a permission a role grants, and runs even for a
   `public: true` permission — including for an anonymous caller.
3. Otherwise the permission must be held via a role or an explicit grant.
4. Default deny.

Every failure — an unknown permission (outside strict/dev mode), a store
outage, a thrown policy — denies rather than throwing through to the caller.

`permissionsFor()` (on the resolver returned by `createAuthzResolver`) is a
coarse hint for hiding UI (e.g. a menu section), **never authoritative**. A
`Set<string>` cannot represent "granted `post:*` except `post:delete`", so a
narrow deny beneath a broad grant is invisible to it — the set still contains
`post:*` while `can()` / `decide()` correctly refuse `post:delete`. Gate real
actions with `can()`, `decideFor()`, or `filterCan()`; never by matching
against `permissionsFor()`'s result.

## CLI

```bash
wrnexus authz list      # every registered permission, role, and policy
wrnexus authz generate  # app/authz/permissions.gen.ts type unions
wrnexus authz init      # scaffold the assignment-table migration
```

`wrnexus authz generate`'s output is a plain `Permission | Role` string-literal
union — `can()`, `guardPermission()`, and `decideFor()` all take a bare
`string` and nothing reads this file automatically, so import it to type your
own helpers/constants against the registered catalog, e.g.:

```ts
import type { Permission } from "app/authz/permissions.gen.ts";

function guard(permission: Permission) {
  return guardPermission(permission);
}
```

### Exported TypeScript declarations

```ts
import { Context, Middleware } from '@wrnexus/core';

/**
 * Validate and freeze one authorization declaration. Called from
 * `app/authz/<name>.ts` as the module's default export.
 */
declare function defineAuthz(module: AuthzModule): AuthzModule;

interface CatalogSource {
    /** File or package that declared this module, used in conflict messages. */
    source: string;
    module: AuthzModule;
}
declare function emptyCatalog(): AuthzCatalog;
declare function mergeCatalogs(sources: CatalogSource[]): AuthzCatalog;

/**
 * A process-wide authorization catalog registry, mirroring `@wrnexus/db`'s
 * `client.ts` (`setDb`/`getDb`/`hasDb`). It exists for the same reason: app
 * middleware runs at module-eval time — `app/middleware/*.ts` registers
 * `authzMiddleware({ catalog, store, ... })` itself, an EAGER call (the same
 * shape as `logger.ts`'s `export default requestLogger({...})`), and it needs
 * the merged catalog *then*, before its own module body finishes running.
 * Passing it through `ctx` does not work at that point, so the framework
 * loads and merges every `app/authz/*.ts` declaration and stashes it here
 * before any other module can observe it:
 *
 *  - dev: `startServer` calls `loadAppAuthzCatalog` + `setAuthzCatalog`
 *    before middleware is resolved.
 *  - prod (the normal `wrnexus build` output): the generated entry statically
 *    imports a small `.authz-setup.ts` module FIRST — before any page, API,
 *    or middleware import — which calls `setAuthzCatalog` at ITS OWN module
 *    scope. ES modules evaluate every static import before the importing
 *    module's body runs, and evaluate sibling imports in declaration order,
 *    so import position is evaluation order: this guarantees the catalog
 *    exists before app middleware's own module body (which may read it
 *    eagerly) ever evaluates. `createProductionHandlers` (`prod.ts`) then
 *    repeats the merge as an idempotent second pass, mainly so a caller who
 *    bypasses the generated entry and invokes it directly still gets a
 *    catalog — for THAT path specifically, an eager module-scope read in
 *    middleware is only safe if the caller sets the catalog before importing
 *    the middleware itself, since no generated `.authz-setup.ts` runs first.
 *
 * The framework never installs `authzMiddleware` itself — the app always
 * chooses its own store and registers the middleware; this registry only
 * makes the merged catalog reachable when it does.
 */

/** Set the process-wide authorization catalog (called by the framework at boot). */
declare function setAuthzCatalog(next: AuthzCatalog): AuthzCatalog;
/** The process-wide authorization catalog. Throws if it hasn't been set. */
declare function getAuthzCatalog(): AuthzCatalog;
/** Whether the process-wide authorization catalog has been set. */
declare function hasAuthzCatalog(): boolean;

interface AuthzAuditEvent {
    subjectId?: string;
    scope?: AuthzScope;
    permission: string;
    allowed: boolean;
    reason?: string;
    policy?: string;
    /** Epoch milliseconds. */
    at: number;
}
interface AuthzAuditSink {
    record(event: AuthzAuditEvent): void | Promise<void>;
}
interface MemoryAuditSink extends AuthzAuditSink {
    events: AuthzAuditEvent[];
    clear(): void;
}
declare function memoryAuditSink(): MemoryAuditSink;
declare function consoleAuditSink(): AuthzAuditSink;
/** Record without ever letting a sink failure escape into the request path. */
declare function safeRecord(sink: AuthzAuditSink | undefined, event: AuthzAuditEvent): void;

interface AuthzResolverOptions {
    catalog: AuthzCatalog;
    store: PermissionStore;
    audit?: AuthzAuditSink;
    /**
     * Throw on an unregistered permission instead of denying. Defaults to true
     * outside production, so typos surface during development.
     */
    strict?: boolean;
    /** Record allows as well as denies. Off by default to bound write volume. */
    auditAllows?: boolean;
}
interface DecideInput {
    subject: {
        id?: string;
        [key: string]: unknown;
    } | null | undefined;
    permission: string;
    resource?: unknown;
    scope?: AuthzScope;
}
interface AuthzResolver {
    /**
     * Effective permissions with denied entries removed — for coarse gating such
     * as hiding a menu section.
     *
     * NOT authoritative. A set of strings cannot express "everything under
     * `post:*` except `post:delete`", so a narrow deny beneath a broad grant is
     * not representable here: the set still contains `post:*` while `decide()`
     * correctly refuses `post:delete`. Gate individual actions with `decide()`
     * (or `can()` / `filterCan()`), never by matching against this set.
     */
    permissionsFor(subjectId: string, scope?: AuthzScope): Promise<Set<string>>;
    decide(input: DecideInput): Promise<AuthorizationDecision>;
}
/** Expand roles into their granted entries, following `role:` and stopping on cycles. */
declare function expandRoles(catalog: AuthzCatalog, roles: readonly string[]): Set<string>;
/**
 * Exact match, root wildcard, or a namespace wildcard at any depth.
 *
 * Do NOT gate access by matching against `permissionsFor()`'s result — that set
 * cannot represent a narrow deny beneath a broad grant, so the composition
 * returns true where `decide()` refuses. Use `decide()` / `can()` instead.
 */
declare function permissionMatches(granted: Set<string>, permission: string): boolean;
/**
 * True if any entry in the deny list covers `permission`. Denies honour the
 * same depth-aware wildcards as grants, so denying "post:*" blocks
 * post:comment:delete rather than being accepted and silently doing nothing.
 */
declare function deniedBy(denies: readonly string[], permission: string): boolean;
declare function createAuthzResolver(options: AuthzResolverOptions): AuthzResolver;

/**
 * `can` is deliberately not a Context member: @wrnexus/core must not depend on
 * @wrnexus/authz. The per-request resolver lives here instead.
 */
declare const AUTHZ_LOCALS_KEY = "_authz";
/** Install the per-request resolver. Register early, after sessionAuth. */
declare function authzMiddleware(options: AuthzResolverOptions): Middleware;
/**
 * Object resources are memoised by identity (`byRef`), never by serialising
 * their contents — serialisation is what let unrelated resources collide
 * (same `id` shape, circular references, BigInt fields, throwing getters all
 * funnelled into one bucket). Symbols are memoised by identity too (`bySymbol`)
 * since `String(symbol)` collapses distinct symbols with the same description.
 * Primitive/absent resources are memoised by a
 * `[scope, permission, typeof, String(value)]` tuple, with `-0` rendered
 * distinctly from `0` since `String(-0) === "0"` would otherwise merge them.
 *
 * Subject and scope are both part of the key. A request that reassigns
 * ctx.user (impersonation, step-up auth, session revocation) or ctx.tenant
 * must not be served the previous principal's verdict from the memo.
 */
declare function decideFor(ctx: Context, permission: string, resource?: unknown): Promise<AuthorizationDecision>;
declare function can(ctx: Context, permission: string, resource?: unknown): Promise<boolean>;
interface GuardOptions {
    /** Load the resource a bound policy needs. */
    getResource?: (ctx: Context) => unknown;
    /** Include reason and policy name in the 403 body. Off by default. */
    exposeReason?: boolean;
    /** Redirect page requests here instead of returning 403. Ignored for JSON/API requests and for any non-local target. */
    redirectTo?: string;
}
/**
 * Guard a route on a registered permission. Named `guardPermission` because
 * `requirePermission(rbac, permission)` already exists with a different shape.
 */
declare function guardPermission(permission: string, options?: GuardOptions): Middleware;
/** Keep only the items the current subject may act on. */
declare function filterCan<T>(ctx: Context, permission: string, items: readonly T[]): Promise<T[]>;

/**
 * Emit `Permission`/`Role` string-literal unions from the registered catalog.
 *
 * This does NOT make `can(ctx, "post:wrtie")` a type error — `can()`,
 * `guardPermission()`, and `decideFor()` all take a bare `string`, and
 * nothing in the framework consumes this generated file automatically.
 * Import the unions yourself to type your OWN helpers/constants, e.g.
 * `const PERM: Permission = "post:write"` or a typed wrapper around `can()`.
 */
declare function generatePermissionTypes(catalog: AuthzCatalog): string;

/**
 * @wrnexus/authz — authorization: role-based (RBAC), policy-based (PBAC), and
 * attribute-based (ABAC). Compose freely; all three reduce to a boolean check
 * plus an `authorize()` guard middleware.
 *
 *   const rbac = defineRbac({ admin: ["*"], editor: ["post:read", "post:write"] });
 *   rbac.can(user, "post:write");
 *
 *   // PBAC/ABAC: a policy is a predicate over subject + resource + attributes
 *   const ownsPost: Policy<User, Post> = (u, post) => u.id === post.authorId;
 *   authorize((ctx) => ownsPost(ctx.user, resource))  // middleware
 */

interface Subject {
    id?: string;
    roles?: string[];
    [attribute: string]: unknown;
}
interface Rbac {
    /** True if any of the subject's roles grants `permission` (supports "*" and "ns:*"). */
    can(subject: Subject | undefined, permission: string): boolean;
    /** All permissions granted to a set of roles. */
    permissionsFor(roles: string[]): Set<string>;
}
/** Build an RBAC checker from a role → permissions map. */
declare function defineRbac(roles: Record<string, string[]>): Rbac;
/** True if the subject has ALL of the given roles. */
declare function hasRole(subject: Subject | undefined, ...required: string[]): boolean;
/** A policy predicate: subject (+ optional resource/attributes) → allowed. */
type Policy<S = Subject, R = unknown> = (subject: S, resource?: R) => boolean | Promise<boolean>;
/** Combine policies: allow if ANY passes (OR). */
declare function any<S, R>(...policies: Policy<S, R>[]): Policy<S, R>;
/** Combine policies: allow only if ALL pass (AND). */
declare function all<S, R>(...policies: Policy<S, R>[]): Policy<S, R>;
/** ABAC helper: allow when an attribute matches (equality or predicate). */
declare function attr<S extends Subject>(name: string, match: unknown | ((value: unknown) => boolean)): Policy<S>;
/** Guard a route with a policy over `ctx` (reads `ctx.user` as the subject). */
declare function authorize(policy: (ctx: Context) => boolean | Promise<boolean>): Middleware;
/** Guard requiring one of the given roles. */
declare function requireRole(...roles: string[]): Middleware;
/** Guard requiring an RBAC permission. */
declare function requirePermission(rbac: Rbac, permission: string): Middleware;

interface AuthorizationDecision {
    allowed: boolean;
    reason?: string;
    policy?: string;
    metadata?: Record<string, unknown>;
}
type DecisionPolicy<S = Subject, R = unknown> = (subject: S, resource?: R) => AuthorizationDecision | Promise<AuthorizationDecision>;
declare function allow(reason?: string, metadata?: Record<string, unknown>): AuthorizationDecision;
declare function deny(reason?: string, metadata?: Record<string, unknown>): AuthorizationDecision;
declare function decision<S, R>(name: string, policy: Policy<S, R>, denial?: string): DecisionPolicy<S, R>;
declare function owner<SubjectType extends Subject, Resource extends Record<string, unknown>>(subjectKey?: keyof SubjectType, resourceKey?: keyof Resource | string): DecisionPolicy<SubjectType, Resource>;
declare function anyDecision<S, R>(...policies: DecisionPolicy<S, R>[]): DecisionPolicy<S, R>;
declare function allDecisions<S, R>(...policies: DecisionPolicy<S, R>[]): DecisionPolicy<S, R>;
interface AuthorizeDecisionOptions {
    /**
     * Include `reason` and `policy` in the 403 body. Off by default: policy
     * names describe internal authorization structure and should not reach an
     * unauthenticated caller.
     */
    exposeReason?: boolean;
}
declare function authorizeDecision(evaluate: (ctx: Context) => AuthorizationDecision | Promise<AuthorizationDecision>, options?: AuthorizeDecisionOptions): Middleware;
declare function filterAuthorized<S, R>(subject: S, values: readonly R[], policy: Policy<S, R>): Promise<R[]>;

/** Narrows an assignment to a tenant. Absent means a global assignment. */
interface AuthzScope {
    tenantId?: string;
}
interface PermissionMeta {
    title?: string;
    description?: string;
    risk?: "low" | "medium" | "high";
    /** Granted to anonymous subjects. Every other permission denies without a user. */
    public?: boolean;
}
interface AttributeMeta {
    description?: string;
}
/** One `app/authz/<name>.ts` declaration. */
interface AuthzModule {
    permissions?: Record<string, PermissionMeta>;
    roles?: Record<string, string[]>;
    policies?: Record<string, DecisionPolicy<never, never>>;
    attributes?: Record<string, AttributeMeta>;
    /** permission id -> policy names that must pass for it. */
    bindings?: Record<string, string[]>;
}
/** The merged, frozen view of every declaration in the app. */
interface AuthzCatalog {
    permissions: ReadonlyMap<string, PermissionMeta>;
    roles: ReadonlyMap<string, readonly string[]>;
    policies: ReadonlyMap<string, DecisionPolicy<never, never>>;
    attributes: ReadonlyMap<string, AttributeMeta>;
    bindings: ReadonlyMap<string, readonly string[]>;
}
interface SubjectAssignments {
    roles: string[];
    /** Explicit allows, bypassing roles. */
    grants: string[];
    /** Explicit denies. Win over everything, including "*". */
    denies: string[];
}

type GrantEffect = "allow" | "deny";
interface PermissionStore {
    assignmentsFor(subjectId: string, scope?: AuthzScope): Promise<SubjectAssignments>;
    assignRole(subjectId: string, role: string, scope?: AuthzScope): Promise<void>;
    revokeRole(subjectId: string, role: string, scope?: AuthzScope): Promise<void>;
    grant(subjectId: string, permission: string, effect: GrantEffect, scope?: AuthzScope): Promise<void>;
    revokeGrant(subjectId: string, permission: string, scope?: AuthzScope): Promise<void>;
    listSubjects(scope?: AuthzScope): Promise<string[]>;
}
/**
 * Global assignments are stored under the empty-string scope key. An OMITTED
 * scope means global; an explicitly EMPTY or non-string tenantId is refused,
 * because an empty string is indistinguishable from global (and would let a
 * caller who controls the tenant id read and write global assignments), and a
 * non-string value (e.g. `null` from a JSON body or a nullable column) would
 * otherwise flow through un-normalised and leave the adapters disagreeing
 * about what happened.
 */
declare function scopeKey(scope?: AuthzScope): string;
declare function memoryPermissionStore(): PermissionStore;
interface CachedPermissionStore extends PermissionStore {
    /** Drop one subject. Call after changing roles out of band. */
    invalidate(subjectId: string, scope?: AuthzScope): void;
    invalidateAll(): void;
    /** Cached entry count, for tests and diagnostics. */
    size(): number;
}
interface CacheOptions {
    ttlMs?: number;
    max?: number;
}
/**
 * Caches assignment reads. Writes through this decorator invalidate the
 * affected subject immediately; changes made directly against the inner store
 * need an explicit `invalidate()` call rather than waiting out the TTL.
 */
declare function cachedPermissionStore(inner: PermissionStore, options?: CacheOptions): CachedPermissionStore;

export { AUTHZ_LOCALS_KEY, type AttributeMeta, type AuthorizationDecision, type AuthorizeDecisionOptions, type AuthzAuditEvent, type AuthzAuditSink, type AuthzCatalog, type AuthzModule, type AuthzResolver, type AuthzResolverOptions, type AuthzScope, type CacheOptions, type CachedPermissionStore, type CatalogSource, type DecideInput, type DecisionPolicy, type GrantEffect, type GuardOptions, type MemoryAuditSink, type PermissionMeta, type PermissionStore, type Policy, type Rbac, type Subject, type SubjectAssignments, all, allDecisions, allow, any, anyDecision, attr, authorize, authorizeDecision, authzMiddleware, cachedPermissionStore, can, consoleAuditSink, createAuthzResolver, decideFor, decision, defineAuthz, defineRbac, deniedBy, deny, emptyCatalog, expandRoles, filterAuthorized, filterCan, generatePermissionTypes, getAuthzCatalog, guardPermission, hasAuthzCatalog, hasRole, memoryAuditSink, memoryPermissionStore, mergeCatalogs, owner, permissionMatches, requirePermission, requireRole, safeRecord, scopeKey, setAuthzCatalog };
```

---

## @wrnexus/benchmark

Documentation URL: https://wrnexusjs.dev/packages/benchmark

# @wrnexus/benchmark

Deterministic benchmark execution, percentiles, baseline comparisons, and regression budgets for builds, SSR, hydration, stores, and application hot paths.

```ts
import { runBenchmark, assertBenchmarkBudget } from "@wrnexus/benchmark";
const result = await runBenchmark("render", render, { iterations: 100 });
assertBenchmarkBudget(result, baseline, { p95Percent: 5 });
```

### Exported TypeScript declarations

```ts
interface BenchmarkOptions {
    iterations?: number;
    warmup?: number;
    clock?: () => number;
    setup?: () => void | Promise<void>;
    teardown?: () => void | Promise<void>;
}
interface BenchmarkResult {
    name: string;
    iterations: number;
    totalMs: number;
    meanMs: number;
    minMs: number;
    maxMs: number;
    p50Ms: number;
    p95Ms: number;
    p99Ms: number;
    operationsPerSecond: number;
    samples: number[];
}
interface RegressionBudget {
    meanPercent?: number;
    p95Percent?: number;
    maxAbsoluteMs?: number;
    minOperationsPerSecond?: number;
}
interface RegressionViolation {
    metric: "meanMs" | "p95Ms" | "maxMs" | "operationsPerSecond";
    baseline?: number;
    current: number;
    limit: number;
    message: string;
}
declare function percentile(values: readonly number[], quantile: number): number;
declare function runBenchmark(name: string, operation: () => void | Promise<void>, options?: BenchmarkOptions): Promise<BenchmarkResult>;
declare function compareBenchmark(current: BenchmarkResult, baseline: BenchmarkResult | undefined, budget?: RegressionBudget): RegressionViolation[];
declare function assertBenchmarkBudget(current: BenchmarkResult, baseline: BenchmarkResult | undefined, budget: RegressionBudget): void;

export { type BenchmarkOptions, type BenchmarkResult, type RegressionBudget, type RegressionViolation, assertBenchmarkBudget, compareBenchmark, percentile, runBenchmark };
```

---

## @wrnexus/cache

Documentation URL: https://wrnexusjs.dev/packages/cache

# @wrnexus/cache

Bounded in-memory/tag caching and HTTP response caching for WRNexusJS. Supports request deduplication, tag invalidation, ETags, fresh/stale states, and optional detached stale revalidation.

```ts
import { connectCacheInvalidation, TagCache, responseCache } from "@wrnexus/cache";
const cache = new TagCache({ ttlMs: 60_000, staleWhileRevalidateMs: 300_000 });
export default responseCache({ cache, tags: ["products"] });
```

`TagCache` bounds entries with LRU-style eviction, deduplicates concurrent
loaders, and prevents an invalidated in-flight loader from repopulating stale
data. Use `lookup()` when fresh/stale state matters, or `getOrLoad()` for
stampede-safe loading.

For multi-instance applications, connect the cache to any compatible pub/sub
bus (including `@wrnexus/pubsub`). Namespaces isolate applications sharing the
same broker. Local invalidation happens first and the returned promise confirms
cross-instance publication; failures remain visible to the caller.

```ts
import { connectCacheInvalidation, TagCache } from "@wrnexus/cache";
import { createPubSub } from "@wrnexus/pubsub";
import { redisDriver } from "@wrnexus/pubsub/redis";

const cache = new TagCache({ maxEntries: 10_000 });
const bus = createPubSub(redisDriver(process.env.REDIS_URL));
const invalidation = connectCacheInvalidation(cache, bus, {
  namespace: "storefront-production",
  onError: (error) => logger.error("cache invalidation failed", { error }),
});

await invalidation.invalidateTag("products");
await invalidation.delete("product:42");

// Unsubscribes this cache only; the shared bus remains owned by the app.
invalidation.close();
await bus.close();
```

## Framework cache layers

`CacheCoordinator` keeps the four cache lifetimes explicit:

- `coordinator.request()` creates request-only deduplication.
- `coordinator.data` caches loader/query results.
- `coordinator.component` caches reusable rendered fragments.
- `coordinator.page` caches complete safe documents.

All cross-request layers are bounded, tag-aware, stale-while-revalidate capable,
stampede-safe, and expose `withLock()` for exclusive per-key work. `inspect()`
returns metadata without cached values. Development applications expose that
inspection through the Cache panel and `GET /__wrnexus/cache`.

Pages and components can opt in declaratively:

```wrn
cache {
  scope = "page"
  strategy = "stale-while-revalidate"
  ttl = "5m"
  stale = "10m"
  tags = ["catalog", "marketing"]
  vary = ["tenant", "language"]
}
```

Omit `scope` to cache named loader data. Use `scope = "page"` for full-page
caching. Component policies cache their rendered fragment. Authenticated user
and tenant identities are always included automatically; page caches also vary
by language, theme, and accent. Add header names or `cookie:name` entries for
other application-specific variation. Pages containing CSRF forms are never
stored in the full-page cache.

### Exported TypeScript declarations

```ts
import { Context, Middleware } from '@wrnexus/core';

interface CacheEntry<V> {
    value: V;
    createdAt: number;
    expiresAt: number;
    staleUntil: number;
    tags: string[];
}
type CacheLookup<V> = {
    state: "miss";
} | {
    state: "fresh" | "stale";
    entry: CacheEntry<V>;
};
interface CacheSetOptions {
    ttlMs?: number;
    staleWhileRevalidateMs?: number;
    tags?: string[];
}
interface TagCacheOptions {
    ttlMs?: number;
    staleWhileRevalidateMs?: number;
    maxEntries?: number;
    clock?: () => number;
    onEvent?: (event: CacheEvent) => void;
}
interface CacheEvent {
    operation: "hit" | "stale" | "miss" | "set" | "delete" | "invalidate" | "clear" | "load";
    key?: string;
    tags?: string[];
    at: number;
}
interface CacheSnapshotEntry {
    key: string;
    state: "fresh" | "stale";
    createdAt: number;
    expiresAt: number;
    staleUntil: number;
    tags: string[];
}
declare class TagCache<V = unknown> {
    private entries;
    private tagIndex;
    private pending;
    private locks;
    private revisions;
    private readonly ttlMs;
    private readonly staleMs;
    private readonly maxEntries;
    private readonly clock;
    private readonly onEvent?;
    constructor(options?: TagCacheOptions);
    private emit;
    lookup(key: string): CacheLookup<V>;
    get(key: string): V | undefined;
    set(key: string, value: V, options?: CacheSetOptions): void;
    private store;
    getOrLoad(key: string, loader: () => V | Promise<V>, options?: CacheSetOptions): Promise<V>;
    /** Serialize arbitrary cache-adjacent work for a key without storing its result. */
    withLock<T>(key: string, task: () => T | Promise<T>): Promise<T>;
    delete(key: string): boolean;
    private removeEntry;
    invalidateTag(tag: string): number;
    invalidateTags(tags: Iterable<string>): number;
    clear(): void;
    get size(): number;
    snapshot(): CacheSnapshotEntry[];
    private revision;
    private bump;
}

type CacheLayerName = "data" | "component" | "page";
interface CacheInspection {
    layers: Record<CacheLayerName, ReturnType<TagCache<unknown>["snapshot"]>>;
    recentEvents: Array<CacheEvent & {
        layer: CacheLayerName;
    }>;
}
interface CacheCoordinatorOptions extends Omit<TagCacheOptions, "onEvent"> {
    eventLimit?: number;
    onEvent?: (event: CacheEvent & {
        layer: CacheLayerName;
    }) => void;
}
/** A request-lifetime cache: deduplicates work without leaking values between requests. */
declare class RequestCache {
    private pending;
    getOrLoad<V>(key: string, loader: () => V | Promise<V>): Promise<V>;
    clear(): void;
}
/** Owns the three cross-request cache layers and creates isolated request caches. */
declare class CacheCoordinator {
    readonly data: TagCache<unknown>;
    readonly component: TagCache<unknown>;
    readonly page: TagCache<unknown>;
    private readonly events;
    private readonly eventLimit;
    constructor(options?: CacheCoordinatorOptions);
    request(): RequestCache;
    layer(name: CacheLayerName): TagCache<unknown>;
    getOrLoad<V>(layer: CacheLayerName, key: string, loader: () => V | Promise<V>, options?: CacheSetOptions): Promise<V>;
    invalidateTags(tags: Iterable<string>): number;
    inspect(): CacheInspection;
    clear(): void;
}

interface CachedResponse {
    status: number;
    statusText: string;
    headers: [string, string][];
    body: Uint8Array;
    etag: string;
}
interface ResponseCacheOptions extends CacheSetOptions {
    cache?: TagCache<CachedResponse>;
    key?: (ctx: Context) => string;
    vary?: string[];
    shouldCache?: (ctx: Context, response: Response) => boolean;
    /**
     * Optional detached revalidator used for stale-while-revalidate. Middleware
     * `next()` is deliberately never called after a response has been returned,
     * because many middleware pipelines are single-use.
     */
    revalidate?: (ctx: Context) => Promise<Response>;
    onRevalidateError?: (error: unknown, ctx: Context) => void;
}
declare function responseCache(options?: ResponseCacheOptions): Middleware;

interface CacheInvalidationBus {
    publish(topic: string, message: unknown): void | Promise<void>;
    subscribe(pattern: string, handler: (message: unknown) => void | Promise<void>): () => void;
}
interface DistributedInvalidationOptions {
    namespace?: string;
    instanceId?: string;
    onError?: (error: unknown) => void;
}
interface DistributedInvalidation {
    invalidateTag(tag: string): Promise<number>;
    invalidateTags(tags: Iterable<string>): Promise<number>;
    delete(key: string): Promise<boolean>;
    clear(): Promise<void>;
    close(): void;
}
/**
 * Propagate cache invalidations over any structurally compatible pub/sub bus.
 * The bus is intentionally not closed because applications commonly share it.
 */
declare function connectCacheInvalidation<V>(cache: TagCache<V>, bus: CacheInvalidationBus, options?: DistributedInvalidationOptions): DistributedInvalidation;

export { CacheCoordinator, type CacheCoordinatorOptions, type CacheEntry, type CacheEvent, type CacheInspection, type CacheInvalidationBus, type CacheLayerName, type CacheLookup, type CacheSetOptions, type CacheSnapshotEntry, type CachedResponse, type DistributedInvalidation, type DistributedInvalidationOptions, RequestCache, type ResponseCacheOptions, TagCache, type TagCacheOptions, connectCacheInvalidation, responseCache };
```

---

## @wrnexus/captcha

Documentation URL: https://wrnexusjs.dev/packages/captcha

# @wrnexus/captcha

A first-class CAPTCHA and anti-automation package for WRNexusJS. It supports self-hosted challenges, a managed WRNexus service, external providers, form submission guards, page gates, accessible audio, adaptive risk checks, and a Tailwind-only `.wrn` component.

## Install

```bash
bun add @wrnexus/captcha
```

WRNexusJS automatically discovers the package plugin, component, client runtime, styles, and DevToolbar audit. Use `<Captcha />` directly after installation. The browser runtime is injected once only on responses that render a CAPTCHA; no script tag, public-file copy, or manual plugin registration is required. Call `captchaPlugin(options)` explicitly only when an application needs to override the discovered package configuration.

## Included challenge modes

- Number, alphabet, and alphanumeric image challenges
- Addition, subtraction, multiplication, and exact-division calculations
- Generated shape-selection image challenges
- Audio alternatives for text, numbers, and calculations
- Honeypot and minimum-completion-time invisible checks
- Self-hosted “I’m not a robot” checkbox challenge with one-time server verification
- Always, once-per-session, and adaptive page gates
- Cloudflare Turnstile, Google reCAPTCHA, hCaptcha, managed, and custom providers

## Create the self-hosted engine

```ts
import {
  createCaptchaEngine,
  createCaptchaHttpHandlers,
  RedisCaptchaStore,
} from "@wrnexus/captcha/server";

const engine = createCaptchaEngine({
  secret: process.env.CAPTCHA_SECRET!,
  store: new RedisCaptchaStore(redis),
  basePath: "/api/captcha",
  challengeTtlMs: 2 * 60_000,
  responseTokenTtlMs: 5 * 60_000,
  maxAttempts: 3,
  minCompletionMs: 800,
});

export const handlers = createCaptchaHttpHandlers(engine);
```

Mount the handlers from an API catch-all route:

```ts
import type { Context } from "@wrnexus/core";
import { handlers } from "../../lib/captcha.ts";

export async function POST(ctx: Context) {
  return (await handlers.handle(ctx.req, ctx)) ?? new Response("Not Found", { status: 404 });
}
export const GET = POST;
export const HEAD = POST;
```

## Use the component

```wrn
<Captcha
  type="alphanumeric"
  action="signup"
  endpoint="/api/captcha/challenge"
  verifyEndpoint="/api/captcha/verify"
  difficulty="normal"
  disturbance="50"
  imageStyle="random"
  allowedStyles="classic,snow,distortion,wave"
  size="normal"
  showAudio="true"
  showListen="true"
  @success='captchaToken = event.detail.responseToken'
  @failure='formError = event.detail.extra.message'
/>
```

The component uses Tailwind utilities and `--wire-*` theme variables. It has no companion component CSS file.

### Main props

`provider`, `siteKey`, `type`, `action`, `presentation`, `difficulty`, `disturbance`, `imageStyle`, `allowedStyles`, `excludedStyles`, `randomizeStyle`, `locale`, `size`, `color`, `class`, `name`, `endpoint`, `verifyEndpoint`, `responseField`, labels/messages, `autoLoad`, `autoVerify`, `showVerify`, `showRefresh`, `showAudio`, `showListen`, `showStatus`, `disabled`, `required`, and the backward-compatible `compact` alias.

### Component sizes

Use one of the three supported display modes:

```wrn
<Captcha size="compact" action="small-form" />
<Captcha size="normal" action="standard-form" />
<Captcha size="big" action="security-page" />
```

`small`/`sm` are accepted as aliases for `compact`, while `large`/`lg` are accepted as aliases for `big`. The old `compact="true"` prop still forces compact mode.

### Listen button visibility

Audio remains available by default. Hide the Listen and Use audio controls with either of these props:

```wrn
<Captcha showListen="false" action="without-listen-button" />
<Captcha showAudio="false" action="without-audio-alternative" />
```

`showListen` is the direct UI switch. `showAudio` remains the broader backward-compatible audio switch.

### I’m not a robot checkbox

```wrn
<Captcha
  type="not-robot"
  action="contact-submit"
  size="compact"
  showListen="false"
/>
```

The checkbox is not a client-only boolean. Clicking it completes a self-hosted invisible challenge that is time-limited, attempt-limited, one-time-use, action-bound, optionally session/hostname/IP-bound, and verified on the server. It is a low-friction anti-automation layer; use adaptive escalation to a visual or external provider for high-risk traffic.

### Visual disturbance

Use `disturbance` for visual and image-selection challenges. It accepts an integer from `25` through `75`:

- `25`: light disturbance and easiest readability
- `50`: balanced default
- `75`: maximum supported dots, line crossings, glyph movement, and image-tile noise

The browser sends this value to the challenge API, and the server validates the range before generating the challenge. It is also returned in challenge metadata.

### Generated image renderer styles

Text, number, alphanumeric, and calculation CAPTCHA images support 18 concrete renderers plus a random mode:

`classic`, `collision`, `snow`, `corrosion`, `spiderweb`, `cross-shadow`, `split`, `split2`, `cut`, `darts`, `distortion`, `stitch`, `striped`, `wave`, `grid-noise`, `scribble`, `pixel`, and `broken-lines`.

Use a fixed style:

```wrn
<Captcha
  type="alphanumeric"
  action="signup"
  imageStyle="spiderweb"
  disturbance="55"
/>
```

Use a new random style whenever the challenge is refreshed:

```wrn
<Captcha
  type="number"
  action="login"
  imageStyle="random"
  difficulty="normal"
/>
```

Control the random pool with comma-separated component props or arrays in the TypeScript API:

```wrn
<Captcha
  type="alphanumeric"
  action="checkout"
  imageStyle="random"
  allowedStyles="classic,snow,distortion,wave"
  excludedStyles="collision"
/>
```

```ts
const challenge = await engine.create({
  action: "checkout",
  type: "alphanumeric",
  imageStyle: "random",
  allowedStyles: ["classic", "snow", "distortion", "wave"],
  excludedStyles: ["collision"],
});
```

Set `randomizeStyle: true` to force random selection even when `imageStyle` names a concrete renderer. The resolved style, requested style, and active pool are returned in challenge metadata. The answer is never embedded in metadata or browser JavaScript.

### Events

`@ready`, `@challenge`, `@input`, `@verify`, `@success`, `@failure`, `@expired`, `@refresh`, `@audioStart`, `@audioEnd`, and `@error`.

## Protect a validated form API

Validate a cloned request first, then consume the CAPTCHA response token. This prevents a valid token from being consumed when ordinary field validation fails.

```ts
import { captchaGuard } from "@wrnexus/captcha/server";
import { parseBody } from "@wrnexus/validation";
import contactSchema from "../schemas/contact.ts";
import { engine } from "../lib/captcha.ts";

const guard = captchaGuard({
  action: "contact-submit",
  engine,
  bindHostname: true,
  bindSession: true,
});

export async function POST(ctx) {
  const validation = await parseBody(contactSchema, ctx.req.clone());
  if (!validation.ok) return validation.response;

  return guard(ctx, async () => Response.json({ ok: true, submission: validation.value }));
}
```

The CAPTCHA runtime binds its required-form check in the capture phase, so a `data-schema` validator cannot submit the form before CAPTCHA verification. After a successful form request, the component automatically creates a fresh challenge.

### Retryable operations such as login

A login may consume a valid CAPTCHA and then fail because the password is
incorrect. Configure a short action-bound session grant so the user can correct
their credentials without solving CAPTCHA again:

```ts
const guard = captchaGuard({
  action: "auth-login",
  engine,
  bindHostname: true,
  bindSession: true,
  verifiedForMs: 5 * 60_000,
});
```

Keep the verified widget state for non-CAPTCHA form errors:

```wrn
<Captcha
  action="auth-login"
  required="true"
  resetOnError="false"
/>
```

The grant is stored in the current session and bound to the configured action.
Expired grants and CAPTCHA-specific errors still require and load a fresh
challenge. Keep login rate limits and authentication lockout enabled;
`verifiedForMs` removes repeated human verification, not credential-abuse
controls.

## Validate a schema and CAPTCHA together

```ts
const result = await parseWithCaptcha(signupSchema, body, ctx, {
  action: "signup",
  engine,
});

if (!result.ok) return Response.json({ ok: false, errors: result.errors }, { status: 400 });
```

## Page gate

```ts
export default captchaPageGate({
  action: "reports-access",
  engine,
  challengePath: "/captcha",
  policy: {
    mode: "session",
    verifiedForMs: 15 * 60_000,
    routeGroups: ["/reports"],
  },
});
```

Use `mode: "always"` for every visit, `mode: "session"` for a temporary grant, or `mode: "adaptive"` with `signals(ctx)`.

The challenge page should post the return path as a normal hidden field instead of constructing JavaScript inside the HTML `action` attribute:

```wrn
<form method="post" action="/api/page-grant">
  <input type="hidden" name="returnTo" value='{returnTo}' />
  <Captcha action="reports-access" />
  <button type="submit">Continue</button>
</form>
```

The `/api/page-grant` route reads `returnTo`, restricts it to the current origin, and redirects only after `captchaPageGate()` has verified and stored the temporary session grant.

## External providers

```ts
const turnstile = turnstileProvider({
  secretKey: process.env.TURNSTILE_SECRET!,
  siteKey: process.env.PUBLIC_TURNSTILE_SITE_KEY!,
  expectedHostnames: ["example.com"],
  expectedAction: "signup",
});
```

```wrn
<Captcha
  provider="turnstile"
  siteKey="PUBLIC_SITE_KEY"
  action="signup"
/>
```

Use the matching provider in `captchaGuard({ provider: turnstile })`. reCAPTCHA and hCaptcha adapters follow the same pattern.

## Managed provider

```ts
const managed = managedCaptchaProvider({
  baseUrl: "https://captcha.example.com",
  siteKey: process.env.PUBLIC_CAPTCHA_SITE_KEY!,
  secretKey: process.env.CAPTCHA_SECRET_KEY!,
});
```

For direct browser challenge creation, configure the component’s `endpoint` as the managed `/v1/challenges` URL and its `verifyEndpoint` as `/v1/solve`. Keep the secret key only in the server provider.

## Stores

- `MemoryCaptchaStore`: development and one-process applications
- `SqliteCaptchaStore`: adapter for SQLite-like `prepare().run/get/all()` clients
- `RedisCaptchaStore`: shared TTL storage with Lua-backed atomic consumption when `eval` is available
- `CaptchaStore`: implement this interface for PostgreSQL, MySQL, MongoDB, or another backend

## Audio

`AssetAudioRenderer` concatenates bundled English PCM WAV clips without calling an external service. Supply a custom `CaptchaAudioRenderer` for recorded voices, Hindi or other languages, or managed text-to-speech.

## DevToolbar

The automatically discovered CAPTCHA plugin registers its DevToolbar audit panel. It checks for likely client-side secrets, missing action bindings, missing provider site keys, optional CAPTCHA fields, accessible alternatives, and server-verification reminders. Explicit `captchaPlugin(options)` registration is needed only to override automatic configuration.

## Testing

Use deterministic custom generators in unit tests. Never require users or CI to solve random CAPTCHA images. The package includes engine, provider, policy, HTTP, storage, replay, expiry, binding, and audio authorization tests.

## Custom challenge generator

```ts
import { defineCaptchaGenerator, createCaptchaEngine } from "@wrnexus/captcha";

const wordChallenge = defineCaptchaGenerator({
  type: "word" as const,
  generate(context) {
    const answer = "NEXUS";
    return {
      type: "word",
      presentation: "visual",
      prompt: "Enter the displayed word",
      answer,
      answerKind: "text",
      image: renderYourImage(answer),
      inputMode: "text",
    };
  },
});

const engine = createCaptchaEngine({ secret, generators: [wordChallenge] });
```

Applications may also implement `CaptchaStore`, `CaptchaAudioRenderer`, or use `defineCaptchaProvider()` for a completely custom service.

## Helper and block kit

The package exports `captchaTokenFrom`, `captchaHeaders`, `captchaFields`, `verifyCaptcha`, `verifyCaptchaOrThrow`, `captchaResultResponse`, and `captchaContext` for consistent server and client integration.

Enable the CAPTCHA plugin to use the low-level `<Captcha />` challenge plus complete UI-composed blocks:

- `<CaptchaField />`
- `<CaptchaStatus />`

`CaptchaField` composes `Card` from `@wrnexus/ui` and keeps the CAPTCHA-specific size separate from the surrounding UI size.

### Exported TypeScript declarations

```ts
import { CaptchaVerificationResult, CaptchaProvider, CaptchaEngine, VerifyCaptchaInput } from './types.js';
export { CaptchaAudioRenderer, CaptchaBinding, CaptchaChallenge, CaptchaChallengeGenerator, CaptchaChallengeRecord, CaptchaChallengeType, CaptchaConcreteImageStyle, CaptchaDifficulty, CaptchaEngineOptions, CaptchaFailureCode, CaptchaGeneratorContext, CaptchaGuardOptions, CaptchaHttpHandlers, CaptchaImageItem, CaptchaImageStyle, CaptchaMiddleware, CaptchaPageGateOptions, CaptchaPolicyMode, CaptchaPolicyOptions, CaptchaPresentation, CaptchaProviderClientConfig, CaptchaProviderName, CaptchaResponseTokenRecord, CaptchaRiskResult, CaptchaRiskSignals, CaptchaStore, CreateCaptchaOptions, GeneratedCaptchaChallenge } from './types.js';
export { CaptchaHttpOptions, CaptchaParseResult, CaptchaSessionGrant, DefaultCaptchaEngine, ParseWithCaptchaOptions, bindingHash, bytesToBase64Url, captchaGuard, captchaPageGate, clearCaptchaGrants, constantTimeEqual, createCaptchaEngine, createCaptchaHttpHandlers, defaultRandomBytes, evaluateCaptchaRisk, hmacSha256, parseWithCaptcha, randomId, sha256, shouldRequireCaptcha, validCaptchaGrant } from './server/index.js';
export { CaptchaAuditIssue, CaptchaPluginOptions, captchaComponentsDir, captchaPlugin } from './plugin.js';
export { MemoryCaptchaStore, MemoryCaptchaStoreOptions, createMemoryCaptchaStore } from './stores/memory.js';
export { SqliteCaptchaStore, SqliteCaptchaStoreOptions, SqliteDatabaseLike, SqliteStatementLike, createSqliteCaptchaStore } from './stores/sqlite.js';
export { RedisCaptchaClient, RedisCaptchaStore, RedisCaptchaStoreOptions, createRedisCaptchaStore } from './stores/redis.js';
export { SelfHostedCaptchaProvider, selfHostedProvider } from './providers/self-hosted.js';
export { S as SiteverifyCaptchaProvider, a as SiteverifyPreset, b as SiteverifyProviderOptions } from './siteverify-Cg3TTAp4.js';
export { TurnstileCaptchaProvider, turnstileProvider } from './providers/turnstile.js';
export { RecaptchaProvider, recaptchaProvider } from './providers/recaptcha.js';
export { HcaptchaProvider, hcaptchaProvider } from './providers/hcaptcha.js';
export { ManagedCaptchaProvider, ManagedCaptchaProviderOptions, managedCaptchaProvider } from './providers/managed.js';
export { defineCaptchaProvider } from './providers/custom.js';
export { CAPTCHA_CONCRETE_IMAGE_STYLES, CAPTCHA_IMAGE_STYLES, CalculationCaptchaGenerator, ImageCaptchaGenerator, InvisibleCaptchaGenerator, ResolveCaptchaImageStyleOptions, ResolvedCaptchaImageStyle, Rgba, RgbaImage, TextCaptchaGenerator, alphaCaptchaGenerator, alphanumericCaptchaGenerator, bytesToBase64, calculationCaptchaGenerator, createImage, defaultCaptchaGenerators, defineCaptchaGenerator, drawGlyph, drawLine, drawText, encodePng, fillCircle, fillPolygon, fillRect, honeypotCaptchaGenerator, imageCaptchaGenerator, isCaptchaImageStyle, normalizeCaptchaImageStyle, normalizeCaptchaImageStyleList, notRobotCaptchaGenerator, numberCaptchaGenerator, pngDataUri, resolveCaptchaImageStyle, setPixel, timingCaptchaGenerator } from './challenges/index.js';
export { AssetAudioRenderer, AssetAudioRendererOptions, createAssetAudioRenderer, resolveCaptchaAudioAssetsDir } from './audio/index.js';
import { Context } from '@wrnexus/core';
import '@wrnexus/validation';
import '@wrnexus/plugin';

declare function captchaTokenFrom(value: Request | Headers | FormData | URLSearchParams | Record<string, unknown>, field?: string): Promise<string | undefined> | string | undefined;
declare function captchaHeaders(token: string): HeadersInit;
declare function captchaFields(token: string, field?: string): Record<string, string>;
declare function verifyCaptcha(providerOrEngine: CaptchaProvider | CaptchaEngine, input: VerifyCaptchaInput): Promise<CaptchaVerificationResult>;
declare function verifyCaptchaOrThrow(providerOrEngine: CaptchaProvider | CaptchaEngine, input: VerifyCaptchaInput): Promise<CaptchaVerificationResult>;
declare function captchaResultResponse(result: CaptchaVerificationResult): Response;
declare function captchaContext(ctx: Context): CaptchaVerificationResult | null;

export { CaptchaEngine, CaptchaProvider, CaptchaVerificationResult, VerifyCaptchaInput, captchaContext, captchaFields, captchaHeaders, captchaResultResponse, captchaTokenFrom, verifyCaptcha, verifyCaptchaOrThrow };
```

---

## @wrnexus/cli

Documentation URL: https://wrnexusjs.dev/packages/cli

# @wrnexus/cli

Production parity commands:

```bash
wrnexus build .
wrnexus preview . --port=3000
wrnexus dev . --production-runtime
```

`preview` refuses to start without `dist/server.js` and executes that exact
artifact with the production profile. Production-runtime development rebuilds
the same minified artifact after app, public, or configuration changes and
keeps the last good server running when a rebuild fails.

> The `wrnexus` command-line tool that scaffolds, runs, builds, tests, and manages WrNexus apps.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

`@wrnexus/cli` provides the `wrnexus` executable — the single entry point for developing a WrNexus app. It runs the HMR dev server, produces a self-contained production build, scaffolds apps/pages/components, drives database migrations, regenerates typed routes and queries, runs tests, and manages configuration profiles. It also scaffolds multi-app monorepos and serves them behind a domain-routing gateway. This is a CLI/build-time package (it shells out to the Bun binary for the dev child and tests) and it also exports the workspace config types via a subpath.

## Installation

```bash
bun add @wrnexus/cli
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

Once installed, invoke it from an app directory:

```bash
bunx wrnexus dev
# or add scripts: "dev": "wrnexus dev .", "build": "wrnexus build ."
```

## Commands

### Local production services

`wrnexus dev . --services` starts the application and the bounded local database,
cache, mail, SMS, webhook, storage, queue, cron, authentication and metrics simulator.
It generates a localhost/`*.localhost` development certificate under
`.wrnexus/certificates/` and serves both the application and service console over HTTPS.
Trust that certificate locally to remove the browser warning. Use `--services-http` only
when an external development proxy already terminates TLS.

### Exact production runtime with live updates

`wrnexus dev . --production-runtime` rebuilds and executes `dist/server.js` with
production resolution, serialization, caching, headers and assets. The supervisor keeps
the last good process when a build fails. On a successful rebuild the opt-in production
HMR socket reconnects, requests the new document and morphs it into the browser; ordinary
`wrnexus preview` and deployed production servers never include that client.

### API platform

`wrnexus api generate [app-dir]` (or `api docs`) derives operations from file routes and
emits `generated/api/openapi.json`, safe static documentation, Postman collection, curl examples,
and TypeScript, JavaScript, Java, Go and Python SDKs. Generate one client with
`wrnexus sdk generate <language> [app-dir]`.

Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that read config or `.env` also accept `--profile=<name>` (see [Profiles](#profiles)).

| Command                               | Purpose                                                                |
| ------------------------------------- | ---------------------------------------------------------------------- |
| `wrnexus dev [app-dir] [--port=3000]` | Start the development server with live reload / HMR.                   |
| `wrnexus build [app-dir]`             | Build a self-contained production server bundle + assets into `dist/`. |
| `wrnexus create <app-name>`           | Scaffold a new single app from an inline template.                     |
| `wrnexus workspace <name>`            | Scaffold a monorepo (`apps/*` + shared `packages/*`).                  |
| `wrnexus workspace add <name>`        | Add and register an app in the current workspace.                      |
| `wrnexus gateway [--port=3000]`       | Serve every workspace app behind one port, routed by domain.           |
| `wrnexus production [workspace-dir]`  | Build, migrate, and serve every workspace app in production.           |
| `wrnexus generate <type> <name>`      | Scaffold a `page` \| `component` \| `api` \| `schema`.                 |
| `wrnexus generate routes`             | Regenerate the typed routes file (`app/routes.gen.ts`).                |
| `wrnexus generate docker`             | Scaffold `Dockerfile`, `.dockerignore`, and `docker-compose.yml`.      |
| `wrnexus generate mobile`             | Scaffold a Capacitor shell for iOS and Android.                        |
| `wrnexus mobile add <package...>`     | Install Capacitor plugins and sync native projects.                    |
| `wrnexus eject <name...>`             | Copy Wire UI component `.wrn` sources into `app/components/`.          |
| `wrnexus db <cmd>`                    | Database migrations and tooling (see [db](#wrnexus-db)).               |
| `wrnexus test [app-dir] [--watch]`    | Run the app's tests via `bun test` (defaults to the `test` profile).   |
| `wrnexus profiles [app-dir]`          | List config profiles and their `.env` files, marking the active one.   |
| `wrnexus compatibility check`         | Check whether behavior defaults are explicitly pinned and current.     |
| `wrnexus compatibility explain`       | Explain configured, effective, and current compatibility behavior.     |
| `wrnexus compatibility upgrade`       | Back up config and explicitly opt into reviewed current behavior.      |
| `wrnexus help`                        | Print usage.                                                           |

`wrnexus g` is an alias for `wrnexus generate`.

Compatibility upgrades never happen implicitly. New applications pin
`compatibilityDate` and `frameworkBehaviour`; existing applications use
`wrnexus compatibility explain` before the backed-up, idempotent upgrade command.

### `wrnexus dev`

Supervises a child dev-server process (from `@wrnexus/dev-server`). The child owns file watching and HMR: CSS and client-island edits update the live page over a WebSocket with no restart; when a server module changes, the child exits with a restart code and the supervisor respawns it (the browser reconnects and morphs in the new HTML). On startup it regenerates typed DB queries and typed routes (best effort). Use `--port=` to change the port (default `3000`).

```bash
wrnexus dev . --port=8080
```

### `wrnexus build`

Emits into `<app-dir>/dist/`:

- `server.js` — a single, minified, self-contained Bun server with a **static** manifest of every page / api / realtime / middleware / component / layout module (no runtime filesystem scan or on-the-fly bundling).
- `reactive.js`, `theme.css`, `theme.js`, `ui.css`, and (if present) `styles.css` — hashed, minified browser assets.
- `public/` — copied verbatim.

Before bundling, it regenerates typed queries for the default and every named database. Run the output with:

```bash
bun dist/server.js   # PORT env var optional
# Generated apps also provide: npm start
# Build and start together: npm run production
```

### `wrnexus create`

Scaffolds a complete v0.8 app from an inline template. The generated project includes strict TypeScript, ESLint and Prettier, editor recommendations, environment templates, database migrations, locales, schemas, tests, API/middleware/realtime examples, Tailwind and Iconify, PWA/mobile defaults, and the framework package kits. Its `wrnexus.config.ts` documents the current imports, types, stores, performance, observability, tenancy, build, navigation, theme, i18n, database, storage, realtime, security, and profile configuration.

Use `bun run dev` during development, `bun run check` for the complete typecheck/lint/test/format gate, `bun run build && bun run start` for production, or `bun run production` to build and start in one command.

### `wrnexus update`

`wrnexus update --latest` performs a complete project upgrade. It hands control to the exact target CLI, backs up important project files under `.wrnexus/update-backups/`, updates every `@wrnexus/*` dependency, refreshes framework-owned references, and applies every versioned syntax/config/file migration between the project version and target version. After installation it runs the project's `check` and `build` scripts; the new version is recorded only after verification succeeds.

Use `--dry-run` to preview an upgrade or `--no-verify` when verification is intentionally handled elsewhere. Migrations never overwrite user-owned configuration wholesale: each release must provide a focused, idempotent transformation for any changed syntax or config contract.

```bash
wrnexus create my-app
```

### `wrnexus generate`

Scaffolds a single file from a template, refusing to overwrite an existing file. Types (with aliases): `page`/`p`, `component`/`c`, `api`/`a`, `schema`/`s`. Nested names create nested paths.

```bash
wrnexus generate page about          # app/pages/about.wrn
wrnexus generate component user-card # app/components/user-card.wrn
wrnexus generate api users/list      # app/api/users/list.ts
wrnexus generate schema signup       # app/schemas/signup.ts
wrnexus generate routes              # regenerate app/routes.gen.ts
wrnexus generate docker              # Dockerfile + compose + .dockerignore
wrnexus generate mobile --mode=webview --app-id=com.example.app --app-name="Example" --url=https://app.example.com
wrnexus generate mobile --mode=native
```

The mobile generator creates a separate `mobile/` package and reads
`config.mobile.mode`. `webview` creates a Capacitor shell that renders the hosted
WrNexus application. `native` creates a WebView-free Expo/React Native app whose
screens call the shared backend through `mobile/src/wrnexus.ts`. Native screens
do not render `.wrn` HTML. In either mode, run `bun install` in `mobile/`; iOS
device builds require macOS and Xcode.

Install official or community Capacitor plugins through the root CLI:

```bash
wrnexus mobile add @capacitor/camera @capacitor/haptics
wrnexus mobile sync
wrnexus mobile assets # generate native icons from config.mobile.icon
```

In native mode, `mobile add` runs `expo install` and `mobile sync` runs Expo
prebuild. In WebView mode they retain the Capacitor install/sync behavior.
`wrnexus mobile compile` maps portable `app/pages/**/*.wrn` pages to Expo Router
TSX routes. Native `bun run start` invokes this compilation automatically.

Browser code can access installed plugins through the SSR-safe
`@wrnexus/mobile` bridge. The command adds each plugin to both the WrNexus app
(JavaScript proxy) and `mobile/` (native synchronization).

`wrnexus mobile sync` also configures Android so only true network failures use
the local connection-error screen. HTTP errors such as 404 and 500 keep their
WrNexus response pages.

### `wrnexus eject`

Copies a Wire UI component's `.wrn` source out of `@wrnexus/ui` into `app/components/`, so the app owns and can edit it (the app copy shadows the library one by name). Run with no names to list available components. It skips components that already exist in the app.

```bash
wrnexus eject button card modal
```

### `wrnexus db`

Database migrations and tooling. Without a flag, commands target the **default** database (`db` in `wrnexus.config.ts`, files under `app/db/`). Pass `--db=<name>` to target a named database (`databases.<name>`, files under `app/db/<name>/`).

| Subcommand                      | Purpose                                                                             |
| ------------------------------- | ----------------------------------------------------------------------------------- |
| `db new <name> [--from-models]` | Scaffold a migration; `--from-models` derives it from the TS models in `schema.ts`. |
| `db migrate`                    | Apply all pending migrations.                                                       |
| `db rollback`                   | Revert the last applied migration.                                                  |
| `db status`                     | List applied / pending migrations.                                                  |
| `db generate`                   | Regenerate typed queries (`queries/*.sql` → `queries.gen.ts`).                      |
| `db seed`                       | Run the database's `seed.ts` (default export / `seed` function).                    |
| `db studio [table]`             | Inspect tables — list row counts, or dump the first 50 rows of one table.           |

```bash
wrnexus db new create_users --from-models
wrnexus db migrate
wrnexus db studio users
wrnexus db status --db=analytics
```

### `wrnexus workspace` and `wrnexus gateway`

`workspace <name>` scaffolds a monorepo: complete v0.8 apps under `apps/*`, shared libraries under `packages/*`, root TypeScript/lint/format/editor/environment tooling, and a `wrnexus.workspace.ts` that maps each app to the domains it serves. `gateway` runs every app behind one port and routes by `Host` header, with optional per-app auth and gateway-wide security (trusted hosts, rate limit, security headers, access log). Newly added workspace apps use the same current scaffold.

```bash
wrnexus workspace acme
wrnexus gateway --port=3000
```

For a complete production start, use the first-class workspace orchestrator:

```bash
wrnexus production --host=0.0.0.0 --port=3000
```

It builds every registered app, applies default and named-database SQL migrations
when present, and starts the production gateway only after preparation succeeds.
Use `--prepare-only`, `--no-build`, or `--no-migrate` when deployment stages are
managed separately; `--environment=<name>` selects another workspace environment.

From a workspace root, add and register another app in one command:

```bash
wrnexus workspace add reports --domain=reports.localhost
bun install
```

Development gateways bind to `127.0.0.1` by default for reliable access on Windows,
macOS, and Linux. Open the configured app domain on the gateway port (for example
`http://localhost:3000` or `http://admin.localhost:3000`), not the internal child ports
printed while apps start. Pass `--host=0.0.0.0` to accept connections from other devices.

### `wrnexus test`

Runs the app's tests with `bun test`. Defaults to the `test` profile (config + `.env.test`). Pass `--watch` to re-run on change; extra flags pass straight through to `bun test`.

```bash
wrnexus test . --watch
```

## Usage

### Create and run a single application

```bash
bunx @wrnexus/cli create customer-portal
cd customer-portal
bun install
bun run dev
```

### Add routes and shared UI to an existing app

```bash
wrnexus generate page reports/monthly
wrnexus generate api reports/export
wrnexus generate component report-filter
wrnexus generate routes
```

### Create a multi-app workspace and add another app

```bash
wrnexus workspace company-suite
cd company-suite
wrnexus workspace add reports --domain=reports.localhost
bun install
wrnexus gateway --port=3000
```

Open `http://reports.localhost:3000`; the gateway selects `apps/reports` from the
request host.

### Upgrade with migrations and verification

```bash
wrnexus update --latest --dry-run
wrnexus update --latest
wrnexus doctor
```

Use `wrnexus doctor --fix [app-dir]` to apply conservative repairs before the
health check: create missing `app/pages` and a default config, align skewed
`@wrnexus/*` dependency ranges, record the current migration marker, and format
only syntax-valid `.wrn` files. Invalid JSON or WRN sources are reported/skipped
instead of overwritten; repeat runs are idempotent.

## Profiles

Pass `--profile=<name>` to `dev`, `build`, `db` (or set `WRNEXUS_PROFILE`) to select a config profile. The CLI publishes `WRNEXUS_PROFILE` so config loaders and the dev child pick it up, and loads that profile's `.env` cascade (`.env`, `.env.local`, `.env.<profile>`, `.env.<profile>.local`) into `process.env`.

```bash
wrnexus dev --profile=uat
wrnexus profiles          # ● development  (config, .env.development)
                          # ○ production
                          # ○ uat          (config, .env.uat)
```

## Subpath exports

`@wrnexus/cli/workspace` exposes the workspace configuration types used by `wrnexus.workspace.ts`:

```ts
import type { WorkspaceConfig, WorkspaceApp } from "@wrnexus/cli/workspace";

const config: WorkspaceConfig = {
  security: { trustedHostsOnly: true, headers: true, accessLog: true },
  apps: [{ name: "web", dir: "apps/web", domains: ["localhost", "web.localhost"] }],
};

export default config;
```

## Requirements / Notes

- **Bun-only.** The CLI runs on Bun, spawns the Bun binary for the dev child and `bun test`, and the production build uses `Bun.build`. Node is not supported.
- Orchestrates the rest of the framework: `@wrnexus/dev-server` (dev/prod server + gateway), `@wrnexus/router` (route + typed-routes codegen), `@wrnexus/compiler` (`.wrn` → `.ts`), `@wrnexus/db` (migrations, typed queries), `@wrnexus/styles` (config, profiles, `.env`, themes, styles), `@wrnexus/ui` (ejectable Wire UI components), `@wrnexus/validation`, `@wrnexus/csr`, and `@wrnexus/i18n`.
- Reads `wrnexus.config.ts` for `db` / `databases`, `theme`, `styles`, `seo`, `security`, `i18n`, and `profiles`, and `wrnexus.workspace.ts` for the gateway.

### Exported TypeScript declarations

```ts
#!/usr/bin/env bun
```

---

## @wrnexus/compiler

Documentation URL: https://wrnexusjs.dev/packages/compiler

# @wrnexus/compiler

## Partial-static rendering

Pages can select `render = "partial-static"` and divide their view with `<Static>` and
`<Dynamic>` boundaries. The compiler emits a build-only shell renderer that never evaluates
dynamic-boundary children. `wrnexus build` expands static component mounts into
`dist/partial-shells.json`, records byte/region evidence in `build-report.json`, and embeds
the shell in the production route manifest. At request time the production runtime retains
request-aware layouts, locale/theme metadata and security nonces while streaming dynamic
regions into stable placeholders.

> Compiler for the `.wrn` language — tokenizes, parses, and lowers `.wrn` page and component files to TypeScript.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

Production adapters use `analyzeRuntimeImports` before bundling. Edge, worker,
service-worker, and browser targets reject Node filesystem, TCP, and process
modules with `WRN-RUNTIME-CAPABILITY`. Package manifests can declare supported
`wrnexus.runtimes` and required `wrnexus.requires` capabilities; discovery fails
when the selected deployment cannot satisfy them.

## Server actions

```wrn
action createUser using CreateUserSchema {
  const user = await users.create(input)
  invalidate("users")
  return user
}

view {
  <form @submit="createUser">...</form>
}
```

The compiler produces a schema-aware server registry, a fully inferred action
client, and progressively enhanced form metadata. The shared runtime performs
validation, authentication/permission checks, CSRF verification, serialization,
invalidation reporting, and browser lifecycle events.

## Overview

`@wrnexus/compiler` turns `.wrn` source into TypeScript that targets the framework's runtime primitives. A `.wrn` file declares either a `page` (a route) or a `component` (a reusable, prop-driven fragment) with blocks for `state`, `view` (plain HTML), `seo`, `style`, `functions`, `api`, `ssr`/`client` data bindings, and `realtime` websocket handlers. The pipeline is `source → Lexer → parse() → PageAst → generate() → TypeScript`. It is a build/server-side library — the WrNexus dev loader calls it to compile `.wrn` files on the fly, surfacing `ParseError` as a readable error page.

Static ES module imports may appear before the root declaration. Imported values are
available to server-rendered expressions, including component props:

```wrn
import { appUrl } from "@wrnexus/helpers";

layout PublicLayout {
  view {
    <PublicHeader signInHref="{appUrl('sso', '/sign-in')}" />
  }
}
```

## Installation

```bash
bun add @wrnexus/compiler
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

## API

All exports come from the package root (`@wrnexus/compiler`).

### `compileWireFile(source: string): string`

Compile `.wrn` source to a TypeScript module string. Throws `ParseError` on invalid input. The output is prefixed with a `// compiled from .wrn` comment.

### `compile(source: string): CompileResult`

Richer entry point that returns the generated code, the AST, and any diagnostics.

```ts
interface CompileResult {
  code: string;
  ast: PageAst;
  diagnostics: string[];
}
```

On a `ParseError` it pushes the message into `diagnostics` and re-throws.

### `parse(source: string): PageAst`

Run the lexer + recursive-descent parser and return the AST. Throws `ParseError` (lexer `LexError`s are caught and rethrown as `ParseError`).

### `generate(ast: PageAst): string`

Lower a `PageAst` to TypeScript. `page` ASTs become a default-export page component (plus `meta`, optional `layout`, `__wrnexusApi`/method handlers, `websocket`, and SSR/CSR data bindings); `component` ASTs become a module exporting `render(props)` and `__wrnexusComponent`.

### `Lexer`

On-demand lexer for `.wrn`. Yields structural tokens and exposes raw-span readers for the parser.

```ts
class Lexer {
  pos: number;
  constructor(src: string);
  next(): Token; // consume next structural token
  peek(): Token; // look ahead without consuming
  readPath(): string; // route path, e.g. /users/[id]
  readToLineEnd(): string; // rest of line (state/prop initializers)
  readBalancedBraces(): string; // inner text of a { ... } block, string-aware
}
```

`Token` is `{ type: TokenType; value: string; pos: number }`, where `TokenType` is one of `ident`, `string`, `lbrace`, `rbrace`, `lparen`, `rparen`, `at`, `eq`, `comma`, `eof`.

### Errors

| Class        | Thrown by                                         | Meaning                                                         |
| ------------ | ------------------------------------------------- | --------------------------------------------------------------- |
| `ParseError` | `parse`, `compile`, `compileWireFile`, `generate` | Invalid `.wrn` grammar or (rewrapped) lex failure.              |
| `LexError`   | `Lexer`                                           | Unexpected character / unterminated string / unbalanced braces. |

### AST types

Exported type-only symbols describing the parsed tree:

| Type                 | Description                                                                                                                                                   |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PageAst`            | Root node including top-level `imports`, `kind`, `name`, `types`, typed `props`, typed `states`, `view`, styles, functions, data APIs, lifecycle, and routes. |
| `ViewNode`           | `{ type: "text"; value }` or `{ type: "element"; tag; attrs; children }`.                                                                                     |
| `Attr`               | `{ name; value; event; boolean? }` — `event` marks `@event` bindings.                                                                                         |
| `StateDecl`          | `{ name; valueType?; expr }` — a typed `state x: Type = <expr>` declaration.                                                                                  |
| `PropDecl`           | `{ name; valueType?; required; default }` — a typed prop declaration.                                                                                         |
| `SeoBlock`           | `Record<string, string>` from the `seo { ... }` block.                                                                                                        |
| `ApiBlock`           | `{ method; path; body }` — a top-level `api METHOD /path { ... }`.                                                                                            |
| `DataApiBlock`       | `{ mode; name; method; path; body }` — an `api` inside an `ssr`/`client` block.                                                                               |
| `DataMode`           | `"ssr" \| "client"`.                                                                                                                                          |
| `ModeFunctionsBlock` | `{ mode; body }` — a `functions { ... }` inside an `ssr`/`client` block.                                                                                      |
| `RealtimeBlock`      | `{ name; handlers }` — a `realtime <name> { on evt(args) { ... } }` block.                                                                                    |

## Usage

Compile a page:

```ts
import { compileWireFile } from "@wrnexus/compiler";

const ts = compileWireFile(`
page Home {
  state count = 0
  seo { title = "Home" description = "Welcome" }
  view {
    <button @click="count++">Clicked {count} times</button>
  }
}
`);
// ts is a TypeScript module: exports `meta`, and a default page component
// returning an HTML string, wrapped in a data-scope for the reactive runtime.
```

Inspect the AST and diagnostics:

```ts
import { compile, ParseError } from "@wrnexus/compiler";

try {
  const { code, ast, diagnostics } = compile(source);
  console.log(ast.kind, ast.name, ast.states.length);
} catch (err) {
  if (err instanceof ParseError) console.error(err.message);
}
```

Drive the parse/codegen stages directly:

```ts
import { parse, generate } from "@wrnexus/compiler";

const ast = parse(componentSource); // ast.kind === "component"
const module = generate(ast); // exports render(props) + __wrnexusComponent
```

Use the lexer standalone:

```ts
import { Lexer } from "@wrnexus/compiler";

const lx = new Lexer("page Home {");
lx.next(); // { type: "ident", value: "page", pos: 0 }
lx.next(); // { type: "ident", value: "Home", pos: 5 }
lx.next(); // { type: "lbrace", value: "{", pos: 10 }
```

## The `.wrn` language (as parsed)

A file opens with `page <Name>` or `component <Name>` followed by a `{ ... }` body containing zero or more members:

- `layout = "<name>"` — selects `app/layouts/<name>.wrn` (pages only).
- `types { <TypeScript declarations> }` — reusable interfaces and aliases for the current file.
- `props { name: Type = <default> ... }` — typed component props. Omit `= <default>` to make a prop required. Legacy inferred props remain supported.
- `@event name = function` inside `props` — declares a public component event. Emit it from component behavior with `name(detail)` or `$emit("name", detail)`, and consume it with `<Component @name="handler(event)" />`.
- `state <ident>: Type = <expr>` — typed reactive state seeded from a raw JS expression, including native array and object literals. The annotation is optional for backward compatibility.
- `view { <html> }` — plain HTML with `{expr}` interpolation in text and attributes, JSX-style component props such as `items={items}`, `items={[...]}`, and `options={{...}}`, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and `<!-- comments -->`. Structured component props are serialized safely for SSR; expressions that reference `state` retain their initial value and update reactively in the browser.
- Client functions automatically commit state changed by `setTimeout` callbacks. For other deferred callbacks (observers, third-party APIs, or detached promise callbacks), call the injected `commit()` function after changing local state; returning/awaiting a promise also commits through the normal function boundary.
- `seo { key = "value" ... }` — metadata merged into the generated `meta`.
- `style { <raw css> }` — inlined page/component stylesheet (repeatable).
- `functions { <TypeScript> }` — helpers with typed parameters and return values. Types remain in server output and are safely erased from browser behavior code.
- `api <METHOD> <path> { <raw js> }` — route handler, lowered to a `METHOD` export (repeatable).
- `ssr { ... }` / `client { ... }` — data blocks holding `api <name> <METHOD> <path> { ... }` bindings and their own `functions { ... }`.
- `realtime <name> { on <evt>(<args>) { <raw js> } ... }` — websocket handlers, lowered to a `websocket` export.

`view` markup is parsed by a lenient dedicated HTML parser (`parseHtmlView`); HTML void elements (`<br>`, `<img>`, …) take no closing tag. Line comments (`//`) are skipped by the lexer.

## Requirements / Notes

- Pure TypeScript with no runtime dependencies; runs under **Bun** as part of the WrNexus toolchain (Node is not supported).
- Generated modules target WrNexus runtime primitives (`data-scope`, `data-text`, `data-on-*`, `data-for`, `data-component`, `__wrnexus*`/`__wire*` helpers) — consume the output within a WrNexus app, e.g. via `@wrnexus/core`'s dev loader.

### Exported TypeScript declarations

```ts
import { PageAst as PageAst$1, StructuredImportDecl, WrnDiagnostic } from '@wrnexus/syntax';
export { ActionBlock, ApiBlock, Attr, ComputedDecl, DataApiBlock, DataMode, EffectBlock, EventDecl, FormatWrnOptions, LexError, Lexer, LoadBlock, ModeFunctionsBlock, OutputDecl, PageAst, ParseError, PropDecl, RealtimeBlock, RuntimeFunctionDecl, SeoBlock, StateDecl, StateRuntime, StoreKind, StructuredImportDecl, ViewNode, WrnDiagnostic, assertValidAst, diagnose, diagnosticFromError, eraseFunctionTypes, formatDiagnostic, formatWrn, inferredRuntimeType, parse, runtimeTypeOf } from '@wrnexus/syntax';
import { PageAst } from '@wrnexus/syntax/parser';

/**
 * Code generation: lower a `.wrn` AST to TypeScript that targets the framework's
 * existing primitives.
 *
 *   state              -> a `data-scope` declaration consumed by the runtime
 *   view               -> an HTML string returned by a page component
 *   @event="..."       -> data-on-<event>="..."
 *   "...{expr}..."     -> text kept verbatim ({expr} is mustache for runtime)
 *   api="<name>"       -> SSR/client data binding declared in a mode block
 *   ssrGet/ssrText     -> legacy server-side API fetch + render
 *   csrGet/csrText     -> legacy browser-side API fetch + render
 *   style              -> tagged local stylesheet metadata promoted by SSR
 *   functions          -> server-only helpers for API/realtime code
 *   api M /p {b}       -> export const M = async (ctx) => { b }
 *   realtime {..}      -> export const websocket = { evt(ws, ...args) { b } }
 */

declare function generate(ast: PageAst): string;

interface ComponentContractMetadata {
    name: string;
    kind: PageAst$1["kind"];
    props: Array<{
        name: string;
        type: string;
        required: boolean;
        default?: string;
        options?: string[];
    }>;
    outputs: Array<{
        name: string;
        payloadName?: string;
        payloadType?: string;
    }>;
    functions: Array<{
        name: string;
        runtime: string;
        async: boolean;
        parameters: Array<{
            name: string;
            type: string;
            optional: boolean;
        }>;
        returnType: string;
    }>;
    states: Array<{
        name: string;
        runtime: string;
        type: string;
        initializer: string;
    }>;
    computed: Array<{
        name: string;
        type: string;
        expression: string;
    }>;
    imports: Array<{
        source: string;
        typeOnly: boolean;
        defaultImport?: string;
        namedImports: string[];
    }>;
}
declare function createComponentContract(ast: PageAst$1): ComponentContractMetadata;

interface RpcManifestEntry {
    id: string;
    component: string;
    function: string;
    parameters: Array<{
        name: string;
        type: string;
        optional: boolean;
    }>;
    returnType: string;
}
declare function rpcManifest(ast: PageAst$1): RpcManifestEntry[];
declare function generateServerFunctionsModule(ast: PageAst$1): string;

interface CompileTargets {
    server: string;
    browser: string;
    declarations: string;
    contract: ReturnType<typeof createComponentContract>;
    rpc: ReturnType<typeof rpcManifest>;
}
declare function generateTargets(ast: PageAst$1): CompileTargets;

declare function generateBrowserModule(ast: PageAst$1): string;

declare function generateDeclarations(ast: PageAst$1): string;

declare function generateStoreModule(ast: PageAst$1): string;
/** Standalone browser artifact for an imported `.wrn` store. */
declare function generateStoreBrowserModule(ast: PageAst$1): string;

type ImportMode = "legacy" | "compatible" | "explicit";
interface ImportResolverOptions {
    appRoot: string;
    mode?: ImportMode;
    aliases?: Record<string, string>;
}
interface ResolvedImport {
    declaration: StructuredImportDecl;
    resolved?: string;
    diagnostic?: {
        code: string;
        message: string;
        severity: "error" | "warning";
    };
}
declare function resolveWrnImport(declaration: StructuredImportDecl, importer: string, options: ImportResolverOptions): ResolvedImport;
declare function resolveWrnImports(declarations: StructuredImportDecl[], importer: string, options: ImportResolverOptions): ResolvedImport[];

interface WrnSourceMapEntry {
    generatedLine: number;
    sourceLine: number;
    sourceColumn: number;
    kind: string;
}
interface WrnSourceMap {
    version: 1;
    source: string;
    generated: string;
    mappings: WrnSourceMapEntry[];
}
declare function createWrnSourceMap(source: string, generated: string): WrnSourceMap;

type RouteExecutionKind = "static" | "static-interactive" | "request-ssr" | "authenticated-ssr" | "streaming-ssr" | "dynamic";
interface RuntimeRequirements {
    kind: RouteExecutionKind;
    canPrerender: boolean;
    needsClientRuntime: boolean;
    needsServerRuntime: boolean;
    hydrationStrategy: string | null;
    reasons: string[];
    optimization: OptimizationReport;
    cachePolicy: Record<string, string>;
    requiredPermission: string | null;
}
interface OptimizationReport {
    staticNodes: number;
    reactiveRegions: number;
    eliminatedBranches: number;
    unusedState: string[];
    unusedHandlers: string[];
    constantProps: string[];
    unusedLocalCssClasses: string[];
    batchableStateUpdates: number;
    memoizableComponents: string[];
    preloadDependencies: string[];
    serverOnlyModules: string[];
}
/** Safe compile-time folding for literal conditional branches. */
declare function optimizeAst(ast: PageAst$1): {
    ast: PageAst$1;
    eliminatedBranches: number;
};
declare function analyzeOptimizations(ast: PageAst$1): OptimizationReport;
declare function analyzeRuntimeRequirements(ast: PageAst$1): RuntimeRequirements;

type DeploymentRuntime = "bun" | "node" | "edge" | "worker" | "service-worker" | "browser";
type RuntimeCapability = "filesystem" | "tcp" | "process" | "websocket" | "crypto" | "streams" | "background-tasks";
interface RuntimeCapabilityDiagnostic {
    code: "WRN-RUNTIME-CAPABILITY";
    runtime: DeploymentRuntime;
    module: string;
    capability: RuntimeCapability;
    message: string;
}
declare function runtimeCapabilities(runtime: DeploymentRuntime): ReadonlySet<RuntimeCapability>;
declare function analyzeRuntimeImports(source: string, runtime: DeploymentRuntime): RuntimeCapabilityDiagnostic[];

declare class NativeCompileError extends Error {
    constructor(message: string);
}
/** Compile a parsed `.wrn` page to an Expo Router React Native screen. */
declare function generateNative(ast: PageAst): string;

interface CompilationCacheEntry extends CompileResult {
    key: string;
    file: string;
    sourceHash: string;
    createdAt: number;
}
interface CompilationCacheOptions {
    maxEntries?: number;
    now?: () => number;
}
interface CompilationCache {
    compile(source: string, file?: string, salt?: string): CompilationCacheEntry;
    get(key: string): CompilationCacheEntry | undefined;
    invalidate(file?: string): number;
    clear(): void;
    size(): number;
    stats(): {
        hits: number;
        misses: number;
        entries: number;
    };
}
declare function compilationKey(source: string, file?: string, salt?: string): string;
declare function createCompilationCache(options?: CompilationCacheOptions): CompilationCache;
declare class DependencyGraph {
    #private;
    set(file: string, dependencies: Iterable<string>): void;
    remove(file: string): void;
    dependencies(file: string): string[];
    dependents(file: string): string[];
    affected(file: string): string[];
}

/**
 * @wrnexus/compiler — the `.wrn` language compiler.
 *
 * Parsing and language diagnostics are provided by the canonical
 * `@wrnexus/syntax` package. This package owns platform-specific codegen.
 */

interface CompileResult {
    code: string;
    ast: PageAst$1;
    /** Backward-compatible plain diagnostic messages. */
    diagnostics: string[];
    /** Structured diagnostics for editors, CI, and the DevToolbar. */
    richDiagnostics: WrnDiagnostic[];
}
/** Compile `.wrn` source into an Expo Router React Native screen. */
declare function compileNativeWireFile(source: string): string;
/**
 * Compile `.wrn` source into TypeScript source. Errors include a stable code,
 * source location, code frame, and actionable hint whenever available.
 */
declare function compileWireFile(source: string, filePath?: string): string;
/** Richer entry point returning the AST and structured diagnostics. */
declare function compile(source: string, filePath?: string): CompileResult;

export { type CompilationCache, type CompilationCacheEntry, type CompilationCacheOptions, type CompileResult, DependencyGraph, type DeploymentRuntime, NativeCompileError, type OptimizationReport, type RouteExecutionKind, type RuntimeCapability, type RuntimeCapabilityDiagnostic, type RuntimeRequirements, analyzeOptimizations, analyzeRuntimeImports, analyzeRuntimeRequirements, compilationKey, compile, compileNativeWireFile, compileWireFile, createCompilationCache, createComponentContract, createWrnSourceMap, generate, generateBrowserModule, generateDeclarations, generateNative, generateServerFunctionsModule, generateStoreBrowserModule, generateStoreModule, generateTargets, optimizeAst, resolveWrnImport, resolveWrnImports, rpcManifest, runtimeCapabilities };
```

---

## @wrnexus/content

Documentation URL: https://wrnexusjs.dev/packages/content

# @wrnexus/content

Typed content collections for Markdown/MDX-like documents and remote CMS records. Collections
validate frontmatter through any `{ parse(input) }` schema, render escaped HTML, and expose draft
preview, versions, references, headings, search indexes, pagination, RSS and sitemaps.

```ts
const posts = defineCollection({
  name: "posts",
  schema: PostSchema,
  loader: localContentLoader("content/posts"),
  previewToken: process.env.CONTENT_PREVIEW_TOKEN,
});

const published = await posts.load();
const preview = await posts.load({ previewToken: request.headers.get("x-preview-token") ?? "" });
```

Remote systems implement `CmsAdapter`, use `cmsContentLoader`, or return JSON records through
`remoteContentLoader`. Markdown HTML is escaped by default; raw executable HTML is never trusted.

### Exported TypeScript declarations

```ts
type MdxComponent = (props: Record<string, string>, children: string) => string;
/** Execute explicitly registered MDX components without evaluating arbitrary JavaScript. */
declare function renderMdxComponents(source: string, components: Record<string, MdxComponent>): string;
interface SyntaxLanguageBundle {
    highlight(source: string): string;
}
declare function createIncrementalHighlighter(loaders: Record<string, () => SyntaxLanguageBundle | Promise<SyntaxLanguageBundle>>): {
    languages: () => string[];
    highlight(language: string, source: string): Promise<string>;
    render(html: string): Promise<string>;
};
interface VendorAdapterOptions {
    fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
    token?: string;
    map?: (entry: any) => {
        id: string;
        content: string;
        source?: string;
    };
}
declare function contentfulAdapter(space: string, environment?: string, options?: VendorAdapterOptions): {
    list(collection: string): Promise<any>;
};
declare function sanityAdapter(project: string, dataset: string, options?: VendorAdapterOptions & {
    apiVersion?: string;
}): {
    list(collection: string): Promise<any>;
};
declare function strapiAdapter(baseUrl: string, options?: VendorAdapterOptions): {
    list(collection: string): Promise<any>;
};

interface ContentSchema<T> {
    parse(input: unknown): T;
}
interface ContentEntry<T = Record<string, unknown>> {
    id: string;
    slug: string;
    collection: string;
    data: T;
    body: string;
    html: string;
    excerpt: string;
    headings: ContentHeading[];
    draft: boolean;
    version?: string;
    source: string;
}
interface ContentHeading {
    depth: number;
    text: string;
    slug: string;
}
interface ContentLoaderResult {
    id: string;
    source: string;
    content: string;
}
interface ContentLoader {
    load(): ContentLoaderResult[] | Promise<ContentLoaderResult[]>;
}
interface ContentCollectionOptions<T> {
    name: string;
    schema: ContentSchema<T>;
    loader: ContentLoader;
    includeDrafts?: boolean;
    previewToken?: string;
    references?: Record<string, ContentCollection<unknown>>;
}
interface ContentCollection<T> {
    name: string;
    load(options?: {
        drafts?: boolean;
        previewToken?: string;
        version?: string;
    }): Promise<ContentEntry<T>[]>;
    get(id: string, options?: {
        drafts?: boolean;
        previewToken?: string;
        version?: string;
    }): Promise<ContentEntry<T> | null>;
}
declare function parseFrontmatter(source: string): {
    data: Record<string, unknown>;
    body: string;
};
declare function renderMarkdown(source: string): {
    html: string;
    headings: ContentHeading[];
    excerpt: string;
};
declare function localContentLoader(directory: string): ContentLoader;
declare function remoteContentLoader(url: string, options?: {
    fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
    headers?: HeadersInit;
}): ContentLoader;
declare function defineCollection<T>(options: ContentCollectionOptions<T>): ContentCollection<T>;
declare function resolveContentReference(collections: Record<string, ContentCollection<unknown>>, reference: string): Promise<ContentEntry<unknown> | null>;
declare function paginateContent<T>(entries: T[], page?: number, pageSize?: number): {
    items: T[];
    page: number;
    pageSize: number;
    total: number;
    totalPages: number;
    hasNext: boolean;
    hasPrevious: boolean;
};
declare function createSearchIndex(entries: ContentEntry[]): {
    id: string;
    slug: string;
    title: string;
    text: string;
}[];
declare function searchContent(index: ReturnType<typeof createSearchIndex>, query: string): {
    id: string;
    slug: string;
    title: string;
    text: string;
}[];
declare function contentSitemap(entries: ContentEntry[], baseUrl: string): string;
declare function contentRss(entries: ContentEntry[], options: {
    title: string;
    baseUrl: string;
    description?: string;
}): string;
interface CmsAdapter {
    list(collection: string): Promise<Array<{
        id: string;
        content: string;
        source?: string;
    }>>;
}
declare function cmsContentLoader(adapter: CmsAdapter, collection: string): ContentLoader;

export { type CmsAdapter, type ContentCollection, type ContentCollectionOptions, type ContentEntry, type ContentHeading, type ContentLoader, type ContentLoaderResult, type ContentSchema, type MdxComponent, type SyntaxLanguageBundle, type VendorAdapterOptions, cmsContentLoader, contentRss, contentSitemap, contentfulAdapter, createIncrementalHighlighter, createSearchIndex, defineCollection, localContentLoader, paginateContent, parseFrontmatter, remoteContentLoader, renderMarkdown, renderMdxComponents, resolveContentReference, sanityAdapter, searchContent, strapiAdapter };
```

---

## @wrnexus/core

Documentation URL: https://wrnexusjs.dev/packages/core

# @wrnexus/core

> The framework core: the request `Context`, middleware contract, and the security, session, caching, streaming, realtime, and JSX primitives every other WrNexus package builds on.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

`@wrnexus/core` is the shared foundation of WrNexus. It defines the `Context`
object that flows through every middleware, page, and API route, plus the
`Middleware`/`Next` contract they implement. On top of that it ships the
building blocks a real app needs: cookie-backed sessions, password auth, CSRF
protection, rate limiting, request logging, HTTP + in-memory caching, file
uploads, streaming/SSE responses, WebSocket "rooms", security headers/CORS, and
a server-side JSX runtime that renders to HTML strings. Everything here is
**server-side** and Bun-native (it uses `Bun.password`, `Bun.write`, the
web-standard `Request`/`Response`, and `crypto`). You depend on it directly and
transitively through the rest of the framework.

## Installation

```bash
bun add @wrnexus/core
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

## API

### Context & middleware — `@wrnexus/core`

The `Context` (`ctx`) is the single value passed to middleware and handlers.

| Export                         | Kind | Description                                                                                                              |
| ------------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------ |
| `Context`                      | type | Per-request object: `req`, `url`, `lang`, `t`, `params`, `locals`, `user?`, `ip?`, `cookies`, `session`, `localStorage`. |
| `Next`                         | type | `() => Promise<Response> \| Response` — invokes the next middleware/handler.                                             |
| `Middleware`                   | type | `(ctx, next) => Promise<Response> \| Response`. Return `next()` to continue, or a `Response` to short-circuit.           |
| `createContext(req, url)`      | fn   | Build a fresh `Context` for an incoming request (wires up cookies, session, localStorage snapshot).                      |
| `withContextHeaders(ctx, res)` | fn   | Apply accumulated headers (e.g. `Set-Cookie`) from the context onto a response.                                          |
| `PageComponent`                | type | `(ctx) => string \| Promise<string>` — a page module's default export.                                                   |
| `PageMeta` / `SeoConfig`       | type | `<head>` metadata: `title`, `description`, `canonical`, `robots`, `image`, `twitterCard`, `themeColor`, …                |
| `TFunction`                    | type | `(key, params?) => string` — translate a key for `ctx.lang`, interpolating `{param}` placeholders.                       |

Key `Context` fields:

- `ctx.locals` — per-request scratch space for passing values between middleware.
- `ctx.user` — the authenticated user (populated by `sessionAuth`/`logIn`), or `null`.
- `ctx.ip` — the direct socket peer IP (not spoofable via headers).
- `ctx.cookies` / `ctx.session` / `ctx.localStorage` — see **Storage** below.

### Authentication — `@wrnexus/core`

Passwords are hashed with argon2id via `Bun.password`; sessions ride the
cookie-backed `SessionStore`.

| Export                           | Signature                              | Notes                                                                                                                 |
| -------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `hashPassword(password)`         | `(string) => Promise<string>`          | argon2id hash to store.                                                                                               |
| `verifyPassword(password, hash)` | `(string, string) => Promise<boolean>` | Constant-safe; returns `false` on bad/empty hash.                                                                     |
| `logIn(ctx, user)`               | `(Context, U) => void`                 | Regenerates the session id (fixation defense), stores the user, sets `ctx.user`.                                      |
| `logOut(ctx)`                    | `(Context) => void`                    | Clears the session and `ctx.user`.                                                                                    |
| `getUser(ctx)`                   | `(Context) => U \| null`               | Current user from `ctx.user`, falling back to the session.                                                            |
| `sessionAuth()`                  | `() => Middleware`                     | Hydrates `ctx.user` from the session each request. Register early.                                                    |
| `requireAuth(options?)`          | `(RequireAuthOptions?) => Middleware`  | Guard: API/fetch requests get `401 JSON`, page navigations get `302` to `loginPath` (default `/login`) with `?next=`. |
| `SESSION_USER_KEY`               | `"user"`                               | Session key holding the user.                                                                                         |

`RequireAuthOptions`: `{ loginPath?: string }`.

### CSRF — `@wrnexus/core`

Double-submit cookie pattern: a readable `wire-csrf` cookie is echoed in an
`x-csrf-token` header on unsafe requests.

| Export                        | Signature                        | Notes                                                                                                            |
| ----------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `csrfToken(ctx)`              | `(Context) => string`            | Ensures the CSRF cookie exists and returns its token.                                                            |
| `verifyCsrf(ctx)`             | `(Context) => boolean`           | Safe methods (GET/HEAD/OPTIONS) pass; otherwise header/`ctx.locals._csrf` must match the cookie (constant-time). |
| `csrfProtection()`            | `() => Middleware`               | 403s unsafe requests with a missing/mismatched token.                                                            |
| `CSRF_COOKIE` / `CSRF_HEADER` | `"wire-csrf"` / `"x-csrf-token"` | Cookie & header names.                                                                                           |

### Rate limiting — `@wrnexus/core`

Fixed-window limiter that returns `429` with `Retry-After` and emits
`RateLimit-Limit`/`-Remaining`/`-Reset` headers.

| Export                | Signature                           | Notes                                                                  |
| --------------------- | ----------------------------------- | ---------------------------------------------------------------------- |
| `rateLimit(options?)` | `(RateLimitOptions?) => Middleware` | Main middleware.                                                       |
| `peerKey(ctx)`        | `(Context) => string`               | Non-spoofable key from `ctx.ip` (default).                             |
| `proxyKey(ctx)`       | `(Context) => string`               | Trusts `x-forwarded-for`/`x-real-ip`. Use only behind a trusted proxy. |
| `defaultKey`          | —                                   | **Deprecated** alias of `proxyKey`.                                    |

`RateLimitOptions`: `windowMs` (default `60_000`), `max` (default `60`),
`key`, `trustProxy` (default `false` → keys on `peerKey`; `true` → `proxyKey`),
`message`, `headers` (default `true`), `store`.

`RateLimitStore` is pluggable — implement `hit(key, windowMs, now) => Bucket | Promise<Bucket>`
(a `Bucket` is `{ count, resetAt }`) to back limits with Redis/SQL across
instances. The default store is process-local memory.

### Request logging — `@wrnexus/core`

| Export                    | Signature                               | Notes                                                                            |
| ------------------------- | --------------------------------------- | -------------------------------------------------------------------------------- |
| `requestLogger(options?)` | `(RequestLoggerOptions?) => Middleware` | One record per request with a request id (stored on `ctx.locals[requestIdKey]`). |

`RequestLoggerOptions`: `format` (`"pretty"` default \| `"json"`), `sink(line, record)`
(default `console.log`), `requestIdKey` (default `"requestId"`), `now`.
`RequestRecord` = `{ time, id, method, path, status, durationMs }`.

### Resilience — `@wrnexus/core`

`resilientCall` standardizes cancellation-aware timeouts, controlled retries,
fixed or exponential backoff, fallback responses, circuit breaking, and bounded
concurrency. Reuse a declarative circuit/bulkhead options object, or an explicit
`CircuitBreaker`/`Bulkhead` instance, wherever calls must share health and
capacity state.

```ts
import { resilientCall } from "@wrnexus/core";

const paymentCircuit = { failures: 5, resetAfter: "30s" } as const;

const status = await resilientCall({
  timeout: "5s",
  retries: 3,
  retryDelay: "100ms",
  backoff: "exponential",
  circuitBreaker: paymentCircuit,
  bulkhead: { concurrency: 20, queue: 100 },
  run: (signal) => paymentProvider.checkStatus({ signal }),
  fallback: () => ({ state: "unavailable" }),
});
```

`CircuitBreaker.snapshot()` reports `closed`, `open`, or `half-open`, failure
and success counts, and the remaining retry delay for health endpoints and
development tooling. Fail-fast conditions use stable `WRN-RESILIENCE-*` codes.
Core's existing `HealthRegistry`, `withIdempotency`, and pluggable stores/locks
cover health reporting, idempotent requests, and distributed coordination.

### Caching — `@wrnexus/core`

| Export                           | Kind  | Notes                                                                                                                                      |
| -------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `TTLCache<V>`                    | class | In-memory TTL cache: `get`, `set`, `getOrLoad(key, loader, ttlMs?)`, `delete`, `clear`, `size`. Constructor takes a default `ttlMs` (60s). |
| `cacheControl(options)`          | fn    | Build a `Cache-Control` value from `CacheControlOptions`.                                                                                  |
| `withCacheControl(res, options)` | fn    | Apply `Cache-Control` to a response.                                                                                                       |
| `etag(body, weak?)`              | fn    | Stable quoted FNV-1a ETag (weak by default).                                                                                               |
| `notModified(req, tag)`          | fn    | `true` when `If-None-Match` matches — send a `304`.                                                                                        |

`CacheControlOptions`: `maxAge`, `sMaxAge`, `private`, `noStore`, `noCache`,
`staleWhileRevalidate`, `immutable`.

### File uploads — `@wrnexus/core`

Bun parses `multipart/form-data` via `Request.formData()`; these helpers
validate and persist the resulting `File`s.

| Export                      | Signature                                           | Notes                                                                                  |
| --------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `collectUploads(form)`      | `(FormData) => { field, file }[]`                   | Every non-empty `File` in a parsed form.                                               |
| `saveUpload(file, options)` | `(File, SaveUploadOptions) => Promise<SavedUpload>` | Validates size/type, sanitizes the name, writes via `Bun.write`. Throws `UploadError`. |
| `sanitizeFilename(name)`    | `(string) => string`                                | Strips separators, traversal, control/illegal chars; caps at 255.                      |
| `UploadError`               | class                                               | Thrown on rejected uploads.                                                            |

`SaveUploadOptions`: `dir` (required), `maxBytes`, `allowedTypes` (MIME types
like `"image/png"` and/or extensions like `".png"`), `filename(file)`.
`SavedUpload` = `{ path, filename, size, type }`.

### Streaming & SSE — `@wrnexus/core`

| Export                          | Signature                                                                        | Notes                                                               |
| ------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `streamResponse(source, init?)` | `(Iterable\|AsyncIterable<string\|Uint8Array>, StreamResponseInit?) => Response` | Streaming `Response` from a chunk source (basis for streaming SSR). |
| `sse(source)`                   | `(Iterable\|AsyncIterable<ServerSentEvent>) => Response`                         | `text/event-stream` response.                                       |

`StreamResponseInit`: `status`, `headers`, `contentType` (default
`"text/html; charset=utf-8"`). `ServerSentEvent`: `{ data, event?, id?, retry? }`.

### Realtime rooms — `@wrnexus/core`

WebSocket rooms. A file in `app/realtime/` exports
`default defineRoom({ ... })` and is served at `ws://host/realtime/<name>`.

| Export                                  | Signature                                                | Notes                                                                |
| --------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------- |
| `defineRoom(handlers)`                  | `(RoomHandlers) => RoomDefinition`                       | Define a room. Export the result as `default`.                       |
| `isRoomDefinition(value)`               | `(unknown) => boolean`                                   | Type guard for a room definition.                                    |
| `createRealtimeRegistry()`              | `() => RealtimeRegistry`                                 | Server-side connection manager mapping sockets ↔ rooms.              |
| `bridgeRealtime(registry, bus, topic?)` | `(RealtimeRegistry, RealtimeBus, string?) => () => void` | Bridge broadcasts/`toUser` sends across processes via a pub/sub bus. |

`RoomHandlers`: `authorize(info) => boolean` (gate before accept — return
`false` to reject with 403), `onConnect(client)`, `onMessage(client, message)`
(JSON auto-parsed), `onLeave(client)`. A handler receives a `RoomClient` with
`id`, `user`, `query`, `data`, `room`, and `send` / `broadcast` /
`to(id)` / `toUser(user)` / `close`. The `Room` API adds `state`, `clients()`,
`count()`, and `broadcast`. `RealtimeBus` is structurally satisfied by
`@wrnexus/pubsub`. Legacy `RealtimeHandler`/`RealtimeSocket` raw handlers are
still exported. Connection-targeted sends (`send`, `to(id)`) stay local; room
broadcasts and `toUser` cross the bridge.

### Error pages — `@wrnexus/core`

| Export                         | Signature                        | Notes                                                 |
| ------------------------------ | -------------------------------- | ----------------------------------------------------- |
| `renderError(err, mode)`       | `(unknown, Mode) => Response`    | Dev page (with stack) or generic prod page by `mode`. |
| `renderDevError(err, status?)` | `(unknown, number?) => Response` | Readable HTML error page including the stack trace.   |
| `renderProdError(status?)`     | `(number?) => Response`          | Generic page that never leaks file paths.             |
| `renderNotFound()`             | `() => Response`                 | Simple 404 page.                                      |

`Mode` = `"development" | "production"`.

### Security headers & CORS — `@wrnexus/core`

| Export                                                   | Signature            | Notes                                                                                                                                                    |
| -------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `withSecurityHeaders(req, res, mode, security?, nonce?)` | → `Response`         | Applies CORS + CSP, HSTS, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, COOP, Trusted Types, and `extraHeaders`. |
| `createCorsPreflightResponse(req, security?)`            | → `Response \| null` | Builds a `204`/`403` preflight response for CORS `OPTIONS` requests.                                                                                     |
| `isWebSocketOriginAllowed(req, security?)`               | → `boolean`          | Guards WS upgrades against cross-site hijacking (allows same-origin, configured CORS origins, and non-browser clients).                                  |

Config types: `SecurityConfig` (top-level), `CorsConfig`/`CorsOrigin`,
`ContentSecurityPolicyConfig`/`CspDirectiveValue`, `HstsConfig`,
`TrustedTypesConfig`, `PermissionsPolicyConfig`. WrNexus applies sensible
defaults (self-only CSP, `frame-ancestors 'none'`, restrictive Permissions-Policy,
HSTS in production, Trusted Types in production); each is individually
overridable or disable-able via `false`.

### Storage: cookies, sessions, localStorage — `@wrnexus/core`

These back the `ctx.cookies`, `ctx.session`, and `ctx.localStorage` fields.

| Export                                                    | Kind              | Notes                                                                                                                                                             |
| --------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setSessionBackend(backend)`                              | fn                | Swap the **sync** session persistence backend (`SessionBackend`) — e.g. `bun:sqlite`. Default is process-local memory. Call once at startup.                      |
| `loadSession(backend, options?)`                          | fn → `Middleware` | Back `ctx.session` with an **async** store (`AsyncSessionBackend`: `load`/`save`/`destroy`) — loads before the request, saves after. `options.ttlMs` default 24h. |
| `CookieStore`                                             | type              | `get`/`getAll`/`has`/`set(name, value, opts?)`/`delete`/`headers`.                                                                                                |
| `SessionStore`                                            | type              | `id`/`get`/`getAll`/`set`/`delete`/`regenerate`/`clear`.                                                                                                          |
| `LocalStorageSnapshot`                                    | type              | Read-only view of the browser's localStorage sent via header for CSR bindings.                                                                                    |
| `CookieOptions`                                           | type              | `path`, `domain`, `maxAge`, `expires`, `httpOnly`, `secure`, `sameSite`.                                                                                          |
| `SessionEntry` / `SessionBackend` / `AsyncSessionBackend` | types             | Session persistence contracts.                                                                                                                                    |

### Low-level security helpers — `@wrnexus/core`

| Export                        | Signature             | Notes                                               |
| ----------------------------- | --------------------- | --------------------------------------------------- |
| `escapeHtml(value)`           | `(string) => string`  | Escape for HTML text/attributes.                    |
| `isSafeIslandName(name)`      | `(string) => boolean` | Allow only a conservative `[A-Za-z0-9_-]+` charset. |
| `isSafeRequestPath(pathname)` | `(string) => boolean` | Reject NULs, `..` traversal, and backslashes.       |

### JSX runtime — `@wrnexus/core`, `@wrnexus/core/jsx-runtime`, `@wrnexus/core/jsx-dev-runtime`

A server-side JSX runtime that renders to HTML **strings** (no virtual DOM).
Point `tsconfig`'s `jsxImportSource` at `@wrnexus/core`.

| Export                                     | Kind   | Notes                                                                                   |
| ------------------------------------------ | ------ | --------------------------------------------------------------------------------------- |
| `jsx` / `jsxs`                             | fn     | The runtime factory (TypeScript calls these automatically). Returns an `Html` instance. |
| `Fragment`                                 | symbol | JSX fragment marker.                                                                    |
| `Html`                                     | class  | Wraps a raw, already-safe HTML string (`toString()` returns it).                        |
| `mustache(expr)`                           | fn     | Emit a `{{expr}}` placeholder (tagged-template or string form) for the client binder.   |
| `JSXComponent` / `JSXProps` / `Renderable` | types  | Component signature and renderable value types.                                         |

Values interpolated as children are HTML-escaped unless they are an `Html`
instance; use `dangerouslySetInnerHTML={{ __html }}` for trusted markup. Void
elements render without a closing tag; `className`→`class`, `htmlFor`→`for`, and
`style` objects are serialized to CSS text.

The subpath exports map to the runtime TypeScript's JSX transform expects:

```jsonc
// tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@wrnexus/core",
  },
}
```

## Usage

### A minimal middleware chain

```ts
import {
  createContext,
  withContextHeaders,
  sessionAuth,
  requireAuth,
  requestLogger,
  rateLimit,
  csrfProtection,
  type Middleware,
} from "@wrnexus/core";

const chain: Middleware[] = [
  requestLogger({ format: "json" }),
  rateLimit({ max: 100, windowMs: 60_000 }),
  csrfProtection(),
  sessionAuth(),
  requireAuth({ loginPath: "/login" }),
];
```

### Password auth

```ts
import { hashPassword, verifyPassword, logIn, getUser } from "@wrnexus/core";

// Registration
const passwordHash = await hashPassword(form.password);

// Login
if (await verifyPassword(form.password, user.passwordHash)) {
  logIn(ctx, { id: user.id, email: user.email });
}

const current = getUser<{ id: string }>(ctx); // or null
```

### HTTP caching with ETags

```ts
import { etag, notModified, withCacheControl } from "@wrnexus/core";

const body = JSON.stringify(data);
const tag = etag(body);
if (notModified(ctx.req, tag)) {
  return new Response(null, { status: 304, headers: { ETag: tag } });
}
const res = new Response(body, { headers: { ETag: tag, "content-type": "application/json" } });
return withCacheControl(res, { maxAge: 60, staleWhileRevalidate: 300 });
```

### Streaming SSE

```ts
import { sse } from "@wrnexus/core";

async function* ticks() {
  for (let n = 0; ; n++) {
    yield { event: "tick", data: String(n) };
    await Bun.sleep(1000);
  }
}
export default (ctx) => sse(ticks());
```

### A realtime room

```ts
// app/realtime/chat.ts
import { defineRoom } from "@wrnexus/core";

export default defineRoom({
  authorize: (info) => !!info.user, // require auth
  onConnect(client) {
    client.user = client.query.user;
    client.room.broadcast({ type: "join", id: client.id });
  },
  onMessage(client, msg) {
    client.broadcast({ type: "say", from: client.id, text: msg.text });
  },
});
```

Scale it across processes:

```ts
import { createRealtimeRegistry, bridgeRealtime } from "@wrnexus/core";
import { createPubSub } from "@wrnexus/pubsub";
import { redisDriver } from "@wrnexus/pubsub/redis";

const registry = createRealtimeRegistry();
bridgeRealtime(registry, createPubSub(redisDriver(process.env.REDIS_URL)));
```

### JSX rendering

```tsx
import { Html } from "@wrnexus/core";

function Card({ title, body }: { title: string; body: string }) {
  return (
    <article class="card">
      <h2>{title}</h2>
      <p>{body}</p>
    </article>
  );
}

const html: Html = <Card title="Hi" body="<b>escaped</b> automatically" />;
return new Response(html.toString(), { headers: { "content-type": "text/html" } });
```

## Requirements / Notes

- **Bun-only.** Uses `Bun.password` (argon2id), `Bun.write`, web-standard
  `Request`/`Response`/`FormData`/`ReadableStream`, and the global `crypto`.
  Node is not supported.
- Session and rate-limit backends default to **process-local memory**. For
  multi-instance deployments, swap in a shared backend: `setSessionBackend` (sync,
  e.g. `bun:sqlite`) or `loadSession` (async, e.g. Redis) for sessions, a custom
  `RateLimitStore` for limits, and `bridgeRealtime` for realtime.
- Works with the rest of the framework: realtime bridging is structurally
  compatible with [`@wrnexus/pubsub`](../pubsub); the security, auth, and JSX
  primitives here are consumed by the WrNexus server/router packages.
- Subpath exports: `@wrnexus/core/jsx-runtime` and `@wrnexus/core/jsx-dev-runtime`
  for TypeScript's automatic JSX transform.

### Exported TypeScript declarations

```ts
export { Fragment, Html, Component as JSXComponent, Props as JSXProps, Renderable, jsx, jsxs, mustache } from './jsx-runtime.js';

interface Tenant {
    id: string;
    slug?: string;
    name?: string;
    metadata?: Record<string, unknown>;
}
interface TenantResource {
    tenantId: string;
}
interface TenantMembership {
    tenantId: string;
    userId: string;
    roles?: string[];
    workspaceIds?: string[];
}
interface TenantAuditEvent {
    tenantId: string;
    action: string;
    actorId?: string;
    resource?: string;
    metadata?: Record<string, unknown>;
    createdAt: number;
}
interface TenantQuota {
    tenantId: string;
    resource: string;
    limit: number;
    usage: number;
}
interface TenantDirectoryStore {
    putMembership(value: TenantMembership): Promise<void>;
    getMembership(tenantId: string, userId: string): Promise<TenantMembership | null>;
    listMemberships(tenantId: string): Promise<TenantMembership[]>;
    putQuota(value: TenantQuota): Promise<void>;
    getQuota(tenantId: string, resource: string): Promise<TenantQuota | null>;
}
type TenantResolver = (ctx: Context) => Tenant | null | Promise<Tenant | null>;
interface TenantMiddlewareOptions {
    required?: boolean;
    status?: number;
}
declare function tenantMiddleware(resolveTenant: TenantResolver, options?: TenantMiddlewareOptions): Middleware;
declare function tenantFromSubdomain(lookup: (slug: string, ctx: Context) => Tenant | null | Promise<Tenant | null>, rootDomains?: string[]): TenantResolver;
declare function tenantFromDomain(lookup: (domain: string, ctx: Context) => Tenant | null | Promise<Tenant | null>): TenantResolver;
declare function tenantFromPath(lookup: (slug: string, ctx: Context) => Tenant | null | Promise<Tenant | null>, prefix?: string): TenantResolver;
/** Header resolution is intentionally opt-in and must only be used behind a trusted proxy. */
declare function tenantFromHeader(lookup: (id: string, ctx: Context) => Tenant | null | Promise<Tenant | null>, header?: string): TenantResolver;
declare function tenantFromSession(resolveId: (ctx: Context) => string | null | Promise<string | null>, lookup: (id: string, ctx: Context) => Tenant | null | Promise<Tenant | null>): TenantResolver;
declare function composeTenantResolvers(...resolvers: TenantResolver[]): TenantResolver;
declare function requireTenant(ctx: Context): Tenant;
/** Wrap a repository so every operation receives the current tenant id. */
declare function tenantScope<T extends object>(tenant: Tenant, repository: T): T & {
    tenantId: string;
};
declare function assertTenantAccess(tenant: Tenant, resource: TenantResource): void;
declare function tenantKey(tenant: Tenant | string, ...parts: Array<string | number>): string;
declare function createTenantDirectory(options?: {
    audit?: (event: TenantAuditEvent) => void | Promise<void>;
    now?: () => number;
}): {
    addMembership(membership: TenantMembership, actorId?: string): Promise<void>;
    membership(tenantId: string, userId: string): TenantMembership | null;
    switchWorkspace(tenantId: string, userId: string, workspaceId: string): Promise<{
        tenantId: string;
        workspaceId: string;
    }>;
    setQuota(tenantId: string, resource: string, limit: number): void;
    enforceQuota(tenantId: string, resource: string, usage: number, requested?: number): {
        usage: number;
        requested: number;
        limit: number | undefined;
    };
};
declare function memoryTenantDirectoryStore(): TenantDirectoryStore;
declare function createPersistentTenantDirectory(store: TenantDirectoryStore, options?: {
    audit?: (event: TenantAuditEvent) => void | Promise<void>;
    now?: () => number;
}): {
    addMembership(membership: TenantMembership, actorId?: string): Promise<void>;
    membership: (tenantId: string, userId: string) => Promise<TenantMembership | null>;
    memberships: (tenantId: string) => Promise<TenantMembership[]>;
    switchWorkspace(tenantId: string, userId: string, workspaceId: string): Promise<{
        tenantId: string;
        workspaceId: string;
    }>;
    setQuota(tenantId: string, resource: string, limit: number, usage?: number): Promise<void>;
    consumeQuota(tenantId: string, resource: string, requested: number): Promise<TenantQuota | null>;
};
interface TenantSqlClient {
    query<T = Record<string, unknown>>(sql: string, parameters?: unknown[]): Promise<{
        rows: T[];
    }>;
}
declare function postgresTenantDirectoryStore(db: TenantSqlClient): TenantDirectoryStore;
declare const POSTGRES_TENANT_DIRECTORY_SCHEMA = "CREATE TABLE IF NOT EXISTS wrnexus_tenant_memberships (tenant_id text NOT NULL,user_id text NOT NULL,roles jsonb NOT NULL DEFAULT '[]',workspace_ids jsonb NOT NULL DEFAULT '[]',PRIMARY KEY (tenant_id,user_id)); CREATE TABLE IF NOT EXISTS wrnexus_tenant_quotas (tenant_id text NOT NULL,resource text NOT NULL,quota_limit bigint NOT NULL,usage bigint NOT NULL DEFAULT 0,PRIMARY KEY (tenant_id,resource));";
declare function migrateTenants<T extends Tenant>(tenants: T[], migrate: (tenant: T) => void | Promise<void>, options?: {
    concurrency?: number;
    continueOnError?: boolean;
}): Promise<{
    migrated: string[];
    failed: {
        tenantId: string;
        error: string;
    }[];
}>;

interface SpanRecord {
    name: string;
    startTime: number;
    endTime?: number;
    durationMs?: number;
    status?: "ok" | "error";
    attributes: Record<string, string | number | boolean>;
    error?: unknown;
}
interface Tracer {
    startSpan(name: string, attributes?: SpanRecord["attributes"]): Span;
    records(): readonly SpanRecord[];
}
interface Span {
    setAttribute(name: string, value: string | number | boolean): void;
    end(status?: "ok" | "error", error?: unknown): SpanRecord;
}
declare function createTracer(clock?: () => number): Tracer;
declare function withSpan<T>(tracer: Tracer, name: string, run: (span: Span) => T | Promise<T>, attributes?: SpanRecord["attributes"]): Promise<T>;
interface TracingMiddlewareOptions {
    /** Include W3C Server-Timing response headers. Defaults to true. */
    serverTiming?: boolean;
    /** Fraction of requests to trace, from 0 to 1. Defaults to 1. */
    sampleRate?: number;
    /** Called after a traced response completes. */
    onComplete?: (ctx: Context, records: readonly SpanRecord[]) => void | Promise<void>;
}
declare function tracingMiddleware(tracerFactory?: (ctx: Context) => Tracer, options?: TracingMiddlewareOptions): Middleware;

interface CookieOptions {
    path?: string;
    domain?: string;
    maxAge?: number;
    expires?: Date | string;
    httpOnly?: boolean;
    secure?: boolean;
    sameSite?: "Strict" | "Lax" | "None" | "strict" | "lax" | "none";
}
interface CookieStore {
    get(name: string): string | undefined;
    getAll(): Record<string, string>;
    has(name: string): boolean;
    set(name: string, value: string, options?: CookieOptions): void;
    delete(name: string, options?: CookieOptions): void;
    headers(): string[];
}
interface SessionStore {
    id(): string;
    get<T = unknown>(key: string): T | undefined;
    getAll(): Record<string, unknown>;
    set(key: string, value: unknown): void;
    delete(key: string): void;
    /** Issue a fresh session id, keeping the data — defends against fixation. */
    regenerate(): void;
    clear(): void;
}
interface LocalStorageSnapshot {
    get(key: string): string | undefined;
    getAll(): Record<string, string>;
    has(key: string): boolean;
}
interface SessionPolicy {
    cookieName?: string;
    idleTimeoutMs?: number;
    absoluteTimeoutMs?: number;
    sameSite?: NonNullable<CookieOptions["sameSite"]>;
    secure?: boolean;
}
declare function setSessionPolicy(policy: SessionPolicy): void;
/** A stored session: its data plus an absolute expiry timestamp (ms). */
interface SessionEntry {
    data: Record<string, unknown>;
    expiresAt: number;
    /** Creation time used for the absolute session lifetime. Optional for old backends. */
    createdAt?: number;
    lastAccessAt?: number;
}
/**
 * Pluggable session persistence. The default is process-local memory; swap in a
 * shared backend (Redis, SQL, etc.) via `setSessionBackend` so sessions survive
 * restarts and work across multiple instances. Methods are synchronous, so a
 * backend must be sync (e.g. `bun:sqlite`); async stores need a load/save
 * wrapper around the request (future work).
 */
interface SessionBackend {
    get(id: string): SessionEntry | undefined;
    set(id: string, entry: SessionEntry): void;
    delete(id: string): void;
    /** Optional: drop expired entries. Called periodically by the store. */
    gc?(now: number): void;
}
/** Replace the session persistence backend (call once at startup). */
declare function setSessionBackend(backend: SessionBackend): void;
/**
 * An ASYNC session store (Redis, a remote DB). Use it via the `loadSession`
 * middleware, which loads the session before the request and saves it after —
 * keeping the `ctx.session` API synchronous while persistence is shared across
 * instances.
 */
interface AsyncSessionBackend {
    load(id: string): Promise<SessionEntry | undefined>;
    save(id: string, entry: SessionEntry): Promise<void>;
    destroy(id: string): Promise<void>;
}
/**
 * Back `ctx.session` with an async store. Register early (before anything reads
 * `ctx.session`). Loads once at the start of the request and saves once at the
 * end; regenerate/clear destroy the old id.
 */
declare function loadSession(backend: AsyncSessionBackend, options?: {
    ttlMs?: number;
    absoluteTtlMs?: number;
    cookieName?: string;
    sameSite?: NonNullable<CookieOptions["sameSite"]>;
    secure?: boolean;
}): Middleware;

/**
 * Core request context and middleware contracts.
 *
 * The `Context` object is the single value that flows through middleware,
 * pages and API routes. It is intentionally small and framework-agnostic so
 * it can later be reused by the `.wrn` compiler output.
 */

/** Translate a key for the active language, interpolating `{param}` placeholders. */
type TFunction = (key: string, params?: Record<string, string | number>) => string;
type Context = {
    /** The raw incoming web-standard Request. */
    req: Request;
    /** Parsed URL of the request (pathname, query, etc.). */
    url: URL;
    /** Active language for this request (resolved by the runtime); "" if i18n is unused. */
    lang: string;
    /** Translate a key for the active language (identity until the runtime sets it). */
    t: TFunction;
    /** Dynamic route params, e.g. `/users/[id]` -> `{ id: "42" }`. */
    params: Record<string, string>;
    /**
     * Per-request scratch space. Middleware can attach values here
     * (e.g. the authenticated user) and downstream handlers can read them.
     */
    locals: Record<string, unknown>;
    /**
     * The authenticated user for this request, or null when anonymous. Populated
     * by the `sessionAuth` middleware (or `logIn`); read via `getUser(ctx)`.
     */
    user?: unknown;
    /** Active tenant/workspace resolved by tenant middleware. */
    tenant?: Tenant;
    /** Request tracer installed by observability middleware. */
    tracer?: Tracer;
    /**
     * The direct socket peer IP, set by the server from `server.requestIP`. This
     * is NOT spoofable by request headers — prefer it over `x-forwarded-for` for
     * rate limiting unless you run behind a trusted proxy.
     */
    ip?: string;
    /** Read/write HTTP cookies for the current response. */
    cookies: CookieStore;
    /** In-memory cookie-backed session store. */
    session: SessionStore;
    /** Read-only localStorage snapshot sent by the browser for CSR data bindings. */
    localStorage: LocalStorageSnapshot;
};
/** Calls the next middleware in the chain (or the final route handler). */
type Next = () => Promise<Response> | Response;
/**
 * Middleware runs before pages and API routes. It can:
 *  - inspect/modify `ctx`
 *  - short-circuit by returning a `Response` without calling `next()`
 *  - continue by returning `await next()`
 */
type Middleware = (ctx: Context, next: Next) => Promise<Response> | Response;
/** SEO metadata rendered into the document `<head>`. */
type SeoConfig = {
    /** BCP 47 document language used on `<html lang>` (default: `en`). */
    lang?: string;
    title?: string;
    titleTemplate?: string;
    description?: string;
    canonical?: string;
    canonicalBase?: string;
    robots?: string;
    keywords?: string | string[];
    image?: string;
    siteName?: string;
    type?: string;
    locale?: string;
    twitterCard?: string;
    twitterSite?: string;
    themeColor?: string;
};
/** Page metadata rendered into the document `<head>`. */
type PageMeta = SeoConfig;
/** A page module's default export. Returns an HTML string for the body. */
type PageComponent = (ctx: Context) => string | Promise<string>;
/** Create a fresh context for an incoming request. */
declare function createContext(req: Request, url: URL): Context;
/** Apply headers accumulated on the context, such as Set-Cookie. */
declare function withContextHeaders(ctx: Context, res: Response): Response;

type ExecutionKind = "http" | "api" | "action" | "loader" | "middleware" | "realtime" | "queue" | "cron" | "webhook";
interface ResponseContext {
    status: number;
    headers: Headers;
    setStatus(status: number): void;
}
interface ExecutionContext {
    kind: ExecutionKind;
    id: string;
    request: Request;
    response: ResponseContext;
    user: unknown | null;
    session: unknown | null;
    tenant: Tenant | null;
    locale: string;
    timezone: string;
    db?: unknown;
    cache?: unknown;
    logger?: unknown;
    trace?: Tracer;
    signal: AbortSignal;
    deadline: Date | null;
    metadata: Record<string, unknown>;
    authorize(permission: string): void | Promise<void>;
}
interface ExecutionContextInput extends Partial<Omit<ExecutionContext, "kind" | "id" | "request" | "response" | "signal" | "deadline" | "metadata" | "authorize">> {
    kind: ExecutionKind;
    id?: string;
    request?: Request;
    response?: Partial<Pick<ResponseContext, "status">> & {
        headers?: HeadersInit;
    };
    signal?: AbortSignal;
    deadline?: Date | number | null;
    timeoutMs?: number;
    metadata?: Record<string, unknown>;
    authorize?: (permission: string) => void | Promise<void>;
}
declare function createExecutionContext(input: ExecutionContextInput): ExecutionContext;
declare function executionContextFromHttp(context: Context, kind?: Extract<ExecutionKind, "http" | "api" | "action" | "loader" | "middleware" | "webhook">, input?: Omit<ExecutionContextInput, "kind" | "request" | "user" | "tenant" | "locale" | "trace">): ExecutionContext;

/**
 * Small, dependency-free security helpers shared across packages.
 */
/**
 * Escape a string for safe interpolation into HTML text or attributes.
 * Used for page metadata (title/description) so untrusted values can't
 * break out of an attribute or inject markup.
 */
declare function escapeHtml(value: string): string;
declare function isSafeIslandName(name: string): boolean;
/**
 * Reject obvious path-traversal in a request path before it is ever used to
 * resolve a file. The router never builds file paths from request input
 * (routes are resolved against a pre-scanned table), but this is a cheap
 * defense-in-depth guard.
 */
declare function isSafeRequestPath(pathname: string): boolean;

/**
 * CSRF protection via the double-submit cookie pattern plus origin/fetch
 * metadata validation for unsafe requests.
 */

declare const CSRF_COOKIE = "wire-csrf";
declare const CSRF_HEADER = "x-csrf-token";
interface CsrfProtectionOptions {
    /** Validate Origin when present. Defaults to true. */
    verifyOrigin?: boolean;
    /** Additional exact origins permitted for trusted cross-origin clients. */
    trustedOrigins?: string[];
    /** Reject Sec-Fetch-Site: cross-site on unsafe requests. Defaults to true. */
    verifyFetchMetadata?: boolean;
}
/** Ensure the CSRF cookie exists (readable by JS) and return its token. */
declare function csrfToken(ctx: Context): string;
/** Verify an unsafe request's token, origin, and browser fetch metadata. */
declare function verifyCsrf(ctx: Context, options?: CsrfProtectionOptions): boolean;
/** Middleware that 403s unsafe requests with a missing/mismatched token. */
declare function csrfProtection(options?: CsrfProtectionOptions): Middleware;

/**
 * Authentication primitives.
 *
 * Passwords are hashed with argon2id via `Bun.password`. Sessions ride on the
 * existing cookie-backed `SessionStore`: logging a user in stores a serializable
 * user object under the "user" key, and `sessionAuth` hydrates `ctx.user` from
 * it on every request. `requireAuth` is a guard middleware for protected routes.
 */

/** Session key under which the authenticated user is stored. */
declare const SESSION_USER_KEY = "user";
/** Hash a plaintext password (argon2id). Store the returned string. */
declare function hashPassword(password: string): Promise<string>;
/** Verify a plaintext password against a stored hash. Safe against bad hashes. */
declare function verifyPassword(password: string, hash: string): Promise<boolean>;
/** Persist the authenticated user in the session and on the context. */
declare function logIn<U = unknown>(ctx: Context, user: U): void;
/** Clear the session and forget the current user. */
declare function logOut(ctx: Context): void;
/**
 * The currently-authenticated user, or null. Reads `ctx.user` first (set by
 * `sessionAuth`/`logIn`), falling back to the session store.
 */
declare function getUser<U = unknown>(ctx: Context): U | null;
/**
 * Hydrate `ctx.user` from the session for every request. Register this early in
 * the middleware chain so downstream pages and API routes can read `ctx.user`.
 */
declare function sessionAuth(): Middleware;
interface RequireAuthOptions {
    /** Where to redirect unauthenticated page requests. Default "/login". */
    loginPath?: string;
}
/**
 * Guard that requires an authenticated user. Unauthenticated requests that look
 * like an API/fetch call get a 401 JSON response; page navigations get a 302
 * redirect to the login page with the original target preserved as `?next=`.
 */
declare function requireAuth(options?: RequireAuthOptions): Middleware;

/**
 * Fixed-window rate limiting middleware. Keeps an in-memory counter per key
 * (client IP by default, read from `x-forwarded-for` / `x-real-ip`) and rejects
 * requests over the limit with a 429 and a `Retry-After` header. Sets the
 * `RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset` headers.
 *
 * The store is process-local; behind multiple instances use a shared store
 * (out of scope here). Suitable as-is for single-process apps and dev.
 */

interface RateLimitOptions {
    /** Window length in milliseconds. Default 60_000 (1 minute). */
    windowMs?: number;
    /** Max requests allowed per key per window. Default 60. */
    max?: number;
    /** Derive the bucket key from the request. Default: client IP. */
    key?: (ctx: Context) => string;
    /**
     * Trust `x-forwarded-for` / `x-real-ip` for the client IP. Default false —
     * those headers are attacker-spoofable, so by default we key on the direct
     * socket peer (`ctx.ip`). Enable ONLY when behind a proxy that overwrites
     * these headers (nginx, a load balancer, Cloudflare).
     */
    trustProxy?: boolean;
    /** Body returned on 429. Default "Too Many Requests". */
    message?: string;
    /** Emit RateLimit-* headers. Default true. */
    headers?: boolean;
    /** Persistence for the counters. Default: process-local memory. */
    store?: RateLimitStore;
    /** Maximum in-memory keys before oldest buckets are evicted. Ignored for custom stores. */
    maxKeys?: number;
}
interface Bucket {
    count: number;
    resetAt: number;
}
/**
 * Pluggable rate-limit counter store. The default is process-local memory; swap
 * in a shared store (Redis/SQL) so limits hold across instances. `hit` records
 * one request for `key` in the current window and returns the running bucket.
 * It may be async (e.g. a Redis INCR + PEXPIRE) — the middleware awaits it.
 */
interface RateLimitStore {
    hit(key: string, windowMs: number, now: number): Bucket | Promise<Bucket>;
}
declare function rateLimit(options?: RateLimitOptions): Middleware;
/** Non-spoofable key: the direct socket peer IP (set by the server). */
declare function peerKey(ctx: Context): string;
/** Proxy-aware key: trusts `x-forwarded-for` / `x-real-ip`, else the peer IP. */
declare function proxyKey(ctx: Context): string;
/** @deprecated Use `peerKey` (default) or `proxyKey`. Kept for compatibility. */
declare const defaultKey: typeof proxyKey;

/**
 * Structured request logging middleware. Emits one record per request with a
 * request id, method, path, status, and duration — as pretty text (dev) or JSON
 * (production/log aggregation). The request id is stored on `ctx.locals` so
 * downstream handlers can correlate their own logs.
 */

interface RequestRecord {
    time: string;
    id: string;
    method: string;
    path: string;
    status: number;
    durationMs: number;
}
interface RequestLoggerOptions {
    /** "pretty" (default) for humans, "json" for machines. */
    format?: "pretty" | "json";
    /** Where each finished record goes. Default console.log. */
    sink?: (line: string, record: RequestRecord) => void;
    /** ctx.locals key for the request id. Default "requestId". */
    requestIdKey?: string;
    /** Clock injection for tests. Default Date.now. */
    now?: () => number;
}
declare function requestLogger(options?: RequestLoggerOptions): Middleware;

/**
 * Caching primitives:
 *  - `TTLCache` — a small in-memory time-to-live cache with `getOrLoad`, for
 *    memoising expensive data (query results, computed pages).
 *  - HTTP helpers — `cacheControl` to build a directive, `withCacheControl` to
 *    apply it, and `etag` / `notModified` for conditional requests (304s).
 */
declare class TTLCache<V = unknown> {
    private readonly ttlMs;
    private store;
    private loading;
    private revisions;
    private generation;
    constructor(ttlMs?: number);
    get(key: string): V | undefined;
    set(key: string, value: V, ttlMs?: number): void;
    /** Return the cached value or compute, cache, and return it. */
    getOrLoad(key: string, loader: () => Promise<V> | V, ttlMs?: number): Promise<V>;
    delete(key: string): void;
    clear(): void;
    get size(): number;
}
interface CacheControlOptions {
    /** max-age in seconds. */
    maxAge?: number;
    /** s-maxage (shared/CDN cache) in seconds. */
    sMaxAge?: number;
    /** Mark private (per-user) rather than public. */
    private?: boolean;
    /** no-store: never cache. Overrides other directives. */
    noStore?: boolean;
    /** no-cache: revalidate before use. */
    noCache?: boolean;
    /** stale-while-revalidate window in seconds. */
    staleWhileRevalidate?: number;
    /** stale-if-error window in seconds. */
    staleIfError?: number;
    immutable?: boolean;
}
/** Build a Cache-Control header value from options. */
declare function cacheControl(options: CacheControlOptions): string;
/** Apply a Cache-Control header to a response (returns the same response). */
declare function withCacheControl(res: Response, options: CacheControlOptions): Response;
/** A stable, quoted ETag for a string/bytes body (FNV-1a, weak by default). */
declare function etag(body: string | ArrayBuffer | Uint8Array, weak?: boolean): string;
/** True when the request's If-None-Match matches the given ETag (send a 304). */
declare function notModified(req: Request, tag: string): boolean;

/**
 * File upload helpers. The legacy `saveUpload` keeps the original sanitized
 * filename for compatibility. New applications should use `saveUploadSecure`,
 * which stores a random name and supports content inspection/scanning hooks.
 */
declare class UploadError extends Error {
    readonly code: string;
    constructor(message: string, code?: string);
}
interface UploadInspectionResult {
    allowed: boolean;
    detectedType?: string;
    reason?: string;
}
type UploadInspector = (input: {
    file: File;
    bytes: Uint8Array;
    filename: string;
}) => UploadInspectionResult | Promise<UploadInspectionResult>;
type UploadScanner = (input: {
    file: File;
    bytes: Uint8Array;
    filename: string;
}) => boolean | {
    clean: boolean;
    reason?: string;
} | Promise<boolean | {
    clean: boolean;
    reason?: string;
}>;
interface SaveUploadOptions {
    /** Destination directory. Keep this outside the public web root. */
    dir: string;
    /** Reject files larger than this many bytes. */
    maxBytes?: number;
    /** Allowed MIME types (e.g. "image/png") and/or extensions (e.g. ".png"). */
    allowedTypes?: string[];
    /** Choose the stored filename. Default: the sanitised original name. */
    filename?: (file: File) => string;
    /** Content/magic-byte inspection hook. */
    inspect?: UploadInspector;
    /** Malware scanning hook. */
    scan?: UploadScanner;
    /** Called after validation but before persistence. */
    beforeSave?: (input: {
        file: File;
        bytes: Uint8Array;
        filename: string;
    }) => void | Promise<void>;
}
interface SecureUploadOptions extends Omit<SaveUploadOptions, "filename"> {
    /** Preserve the original sanitized name instead of a random server name. */
    preserveOriginalName?: boolean;
    /** Optional custom secure filename generator. */
    filename?: (file: File) => string;
    /** Preserve a conservative extension on random filenames. Defaults to true. */
    preserveExtension?: boolean;
}
interface SavedUpload {
    path: string;
    filename: string;
    size: number;
    type: string;
    detectedType?: string;
}
/** All `File` values in a parsed form, with their field names. */
declare function collectUploads(form: FormData, options?: {
    maxFiles?: number;
    maxTotalBytes?: number;
}): {
    field: string;
    file: File;
}[];
/** Validate and write one uploaded file using a compatibility filename policy. */
declare function saveUpload(file: File, options: SaveUploadOptions): Promise<SavedUpload>;
/** Store an upload under a random server-generated name by default. */
declare function saveUploadSecure(file: File, options: SecureUploadOptions): Promise<SavedUpload>;
/** Strip directory separators, traversal, and control chars from a filename. */
declare function sanitizeFilename(name: string): string;
declare function randomUploadFilename(originalName?: string, preserveExtension?: boolean): string;
declare function secureDownloadHeaders(filename: string, type?: string): Headers;

/**
 * Streaming response primitives.
 *
 * `streamResponse` turns a (sync or async) iterable of strings/bytes into a
 * streaming `Response` — the basis for streaming SSR (send the shell, then flush
 * page chunks as they render) and any progressively-generated output. `sse`
 * builds a Server-Sent Events stream from an async iterable of events.
 *
 * API routes and pages can already return a `Response` with a `ReadableStream`
 * body and the framework streams it unbuffered; these helpers just make the
 * common cases ergonomic.
 */
interface StreamResponseInit {
    status?: number;
    headers?: HeadersInit;
    /** Content-Type; default "text/html; charset=utf-8". */
    contentType?: string;
}
type Chunk = string | Uint8Array;
type ChunkSource = Iterable<Chunk> | AsyncIterable<Chunk>;
/** Build a streaming Response from an (async) iterable of chunks. */
declare function streamResponse(source: ChunkSource, init?: StreamResponseInit): Response;
interface ServerSentEvent {
    data: string;
    event?: string;
    id?: string;
    /** Client reconnection hint in milliseconds. */
    retry?: number;
}
/** Build a Server-Sent Events (text/event-stream) Response from events. */
declare function sse(source: Iterable<ServerSentEvent> | AsyncIterable<ServerSentEvent>): Response;

/**
 * Realtime rooms.
 *
 * A file in `app/realtime/` exports `default defineRoom({ onConnect, onMessage,
 * onLeave })` and is served at `ws://host/realtime/<name>`. The framework's
 * client runtime (`/__wrnexus/realtime.js`) handles the browser side, so pages
 * ship NO hand-written WebSocket code.
 *
 * Handlers get a `RoomClient` with everything you need:
 *   client.send(msg)                 → this connection
 *   client.broadcast(msg)            → everyone else in the room
 *   client.room.broadcast(msg)       → everyone (incl. sender)
 *   client.to(id | ids).send(msg)    → specific connection(s)
 *   client.toUser(u | users).send()  → a user / selected users (all their tabs)
 *   client.user = "u1"               → identify a connection for targeting
 *   client.data / client.room.state  → per-connection / shared room state
 *
 * The dynamic route `app/realtime/[room].ts` gives one handler many independent
 * rooms — `/realtime/lobby` and `/realtime/game-7` are separate room instances.
 */
interface RawSocket {
    send(data: string): unknown;
    close(code?: number, reason?: string): void;
}
interface RealtimeSocket<Data = unknown> {
    readonly data: Data;
    send(data: string | Uint8Array): number;
    subscribe(topic: string): void;
    unsubscribe(topic: string): void;
    publish(topic: string, data: string | Uint8Array): number;
    isSubscribed(topic: string): boolean;
    close(code?: number, reason?: string): void;
}
interface RealtimeHandler<Data = unknown> {
    open?(ws: RealtimeSocket<Data>): void | Promise<void>;
    message?(ws: RealtimeSocket<Data>, message: string | Uint8Array): void | Promise<void>;
    close?(ws: RealtimeSocket<Data>, code?: number, reason?: string): void | Promise<void>;
    drain?(ws: RealtimeSocket<Data>): void | Promise<void>;
}
interface Target {
    /** Send a message (objects are JSON-serialized). */
    send(message: unknown): void;
}
interface Room<TData = Record<string, unknown>> {
    readonly name: string;
    /** Shared, in-memory room state (lives while ≥1 client is connected). */
    readonly state: Record<string, unknown>;
    /** All connected clients. */
    clients(): RoomClient<TData>[];
    /** Number of connected clients. */
    count(): number;
    /** Send to everyone in the room, including the sender. */
    broadcast(message: unknown): void;
    /** Target specific connection id(s). */
    to(id: string | string[]): Target;
    /** Target a user / users by identity (reaches all their connections). */
    toUser(user: string | string[]): Target;
}
interface RoomClient<TData = Record<string, unknown>> {
    /** Unique per connection (a tab). */
    readonly id: string;
    /** App identity for targeting; assign it in `onConnect`. */
    user: string | undefined;
    /** Query params from the connection URL. */
    readonly query: Record<string, string>;
    /** Per-connection scratch state. */
    readonly data: TData;
    readonly room: Room<TData>;
    /** Send to THIS connection. */
    send(message: unknown): void;
    /** Send to everyone else in the room. */
    broadcast(message: unknown): void;
    /** Target specific connection id(s). */
    to(id: string | string[]): Target;
    /** Target a user / users by identity. */
    toUser(user: string | string[]): Target;
    /** Close this connection. */
    close(code?: number, reason?: string): void;
}
/** Info available when authorizing a connection, before it is accepted. */
interface RoomAuthInfo {
    /** Authenticated session user id, or `?user=` — undefined when anonymous. */
    user?: string;
    /** Connection URL query params. */
    query: Record<string, string>;
    /** The upgrade request's headers (cookies, etc.). */
    headers: Headers;
}
interface RealtimeSecurityOptions {
    /** Maximum inbound or outbound serialized message size. Defaults to 64 KiB. */
    maxMessageBytes?: number;
    /** Maximum messages accepted per connection per rolling second. Defaults to 30. */
    maxMessagesPerSecond?: number;
    /** Maximum live connections in one room. Defaults to 1,000. */
    maxConnectionsPerRoom?: number;
    /** Maximum connections for one authenticated user in a room. Defaults to 10. */
    maxConnectionsPerUser?: number;
    /** Reject anonymous connections before onConnect. */
    requireUser?: boolean;
    /** Maximum nested JSON depth. Defaults to 32. */
    maxJsonDepth?: number;
    /** Optional message schema/authorization predicate. */
    validateMessage?(message: unknown, client: RoomClient): boolean | Promise<boolean>;
    /** Called when a connection is rejected or closed for a policy violation. */
    onViolation?(reason: string, client?: RoomClient): void;
}
interface RoomHandlers<TData = Record<string, unknown>, TMessage = any> {
    /** Per-room abuse and payload controls. */
    security?: RealtimeSecurityOptions;
    /**
     * Gate the connection BEFORE it is accepted. Return false to reject the
     * upgrade with 403 (e.g. `authorize: (info) => !!info.user` to require auth).
     */
    authorize?(info: RoomAuthInfo): boolean | Promise<boolean>;
    /** A client connected (a new tab joined the room). */
    onConnect?(client: RoomClient<TData>): void | Promise<void>;
    /** A message arrived (JSON is parsed; non-JSON arrives as a string). */
    onMessage?(client: RoomClient<TData>, message: TMessage): void | Promise<void>;
    /** A client disconnected. */
    onLeave?(client: RoomClient<TData>): void | Promise<void>;
}
interface RoomDefinition<TData = Record<string, unknown>, TMessage = any> {
    readonly __wrnexusRoom: true;
    readonly handlers: RoomHandlers<TData, TMessage>;
}
/** Define a realtime room. Export the result as the `default` of a realtime file. */
declare function defineRoom<TData = Record<string, unknown>, TMessage = any>(handlers: RoomHandlers<TData, TMessage>): RoomDefinition<TData, TMessage>;
declare function isRoomDefinition(value: unknown): value is RoomDefinition;
interface RealtimeConnectMeta {
    room: string;
    def: RoomDefinition;
    query?: Record<string, string>;
    user?: string;
}
/** One cross-instance message: a room broadcast, or a targeted user send. */
interface RealtimeEnvelope {
    room: string;
    /** If set, deliver only to these user identities; otherwise the whole room. */
    users?: string[];
    message: unknown;
}
/**
 * A pub/sub bridge for horizontal scaling. Wire the registry to a shared bus
 * (Redis pub/sub, NATS, …): local broadcasts/`toUser` sends are published to
 * peers, and messages received from peers are delivered via `registry.deliver`.
 * Connection-targeted sends (`send`, `to(id)`) stay local (ids are per-process).
 */
interface RealtimeBridge {
    publish(envelope: RealtimeEnvelope): void;
}
interface RealtimeRegistryOptions extends RealtimeSecurityOptions {
    now?: () => number;
}
interface RealtimeRegistry {
    open(socket: RawSocket, meta: RealtimeConnectMeta): void | Promise<void>;
    message(socket: RawSocket, raw: string | Uint8Array): void | Promise<void>;
    close(socket: RawSocket): void | Promise<void>;
    /** Attach a cross-instance bridge (call once at startup). */
    setBridge(bridge: RealtimeBridge): void;
    /** Deliver an envelope received from a peer to LOCAL connections only. */
    deliver(envelope: RealtimeEnvelope): void;
    /** Number of live connections (across all rooms) — for tests/metrics. */
    size(): number;
}
/** Create the registry that maps sockets ↔ rooms and drives room handlers. */
declare function createRealtimeRegistry(options?: RealtimeRegistryOptions): RealtimeRegistry;
/**
 * A minimal pub/sub bus (structurally satisfied by `@wrnexus/pubsub`). Used to
 * bridge realtime broadcasts across processes without a hard dependency.
 */
interface RealtimeBus {
    publish(topic: string, message: unknown): void | Promise<void>;
    subscribe(topic: string, handler: (message: unknown, topic: string) => void): () => void;
}
/**
 * Bridge a realtime registry across processes/instances via a pub/sub bus (use
 * the Redis driver so it crosses machines). After this, `client.room.broadcast`
 * and `client.toUser(...)` reach connected clients on **every** app process/
 * instance subscribed to the same bus — the foundation for realtime that works
 * with multiple running apps behind the gateway. Connection-targeted sends
 * (`send`, `to(id)`) stay local. Returns an unsubscribe function.
 *
 *   import { createRealtimeRegistry, bridgeRealtime } from "@wrnexus/core";
 *   import { createPubSub } from "@wrnexus/pubsub";
 *   import { redisDriver } from "@wrnexus/pubsub/redis";
 *   bridgeRealtime(registry, createPubSub(redisDriver(process.env.REDIS_URL)));
 */
declare function bridgeRealtime(registry: RealtimeRegistry, bus: RealtimeBus, topic?: string): () => void;

/**
 * Error + status pages. Every page here is a self-contained HTML document —
 * inline CSS only, no external stylesheet, no JavaScript (so it renders under the
 * strict CSP, even when the app's assets are what failed). Theme-aware via
 * `prefers-color-scheme`, styled in the WrNexus design language (ink-navy,
 * azure, a faint blueprint grid + glow). Development shows the stack trace;
 * production never leaks internal paths.
 */
type Mode = "development" | "production";
/** A beautiful, self-contained HTML page for any 4xx/5xx status. */
declare function renderStatusPage(status: number): Response;
/** Readable, styled development error page — includes the stack trace. */
declare function renderDevError(err: unknown, status?: number): Response;
/** Generic production error page — no stack, no file paths. */
declare function renderProdError(status?: number): Response;
/** Pick the right error page for the current mode. */
declare function renderError(err: unknown, mode: Mode): Response;
/** Beautiful 404 page. */
declare function renderNotFound(): Response;

type CorsOrigin = "*" | string | string[];
interface CorsConfig {
    /** Enable CORS headers and preflight handling. Defaults to false. */
    enabled?: boolean;
    /** Allowed origins. Use "*" for public APIs. Defaults to "*". */
    origin?: CorsOrigin;
    /** Allowed methods for preflight responses. */
    methods?: string[];
    /** Allowed request headers. Defaults to the browser's requested headers. */
    allowedHeaders?: string[];
    /** Response headers exposed to browser JavaScript. */
    exposedHeaders?: string[];
    /** Whether to send Access-Control-Allow-Credentials. */
    credentials?: boolean;
    /** Access-Control-Max-Age, in seconds. */
    maxAge?: number;
}
type CspDirectiveValue = string | string[] | false | null | undefined;
interface ContentSecurityPolicyConfig {
    /** Defaults to true. */
    enabled?: boolean;
    /** Use Content-Security-Policy-Report-Only instead of enforcing. */
    reportOnly?: boolean;
    /** Merge or remove directives. Set a directive to false/null to remove it. */
    directives?: Record<string, CspDirectiveValue>;
    /** Set false to start from an empty policy instead of WrNexus defaults. */
    useDefaults?: boolean;
}
interface HstsConfig {
    /** Defaults to true in production, false in development. */
    enabled?: boolean;
    /** Defaults to 31536000 seconds (1 year). */
    maxAge?: number;
    /** Defaults to true. */
    includeSubDomains?: boolean;
    /** Defaults to true. */
    preload?: boolean;
}
interface TrustedTypesConfig {
    /** Defaults to true in production, false in development. */
    enabled?: boolean;
    /**
     * Defaults to ["*"] in production so browser extensions and dev tooling can
     * create their own policies without noisy console errors. Set this to a
     * concrete list, e.g. ["wrnexus", "default"], for stricter deployments.
     */
    policyNames?: string[];
    /** Defaults to true. */
    requireForScript?: boolean;
    /** Adds "allow-duplicates" to the trusted-types directive. */
    allowDuplicates?: boolean;
}
type PermissionsPolicyConfig = Record<string, string | string[] | false | null | undefined>;
interface RequestLimitsConfig {
    maxUrlLength?: number;
    maxHeaderCount?: number;
    maxHeaderBytes?: number;
    maxQueryParameters?: number;
    maxBodyBytes?: number;
    timeoutMs?: number;
    maxConcurrent?: number;
    trustedHosts?: string[];
    fetchMetadata?: boolean;
}
interface SecurityConfig {
    /** Set false to skip all framework security headers except explicitly enabled CORS. */
    headers?: boolean;
    /** Built-in request size, timeout, concurrency, host, and Fetch Metadata limits. */
    requestLimits?: RequestLimitsConfig;
    /**
     * Trust `X-Forwarded-Proto` / `X-Forwarded-Host` when building `ctx.url` — set
     * this when the app runs behind a TLS-terminating reverse proxy (nginx, the
     * WrNexus gateway, a load balancer). Without it, a proxied app sees the internal
     * `http://` request and marks cookies (e.g. CSRF/session) non-`Secure`. Default
     * false; enable ONLY when a trusted proxy actually sets these headers.
     */
    trustProxy?: boolean;
    cors?: boolean | CorsConfig;
    contentSecurityPolicy?: false | ContentSecurityPolicyConfig;
    hsts?: false | HstsConfig;
    trustedTypes?: false | TrustedTypesConfig;
    /** Defaults to "same-origin". */
    crossOriginOpenerPolicy?: false | "same-origin" | "same-origin-allow-popups" | "unsafe-none";
    /** Defaults to "DENY". */
    frameOptions?: false | "DENY" | "SAMEORIGIN";
    /** Defaults to "strict-origin-when-cross-origin". */
    referrerPolicy?: false | string;
    /** Defaults to "same-origin". */
    crossOriginResourcePolicy?: false | "same-origin" | "same-site" | "cross-origin";
    /** Isolate the origin in its own agent cluster. Defaults to true. */
    originAgentCluster?: boolean;
    /** Disable speculative DNS prefetching. Defaults to true. */
    disableDnsPrefetch?: boolean;
    /** Defaults to a restrictive browser capability policy. */
    permissionsPolicy?: false | PermissionsPolicyConfig;
    /** Extra static headers applied last. */
    extraHeaders?: Record<string, string>;
}
/**
 * Guard a WebSocket upgrade against Cross-Site WebSocket Hijacking: browsers
 * always send an `Origin` header on a WS handshake, and — unlike fetch — WS is
 * NOT subject to CORS, so cookies would otherwise flow cross-site. We allow
 * same-origin (Origin host === Host header), configured CORS origins, and
 * non-browser clients (no Origin, which also carry no ambient cookies).
 */
declare function isWebSocketOriginAllowed(req: Request, security?: SecurityConfig): boolean;
declare function createCorsPreflightResponse(req: Request, security?: SecurityConfig): Response | null;
/**
 * Build the request URL, honoring `X-Forwarded-Proto` / `X-Forwarded-Host` when
 * `trustProxy` is set (app behind a TLS-terminating reverse proxy). This makes
 * `ctx.url.protocol` reflect the EXTERNAL scheme, so protocol-dependent logic —
 * `Secure` cookies, canonical URLs — is correct behind nginx / the gateway.
 * Security checks that compare the raw `Host`/`Origin` headers don't use this URL,
 * so they are unaffected. An invalid forwarded value is ignored by the URL setter.
 */
declare function resolveRequestUrl(req: Request, trustProxy?: boolean): URL;
declare function withSecurityHeaders(req: Request, res: Response, mode: Mode, security?: SecurityConfig, nonce?: string): Response;

interface SchemaLike<T> {
    parse(input: unknown): T;
}
interface OutputSchemaLike<T> {
    readonly __output: T;
    parse(input: unknown): unknown;
}
type InferEndpointSchema<TSchema> = TSchema extends OutputSchemaLike<infer TValue> ? TValue : never;
interface EndpointErrorBody {
    code: string;
    message: string;
    details?: unknown;
}
declare class EndpointError extends Error {
    readonly status: number;
    readonly code: string;
    readonly details?: unknown | undefined;
    constructor(status: number, code: string, message: string, details?: unknown | undefined);
}
interface EndpointDefinition<I, O> {
    input?: SchemaLike<I> | OutputSchemaLike<I>;
    output?: SchemaLike<O> | OutputSchemaLike<O>;
    auth?: "optional" | "required";
    description?: string;
    tags?: string[];
    handler(input: I, ctx: Context): O | Promise<O>;
}
interface DefinedEndpoint<I, O> {
    readonly definition: EndpointDefinition<I, O>;
    (ctx: Context, input?: unknown): Promise<Response>;
}
/** Define a validated, typed endpoint that can also drive SDK/OpenAPI generation. */
declare function defineEndpoint<InputSchema extends OutputSchemaLike<unknown>, OutputSchema extends OutputSchemaLike<unknown>>(definition: Omit<EndpointDefinition<InferEndpointSchema<InputSchema>, InferEndpointSchema<OutputSchema>>, "input" | "output"> & {
    input: InputSchema;
    output: OutputSchema;
}): DefinedEndpoint<InferEndpointSchema<InputSchema>, InferEndpointSchema<OutputSchema>>;
declare function defineEndpoint<I = unknown, O = unknown>(definition: EndpointDefinition<I, O>): DefinedEndpoint<I, O>;
interface RpcClientOptions {
    baseUrl?: string;
    fetch?: typeof globalThis.fetch;
    headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
}
/** Create a tiny typed RPC caller for endpoints exposed by a WrNexus app. */
declare function createRpcClient(options?: RpcClientOptions): <I, O>(path: string, input: I) => Promise<O>;

interface CachePolicy {
    ttlMs?: number;
    staleWhileRevalidateMs?: number;
    tags?: string[] | ((ctx: Context) => string[]);
}
interface LoaderDefinition<T> {
    cache?: CachePolicy;
    load(ctx: Context): T | Promise<T>;
}
interface ActionDefinition<I, O> {
    csrf?: boolean;
    run(input: I, ctx: Context): O | Promise<O>;
    invalidate?: string[] | ((output: O, ctx: Context) => string[]);
}
interface DefinedLoader<T> {
    readonly definition: LoaderDefinition<T>;
    (ctx: Context): Promise<T>;
}
interface DefinedAction<I, O> {
    readonly definition: ActionDefinition<I, O>;
    (input: I, ctx: Context): Promise<O>;
}
declare function defineLoader<T>(definition: LoaderDefinition<T>): DefinedLoader<T>;
declare function defineAction<I, O>(definition: ActionDefinition<I, O>): DefinedAction<I, O>;
/** Request-local fetch deduplication keyed by a stable string. */
declare function dedupe<T>(ctx: Context, key: string, load: () => T | Promise<T>): Promise<T>;

type FeatureValue = boolean | string | number;
type FeatureRule = FeatureValue | ((ctx: Context) => FeatureValue | Promise<FeatureValue>);
interface FeatureFlags {
    get(name: string, ctx: Context): Promise<FeatureValue | undefined>;
    enabled(name: string, ctx: Context): Promise<boolean>;
}
declare function defineFeatureFlags(rules: Record<string, FeatureRule>): FeatureFlags;

interface PerformanceBudgets {
    routeJsBytes?: number;
    routeCssBytes?: number;
    htmlBytes?: number;
    imageBytes?: number;
    hydrationMs?: number;
    serverRenderMs?: number;
    /** Largest Contentful Paint in milliseconds. Recommended <= 2500. */
    lcpMs?: number;
    /** Interaction to Next Paint in milliseconds. Recommended <= 200. */
    inpMs?: number;
    /** Cumulative Layout Shift score. Recommended <= 0.1. */
    cls?: number;
    /** Time to First Byte in milliseconds. */
    ttfbMs?: number;
    /** Longest main-thread task in milliseconds. Recommended <= 50. */
    longTaskMs?: number;
    /** Number of client hydration boundaries on the route. */
    hydratedComponents?: number;
    /** Total request count for the initial navigation. */
    requests?: number;
}
interface PerformanceMeasurement {
    routeJsBytes?: number;
    routeCssBytes?: number;
    htmlBytes?: number;
    imageBytes?: number;
    hydrationMs?: number;
    serverRenderMs?: number;
    lcpMs?: number;
    inpMs?: number;
    cls?: number;
    ttfbMs?: number;
    longTaskMs?: number;
    hydratedComponents?: number;
    requests?: number;
}
interface BudgetViolation {
    metric: keyof PerformanceBudgets;
    budget: number;
    actual: number;
    overBy: number;
}
declare const recommendedWebBudgets: Readonly<PerformanceBudgets>;
declare function checkPerformanceBudgets(budgets: PerformanceBudgets, measurement: PerformanceMeasurement): BudgetViolation[];

type Duration = number | `${number}${"ms" | "s" | "m" | "h"}`;
type BackoffStrategy = "fixed" | "exponential" | ((attempt: number) => Duration);
interface CircuitBreakerOptions {
    failures: number;
    resetAfter: Duration;
    successesToClose?: number;
}
interface CircuitBreakerSnapshot {
    state: "closed" | "open" | "half-open";
    failures: number;
    successes: number;
    retryAfterMs: number;
}
declare class ResilienceError extends Error {
    readonly code: "WRN-RESILIENCE-TIMEOUT" | "WRN-RESILIENCE-ABORTED" | "WRN-RESILIENCE-CIRCUIT-OPEN" | "WRN-RESILIENCE-BULKHEAD-FULL";
    constructor(code: "WRN-RESILIENCE-TIMEOUT" | "WRN-RESILIENCE-ABORTED" | "WRN-RESILIENCE-CIRCUIT-OPEN" | "WRN-RESILIENCE-BULKHEAD-FULL", message: string, options?: ErrorOptions);
}
declare function durationMs(value: Duration): number;
declare class CircuitBreaker {
    private readonly options;
    private failures;
    private successes;
    private openedAt;
    private probing;
    constructor(options: CircuitBreakerOptions);
    snapshot(now?: number): CircuitBreakerSnapshot;
    execute<T>(operation: () => Promise<T>): Promise<T>;
}
interface BulkheadOptions {
    concurrency: number;
    queue?: number;
}
declare class Bulkhead {
    private readonly options;
    private active;
    private readonly waiting;
    constructor(options: BulkheadOptions);
    get snapshot(): Readonly<{
        active: number;
        queued: number;
        capacity: number;
    }>;
    execute<T>(operation: () => Promise<T>): Promise<T>;
}
interface ResilientCallOptions<T> {
    run: (signal: AbortSignal, attempt: number) => Promise<T>;
    timeout?: Duration;
    retries?: number;
    retryDelay?: Duration;
    backoff?: BackoffStrategy;
    circuitBreaker?: CircuitBreaker | CircuitBreakerOptions;
    bulkhead?: Bulkhead | BulkheadOptions;
    signal?: AbortSignal;
    retryWhen?: (error: unknown, attempt: number) => boolean | Promise<boolean>;
    fallback?: (error: unknown, signal: AbortSignal) => T | Promise<T>;
    onRetry?: (error: unknown, attempt: number, delayMs: number) => void;
}
declare function resilientCall<T>(options: ResilientCallOptions<T>): Promise<T>;

interface ProblemDetails {
    type: string;
    title: string;
    status: number;
    detail?: string;
    instance?: string;
    code?: string;
    [key: string]: unknown;
}
interface ProblemDetailsInput {
    type?: string;
    title: string;
    status: number;
    detail?: string;
    instance?: string;
    code?: string;
    [key: string]: unknown;
}
declare function problem(details: ProblemDetailsInput, headers?: HeadersInit): Response;
type ServiceToken<T> = string | symbol | {
    readonly key: symbol;
    readonly __type?: T;
};
declare function serviceToken<T>(description: string): ServiceToken<T>;
declare class ServiceContainer {
    #private;
    private readonly parent?;
    constructor(parent?: ServiceContainer | undefined);
    set<T>(token: ServiceToken<T>, value: T): this;
    has<T>(token: ServiceToken<T>): boolean;
    get<T>(token: ServiceToken<T>): T;
    tryGet<T>(token: ServiceToken<T>): T | undefined;
    scope(): ServiceContainer;
}
type LifecyclePhase = "starting" | "started" | "stopping" | "stopped";
type LifecycleHandler = (signal: AbortSignal) => void | Promise<void>;
declare class ApplicationLifecycle {
    #private;
    on(phase: LifecyclePhase, handler: LifecycleHandler): () => void;
    run(phase: LifecyclePhase): Promise<void>;
    get signal(): AbortSignal;
}
interface HealthCheckResult {
    status: "up" | "down" | "degraded";
    message?: string;
    details?: unknown;
    durationMs?: number;
}
type HealthCheck = () => HealthCheckResult | Promise<HealthCheckResult>;
declare class HealthRegistry {
    #private;
    register(name: string, check: HealthCheck): () => void;
    check(): Promise<{
        status: "up" | "down" | "degraded";
        checks: Record<string, HealthCheckResult>;
    }>;
}
declare function requestId(headers: Headers, preferred?: string): string;
interface IdempotencyRecord<T = unknown> {
    key: string;
    value: T;
    expiresAt: number;
}
interface IdempotencyStore<T = unknown> {
    get(key: string): Promise<IdempotencyRecord<T> | null>;
    set(record: IdempotencyRecord<T>): Promise<void>;
    delete(key: string): Promise<void>;
}
declare function memoryIdempotencyStore<T = unknown>(now?: () => number): IdempotencyStore<T>;
declare function withIdempotency<T>(store: IdempotencyStore<T>, key: string, execute: () => Promise<T>, ttlMs?: number): Promise<{
    value: T;
    replayed: boolean;
}>;

export { type ActionDefinition, ApplicationLifecycle, type AsyncSessionBackend, type BackoffStrategy, type Bucket, type BudgetViolation, Bulkhead, type BulkheadOptions, CSRF_COOKIE, CSRF_HEADER, type CacheControlOptions, type CachePolicy, CircuitBreaker, type CircuitBreakerOptions, type CircuitBreakerSnapshot, type ContentSecurityPolicyConfig, type Context, type CookieOptions, type CookieStore, type CorsConfig, type CorsOrigin, type CspDirectiveValue, type CsrfProtectionOptions, type DefinedAction, type DefinedEndpoint, type DefinedLoader, type Duration, type EndpointDefinition, EndpointError, type EndpointErrorBody, type ExecutionContext, type ExecutionContextInput, type ExecutionKind, type FeatureFlags, type FeatureRule, type FeatureValue, type HealthCheck, type HealthCheckResult, HealthRegistry, type HstsConfig, type IdempotencyRecord, type IdempotencyStore, type InferEndpointSchema, type LifecycleHandler, type LifecyclePhase, type LoaderDefinition, type LocalStorageSnapshot, type Middleware, type Mode, type Next, type OutputSchemaLike, POSTGRES_TENANT_DIRECTORY_SCHEMA, type PageComponent, type PageMeta, type PerformanceBudgets, type PerformanceMeasurement, type PermissionsPolicyConfig, type ProblemDetails, type ProblemDetailsInput, type RateLimitOptions, type RateLimitStore, type RawSocket, type RealtimeBridge, type RealtimeBus, type RealtimeConnectMeta, type RealtimeEnvelope, type RealtimeHandler, type RealtimeRegistry, type RealtimeRegistryOptions, type RealtimeSecurityOptions, type RealtimeSocket, type RequestLimitsConfig, type RequestLoggerOptions, type RequestRecord, type RequireAuthOptions, ResilienceError, type ResilientCallOptions, type ResponseContext, type Room, type RoomAuthInfo, type RoomClient, type RoomDefinition, type RoomHandlers, type RpcClientOptions, SESSION_USER_KEY, type SaveUploadOptions, type SavedUpload, type SchemaLike, type SecureUploadOptions, type SecurityConfig, type SeoConfig, type ServerSentEvent, ServiceContainer, type ServiceToken, type SessionBackend, type SessionEntry, type SessionPolicy, type SessionStore, type Span, type SpanRecord, type StreamResponseInit, type TFunction, TTLCache, type Target, type Tenant, type TenantAuditEvent, type TenantDirectoryStore, type TenantMembership, type TenantMiddlewareOptions, type TenantQuota, type TenantResolver, type TenantResource, type TenantSqlClient, type Tracer, type TrustedTypesConfig, UploadError, type UploadInspectionResult, type UploadInspector, type UploadScanner, assertTenantAccess, bridgeRealtime, cacheControl, checkPerformanceBudgets, collectUploads, composeTenantResolvers, createContext, createCorsPreflightResponse, createExecutionContext, createPersistentTenantDirectory, createRealtimeRegistry, createRpcClient, createTenantDirectory, createTracer, csrfProtection, csrfToken, dedupe, defaultKey, defineAction, defineEndpoint, defineFeatureFlags, defineLoader, defineRoom, durationMs, escapeHtml, etag, executionContextFromHttp, getUser, hashPassword, isRoomDefinition, isSafeIslandName, isSafeRequestPath, isWebSocketOriginAllowed, loadSession, logIn, logOut, memoryIdempotencyStore, memoryTenantDirectoryStore, migrateTenants, notModified, peerKey, postgresTenantDirectoryStore, problem, proxyKey, randomUploadFilename, rateLimit, recommendedWebBudgets, renderDevError, renderError, renderNotFound, renderProdError, renderStatusPage, requestId, requestLogger, requireAuth, requireTenant, resilientCall, resolveRequestUrl, sanitizeFilename, saveUpload, saveUploadSecure, secureDownloadHeaders, serviceToken, sessionAuth, setSessionBackend, setSessionPolicy, sse, streamResponse, tenantFromDomain, tenantFromHeader, tenantFromPath, tenantFromSession, tenantFromSubdomain, tenantKey, tenantMiddleware, tenantScope, tracingMiddleware, verifyCsrf, verifyPassword, withCacheControl, withContextHeaders, withIdempotency, withSecurityHeaders, withSpan };
```

---

## @wrnexus/csr

Documentation URL: https://wrnexusjs.dev/packages/csr

# @wrnexus/csr

## Navigation state preservation

Pages can opt into restoration across client navigation:

```wrn
page Users {
  navigation {
    preserve = ["filters", "pagination", "scroll", "tabs", "expanded"]
  }
}
```

Form-like categories restore named inputs, selects, and textareas. Password,
file, hidden, CSRF/token/secret/credential fields, and elements marked
`data-no-preserve` are never saved. For tab, expanded, or component UI state,
mark stable elements with `data-wrn-preserve="key"`; their value and ARIA
selected/expanded state are restored. State is scoped to pathname plus query.

## Typed server actions

`createActionClient<Input, Output>(route, name)` supports programmatic calls.
Schema-backed WRN actions also export `__wrnexusActionClients`, whose input and
output are inferred automatically. Enhanced forms expose
`data-wrn-action-state="pending|success|error"` and dispatch bubbling
`wrnexus:action:optimistic`, `:pending`, `:success`, and `:error` events.
Success details contain returned data and invalidated cache tags; error details
contain field errors. Without JavaScript, the same form posts to its page and
receives a 303 redirect or accessible validation response.

> The browser-side client runtime for WrNexus — generic, self-contained JS that hydrates server-rendered pages with reactivity, client-side navigation, and realtime rooms.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

`@wrnexus/csr` holds the three client runtimes that WrNexus serves to the browser. Components are authored as `.wrn` files and rendered on the **server**; this package provides the single, generic runtime that **hydrates** that HTML in the browser — there are no per-component browser bundles. Each runtime is exported as a plain-JS string (no build step, no imports) intended to be served verbatim from a well-known URL:

- **reactive** at `/__wrnexus/reactive.js` — reactive directives (`data-scope`, `data-text`, `data-for`, …)
- **nav** at `/__wrnexus/nav.js` — SPA-style client navigation with graceful fallback
- **realtime** at `/__wrnexus/realtime.js` — WebSocket "rooms", declarative or programmatic

The package itself runs on the server (it just returns strings); the strings it returns run in the browser. A dev/prod server (see `@wrnexus/core`) is responsible for actually serving them.

## Installation

```bash
bun add @wrnexus/csr
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

## API

All exports come from the package root (`@wrnexus/csr`). The runtime source is delivered as strings, so the "API" on the server side is small; the real surface is the browser directives/globals each string installs.

### Runtime strings

| Export             | Type     | Served at                | Contents                       |
| ------------------ | -------- | ------------------------ | ------------------------------ |
| `REACTIVE_RUNTIME` | `string` | `/__wrnexus/reactive.js` | Reactive directive runtime     |
| `NAV_RUNTIME`      | `string` | `/__wrnexus/nav.js`      | Client-side navigation runtime |
| `REALTIME_RUNTIME` | `string` | `/__wrnexus/realtime.js` | Realtime rooms runtime         |

### Accessor functions

Convenience getters that return the same strings.

```ts
getReactiveRuntime(): string   // → REACTIVE_RUNTIME
getNavRuntime(): string        // → NAV_RUNTIME
getRealtimeRuntime(): string   // → REALTIME_RUNTIME
```

### Browser: reactive directives

Applied to any subtree containing `data-scope`. Expressions are parsed by a tiny eval-free evaluator, so a strict CSP with no `unsafe-eval` works.

| Directive                          | Purpose                                              |
| ---------------------------------- | ---------------------------------------------------- |
| `data-scope="count: 0, name: 'x'"` | Declare reactive state on a subtree                  |
| `data-on-<event>="count++"`        | Run a statement in scope on a DOM event              |
| `data-text="expr"`                 | Bind an element's `textContent` to an expression     |
| `data-show="expr"`                 | Toggle visibility while preserving interactive state |

Compiled conditional rendering and dynamic component cases omit inactive elements from the live
DOM. `data-show` is a visibility directive for stateful controls and keeps its element mounted.
Neither mechanism is authorization: never place secrets in client-rendered branches. Authorize on
the server and return only data the current request may access.
| `data-for="item in list"` (opt. index and `key item.id`) | Per-item rendering; stable keys preserve DOM identity during reorder |
| `data-key="item.id"` | Alternative key declaration for `data-for` templates |
| `{{expr}}` or `{expr}` | Interpolation inside text nodes and attribute values |
| `data-wrnexus-csr="id"` | Target for a generated CSR fetch binding (fetches `/__wrnexus/csr?...`) |

Supported expression features: literals, identifiers, member access (`a.b`, `a[b]`), function/method calls, arrays, objects, arithmetic, comparison, equality, logical (`&& ||`), unary (`! - +`), and ternary. Statements support `++`/`--`, assignment operators (`= += -= *= /= %=`), and bare expression/method calls. Rendering is dependency-tracked: a signal change only re-runs the renderers that actually read it.

Browser globals installed: `window.__wrnexusHydrateScopes(root)` and `window.__wrnexusHydrateCsrFetches(root)` — both idempotent, so re-running after a DOM swap or HMR morph is safe. Both run automatically on `DOMContentLoaded`.

### Browser: navigation

Intercepts same-origin `<a>` clicks, fetches the target page, and swaps the `#app` container in place (via `importNode` — not `innerHTML` — so it works under a Trusted-Types CSP), updating history, title, and scroll, then re-hydrates. Cross-origin links, modified clicks, `download`/`data-no-nav`/`rel="external"`/`target` links, non-HTML responses, or a missing `#app` fall back to a full browser navigation.

- Programmatic navigation: `window.__wrnexusNavigate(url)`
- Emits a `wrnexus:navigated` `CustomEvent` (`detail.url`) after each swap
- Sends `x-wrnexus-nav: 1` on fetches so the server can return the page fragment
- Appends any `/__wrnexus/*` runtime scripts the incoming page needs but the current document lacks

### Browser: realtime rooms

Connects to `/realtime/<name>` over WebSocket (`ws`/`wss` chosen from `location.protocol`). Two usage modes.

Programmatic API via `window.wire`:

```ts
wire.room(name): Room          // open (or reuse) a room connection
wire.bindRooms(root?)          // (re)bind declarative [data-room] containers

interface Room {
  name: string;
  send(obj: object | string): Room;         // JSON-stringifies objects; queues until open
  on(type: string, cb): Room;               // filter by msg.type; "*" or a fn = all messages
  on(cb): Room;
  close(): Room;
}
```

Internal lifecycle messages are emitted to listeners as `{ type }`: `__open`, `__close`, `__error`, and `__raw` (non-JSON frames, with `data`). Reconnect uses exponential backoff capped at 5s; queued sends flush on reconnect.

Declarative binding (zero JS) on a `data-room="<name>"` container:

| Attribute                            | On              | Purpose                                                                  |
| ------------------------------------ | --------------- | ------------------------------------------------------------------------ |
| `data-room="<name>"`                 | container       | Connect to room `<name>`                                                 |
| `data-room-user="<id>"`              | container       | Identify the connection (`?user=<id>`)                                   |
| `data-room-log`                      | element         | Where incoming messages are appended                                     |
| `<template data-room-item="<type>">` | template        | Row template for messages of that `type` (empty = fallback)              |
| `%field%`                            | inside template | Placeholder filled from the message field (text/attr only, HTML-escaped) |
| `data-room-status`                   | element         | Reflects connection state text (`connected`/`disconnected`/`error`)      |
| `data-room-status-class`             | status element  | Base class; a state variant (`is-connected`, …) is appended              |
| `<form data-room-send>`              | form            | Submits named fields as a JSON message                                   |
| `data-room-reset`                    | form field      | Clears that field after send                                             |

Rebinds on `wrnexus:navigated` and closes rooms whose container has left the page.

## Usage

Server side — serve the runtime strings from your router (example with `Bun.serve`):

```ts
import { getReactiveRuntime, getNavRuntime, getRealtimeRuntime } from "@wrnexus/csr";

const routes: Record<string, string> = {
  "/__wrnexus/reactive.js": getReactiveRuntime(),
  "/__wrnexus/nav.js": getNavRuntime(),
  "/__wrnexus/realtime.js": getRealtimeRuntime(),
};

Bun.serve({
  fetch(req) {
    const body = routes[new URL(req.url).pathname];
    if (body) {
      return new Response(body, {
        headers: { "content-type": "text/javascript; charset=utf-8" },
      });
    }
    return new Response("Not found", { status: 404 });
  },
});
```

Browser side — server-rendered HTML that the reactive runtime hydrates:

```html
<div data-scope="count: 0, showPassword: false">
  <button data-on-click="count++">+1</button>
  <span data-text="count"></span>
  <p>Total: {{count}}</p>
  <input type="{showPassword ? 'text' : 'password'}" />
  <button
    data-on-click="showPassword = !showPassword"
    aria-label="{showPassword ? 'Hide password' : 'Show password'}"
  >
    Toggle password
  </button>
</div>
<script src="/__wrnexus/reactive.js"></script>
```

State interpolation in ordinary attributes is reactive. The compiler keeps the
initial SSR value and emits an internal binding so attributes such as `type`,
`aria-label`, `aria-pressed`, `class`, and `href` update after state changes.

A realtime chat, fully declarative:

```html
<div data-room="lobby" data-room-user="ada">
  <div data-room-status></div>
  <ul data-room-log></ul>
  <template data-room-item="chat"><li>%user%: %text%</li></template>
  <form data-room-send>
    <input name="text" data-room-reset />
    <input type="hidden" name="type" value="chat" />
    <button>Send</button>
  </form>
</div>
<script src="/__wrnexus/realtime.js"></script>
```

Or drive a room from code:

```ts
const room = wire.room("lobby");
room.on("chat", (msg) => console.log(msg.user, msg.text));
room.send({ type: "chat", user: "ada", text: "hi" });
```

## Requirements / Notes

- **Bun-only** on the server (the package integrates with Bun-based WrNexus servers); the emitted strings are plain browser JS with no dependencies.
- Browser runtimes are **self-contained** (no imports, no build step) and **idempotent**, so re-hydration after navigation or HMR is safe.
- Designed for a **strict CSP**: the reactive expression evaluator avoids `eval`/`new Function` (no `unsafe-eval`), and DOM swaps use `importNode`/attribute writes rather than `innerHTML` (Trusted-Types friendly).
- Peer packages: rendered `.wrn` components and the serving layer come from `@wrnexus/core` (the sole dependency); pages are rendered by the WrNexus dev/prod server.

### Exported TypeScript declarations

```ts
/**
 * Browser reactive runtime (Point 2: reactive directives).
 *
 * Served verbatim at `/__wrnexus/reactive.js` for any page that contains a
 * `data-scope`. It is plain browser JS (no build step) and self-contained: it
 * inlines a tiny `signal()` so it has no imports to resolve.
 *
 * Supported directives (this is exactly what the `.wrn` compiler emits):
 *   data-scope="count: 0, name: 'x'"   declare reactive state on a subtree
 *   data-on-<event>="count++"          run a statement in scope on an event
 *   data-text="expr"                   element textContent follows an expression
 *   data-wrn-loop-locals="base64-json"   preserves SSR {#each} item/index values
 *   data-wrnexus-csr="id"                 target for generated CSR fetch bindings
 *   {{expr}} or {expr}                 interpolation inside text nodes
 *
 * Expressions are evaluated by a tiny parser instead of `eval`/`new Function`,
 * so production can use a strong CSP without `unsafe-eval`.
 */
declare const REACTIVE_RUNTIME: string;

/**
 * Client-side navigation runtime, served at `/__wrnexus/nav.js`.
 *
 * Progressive enhancement over normal links: intercepts same-origin `<a>`
 * clicks, fetches the target page's HTML, swaps the `#app` container in place,
 * updates history/title/scroll, ensures any framework runtimes the new page
 * needs are present, and re-hydrates.
 *
 * Before replacing the current page, component lifecycle behaviors are
 * explicitly disposed. This ensures `unmount` hooks and watcher cleanups run
 * before the old DOM is removed.
 *
 * Programmatic navigation is exposed as:
 *
 *   window.__wrnexusNavigate(url)
 */
declare const NAV_RUNTIME: string;

/**
 * Client realtime runtime, served at `/__wrnexus/realtime.js`.
 *
 * Two ways to use it — no hand-written WebSocket code either way:
 *
 * 1. Declarative (zero JS). Put `data-room="<name>"` on a container; the runtime
 *    connects, appends incoming messages to `[data-room-log]` using a
 *    `<template data-room-item="<type>">` (fields via `%field%`, HTML-escaped),
 *    reflects connection state on `[data-room-status]`, and sends a
 *    `<form data-room-send>`'s named fields as JSON on submit (fields marked
 *    `data-room-reset` clear after send). Optional `data-room-user` identifies
 *    the connection.
 *
 * 2. Programmatic: `const room = wire.room("chat"); room.on("chat", fn);
 *    room.send({ type: "chat", text })`. Handles connect, JSON, reconnect.
 *
 * Rebinds on `wrnexus:navigated` (client-side nav) and closes rooms whose
 * container has left the page.
 */
declare const REALTIME_RUNTIME: string;

declare const ACTION_RUNTIME: string;

type OutputHandler<T = unknown> = (payload: T) => void | Promise<void>;
interface OutputHost extends HTMLElement {
    __wrnexusOutputHandlers?: Map<string, Set<OutputHandler>>;
}
declare function registerOutputHandler<T>(host: OutputHost, name: string, handler: OutputHandler<T>): () => void;
declare function invokeOutput<T>(host: OutputHost, name: string, payload?: T): Promise<void>;
declare function createOutputProxy<T extends Record<string, (...args: any[]) => void>>(host: OutputHost): T;

interface ServerCallOptions {
    endpoint?: string;
    signal?: AbortSignal;
    headers?: HeadersInit;
    csrfToken?: string;
}
declare class WrnServerCallError extends Error {
    readonly code: string;
    readonly status: number;
    readonly details?: unknown | undefined;
    constructor(message: string, code: string, status: number, details?: unknown | undefined);
}
declare function callServerFunction<TInput extends unknown[], TOutput>(component: string, functionName: string, args: TInput, options?: ServerCallOptions): Promise<TOutput>;
declare function createServerProxy<T extends Record<string, (...args: any[]) => Promise<any>>>(component: string, options?: ServerCallOptions): T;

declare function collectRefs(root: ParentNode): Record<string, Element>;

interface ClientModuleScope {
    output: Record<string, (payload?: unknown) => void>;
    server: Record<string, (...args: unknown[]) => Promise<unknown>>;
    props: Readonly<Record<string, unknown>>;
    refs: Record<string, Element>;
}
declare function loadClientFunctions(url: string, scope: ClientModuleScope): Promise<Record<string, (...args: unknown[]) => unknown>>;
declare function invalidateClientModule(url: string): void;

interface ActionClientOptions<I> {
    signal?: AbortSignal;
    csrfToken?: string;
    headers?: HeadersInit;
    serialize?: (input: I) => BodyInit;
}
interface ActionResult<O> {
    data: O;
    invalidated: string[];
}
declare class ActionClientError extends Error {
    readonly status: number;
    readonly errors?: Record<string, string> | undefined;
    constructor(status: number, errors?: Record<string, string> | undefined);
}
declare function createActionClient<I, O>(route: string, name: string): (input: I, options?: ActionClientOptions<I>) => Promise<ActionResult<O>>;

interface HydrationScopeApi {
    get(name: string): unknown;
    set(name: string, value: unknown): void;
    call(name: string, ...args: unknown[]): unknown;
    snapshot(): Readonly<Record<string, unknown>>;
    dispose(): void;
}
interface WrnexusBrowserGlobals {
    __wrnexusHydrateScopes?(root?: ParentNode): void;
    __wrnexusDisposeBehaviors?(root?: ParentNode): void;
}

/**
 * @wrnexus/csr — the browser reactive runtime.
 *
 * Components are `.wrn` files rendered on the SERVER (see @wrnexus/dev-server)
 * and hydrated in the browser by this single, generic runtime — served once at
 * `/__wrnexus/reactive.js` for any page that contains a `data-scope`. There are
 * no per-component browser bundles: SSR stays cleanly separated from CSR.
 */

/** The reactive runtime served at `/__wrnexus/reactive.js` (plain browser JS). */
declare function getReactiveRuntime(development?: boolean): string;
/** Component-specific controllers, loaded only when their marker is present. */
declare function getComponentControllerRuntime(development?: boolean): string;
/** The client-side navigation runtime served at `/__wrnexus/nav.js`. */
declare function getNavRuntime(): string;
/** The realtime client runtime served at `/__wrnexus/realtime.js`. */
declare function getRealtimeRuntime(): string;
declare function getActionRuntime(): string;

export { ACTION_RUNTIME, ActionClientError, type ActionClientOptions, type ActionResult, type ClientModuleScope, type HydrationScopeApi, NAV_RUNTIME, type OutputHandler, type OutputHost, REACTIVE_RUNTIME, REALTIME_RUNTIME, type ServerCallOptions, WrnServerCallError, type WrnexusBrowserGlobals, callServerFunction, collectRefs, createActionClient, createOutputProxy, createServerProxy, getActionRuntime, getComponentControllerRuntime, getNavRuntime, getReactiveRuntime, getRealtimeRuntime, invalidateClientModule, invokeOutput, loadClientFunctions, registerOutputHandler };
```

---

## @wrnexus/db

Documentation URL: https://wrnexusjs.dev/packages/db

# @wrnexus/db

## Rollout-safe migrations

Run `wrnexus db check` in CI before deployment. The analyzer reports stable
diagnostics for drops, renames, type changes, new/enforced required columns,
and potentially blocking index creation, with an expand/backfill/switch/contract
recommendation. `wrnexus db migrate` blocks critical issues in pending
migrations. `--allow-breaking` is an explicit operator override; already-applied
migrations do not block later releases.

> The database layer for WrNexus: TS models as the single source of truth for DDL, validation, and result typing, plus a driver-based `Db` client, migrations, and a sqlc-style query generator.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

`@wrnexus/db` is the server-side data layer. You describe tables as TypeScript
models (the `v` column builder + `table()`); those models drive migrations,
coerce raw DB rows into typed objects, and feed the query generator. A thin
`Driver` interface is implemented by adapters for SQLite (`bun:sqlite`),
Postgres/MySQL (`Bun.SQL`), and MongoDB. The `Db` client adds ergonomics —
model-mapped `all`/`one`, transactions, `createTable`, pagination, and batched
relation loading. A process-wide registry (`getDb`/`setDb`) exposes configured
connections to pages and API routes. Reach for it whenever a WrNexus app needs
persistence.

## Installation

```bash
bun add @wrnexus/db
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

## API

The core entry (`@wrnexus/db`) is dependency-free; adapters and connectors live
in subpaths so importing the core doesn't pull in every driver.

| Subpath                | Exports                                                                                                                                           |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@wrnexus/db`          | `v`, `table`, `Column`, `createDb`, `createTableSql`, the client registry (`setDb`/`getDb`/…), migrations, the query generator, and query helpers |
| `@wrnexus/db/connect`  | `connectFromConfig`, `resolveDbUrl`, `DbConfig` — resolve a config to a live SQL `Db`                                                             |
| `@wrnexus/db/session`  | `sqliteSessionStore` — a `bun:sqlite` session backend for `@wrnexus/core`                                                                         |
| `@wrnexus/db/sqlite`   | `sqlite(url?)` driver                                                                                                                             |
| `@wrnexus/db/postgres` | `postgres(url)` driver                                                                                                                            |
| `@wrnexus/db/mysql`    | `mysql(url)` driver                                                                                                                               |
| `@wrnexus/db/mongo`    | `mongo(url, dbName?)` document API                                                                                                                |

### Schema — `v`, `table`, `Column`

`table(name, columns)` returns a `Model<T>`. Columns are built with `v`:

```ts
import { v, table } from "@wrnexus/db";

const users = table("users", {
  id: v.id(), // auto-increment primary key
  email: v.text().unique(),
  name: v.text().optional(), // NULLable
  age: v.int().default(0),
  active: v.bool().default(true),
  createdAt: v.timestamp().default("now"), // CURRENT_TIMESTAMP
});
```

Column builders: `v.id`, `v.text` (alias `v.string`), `v.int`, `v.real` (alias
`v.number`), `v.bool` (alias `v.boolean`), `v.timestamp`, `v.json`. `BaseType`
values are `"id" | "text" | "int" | "real" | "bool" | "timestamp" | "json"`.

`Column` modifiers (chainable): `.optional()`, `.unique()`, `.default(value)`
(use the sentinel `"now"` for a current-timestamp default), `.primaryKey()`,
`.references(table, column = "id")`. `.coerce(raw)` converts a raw DB value to
its JS type.

A `Model<T>` exposes: `name`, `columns`, `parse(row)` (coerces a raw row into a
typed `T`; unknown columns pass through), and `describe()` (returns each
column's `ColumnDef`, for migrations and the generator).

### Driver & client — `createDb`, `Db`, `Driver`

```ts
createDb(driver: Driver): Db
```

A `Driver` (implemented by adapters) exposes `dialect`, `query(sql, params?)`,
`exec(sql, params?)`, `transaction(fn)`, and `close()`. `createDb` wraps it in a
`Db`:

- `all<T>(sql, params?, model?)` — all rows, mapped through `model.parse` when a model is given.
- `one<T>(sql, params?, model?)` — first row or `null`.
- `exec(sql, params?)` — `Promise<ExecResult>` (`{ changes, lastInsertId? }`).
- `tx(fn)` — run `fn(db)` in a transaction; rolls back on throw. Nested `tx` reuses the current transaction.
- `createTable(model)` — runs the model's `CREATE TABLE IF NOT EXISTS` DDL.
- `close()` — idempotently rejects new top-level work, drains active queries and
  transactions, then closes the underlying pool.

Every query is parameterized (positional params). `createTableSql(model, dialect, ifNotExists?)`
renders `CREATE TABLE` directly; `Dialect` is `"sqlite" | "postgres" | "mysql"`.

### Client registry — `getDb` / `setDb`

A process-wide registry the runtime configures at startup from `wrnexus.config.ts`
(the `db` setting is the default; `databases.<name>` entries are named).

- `setDb(db)` / `setDb(name, db)` — set the default or a named connection.
- `registerDb(name, db)` — alias of `setDb(name, db)`.
- `getDb(name = "default")` — the default or a named `Db` (throws if unconfigured).
- `hasDb(name?)`, `databaseNames()`, `closeDatabases()`. Registry shutdown clears
  registrations first, attempts every open database, and reports close failures
  together with `AggregateError` instead of leaking later pools.

```ts
const users = await getDb().all("SELECT * FROM users");
const events = await getDb("analytics").all("SELECT * FROM hits");
```

### Adapters

- `@wrnexus/db/sqlite` — `sqlite(url = ":memory:")`. `url` may be `file:./dev.db`, a raw path, or `:memory:`. Built on `bun:sqlite`; no external service.
- `@wrnexus/db/postgres` — `postgres(url)` (e.g. `postgres://user:pass@host:5432/db`, placeholders `$N`).
- `@wrnexus/db/mysql` — `mysql(url)` (e.g. `mysql://user:pass@host:3306/db`, placeholders `?`). Postgres/MySQL both use Bun's native `Bun.SQL` client and its pooled `begin()` for transactions.
- `@wrnexus/db/mongo` — `mongo(url, dbName?)`. A document API, not SQL: `db.collection(model)` returns a `MongoRepo<T>` with `find`, `findOne`, `insert`, `insertMany`, `update`, `delete`, `count`. Reads are coerced through `model.parse` (`_id` is mapped to `id`). The `mongodb` driver is imported lazily — install it to use Mongo.

### Migrations

Migrations are `.sql` files (in e.g. `app/db/migrations`), each split into
`-- +up` and `-- +down` sections. A file with no markers is treated entirely as
`up`. Applied names are recorded in a `_wire_migrations` table so each runs once.

- `parseMigration(name, content)` → `Migration` (`{ name, up, down }`).
- `loadMigrations(dir)` — parse all `.sql` files, sorted by filename.
- `appliedMigrations(db)` — applied names, oldest first.
- `migrate(db, dir, options?)` — apply all pending (each in a transaction); returns applied names.
- `rollback(db, dir, options?)` — roll back the most recent; returns its name or `null`.
- `status(db, dir)` — `{ name, applied }[]` for every migration file.
- `scaffoldMigration(dir, name, dialect, models?)` — write a new numbered migration; with `models` it generates `CREATE`/`DROP` for every table (referenced tables first via topological sort). Returns the file path.

### Query generator (sqlc-style)

Turns annotated SQL into typed TS functions; params and result types are
inferred from the models, and rows map back through `model.parse` when the
selected columns are model columns.

- `parseQueries(content)` → `QueryDef[]` from `-- name: X :one|:many|:exec` blocks.
- `generateQueriesFile(queries, models, dialect)` → the `queries.gen.ts` source. `models` is a `ModelRef[]` (`{ varName, model }`). Rewrites `:name` placeholders to positional (`$N`/`?`) form.

`QueryKind` is `"one" | "many" | "exec"`.

### Query helpers

- `paginate(db, { sql, params?, countSql?, model? }, opts?)` — offset pagination. Pass the base SELECT **without** a LIMIT; it appends the page window and derives `total` via a COUNT subquery. `PageOptions`: `{ page?, perPage?, maxPerPage? }` (defaults page 1, perPage 20, maxPerPage 100). Returns `Paginated<T>` (`items, page, perPage, total, totalPages, hasNext, hasPrev`).
- `loadRelated(db, parents, opts)` — load a relation for many parents in ONE query and attach it (no N+1). `RelationOptions`: `{ table, foreignKey, as, localKey?, single?, model? }` — `single: true` attaches one child (belongsTo), otherwise an array (hasMany). Table/foreign-key names are validated as identifiers.

### Session store

`@wrnexus/db/session` exports `sqliteSessionStore(path = "sessions.db")`, a
persistent, process-shared `SessionBackend` (from `@wrnexus/core`) backed by
`bun:sqlite` (WAL mode). Sessions survive restarts and are shared by every
worker on the same file.

## Usage

Define models, connect, create tables, and query with typed results:

```ts
import { v, table, createDb } from "@wrnexus/db";
import { sqlite } from "@wrnexus/db/sqlite";

const users = table<{ id: number; email: string; name: string | null }>("users", {
  id: v.id(),
  email: v.text().unique(),
  name: v.text().optional(),
  createdAt: v.timestamp().default("now"),
});

const db = createDb(sqlite("file:./dev.db"));
await db.createTable(users);

await db.exec("INSERT INTO users (email) VALUES (?)", ["a@b.com"]);
const list = await db.all("SELECT * FROM users", [], users); // rows typed + coerced
const one = await db.one("SELECT * FROM users WHERE id = ?", [1], users);

await db.tx(async (tx) => {
  await tx.exec("UPDATE users SET name = ? WHERE id = ?", ["Ada", 1]);
});
```

Resolve a config to a live SQL `Db`, and register it:

```ts
import { connectFromConfig } from "@wrnexus/db/connect";
import { setDb, getDb } from "@wrnexus/db";

setDb(connectFromConfig({ driver: "sqlite", url: "file:./dev.db" }, process.cwd()));
const rows = await getDb().all("SELECT * FROM users");
```

Run migrations and paginate:

```ts
import { migrate, paginate } from "@wrnexus/db";

await migrate(db, "app/db/migrations");
const pageTwo = await paginate(
  db,
  { sql: "SELECT * FROM users ORDER BY id", model: users },
  { page: 2 },
);
```

For deployments, `{ dryRun: true }` reports pending names without applying
their SQL, `signal` cancels safely between migrations, and the default
database-backed lock prevents concurrent deploy runners. A live lock produces
`WRN-DB-MIGRATION-LOCKED`; crash-stale locks expire after `lockTimeoutMs` (five
minutes by default). Disable it with `lock: false` only when an external deploy
coordinator already guarantees exclusivity.

```ts
const pending = await migrate(db, "app/db/migrations", { dryRun: true });
await migrate(db, "app/db/migrations", {
  signal: shutdownController.signal,
  lockTimeoutMs: 10 * 60_000,
});
```

MongoDB (document API):

```ts
import { mongo } from "@wrnexus/db/mongo";

const mdb = await mongo(process.env.MONGO_URL!, "app");
const repo = mdb.collection(users);
await repo.insert({ email: "a@b.com" });
const active = await repo.find({ active: true });
```

## Configuration

`connectFromConfig` (and the runtime) read a `DbConfig` (`{ driver, url }`)
where `driver` is `sqlite | postgres | mysql`. `resolveDbUrl(url, appRoot?)`
resolves a relative `file:`/`sqlite:` URL against the app root. MongoDB is not a
SQL driver — use `@wrnexus/db/mongo` directly.

## Requirements / Notes

- **Bun-only.** Uses `bun:sqlite` (SQLite adapter + session store) and `Bun.SQL`
  (Postgres/MySQL). Migrations/scaffolding use `node:fs`/`node:path`.
- Works with `@wrnexus/core` — `sqliteSessionStore` implements its
  `SessionBackend`; `getDb`/`setDb` are wired by the WrNexus runtime from
  `wrnexus.config.ts`.
- The `mongodb` npm package is an optional, lazily-imported peer — install it
  only if you use `@wrnexus/db/mongo`. The core package stays dependency-free.

## Repository and transaction helpers

Repositories accept an immutable equality `scope`, normally `{ column: "tenant_id", value:
ctx.tenant.id }`. The scope is injected into every read, count, update and delete, while create
overwrites any caller-supplied tenant value. This makes accidental cross-tenant CRUD through the
repository API fail closed.

```ts
import { createRepository, retryTransaction, databaseHealth, batch } from "@wrnexus/db";

const users = createRepository<User>(db, {
  table: "users",
  allowedColumns: ["email", "name", "active"],
});

const user = await users.require(42);
await users.update(42, { active: true });
```

Repository identifiers are validated, writes may be restricted to an allowlist, and values always use query parameters. Infrastructure packages remain helper-only and do not add UI dependencies to server code.

## 0.8 repository and transaction helpers

```ts
import { createRepository, databaseHealth, firstOrThrow, retryTransaction } from "@wrnexus/db";

const usersRepo = createRepository<User>(db, {
  table: "users",
  allowedColumns: ["email", "name", "active"],
  maxListLimit: 250,
});

const users = await usersRepo.all({
  orderBy: "name",
  direction: "asc",
  limit: 50,
  offset: 0,
});
```

Repository SQL identifiers are validated and values remain parameterized. Placeholder generation is dialect-aware: PostgreSQL uses `$1`, `$2`, and SQLite/MySQL use `?`. List limits are bounded.

`retryTransaction()` retries recognized serialization, deadlock, and database-lock errors by default. Supply `shouldRetry` for application-specific retryable errors; ordinary validation or business errors are not retried automatically.

### Exported TypeScript declarations

```ts
import { M as Model } from './schema-tVurYsbL.js';
export { B as BaseType, C as Column, a as ColumnDef, b as Columns, t as table, v } from './schema-tVurYsbL.js';
import { a as Db, b as Dialect, R as Row, E as ExecResult } from './driver-DA53QHkO.js';
export { D as Driver, T as TxHandle, c as createDb, d as createTableSql } from './driver-DA53QHkO.js';

/**
 * A process-wide database registry. The framework configures it at server
 * startup from `wrnexus.config.ts`: the `db` setting becomes the **default**
 * connection, and each entry under `databases` becomes a **named** connection.
 * Pages and API routes then call `getDb()` for the default, or `getDb("<name>")`
 * for a named one, to run queries (including the generated typed functions).
 *
 *   const users = await getDb().all("SELECT * FROM users");            // default db
 *   const events = await getDb("analytics").all("SELECT * FROM hits"); // named db
 */

type DbFactory = () => Db;
/** Set the default database (called by the runtime at startup). */
declare function setDb(db: Db): Db;
/** Set a named database (from `databases.<name>` in config). */
declare function setDb(name: string, db: Db): Db;
/** Register a named database. Alias of `setDb(name, db)` for readability. */
declare function registerDb(name: string, db: Db): Db;
/**
 * Register a named database without opening its connection pool. The first
 * `getDb(name)` call creates and caches the connection.
 */
declare function registerLazyDb(name: string, factory: DbFactory): void;
/** The default database, or a named one. Throws if it isn't configured. */
declare function getDb(name?: string): Db;
/** Whether the default (or a named) database has been configured. */
declare function hasDb(name?: string): boolean;
/** Names of all configured databases (the default appears as "default"). */
declare function databaseNames(): string[];
/** Close every configured database and clear the registry. */
declare function closeDatabases(): Promise<void>;

/**
 * Migration runner. Migrations are `.sql` files in `app/db/migrations`, each
 * split into `-- +up` and `-- +down` sections. Applied migrations are recorded
 * in a `_wire_migrations` table so they run exactly once, newest-last.
 *
 * `scaffoldMigration(..., models)` writes an initial migration straight from the
 * TS models — the source of truth — so you don't hand-write the first schema.
 */

interface Migration {
    name: string;
    up: string;
    down: string;
}
interface MigrationRunOptions {
    /** Return pending migration names without executing their SQL. */
    dryRun?: boolean;
    /** Stop safely between migrations. Active database statements cannot be interrupted portably. */
    signal?: AbortSignal;
    /** Coordinate migration runners through the database. Default true. */
    lock?: boolean;
    /** Allow recovery of a lock left by a crashed process. Default 5 minutes. */
    lockTimeoutMs?: number;
}
/** Split a migration file into its `up` and `down` SQL sections. */
declare function parseMigration(name: string, content: string): Migration;
/** Load and parse all migration files in a directory, sorted by filename. */
declare function loadMigrations(dir: string): Migration[];
/** Names of already-applied migrations, oldest first. */
declare function appliedMigrations(db: Db): Promise<string[]>;
/** Apply an ordered migration list (each in a transaction). Returns applied names. */
declare function applyMigrations(db: Db, migrations: readonly Migration[], options?: MigrationRunOptions): Promise<string[]>;
/** Apply all pending migrations from a directory. */
declare function migrate(db: Db, dir: string, options?: MigrationRunOptions): Promise<string[]>;
/** Roll back the most recently applied migration. Returns its name, or null. */
declare function rollback(db: Db, dir: string, options?: Omit<MigrationRunOptions, "dryRun"> & {
    dryRun?: boolean;
}): Promise<string | null>;
/** Full status: every migration file with whether it has been applied. */
declare function status(db: Db, dir: string): Promise<{
    name: string;
    applied: boolean;
}[]>;
/**
 * Write a new migration file. With `models`, the `up`/`down` are generated from
 * the TS models (create/drop every table); otherwise empty stubs are written.
 * Returns the created file path.
 */
declare function scaffoldMigration(dir: string, name: string, dialect: Dialect, models?: Model[]): string;

interface MigrationSafetyIssue {
    code: "WRN-DB-DROP-TABLE" | "WRN-DB-DROP-COLUMN" | "WRN-DB-RENAME" | "WRN-DB-TYPE-CHANGE" | "WRN-DB-SET-NOT-NULL" | "WRN-DB-ADD-REQUIRED" | "WRN-DB-BLOCKING-INDEX";
    severity: "error" | "warning";
    migration: string;
    statement: string;
    recommendation: string;
}
declare function analyzeMigrationSafety(migration: Migration): MigrationSafetyIssue[];
declare function analyzeMigrations(migrations: readonly Migration[]): MigrationSafetyIssue[];

/**
 * sqlc-style query generator. Annotated SQL in `app/db/queries/*.sql` becomes
 * typed TS functions whose params + results are inferred from the TS models and
 * whose rows are mapped back through `model.parse`.
 *
 *   -- name: GetUserByEmail :one
 *   SELECT * FROM users WHERE email = :email;
 *
 * →  GetUserByEmail(db, { email: string }): Promise<{…} | null>
 *
 * Type inference is best-effort (comparisons + INSERT column lists + SELECT list
 * vs the model); anything it can't resolve becomes `unknown`.
 */

type QueryKind = "one" | "many" | "exec";
interface QueryDef {
    name: string;
    kind: QueryKind;
    sql: string;
}
/** A model plus the variable name it is exported under (for imports). */
interface ModelRef {
    varName: string;
    model: Model;
}
/** Parse annotated queries from one `.sql` file's contents. */
declare function parseQueries(content: string): QueryDef[];
/** Generate the full `queries.gen.ts` source. */
declare function generateQueriesFile(queries: QueryDef[], models: ModelRef[], dialect: Dialect): string;

/**
 * Query ergonomics built on the `Db` client: offset pagination and a batched
 * relation loader (avoids N+1). Both are dialect-aware — placeholders follow the
 * driver's style (`$N` for Postgres, `?` for SQLite/MySQL).
 */

interface PageOptions {
    page?: number;
    perPage?: number;
    /** Upper bound on perPage. Default 100. */
    maxPerPage?: number;
}
interface Paginated<T> {
    items: T[];
    page: number;
    perPage: number;
    total: number;
    totalPages: number;
    hasNext: boolean;
    hasPrev: boolean;
}
/**
 * Paginate a SELECT. Pass the base query WITHOUT a LIMIT; the helper appends the
 * page window and derives the total with a COUNT over the same query.
 *
 *   await paginate(db, { sql: "SELECT * FROM users ORDER BY name", model: users }, { page: 2 })
 */
declare function paginate<T = Row>(db: Db, query: {
    sql: string;
    params?: unknown[];
    countSql?: string;
    model?: Model<T>;
}, opts?: PageOptions): Promise<Paginated<T>>;
interface RelationOptions<C> {
    /** Parent field whose value matches the child's foreign key. Default "id". */
    localKey?: string;
    /** Child table to load from. */
    table: string;
    /** Child column that references the parent. */
    foreignKey: string;
    /** Property name to attach on each parent. */
    as: string;
    /** true → attach a single child (belongsTo); false → an array (hasMany). */
    single?: boolean;
    /** Map child rows through a model. */
    model?: Model<C>;
}
/**
 * Load a relation for a set of parent rows in ONE query and attach it to each
 * parent (no N+1). Returns the same parents, each with `opts.as` populated.
 *
 *   await loadRelated(db, users, { table: "posts", foreignKey: "userId", as: "posts" })
 */
declare function loadRelated<P extends Row, C extends Row = Row>(db: Db, parents: P[], opts: RelationOptions<C>): Promise<(P & Record<string, C | C[] | null>)[]>;

interface CursorPageOptions {
    limit?: number;
    after?: string;
    before?: string;
    column?: string;
    direction?: "asc" | "desc";
    maxLimit?: number;
}
interface CursorPage<T> {
    items: T[];
    nextCursor?: string;
    previousCursor?: string;
    hasMore: boolean;
}
declare function cursorPaginate<T extends Row = Row>(db: Db, query: {
    sql: string;
    params?: unknown[];
    model?: Model<T>;
}, options?: CursorPageOptions): Promise<CursorPage<T>>;
declare function optimisticUpdate(db: Db, input: {
    table: string;
    idColumn?: string;
    id: unknown;
    versionColumn?: string;
    version: number;
    values: Record<string, unknown>;
}): Promise<number>;
declare function tenantScope(sql: string, tenantId: unknown, dialect: Dialect, existingParams?: number, column?: string): {
    sql: string;
    params: unknown[];
};
declare function softDeleteClause(column?: string): string;

interface QueryRecord {
    sql: string;
    paramsCount: number;
    durationMs: number;
    rowCount?: number;
    operation: "all" | "one" | "exec";
    duplicateCount: number;
}
interface QueryIssue {
    code: string;
    severity: "error" | "warning" | "info";
    message: string;
    sql: string;
}
interface QueryPolicy {
    slowQueryMs?: number;
    timeoutMs?: number;
    maxRows?: number;
    duplicateWarningCount?: number;
    warnSelectStar?: boolean;
    warnUnboundedSelect?: boolean;
    onQuery?: (record: QueryRecord) => void | Promise<void>;
    onIssue?: (issue: QueryIssue) => void | Promise<void>;
}
declare function getDbPerformanceSnapshot(): {
    queries: QueryRecord[];
    issues: QueryIssue[];
};
declare function resetDbPerformanceSnapshot(): void;
declare function instrumentDb(db: Db, policy?: QueryPolicy): Db;
declare function queryOperation(sql: string): string;

declare class RecordNotFoundError extends Error {
    constructor(message?: string);
}
declare function firstOrThrow<T>(db: Db, sql: string, params?: unknown[], model?: Model<T>, message?: string): Promise<T>;
declare function exists(db: Db, sql: string, params?: unknown[]): Promise<boolean>;
declare function countRows(db: Db, table: string, where?: string, params?: unknown[]): Promise<number>;
declare function withTransaction<T>(db: Db, callback: (tx: Db) => Promise<T>): Promise<T>;
declare function retryTransaction<T>(db: Db, callback: (tx: Db, attempt: number) => Promise<T>, options?: {
    attempts?: number;
    baseDelayMs?: number;
    maxDelayMs?: number;
    jitter?: boolean;
    shouldRetry?: (error: unknown) => boolean;
}): Promise<T>;
declare function batch<T>(values: readonly T[], size?: number): T[][];
declare function databaseHealth(db: Db): Promise<{
    ok: boolean;
    latencyMs: number;
    error?: string;
}>;
interface RepositoryListOptions<T extends Row> {
    limit?: number;
    offset?: number;
    orderBy?: keyof T & string;
    direction?: "asc" | "desc";
}
interface Repository<T extends Row> {
    all(options?: RepositoryListOptions<T>): Promise<T[]>;
    find(id: string | number): Promise<T | null>;
    require(id: string | number): Promise<T>;
    create(values: Partial<T>): Promise<ExecResult>;
    update(id: string | number, values: Partial<T>): Promise<ExecResult>;
    remove(id: string | number): Promise<ExecResult>;
    exists(id: string | number): Promise<boolean>;
    count(): Promise<number>;
}
declare function createRepository<T extends Row>(db: Db, input: {
    table: string;
    idColumn?: string;
    model?: Model<T>;
    allowedColumns?: readonly (keyof T & string)[];
    maxListLimit?: number;
    /** Immutable equality scope (normally tenant_id) applied to every operation. */
    scope?: {
        column: keyof T & string;
        value: unknown;
    };
}): Repository<T>;

export { type CursorPage, type CursorPageOptions, Db, Dialect, ExecResult, type Migration, type MigrationRunOptions, type MigrationSafetyIssue, Model, type ModelRef, type PageOptions, type Paginated, type QueryDef, type QueryIssue, type QueryKind, type QueryPolicy, type QueryRecord, RecordNotFoundError, type RelationOptions, type Repository, Row, analyzeMigrationSafety, analyzeMigrations, appliedMigrations, applyMigrations, batch, closeDatabases, countRows, createRepository, cursorPaginate, databaseHealth, databaseNames, exists, firstOrThrow, generateQueriesFile, getDb, getDbPerformanceSnapshot, hasDb, instrumentDb, loadMigrations, loadRelated, migrate, optimisticUpdate, paginate, parseMigration, parseQueries, queryOperation, registerDb, registerLazyDb, resetDbPerformanceSnapshot, retryTransaction, rollback, scaffoldMigration, setDb, softDeleteClause, status, tenantScope, withTransaction };
```

---

## @wrnexus/dev-server

Documentation URL: https://wrnexusjs.dev/packages/dev-server

# @wrnexus/dev-server

> The WrNexus HTTP + WebSocket server runtime — request dispatch, SSR document assembly, live-reload (HMR), and the portable production handler.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

This package is the server runtime that powers a WrNexus app in both development and production. A single **request runtime** (`createHandlers`) owns HTTP/WebSocket dispatch and SSR document assembly; it knows nothing about _how_ modules and assets are produced, so the dev and prod entry points wire in different backends: dev uses dynamic module loading plus on-the-fly bundling and injects a live-reload client; prod uses a static, pre-built manifest with cache-immutable assets. The package also ships a multi-app **gateway** (route several apps by `Host` header behind one port) and a portable `node:http` adapter for WinterCG hosts. It is entirely server-side and Bun-native (`Bun.serve`, `Bun.file`, `Bun.gzipSync`).

## Installation

```bash
bun add @wrnexus/dev-server
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported for the full server; the `node:http` adapter is for WinterCG embedding only).

## API

### Main entry (`@wrnexus/dev-server`)

| Export                                                    | Kind                      | Purpose                                                                                                                               |
| --------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `startServer(opts: ServeOptions)`                         | `Promise<RunningServer>`  | Start the dev server on `Bun.serve`: builds the router, connects/migrates databases, wires assets + HMR, and starts the file watcher. |
| `createHandlers(deps: RuntimeDeps)`                       | `Handlers`                | The shared request runtime (fetch + websocket handlers). Re-exported from `runtime.ts`.                                               |
| `createProductionServer(manifest, opts)`                  | `Bun.Server`              | Start the production server from a precompiled manifest.                                                                              |
| `createProductionHandlers(manifest, opts)`                | `Handlers`                | Build the portable prod fetch/websocket handlers with no server bound (the deployment-adapter seam).                                  |
| `startGateway(opts: GatewayOptions)`                      | `Promise<RunningGateway>` | Boot multiple apps as child processes and route by `Host`.                                                                            |
| `toRequest`, `writeResponse`, `nodeListener`, `serveNode` | functions                 | `node:http` ↔ WinterCG `Request`/`Response` adapter.                                                                                  |
| `RESTART_EXIT_CODE`                                       | `number` (`97`)           | Exit code the dev child uses to ask the supervisor for a fresh process.                                                               |
| `STYLES_HREF`, `HMR_CLIENT_JS`                            | constants                 | The global stylesheet URL and the inline HMR client script.                                                                           |

Exported types: `ServeOptions`, `RunningServer`, `RuntimeDeps`, `AssetServer`, `WsData`, `GatewayApp`, `GatewayOptions`, `GatewayAuth`, `GatewaySecurity`, `RunningGateway`, `FetchHandler`.

### `startServer(opts)`

```ts
interface ServeOptions {
  appDir: string; // absolute/relative path to the app/ dir
  port?: number; // default 3000
  hostname?: string; // default "localhost"
  mode?: Mode; // "development" | "production"; default "development"
  hmr?: boolean; // inject live-reload client; default (mode === "development")
  styleEntry?: string | null; // resolved absolute path to the global CSS entry
  stylesConfig?: StylesConfig; // custom styles processor (e.g. Tailwind/PostCSS)
  head?: string; // raw HTML appended to every page <head>
  seo?: SeoConfig; // global SEO defaults
  security?: SecurityConfig; // security headers + CORS policy
  theme?: ThemeConfig; // design-token theme (merged over built-in light/dark)
  i18n?: I18nConfig; // default language + supported locales
  db?: { driver: string; url: string }; // default db → getDb(); dev auto-migrates
  databases?: Record<string, { driver: string; url: string }>; // named dbs → getDb("<name>")
  realtime?: { scale?: boolean; redisUrl?: string }; // bridge rooms over Redis across processes
}

interface RunningServer {
  port: number;
  hostname: string;
  url: string;
  router: Router;
  stop(): void;
}
```

In development, `startServer` also connects `app/db/migrations` (and `app/db/<name>/migrations`) and auto-applies migrations, then starts an in-process file watcher. CSS edits hot-swap live. Page, component, layout, API, middleware, realtime, schema, locale, and public-asset edits invalidate only their cached modules, rescan routes where necessary, and morph fresh HTML through the existing HMR WebSocket. The server process and active gateway stay running.

`getWrnCompileMetrics()` exposes cumulative content-addressed compiler cache
`hits`, `misses`, successful `compilations`, `errors`, `totalDurationMs`, and
`lastDurationMs` for the DevToolbar or custom diagnostics. Tests and embedded
servers can call `resetWrnCompileMetrics()` to establish a fresh measurement
window.

### `createHandlers(deps)`

The core runtime shared by dev and prod. It handles CORS preflight, `/healthz` and `/__wrnexus/health`, request-body size limits (413), HMR socket upgrades (`/__wrnexus/hmr`), realtime WebSocket upgrades (`defineRoom` default export or a raw `websocket` export), the middleware pipeline, API routes (`/api/*`), framework assets (`/__wrnexus/*`), public assets, and full SSR page rendering (component mounts, layouts, slots, i18n markers, per-page script selection, ETag/304, gzip).

```ts
interface RuntimeDeps {
  mode: Mode;
  hmr: boolean; // inject the live-reload client into pages
  router: Router;
  loadModule(file: string): Promise<Record<string, unknown>>;
  getMiddleware(): Promise<Middleware[]>;
  assets: AssetServer; // serves /__wrnexus/* (islands, reactive, hmr)
  hasStyles?: boolean; // inject the global stylesheet link
  hasUi?: boolean; // inject the Wire UI stylesheet (/__wrnexus/ui.css)
  theme?: ResolvedTheme; // enables /__wrnexus/theme.css + <html data-theme>
  i18n?: ResolvedI18n; // enables ctx.t, <html lang>, {t:key} markers
  inlineStyles?: string; // inline small prod stylesheets into <head>
  assetVersion?: string; // cache-busting ?v= on framework asset URLs
  head?: string; // raw HTML appended to every page <head>
  seo?: SeoConfig;
  security?: SecurityConfig;
  maxBodyBytes?: number; // 413 above this; default 10 MB
  hub?: HmrHub; // browser HMR sockets (dev only)
  realtimeBus?: RealtimeBus; // cross-process room bridge (Redis pub/sub)
}

interface Handlers {
  fetch(req: Request, server: UpgradeServer): Promise<Response | undefined>;
  websocket: { open; message; close; drain };
}
```

`WsData` is the per-connection socket tag — a discriminated union of `{ kind: "realtime"; handler }`, `{ kind: "room"; meta }`, or `{ kind: "hmr" }`.

### `createProductionServer(manifest, opts)` / `createProductionHandlers(manifest, opts)`

Production runs the _same_ request runtime as dev, but with no filesystem scan and no runtime bundling. `wrnexus build` emits an entry that statically imports every route/component/layout module and passes them as a `ProdManifest`; the route-matching tables are rebuilt from the raw patterns.

```ts
interface ProdManifest {
  pages: { raw: string; mod: RouteModule }[];
  api: { raw: string; mod: RouteModule }[];
  realtime: { raw: string; mod: RouteModule }[];
  middleware: Middleware[];
  components: { name: string; mod: RouteModule }[];
  layouts: { name: string; mod: RouteModule }[];
}

interface ProdOptions {
  stylesPath?: string;
  inlineStyles?: string;
  reactivePath?: string;
  themePath?: string;
  themeJsPath?: string;
  theme?: ResolvedTheme;
  uiCssPath?: string;
  schemasJs?: string;
  i18n?: ResolvedI18n;
  db?: { driver: string; url: string };
  databases?: Record<string, { driver: string; url: string }>;
  realtime?: { scale?: boolean; redisUrl?: string };
  assetVersion?: string;
  publicDir?: string;
  head?: string;
  seo?: SeoConfig;
  security?: SecurityConfig;
  port?: number;
  hostname?: string;
  maxBodyBytes?: number;
}
```

`createProductionServer` also loads the `.env` cascade for the `production` profile, installs `SIGTERM`/`SIGINT` graceful shutdown, and binds `0.0.0.0` (port from `opts.port` or `$PORT`, default 3000). Migrations are **not** run here — apply them first (`wrnexus db migrate`). `createProductionHandlers` returns the bare handlers for edge/serverless/`node:http` deployment.

### `startGateway(opts)` — multi-app gateway

Serves several apps behind one port and routes each request to the right app by its `Host` header. Each app runs as its own child process (full isolation); the gateway is a thin host-based reverse proxy for HTTP and WebSocket. In development, normal application edits are applied inside the existing child and sent through its existing HMR connection. The child supervisor remains as crash recovery rather than the normal update path. Apps communicate at runtime via `@wrnexus/pubsub` (use the Redis driver so messages cross processes).

```ts
interface GatewayOptions {
  port?: number; // default 3000
  hostname?: string; // dev: "127.0.0.1"; production: "0.0.0.0"
  mode?: "development" | "production";
  apps: GatewayApp[];
  security?: GatewaySecurity;
}

interface GatewayApp {
  name: string; // app id (for logs)
  dir: string; // app root (contains app/ + wrnexus.config.ts)
  domains: string[]; // host names routed here
  port?: number; // fixed internal port; else assigned
  auth?: GatewayAuth; // per-app edge access control
}

interface GatewayAuth {
  basic?: { user: string; pass: string } | Array<{ user: string; pass: string }>;
  allowIps?: string[]; // exact-match IP allowlist
  forward?: { url: string }; // forward-auth (SSO): 2xx allows
}

interface GatewaySecurity {
  trustedHostsOnly?: boolean; // 404 unknown hosts instead of first app
  rateLimit?: { max: number; windowMs?: number }; // global by client IP (429)
  headers?: boolean; // add baseline edge security headers
  forwardedHeaders?: boolean; // set X-Forwarded-* (default true)
  accessLog?: boolean;
}
```

Forward auth is a verification hook, not a login page. Configure `forward.url` with a
dedicated endpoint such as `http://sso.localhost:3000/api/verify`. The gateway forwards
the request's `Cookie` and `Authorization` headers plus `X-Forwarded-Host`,
`X-Forwarded-Proto`, `X-Original-Method`, and `X-Original-Uri` (including its query
string). The verifier must return 2xx only for an authenticated session and 401/403
otherwise. Pointing forward auth at an SSO home page that always returns 200 allows
every request and does not implement SSO.

For browser SSO, the verifier may return a `302`/`303`/`307`/`308` with a `Location`
header pointing to its login page. The gateway passes that redirect to the browser. The
login flow should validate a signed `returnTo` value before redirecting back; API clients
should receive `401`/`403` instead of an HTML login redirect.

Open the gateway URL (normally `http://127.0.0.1:3000`), not an app's internal
port. The gateway exposes `/__gateway/health` (JSON list of routed apps) and returns a
`RunningGateway` (`{ port, url, stop() }`). Use `--host=0.0.0.0` when other devices need
to reach a development gateway.

### `node:http` adapter (from `./adapters/node.ts`)

For embedding the WinterCG handler behind an existing Node server or a WinterCG host. Note the full app still needs Bun-compatible globals (`Bun.file`, `bun:sqlite`, etc.); only the `Request`/`Response` conversion is fully portable.

```ts
type FetchHandler = (req: Request) => Response | undefined | Promise<Response | undefined>;

toRequest(req: IncomingMessage, opts?): Promise<Request>
writeResponse(res: ServerResponse, response: Response): Promise<void>  // preserves multiple Set-Cookie
nodeListener(handler: FetchHandler, opts?): (req, res) => Promise<void>
serveNode(handler: FetchHandler, opts?): Promise<Server>
```

### Subpath export: `@wrnexus/dev-server/serve-entry`

The child process the dev supervisor launches:

```bash
bun run serve-entry.ts <appDir> <port> <mode>
```

It loads the optional `wrnexus.config.ts`, resolves the style entry, calls `startServer`, and prints the route table (Pages / API / Realtime / Components). Because it runs in its own process, every restart re-imports all route modules fresh — that is how the supervisor delivers live reload of edited server code. `startGateway` resolves this entry via `import.meta.resolve("@wrnexus/dev-server/serve-entry")` to spawn each dev app.

## Usage

### Programmatic dev server

```ts
import { startServer } from "@wrnexus/dev-server";

const server = await startServer({
  appDir: "./app",
  port: 3000,
  mode: "development",
  theme: {/* design tokens */},
  db: { driver: "sqlite", url: "file:./data/app.db" },
});

console.log(`Running at ${server.url}`);
// server.stop();
```

### Production server from a build manifest

```ts
import { createProductionServer } from "@wrnexus/dev-server";
import { manifest } from "./dist/manifest.js"; // generated by `wrnexus build`

createProductionServer(manifest, {
  stylesPath: "./dist/styles.css",
  reactivePath: "./dist/reactive.js",
  assetVersion: process.env.BUILD_ID,
  db: { driver: "postgres", url: process.env.DATABASE_URL! },
  port: Number(process.env.PORT) || 3000,
});
```

### Embedding the handler on `node:http`

```ts
import { createProductionHandlers, serveNode } from "@wrnexus/dev-server";

const handlers = createProductionHandlers(manifest, opts);
await serveNode(handlers.fetch, { port: 8080 });
```

### Multi-app gateway

```ts
import { startGateway } from "@wrnexus/dev-server";

await startGateway({
  port: 3000,
  apps: [
    { name: "web", dir: "./apps/web", domains: ["localhost", "web.localhost"] },
    {
      name: "admin",
      dir: "./apps/admin",
      domains: ["admin.localhost"],
      auth: { basic: { user: "root", pass: "s3cret" } },
    },
  ],
  security: { trustedHostsOnly: true, rateLimit: { max: 600 } },
});
```

## Framework asset routes

The runtime serves these framework-owned paths (dev builds them live; prod serves pre-built/immutable versions):

- `/__wrnexus/nav.js`, `/__wrnexus/reactive.js`, `/__wrnexus/realtime.js` — client runtimes
- `/__wrnexus/validate.js`, `/__wrnexus/schemas.js`, `/__wrnexus/i18n.js` — validation + i18n runtimes
- `/__wrnexus/theme.css`, `/__wrnexus/theme.js`, `/__wrnexus/ui.css`, `/__wrnexus/styles.css` — styles
- `/__wrnexus/hmr` — dev-only HMR WebSocket
- `/__wrnexus/csr` — server-evaluated CSR bindings for browser-side API fetches

Pages get only the scripts they use: `nav.js` always, `reactive.js` when a page has a `data-scope`/CSR fetch, plus theme/validation/i18n/realtime runtimes when the relevant markup is present.

## Requirements / Notes

- **Bun-only.** Uses `Bun.serve` (HTTP + WebSocket), `Bun.file`, and `Bun.gzipSync`. The full app also relies on `bun:sqlite` / `Bun.SQL` via `@wrnexus/db`.
- Orchestrates the whole framework: `@wrnexus/core` (context, security, realtime registry), `@wrnexus/router`, `@wrnexus/ssr` (`renderDocument`), `@wrnexus/csr` (client runtimes), `@wrnexus/compiler` (`.wrn` → TS), `@wrnexus/styles`, `@wrnexus/ui`, `@wrnexus/validation`, `@wrnexus/i18n`, `@wrnexus/db`, and `@wrnexus/pubsub` (Redis-backed cross-process realtime).
- `.wrn` files compile into a content-addressed hidden `.wrnexus/` cache. Targeted
  invalidation gives changed modules a fresh import identity without restarting
  the development server.
- Responses are gzipped when the client accepts it and the body is a buffered, compressible payload ≥ 1 KB; streaming/SSE responses opt out via `Cache-Control: no-transform`.
</content>

</invoke>

### Exported TypeScript declarations

```ts
import { Mode, Middleware, SeoConfig, SecurityConfig, HealthRegistry, RealtimeBus, RealtimeConnectMeta } from '@wrnexus/core';
import { Router } from '@wrnexus/router';
import { ResolvedTheme, MobileConfig, PwaConfig, ObservabilityConfig, TenancyConfig, NavigationConfig, StylesConfig, ThemeConfig } from '@wrnexus/styles';
import { ResolvedI18n, I18nConfig } from '@wrnexus/i18n';
import { StorageConfig } from '@wrnexus/uploader';
import { DevToolbarConfig, DevToolbarPlatformSnapshot, DevToolbarPanel } from '@wrnexus/dev-toolbar/types';
import { ClientRuntimeDefinition, PluginInput } from '@wrnexus/plugin';
import { CacheCoordinator } from '@wrnexus/cache';
import { DevToolbarCollector } from '@wrnexus/dev-toolbar/server';
import { AuthzModule } from '@wrnexus/authz';
import { IncomingMessage, ServerResponse, Server } from 'node:http';

/** Exit code a dev-server child uses to request a clean supervisor restart. */
declare const RESTART_EXIT_CODE = 97;

interface PartialBuildModule {
    default?: unknown;
    render?: (props?: Record<string, unknown>) => string | Promise<string>;
    layout?: string | {
        name?: string;
        render?: (props?: Record<string, unknown>) => string;
    };
    __wrnexusBuildStaticShell?: (ctx?: Record<string, unknown>) => string | Promise<string>;
}
interface PartialBuildEntry {
    name: string;
    mod: PartialBuildModule;
}
/** Expand compiler component mounts at build time using only their pure render exports. */
declare function expandStaticComponents(html: string, components: readonly PartialBuildEntry[], depth?: number): Promise<string>;
/** Produce the body shell stored in dist; dynamic region bodies are never evaluated here. */
declare function precomputePartialStaticShell(page: PartialBuildModule, components: readonly PartialBuildEntry[]): Promise<{
    shell: string;
    regions: number;
}>;

/**
 * Request pipeline helpers: middleware execution and safe module loading.
 * These are deliberately runtime-agnostic (no Bun APIs) so they could run on
 * Node too.
 */

interface WrnCompileMetrics {
    hits: number;
    misses: number;
    compilations: number;
    errors: number;
    totalDurationMs: number;
    lastDurationMs: number;
}
declare function getWrnCompileMetrics(): Readonly<WrnCompileMetrics>;
declare function resetWrnCompileMetrics(): void;

/**
 * HMR hub — tracks connected browser HMR sockets and broadcasts update events.
 *
 * Each open page holds one WebSocket to `/__wrnexus/hmr`. The in-process file
 * watcher (see index.ts) classifies a change and broadcasts a typed message:
 *
 *   { type: "css" }     -> the browser hot-swaps the stylesheet (no reload)
 *   { type: "reload" }  -> the browser asks for fresh HTML over the HMR socket
 *
 * Page/component/API/middleware/realtime changes invalidate their modules and
 * broadcast `reload` without closing the server or WebSocket. The browser asks
 * the same process for fresh HTML and performs a soft DOM morph.
 */
type HmrMessage = {
    type: "css";
    version: number;
} | {
    type: "reload";
    version: number;
};
/** Minimal shape of a Bun ServerWebSocket we rely on. */
interface Socket {
    send(data: string): unknown;
}
declare class HmrHub {
    private sockets;
    private version;
    add(ws: Socket): void;
    remove(ws: Socket): void;
    broadcastJson(message: unknown): void;
    broadcast(message: HmrMessage): void;
    get size(): number;
    css(): void;
    reload(): void;
}

/**
 * Shared request runtime used by BOTH the dev server and the production server.
 *
 * It owns the HTTP/WebSocket dispatch and the SSR document assembly, but knows
 * nothing about *how* modules or assets are produced — those come in via
 * `RuntimeDeps`. Dev wires in dynamic module loading + on-the-fly bundling;
 * prod wires in a static manifest + pre-built chunks on disk.
 */

/** A realtime module's `websocket` export: a bag of optional lifecycle hooks. */
type WsHandler = Record<string, (...args: any[]) => unknown>;
/**
 * Per-connection socket data. A socket is either an app realtime connection or
 * an internal HMR connection — discriminated by `kind`.
 */
type WsData = {
    kind: "realtime";
    handler: WsHandler;
} | {
    kind: "room";
    meta: RealtimeConnectMeta;
} | {
    kind: "hmr";
    baseUrl: string;
    headers: [string, string][];
};
type RouteModule$1 = Record<string, unknown>;
/** Serves framework-owned assets under `/__wrnexus/*` (islands, reactive, hmr). */
interface AssetServer {
    serve(pathname: string): Promise<Response | null>;
}
interface RuntimeDeps {
    mode: Mode;
    /** When true, inject the live-reload client into rendered pages. */
    hmr: boolean;
    router: Router;
    /** Load a route module by absolute path (dev: dynamic import; prod: manifest). */
    loadModule(file: string): Promise<RouteModule$1>;
    /** Resolve the ordered middleware chain. */
    getMiddleware(): Promise<Middleware[]>;
    /** Serve `/__wrnexus/*` assets. */
    assets: AssetServer;
    /** When true, inject the global stylesheet link into every page head. */
    hasStyles?: boolean;
    /** When true, inject the Wire UI stylesheet link (`/__wrnexus/ui.css`). */
    hasUi?: boolean;
    /** Production build combined theme + UI stylesheet. */
    hasFrameworkStyles?: boolean;
    /** App stylesheet already contains theme + UI CSS and is the only CSS request needed. */
    stylesIncludeFramework?: boolean;
    stylesIncludeUi?: boolean;
    /** Resolved theme config: enables `/__wrnexus/theme.css` + `<html data-theme>`. */
    theme?: ResolvedTheme;
    /** Resolved i18n bundle: enables `ctx.t`, `<html lang>`, and `{t:key}` markers. */
    i18n?: ResolvedI18n;
    /** Small production stylesheets can be inlined to avoid a render-blocking request. */
    inlineStyles?: string;
    /** Production cache-busting version appended to framework asset URLs. */
    assetVersion?: string;
    /** Package browser runtimes resolved by the plugin system. */
    clientRuntimes?: ClientRuntimeDefinition[];
    /** Page navigation strategy. `document` disables same-origin link interception. */
    navigation?: {
        mode?: "auto" | "client" | "document";
    };
    /** Raw HTML appended to every page head (e.g. CDN framework links). */
    head?: string;
    /** Global SEO defaults. */
    seo?: SeoConfig;
    mobile?: MobileConfig;
    pwa?: PwaConfig | false;
    /** Framework security headers and CORS policy. */
    security?: SecurityConfig;
    /** Built-in request tracing and Server-Timing policy. */
    observability?: ObservabilityConfig;
    /** Dependency health checks used by `/readyz` and `/__wrnexus/ready`. */
    health?: HealthRegistry;
    /** Built-in tenant identity resolution. */
    tenancy?: TenancyConfig;
    /** Max request body size in bytes (413 above this). Default 10 MB. */
    maxBodyBytes?: number;
    /** HMR hub for browser live-update sockets (dev only). */
    hub?: HmrHub;
    /**
     * Cross-process realtime bus. When provided, room broadcasts/`toUser` sends are
     * bridged to it so they reach clients on every app process/instance sharing the
     * bus (use the Redis pub/sub driver). Enables realtime across multiple apps.
     */
    realtimeBus?: RealtimeBus;
    /** Shared first-class data/component/page caches. */
    cache?: CacheCoordinator;
    /** Final document transform supplied by the plugin render lifecycle. */
    renderHtml?: (html: string) => string | Promise<string>;
    devToolbar?: {
        config: DevToolbarConfig;
        collector: DevToolbarCollector;
        root: string;
        platform?: DevToolbarPlatformSnapshot;
        panels?: DevToolbarPanel[] | (() => DevToolbarPanel[] | Promise<DevToolbarPanel[]>);
    };
}
interface UpgradeServer {
    upgrade(req: Request, opts: {
        data: WsData;
    }): boolean;
    /** Bun's per-request socket peer address (used for the non-spoofable client IP). */
    requestIP?(req: Request): {
        address: string;
    } | null;
}
/** The subset of Bun's ServerWebSocket the runtime touches. */
interface Ws {
    data: WsData;
    send(data: string | Uint8Array): unknown;
    close(code?: number, reason?: string): void;
}
interface Handlers {
    fetch(req: Request, server: UpgradeServer): Promise<Response | undefined>;
    websocket: {
        open(ws: Ws): void;
        message(ws: Ws, message: string | Uint8Array): void;
        close(ws: Ws, code?: number, reason?: string): void;
        drain(ws: Ws): void;
    };
}
/** Build the fetch + websocket handlers from a set of dependencies. */
declare function createHandlers(deps: RuntimeDeps): Handlers;

/**
 * The multi-app **gateway** — serves several WrNexus apps behind one port and
 * routes each request to the right app by its `Host` header (domain). This is how
 * a monorepo becomes a multi-domain SaaS: `app-a.com` → apps/a, `app-b.com` → apps/b.
 *
 * Each app runs as its own **process** (full isolation — its own database
 * registry, pubsub, in-memory state), and the gateway is a thin host-based
 * reverse proxy for both HTTP and WebSocket. Apps talk to each other at runtime
 * via @wrnexus/pubsub (use the Redis driver so messages cross processes).
 */
type GatewayForwardAuth = ({
    url: string;
    app?: never;
    path?: never;
} | {
    app: string;
    path?: string;
    url?: never;
}) & {
    headers?: string[];
};
interface GatewayAuth {
    basic?: {
        user: string;
        pass: string;
    } | Array<{
        user: string;
        pass: string;
    }>;
    allowIps?: string[];
    forward?: GatewayForwardAuth;
}
interface GatewayApp {
    /** App id (for logs). */
    name: string;
    /** Path to the app root (the dir containing `app/` and wrnexus.config.ts). */
    dir: string;
    /** Host names routed to this app (e.g. ["localhost", "web.localhost"]). */
    domains: string[];
    publicOrigin?: string;
    /** Optional fixed internal port; otherwise assigned from the gateway port. */
    port?: number;
    /** Access control enforced at the edge for this app. */
    auth?: GatewayAuth;
}
/** Gateway-wide security controls, enforced for every app. */
interface GatewayRequestLimits {
    maxUrlLength?: number;
    maxHeaderCount?: number;
    maxHeaderBytes?: number;
    maxQueryParameters?: number;
    maxBodyBytes?: number;
    timeoutMs?: number;
    maxConcurrent?: number;
    fetchMetadata?: boolean;
}
interface GatewayWebSocketSecurity {
    maxMessageBytes?: number;
    maxQueuedMessages?: number;
    allowedOrigins?: string[];
}
interface GatewaySecurity {
    /** Reject requests whose Host matches no app (404) instead of routing to the first. */
    trustedHostsOnly?: boolean;
    /** Global rate limit by client IP (429 over the limit). */
    rateLimit?: {
        max: number;
        windowMs?: number;
    };
    /** Add baseline security headers to responses (only where the app didn't set them). */
    headers?: boolean;
    /** Set X-Forwarded-For/Host/Proto so apps see the real client. Default true. */
    forwardedHeaders?: boolean;
    /** Log each request (host → app, method, path, status). */
    accessLog?: boolean;
    /** URL, header, body, timeout, concurrency, and Fetch Metadata limits. */
    requestLimits?: GatewayRequestLimits;
    /** WebSocket origin, payload, and pre-connect queue limits. */
    websocket?: GatewayWebSocketSecurity;
}
interface GatewayOptions {
    port?: number;
    hostname?: string;
    mode?: "development" | "production";
    environment?: string;
    hmr?: boolean;
    apps: GatewayApp[];
    security?: GatewaySecurity;
}
interface RunningGateway {
    port: number;
    url: string;
    stop(): void;
}
/** Boot every app as a child process, then route by Host on one gateway port. */
declare function startGateway(opts: GatewayOptions): Promise<RunningGateway>;

/**
 * @wrnexus/dev-server/prod — the production server (Point 4).
 *
 * Unlike dev, there is NO filesystem scan and NO on-the-fly bundling at runtime.
 * `wrnexus build` generates an entry that statically imports every route and
 * component module and hands them here as a manifest. We rebuild the (cheap)
 * route-matching tables from the raw patterns and run the exact same request
 * runtime as dev. Normal preview/deploy output has no live-reload client; the
 * supervised `dev --production-runtime` mode can explicitly enable it.
 */

type RouteModule = Record<string, unknown>;
interface ManifestRoute {
    /** URL pattern, e.g. `/users/[id]`. */
    raw: string;
    /** The statically-imported route module. */
    mod: RouteModule;
    /** Body shell precomputed by `wrnexus build` for a partial-static page. */
    staticShell?: string;
}
interface ProdManifest {
    pages: ManifestRoute[];
    api: ManifestRoute[];
    realtime: ManifestRoute[];
    middleware: Middleware[];
    /** Server-rendered components, statically imported and keyed by name. */
    components: {
        name: string;
        mod: RouteModule;
    }[];
    /** Named page layouts (from app/layouts/*.wrn). */
    layouts: {
        name: string;
        mod: RouteModule;
    }[];
    /** RPC service implementations (from app/services/*.ts). */
    services?: {
        name: string;
        mod: RouteModule;
    }[];
}
interface ProductionPluginAsset {
    path: string;
    contentType: string;
    immutable?: boolean;
}
interface ProdOptions {
    /** Absolute path to the pre-built global stylesheet, if any. */
    stylesPath?: string;
    /** Small production stylesheet inlined into the document head. */
    inlineStyles?: string;
    /** `stylesPath` contains theme + UI + app CSS in cascade order. */
    stylesIncludeFramework?: boolean;
    /** `stylesPath` already contains the shared Wire UI stylesheet. */
    stylesIncludeUi?: boolean;
    /** Absolute path to the pre-built reactive runtime. */
    reactivePath?: string;
    /** Absolute path to the on-demand component controller runtime. */
    controllersPath?: string;
    /** Absolute directory containing bundled per-WRN browser modules. */
    clientModulesDir?: string;
    /** Absolute path to the pre-built theme stylesheet (`theme.css`). */
    themePath?: string;
    /** Pre-built active theme/accent stylesheets, loaded on demand. */
    themeAssetsDir?: string;
    /** Absolute path to the pre-built theme runtime (`theme.js`). */
    themeJsPath?: string;
    /** Resolved theme config: enables `<html data-theme>` + `theme.css` link. */
    theme?: ResolvedTheme;
    /** Absolute path to the pre-built Wire UI stylesheet (`ui.css`). */
    uiCssPath?: string;
    /** Combined production theme + Wire UI stylesheet. */
    frameworkCssPath?: string;
    /** Pre-built `window.__wireSchemas = {...}` script for client validation. */
    schemasJs?: string;
    /**
     * Authorization declarations discovered by `wrnexus build` from
     * `app/authz/*.ts`, statically imported into the generated entry (the
     * catalog holds policy FUNCTIONS, so — unlike `schemasJs` — it cannot be
     * JSON-serialised). `module` is `undefined` for a file with no default
     * export. In the NORMAL generated-entry build, the catalog is already set
     * by the generated `.authz-setup.ts` module before this ever runs (see
     * `applyAuthzManifestEarly` below); `createProductionHandlers` merges this
     * same list again as an idempotent second pass — with its warnings — so a
     * caller that bypasses the generated entry and calls it directly still gets
     * a correctly merged catalog.
     */
    authz?: AuthzManifestEntry[];
    /** Resolved i18n bundle (default lang + locale messages). */
    i18n?: ResolvedI18n;
    /** Default database connection (driver + url); enables `getDb()`. */
    db?: {
        driver: string;
        url: string;
    };
    /** Named databases, reached with `getDb("<name>")`. */
    databases?: Record<string, {
        driver: string;
        url: string;
    }>;
    /**
     * Absolute path to the default db's migrations bundled into the build
     * (`dist/migrations`). When set, they are applied on startup — like dev.
     */
    migrationsDir?: string;
    /** Bundled migrations dirs for named dbs (name → `dist/db/<name>/migrations`). */
    databaseMigrationDirs?: Record<string, string>;
    /**
     * Auto-apply bundled migrations on server startup (default: true). Set false
     * for deploys that migrate in a separate release step (e.g. multiple instances
     * behind a load balancer, where you migrate once before rolling out).
     */
    autoMigrate?: boolean;
    /** Realtime scaling: bridge room broadcasts over Redis across app processes. */
    realtime?: {
        scale?: boolean;
        redisUrl?: string;
    };
    /** File-upload storage: named stores (local dir / S3). Local dirs resolve against cwd. */
    storage?: StorageConfig;
    /** Cache-busting version appended to framework asset URLs. */
    assetVersion?: string;
    /** Package browser runtimes already emitted by the production build. */
    clientRuntimes?: ClientRuntimeDefinition[];
    /** Public URL to emitted package asset metadata. */
    pluginAssets?: Record<string, ProductionPluginAsset>;
    /** Absolute path to copied public assets, if any. */
    publicDir?: string;
    /** Raw HTML appended to every page head. */
    head?: string;
    /** Global SEO defaults. */
    seo?: SeoConfig;
    mobile?: MobileConfig;
    pwa?: PwaConfig | false;
    /** Framework security headers and CORS policy. */
    security?: SecurityConfig;
    /** Built-in request tracing and Server-Timing policy. */
    observability?: ObservabilityConfig;
    /** Built-in tenant identity resolution. */
    tenancy?: TenancyConfig;
    /** Page navigation strategy. */
    navigation?: NavigationConfig;
    port?: number;
    hostname?: string;
    maxBodyBytes?: number;
    /** Keep-alive idle timeout in seconds. Defaults to 30. */
    idleTimeout?: number;
    /** Allow multiple Bun workers to share the listening port. */
    reusePort?: boolean;
    /** Enable only for the CLI's supervised exact-production development mode. */
    developmentRuntime?: boolean;
}
/** One `app/authz/*.ts` declaration as passed through `ProdOptions.authz`. */
interface AuthzManifestEntry {
    source: string;
    /** Undefined when the declaration file has no default export. */
    module?: AuthzModule;
}
/**
 * Merge + `setAuthzCatalog` as EARLY as possible, deliberately silently (no
 * missing-default-export warnings). Called ONLY from the generated
 * `.authz-setup.ts` module that `wrnexus build` imports FIRST in the
 * production entry — before any other static import, including app
 * middleware — so that a middleware module reading `getAuthzCatalog()` at its
 * own module scope (the same eager shape `authzMiddleware({ catalog, ... })`
 * itself requires) sees a populated catalog. `createProductionHandlers` below
 * performs the exact same merge again, WITH its warnings, as the canonical,
 * always-warns second pass — this function stays silent specifically so the
 * normal boot path does not print the same "no default export" warning
 * twice. A genuine conflict still throws here (via `mergeCatalogs`), which
 * fails the boot at import time — before the entry body, and thus
 * `createProductionHandlers`, ever runs.
 */
declare function applyAuthzManifestEarly(entries: AuthzManifestEntry[]): void;
/**
 * Build the portable request handler from a precompiled manifest — a
 * WinterCG-style `fetch(request) => Response` plus the websocket handlers, with
 * NO server bound. This is the deployment-adapter seam: `createProductionServer`
 * wraps it in `Bun.serve`, `serveNode` bridges it onto `node:http`, and edge or
 * serverless targets can call `fetch` directly.
 */
declare function createProductionHandlers(manifest: ProdManifest, opts: ProdOptions): ReturnType<typeof createHandlers>;
/** Start the production server on Bun from a precompiled manifest. */
declare function createProductionServer(manifest: ProdManifest, opts: ProdOptions): Promise<Bun.Server<WsData>>;

/**
 * node:http adapter — bridge a WinterCG `fetch(request) => Response` handler
 * onto a Node HTTP server, with no external dependencies. Converts a Node
 * `IncomingMessage` into a web `Request` and writes a web `Response` back into a
 * `ServerResponse` (preserving multiple `Set-Cookie` headers).
 *
 * Caveat: the production handler uses Bun-native APIs (Bun.file for assets,
 * Bun.serve for websockets, Bun.SQL / bun:sqlite for the database), so running
 * the FULL app under plain Node needs Bun-compatible globals. This adapter is
 * for WinterCG hosts and for embedding the handler behind an existing
 * `node:http` server; the Request/Response conversion itself is fully portable.
 */

type FetchHandler = (req: Request) => Response | undefined | Promise<Response | undefined>;
/** Convert a Node IncomingMessage into a web Request (buffers the body). */
declare function toRequest(req: IncomingMessage, opts?: {
    origin?: string;
}): Promise<Request>;
/** Write a web Response into a Node ServerResponse. */
declare function writeResponse(res: ServerResponse, response: Response): Promise<void>;
/** A `node:http` request listener that dispatches to a fetch handler. */
declare function nodeListener(handler: FetchHandler, opts?: {
    origin?: string;
}): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
/** Create and start a `node:http` server for a fetch handler. */
declare function serveNode(handler: FetchHandler, opts?: {
    port?: number;
    hostname?: string;
}): Promise<Server>;

/**
 * @wrnexus/dev-server — the development HTTP + WebSocket server.
 *
 * Thin Bun.serve wrapper around the shared runtime (runtime.ts). Dynamic module
 * loading and targeted cache invalidation keep page/component/API edits inside
 * the running process while the HMR socket morphs fresh HTML into the browser.
 */

interface ServeOptions {
    appDir: string;
    port?: number;
    hostname?: string;
    /** Development TLS material. Production TLS is normally terminated by the deployment proxy. */
    tls?: {
        cert: string;
        key: string;
    };
    mode?: Mode;
    /** Inject the live-reload client (defaults to true in development). */
    hmr?: boolean;
    appConfig?: Record<string, unknown>;
    /** Resolved absolute path to the global CSS entry, or null. */
    styleEntry?: string | null;
    /** Custom styles config (e.g. a Tailwind/PostCSS processor). */
    stylesConfig?: StylesConfig;
    /** Raw HTML appended to every page head (from wrnexus.config.ts). */
    head?: string;
    /** Global SEO defaults. */
    seo?: SeoConfig;
    /** Framework security headers and CORS policy. */
    security?: SecurityConfig;
    /** Design-token theme config (merged over the built-in light/dark). */
    theme?: ThemeConfig;
    /** i18n config (default language + supported locales). */
    i18n?: I18nConfig;
    /** Default database connection (driver + url). Enables `getDb()` and dev auto-migrate. */
    db?: {
        driver: string;
        url: string;
    };
    /** Named databases, reached with `getDb("<name>")`; migrations under app/db/<name>/. */
    databases?: Record<string, {
        driver: string;
        url: string;
    }>;
    /** Realtime scaling: bridge room broadcasts over Redis across app processes. */
    realtime?: {
        scale?: boolean;
        redisUrl?: string;
    };
    /** File-upload storage: named stores (local dir / S3), reached with `getStore()`. */
    storage?: StorageConfig;
    mobile?: MobileConfig;
    pwa?: PwaConfig | false;
    devToolbar?: boolean | DevToolbarConfig;
    plugins?: PluginInput;
    observability?: ObservabilityConfig;
    tenancy?: TenancyConfig;
    navigation?: NavigationConfig;
}
interface RunningServer {
    port: number;
    hostname: string;
    url: string;
    router: Router;
    stop(): void;
}
declare function startServer(opts: ServeOptions): Promise<RunningServer>;

export { type AssetServer, type AuthzManifestEntry, type FetchHandler, type GatewayApp, type GatewayAuth, type GatewayOptions, type GatewaySecurity, RESTART_EXIT_CODE, type RunningGateway, type RunningServer, type RuntimeDeps, type ServeOptions, type WrnCompileMetrics, type WsData, applyAuthzManifestEarly, createHandlers, createProductionHandlers, createProductionServer, expandStaticComponents, getWrnCompileMetrics, nodeListener, precomputePartialStaticShell, resetWrnCompileMetrics, serveNode, startGateway, startServer, toRequest, writeResponse };
```

---

## @wrnexus/dev-toolbar

Documentation URL: https://wrnexusjs.dev/packages/dev-toolbar

# @wrnexus/dev-toolbar

Development-only page quality toolbar for WRNexusJS.

## Features

- Runtime, resource and unhandled promise error capture
- Accessibility, SEO, image, media, color, HTML, form, link, responsive and security checks
- Performance and network observations
- First-class application tabs for runtime, stores, cache, accessibility, SEO, performance,
  security, images, links, and JavaScript
- Plugin-contributed applications with badges, descriptions, issue feeds, and structured data
- Element highlighting and issue filtering
- Server-side issue collector
- Development-only asset strings for direct serving by `@wrnexus/dev-server`
- Safe open-in-editor helper

## Dev-server integration

Serve `DEV_TOOLBAR_RUNTIME` at `/__wrnexus/dev-toolbar.js` and `DEV_TOOLBAR_CSS` at `/__wrnexus/dev-toolbar.css`, then inject this before `</body>` in development:

```html
<script type="module" src="/__wrnexus/dev-toolbar.js" data-wrnexus-dev-toolbar></script>
```

The browser runtime exposes `window.__wrnexusDevToolbar`.

Plugin panels returned through `devToolbarPanels()` are automatically added to the application
strip. Their issue category is filterable, and structured `data` is rendered as escaped diagnostic
content so a plugin never needs to inject toolbar HTML.

### Exported TypeScript declarations

```ts
export { DevToolbarCategory, DevToolbarClientApi, DevToolbarConfig, DevToolbarElementTarget, DevToolbarFix, DevToolbarIssue, DevToolbarMetrics, DevToolbarPageReport, DevToolbarPanel, DevToolbarPlatformSnapshot, DevToolbarServerMessage, DevToolbarSeverity, DevToolbarSourceLocation } from './types.js';
export { DEV_TOOLBAR_RULES, DevToolbarRule, DevToolbarRuleContext, accessibilityRules, accessibleName, colorRules, contrastRatio, createFingerprint, createIssue, effectiveBackground, formRules, getStableSelector, htmlRules, imageRules, isVisible, linkRules, luminance, mediaRules, parseRgb, parseSource, performanceRules, responsiveRules, runDevToolbarRules, securityRules, seoRules } from './rules/index.js';
export { BuiltinPanelOptions, DevToolbarApp, DevToolbarCollector, DevToolbarIssueListener, DevToolbarRegistry, DevToolbarRouteOptions, OpenEditorOptions, OpenEditorRequest, buildEditorCommand, builtinDevToolbarPanels, createDevToolbarCollector, createDevToolbarRegistry, createServerIssue, handleDevToolbarRoute, issueFromError, openInEditor, resolveEditorFile, serializeDevToolbarJson } from './server/index.js';
export { DEV_TOOLBAR_CSS, DEV_TOOLBAR_RUNTIME } from './client/index.js';
```

---

## @wrnexus/encryption

Documentation URL: https://wrnexusjs.dev/packages/encryption

# @wrnexus/encryption

Authenticated encryption, hashing, HMAC, key rotation, and optional encrypted HTTP exchanges for WRNexusJS.

## Core helpers

- `generateKey()` — random 256-bit AES key encoded as base64.
- `deriveKey(password, salt)` — PBKDF2-derived AES key.
- `encrypt(plaintext, key)` / `decrypt(payload, key)` — AES-256-GCM.
- `sha256(data)` — SHA-256 digest.
- `hmacSign(data, secret)` / `hmacVerify(...)` — HMAC-SHA256.
- `createKeyring(keys)` — active/previous key management.
- `seal()` / `open()` — versioned ciphertext with key ID.

## Encrypted HTTP envelope

```ts
import {
  createEncryptedRequest,
  createKeyring,
  createMemoryReplayStore,
  decryptEncryptedResponse,
  encryptedExchange,
} from "@wrnexus/encryption";

const keyring = createKeyring([{ id: "2026-08", secret: process.env.API_BODY_KEY!, active: true }]);

const replayStore = createMemoryReplayStore();

// Server middleware.
app.use(
  encryptedExchange({
    keyring,
    replayStore,
    maxAgeMs: 60_000,
    maxBodyBytes: 1_048_576,
  }),
);

// Controlled service/native client.
const request = await createEncryptedRequest(
  "https://api.example.com/private/report",
  { reportId: "report-1" },
  { method: "POST", keyring },
);
const response = await fetch(request);
const result = await decryptEncryptedResponse(response, request, { keyring });
```

The envelope binds authenticated ciphertext to:

- HTTP method
- URL path and query
- request ID
- timestamp and expiry window
- encryption key ID
- optional replay-store consumption

`encryptedBody()` decrypts request bodies only. `encryptedExchange()` also encrypts successful downstream responses while allowing application exceptions to propagate normally. `encryptedFetch()` provides a convenient controlled-client call.

## Security boundary

Encrypted HTTP bodies **do not replace TLS/HTTPS**. Always use HTTPS.

This layer is appropriate for service-to-service traffic, native/mobile applications, controlled agents, and selected fields protected with server-managed keys. It cannot conceal data from an end user when browser JavaScript receives the decryption key. Never ship a long-lived server encryption key to a browser.

Use a shared replay store such as Redis in multi-instance deployments. The memory replay store is process-local.

### Exported TypeScript declarations

```ts
import { Middleware } from '@wrnexus/core';

interface EncryptionKey {
    id: string;
    secret: string;
    active?: boolean;
    createdAt?: number;
}
interface EncryptionKeyring {
    active(): EncryptionKey;
    get(id: string): EncryptionKey | undefined;
    keys(): EncryptionKey[];
    rotate(key?: EncryptionKey): Promise<EncryptionKey>;
    remove(id: string): boolean;
}
declare function createKeyring(initial: EncryptionKey[]): EncryptionKeyring;
/** Versioned payload: `wrn1.<key-id>.<aes-gcm-payload>`. */
declare function seal(plaintext: string, keyring: EncryptionKeyring): Promise<string>;
declare function open(sealed: string, keyring: EncryptionKeyring): Promise<string>;
declare function sealedKeyId(sealed: string): string | null;
declare function needsRotation(sealed: string, keyring: EncryptionKeyring): boolean;

declare const ENCRYPTED_HTTP_CONTENT_TYPE = "application/wrn+json";
declare const ENCRYPTED_HTTP_VERSION = "wrn-http-1";
interface EncryptedHttpEnvelope {
    version: typeof ENCRYPTED_HTTP_VERSION;
    keyId: string;
    requestId: string;
    timestamp: number;
    ciphertext: string;
}
interface ReplayStore {
    consume(id: string, expiresAt: number): boolean | Promise<boolean>;
}
interface EncryptedHttpOptions {
    keyring: EncryptionKeyring;
    maxAgeMs?: number;
    maxBodyBytes?: number;
    replayStore?: ReplayStore;
    now?: () => number;
    /** Require the clear request-id header used to bind encrypted responses. Default true. */
    requireRequestIdHeader?: boolean;
}
interface DecryptedHttpBody<T> {
    body: T;
    requestId: string;
    timestamp: number;
    keyId: string;
}
declare function createMemoryReplayStore(now?: () => number): ReplayStore;
declare function encryptHttpBody<T>(body: T, input: {
    keyring: EncryptionKeyring;
    method?: string;
    url: string | URL;
    requestId?: string;
    timestamp?: number;
}): Promise<EncryptedHttpEnvelope>;
declare function decryptHttpBody<T>(value: unknown, input: {
    keyring: EncryptionKeyring;
    method?: string;
    url: string | URL;
    maxAgeMs?: number;
    replayStore?: ReplayStore;
    now?: () => number;
    expectedRequestId?: string;
}): Promise<DecryptedHttpBody<T>>;
declare function createEncryptedRequest<T>(url: string | URL, body: T, input: Omit<RequestInit, "body"> & {
    keyring: EncryptionKeyring;
    requestId?: string;
}): Promise<Request>;
declare function decryptRequest<T>(request: Request, options: EncryptedHttpOptions): Promise<DecryptedHttpBody<T>>;
declare function encryptResponse<T>(body: T, request: Request, options: EncryptedHttpOptions & {
    status?: number;
    headers?: HeadersInit;
}): Promise<Response>;
declare function decryptEncryptedResponse<T>(response: Response, request: Request, options: EncryptedHttpOptions): Promise<DecryptedHttpBody<T>>;
declare function encryptedFetch<TRequest, TResponse>(url: string | URL, body: TRequest, input: Omit<RequestInit, "body"> & EncryptedHttpOptions): Promise<TResponse>;
declare function encryptedBody(options: EncryptedHttpOptions): Middleware;
declare function encryptedExchange(options: EncryptedHttpOptions & {
    encryptResponses?: boolean;
}): Middleware;

/**
 * @wrnexus/encryption — authenticated symmetric encryption (AES-256-GCM) via
 * WebCrypto, dependency-free. Use it to encrypt secrets, tokens, or database
 * fields at rest.
 *
 *   const key = await generateKey();                 // store this safely
 *   const box = await encrypt("card #1234", key);    // opaque base64 string
 *   const plain = await decrypt(box, key);           // "card #1234"
 *
 * A key derived from a password (PBKDF2) is also supported via `deriveKey`.
 */
/** SHA-256 hex digest of a string (e.g. content hashing, dedup keys). */
declare function sha256(data: string): Promise<string>;
/** HMAC-SHA256 hex signature of `data` with `secret` (e.g. signing webhooks). */
declare function hmacSign(data: string, secret: string): Promise<string>;
/** Constant-time verify of an HMAC-SHA256 signature. */
declare function hmacVerify(data: string, secret: string, signature: string): Promise<boolean>;
/** Generate a random 256-bit key, base64-encoded. Store it as a secret. */
declare function generateKey(): Promise<string>;
/**
 * Encrypt a string. Output is base64 of `iv(12) || ciphertext+tag`, safe to
 * store or transmit. Each call uses a fresh random IV.
 */
declare function encrypt(plaintext: string, key: string): Promise<string>;
/** Decrypt a value produced by `encrypt`. Throws if the key is wrong or data tampered. */
declare function decrypt(payload: string, key: string): Promise<string>;
/** Derive a base64 AES key from a password + salt (PBKDF2, 100k iterations). */
declare function deriveKey(password: string, salt: string): Promise<string>;

export { type DecryptedHttpBody, ENCRYPTED_HTTP_CONTENT_TYPE, ENCRYPTED_HTTP_VERSION, type EncryptedHttpEnvelope, type EncryptedHttpOptions, type EncryptionKey, type EncryptionKeyring, type ReplayStore, createEncryptedRequest, createKeyring, createMemoryReplayStore, decrypt, decryptEncryptedResponse, decryptHttpBody, decryptRequest, deriveKey, encrypt, encryptHttpBody, encryptResponse, encryptedBody, encryptedExchange, encryptedFetch, generateKey, hmacSign, hmacVerify, needsRotation, open, seal, sealedKeyId, sha256 };
```

---

## @wrnexus/graphql

Documentation URL: https://wrnexusjs.dev/packages/graphql

# @wrnexus/graphql

Optional GraphQL endpoint plugin. Supply the executor from GraphQL.js, GraphQL Yoga, Mercurius,
or another maintained engine; WRNexus owns bounded HTTP input, depth/alias limits, introspection
policy, generic production errors and plugin route integration.

### Exported TypeScript declarations

```ts
export { G as GraphqlExecutionResult, a as GraphqlOptions, b as GraphqlRequest, c as createGraphqlHandler, default as graphqlPlugin } from './plugin.js';
import '@wrnexus/plugin';
```

---

## @wrnexus/helpers

Documentation URL: https://wrnexusjs.dev/packages/helpers

# @wrnexus/helpers

Safe convenience helpers for common WrNexus application flows. The package uses
standard `Context`, `URL`, and `Response` values and has no runtime dependency beyond
`@wrnexus/core`.

## Installation

```bash
bun add @wrnexus/helpers
```

The package is private, so the machine must be authenticated to the `wrnexus` npm
organization.

## Usage

### Redirect an unauthenticated forward-auth request

The gateway calls an SSO verifier on a different URL from the original application.
These helpers reconstruct the original URL from the gateway headers and safely place it
in the login redirect:

```ts
import type { Context } from "@wrnexus/core";
import { redirectToLogin } from "@wrnexus/helpers";

export const GET = async (ctx: Context) => {
  if (await hasValidSession(ctx)) {
    return new Response(null, { status: 204 });
  }

  return redirectToLogin(ctx, "/login", {
    allowedHosts: ["admin.localhost:3000", "reports.localhost:3000"],
  });
};
```

This creates a response such as:

```text
Location: http://sso.localhost:3000/login?returnTo=http%3A%2F%2Fadmin.localhost%3A3000%2F
```

Always list the application hosts that are valid redirect destinations. Forwarded host
headers are rejected when `allowedHosts` is absent or does not match, preventing an open
redirect.

The SSO hostname is the login destination, not an `allowedHosts` entry. For example,
when protecting `admin.localhost:3000`, keep `admin.localhost:3000` in the allowlist even
though the verifier runs at `sso.localhost:3000`. WRNexus preserves both hosts across a
nested gateway request.

### Support dynamic tenant domains

```ts
import type { Context } from "@wrnexus/core";
import { getOriginalRequestOrigin, redirectToLogin } from "@wrnexus/helpers";

export const GET = async (ctx: Context) => {
  const allowedHosts = (host: string) => host === "example.test" || host.endsWith(".example.test");

  console.info("Authentication requested by", getOriginalRequestOrigin(ctx, { allowedHosts }));
  return redirectToLogin(ctx, "https://auth.example.test/login", {
    allowedHosts,
    returnToParam: "continue",
    status: 303,
  });
};
```

## API

- `getOriginalRequestUrl(ctx, options): URL` — reconstruct the gateway URL.
- `getOriginalRequestOrigin(ctx, options): string` — return only its origin.
- `getOriginalRequestPath(ctx): string` — return its path and query string.
- `getOriginalRequestMethod(ctx): string` — return its HTTP method.
- `redirectToLogin(ctx, loginUrl, options): Response` — create a login redirect with an
  encoded `returnTo` parameter.

For direct requests without gateway headers, URL helpers use `ctx.url`.

### Exported TypeScript declarations

```ts
import { Context } from '@wrnexus/core';

declare function appOrigin(appName: string): string;
declare function appUrl(appName: string, path?: string): string;
declare function currentAppName(): string | undefined;
declare function currentAppOrigin(): string | undefined;
declare function workspaceAppOrigins(): Readonly<Record<string, string>>;
/** Shared DNS suffix for configured workspace apps (for example `staging.example.com`). */
declare function workspaceRootDomain(): string;

interface RetryOptions {
    attempts?: number;
    minDelayMs?: number;
    maxDelayMs?: number;
    factor?: number;
    jitter?: number;
    signal?: AbortSignal;
    retryIf?: (error: unknown, attempt: number) => boolean | Promise<boolean>;
    onRetry?: (error: unknown, attempt: number, delayMs: number) => void | Promise<void>;
}
declare function backoffDelay(attempt: number, options?: Pick<RetryOptions, "minDelayMs" | "maxDelayMs" | "factor" | "jitter">): number;
declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
declare function retry<T>(operation: (attempt: number, signal?: AbortSignal) => Promise<T>, options?: RetryOptions): Promise<T>;
declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message?: string, signal?: AbortSignal): Promise<T>;
declare function stableStringify(value: unknown): string;
declare function safeJsonParse<T>(value: string, fallback: T): T;
declare function clamp(value: number, min: number, max: number): number;
declare function once<T extends (...args: any[]) => any>(fn: T): T;

/**
 * @wrnexus/helpers — safe conveniences for common WrNexus application flows.
 *
 * Helpers stay small and composable. They accept the standard WrNexus Context
 * and return web-platform values such as URL and Response.
 */

type RequestContext = Pick<Context, "req" | "url">;
type AllowedHosts = readonly string[] | ReadonlySet<string> | ((host: string, ctx: RequestContext) => boolean);
interface OriginalRequestOptions {
    /**
     * Hosts that the application permits as redirect destinations. This is
     * required when a proxy supplied X-Forwarded-Host is present.
     */
    allowedHosts?: AllowedHosts;
}
interface LoginRedirectOptions extends OriginalRequestOptions {
    /** Query parameter that receives the original absolute URL. */
    returnToParam?: string;
    /** Browser redirect status. Defaults to 302. */
    status?: 301 | 302 | 303 | 307 | 308;
}
/** Get the original path and query string seen by the gateway. */
declare function getOriginalRequestPath(ctx: RequestContext): string;
/** Get the original HTTP method seen by the gateway. */
declare function getOriginalRequestMethod(ctx: RequestContext): string;
/**
 * Reconstruct the absolute URL that reached the gateway.
 *
 * Forwarded hosts are never trusted implicitly: pass allowedHosts when this is
 * used behind the WrNexus gateway. Direct requests fall back to ctx.url.
 */
declare function getOriginalRequestUrl(ctx: RequestContext, options?: OriginalRequestOptions): URL;
/** Get the original request origin, for example http://admin.localhost:3000. */
declare function getOriginalRequestOrigin(ctx: RequestContext, options?: OriginalRequestOptions): string;
/**
 * Redirect to a login page with the original absolute URL encoded as returnTo.
 * Relative login URLs resolve against the current app (normally the SSO app).
 */
declare function redirectToLogin(ctx: RequestContext, loginUrl: string | URL, options?: LoginRedirectOptions): Response;

export { type AllowedHosts, type LoginRedirectOptions, type OriginalRequestOptions, type RequestContext, type RetryOptions, appOrigin, appUrl, backoffDelay, clamp, currentAppName, currentAppOrigin, getOriginalRequestMethod, getOriginalRequestOrigin, getOriginalRequestPath, getOriginalRequestUrl, once, redirectToLogin, retry, safeJsonParse, sleep, stableStringify, withTimeout, workspaceAppOrigins, workspaceRootDomain };
```

---

## @wrnexus/i18n

Documentation URL: https://wrnexusjs.dev/packages/i18n

# @wrnexus/i18n

Recursive locale loading, fallback resolution, SSR/browser translations, locale formatting, and language UI blocks for WRNexusJS.

## Locale files

Both layouts can be used together:

```text
app/locales/en.json
app/locales/en/common.json
app/locales/en/auth.json
app/locales/mr/common.json
```

Namespaced files become keys such as `common.save` and `auth.signIn`.

```ts
import { loadLocales, makeT, resolveI18n, resolveLang } from "@wrnexus/i18n";

const i18n = resolveI18n(loadLocales("app/locales", { strict: true }), {
  default: "en",
  locales: ["en", "mr", "hi"],
  fallbacks: { "mr-IN": ["mr", "en"] },
  cookie: { name: "wire-lang", sameSite: "Lax", secure: true },
});

const lang = resolveLang(i18n, cookieValue, request.headers.get("accept-language"));
const t = makeT(i18n, lang);
t("common.hello", { name: "Ajay" });
```

## Resolution behavior

- normalized BCP-47-style locale names
- cookie preference
- weighted `Accept-Language`
- wildcard language ranges
- regional base fallback
- explicit fallback chains
- configured default language
- automatic RTL for Arabic, Hebrew, Persian, Urdu, and related languages

Locale JSON is size-limited and rejects prototype-pollution keys. Recursive namespace collisions are resolved safely.

## Views and runtime

```html
<h1 data-t="dashboard.title">Dashboard</h1>
<input t:placeholder="search.placeholder" />
```

Text and translated attributes are resolved during SSR. Active/fallback messages are serialized safely for the language runtime, which rebinds `data-t` markers after client navigation.

Enable `i18nPlugin()` to use:

- `<LanguageSwitcher />`
- `<LocaleStatus />`

`LanguageSwitcher` renders a native `select[data-wire-lang]`. The packaged runtime validates the
selection against the configured locales, writes the configured language cookie, updates the
document `lang`/`dir` attributes, emits `wrnexus:language-change`, and reloads so the next SSR
request uses the same cookie. No application-owned browser script is required.

## Formatting

- `formatNumber`
- `formatCurrency`
- `formatDate`
- `formatRelativeTime`
- `plural`
- `createLocaleFormatter`
- `translationCoverage`
  Localization tooling can extract statically discoverable `t("key")`,
  `i18n.t("key")`, and `data-i18n="key"` usage, compare every locale with a
  reference, and create layout-stressing pseudo-locales:

```ts
import {
  auditLocaleKeys,
  createPseudoLocale,
  extractTranslationKeysFromFiles,
} from "@wrnexus/i18n";

const used = extractTranslationKeysFromFiles(sourceFiles);
const coverage = auditLocaleKeys(messages, "en");
const enXA = createPseudoLocale(messages.en);
const arXB = createPseudoLocale(messages.en, { rtl: true });
```

Pseudo-localization preserves interpolation placeholders and markup tags. RTL
pseudo output uses Unicode direction controls, while runtime direction detection
continues to derive `rtl` from Arabic and other RTL language subtags.

### Exported TypeScript declarations

```ts
import { TFunction } from '@wrnexus/core';
export { I18nPluginOptions, i18nComponentsDir, default as i18nPlugin } from './plugin.js';
import '@wrnexus/plugin';

/**
 * Locale-aware formatting helpers (Intl-based) + pluralization. Pair with the
 * request language (`ctx.lang`) so numbers, dates, and currencies render right
 * for each user.
 */
/** Format a number for a locale (e.g. 1234.5 → "1,234.5"). */
declare function formatNumber(value: number, lang: string, options?: Intl.NumberFormatOptions): string;
/** Format a currency amount (e.g. 9.99, "USD" → "$9.99"). */
declare function formatCurrency(value: number, currency: string, lang: string): string;
/** Format a date/timestamp for a locale. */
declare function formatDate(value: Date | number | string, lang: string, options?: Intl.DateTimeFormatOptions): string;
/** Relative time, e.g. -3 days → "3 days ago" (localized). */
declare function formatRelativeTime(value: number, unit: Intl.RelativeTimeFormatUnit, lang: string): string;
/**
 * Pick a plural form for `count` in `lang` using CLDR rules, e.g.
 * `plural(n, { one: "1 item", other: "# items" }, lang)` — "#" is replaced by n.
 */
declare function plural(count: number, forms: Partial<Record<Intl.LDMLPluralRule, string>>, lang: string): string;

interface ExtractedTranslationKey {
    key: string;
    file?: string;
    offset: number;
}
declare function extractTranslationKeys(source: string, file?: string): ExtractedTranslationKey[];
declare function extractTranslationKeysFromFiles(files: Iterable<string>): ExtractedTranslationKey[];
declare function flattenMessageKeys(messages: Messages, prefix?: string): string[];
declare function auditLocaleKeys(messages: Record<string, Messages>, referenceLocale: string): Record<string, {
    missing: string[];
    extra: string[];
}>;
declare function pseudoLocalize(value: string, options?: {
    rtl?: boolean;
}): string;
declare function createPseudoLocale(messages: Messages, options?: {
    rtl?: boolean;
}): Messages;

declare function flattenMessages(messages: Messages, prefix?: string, output?: Record<string, string>): Record<string, string>;
declare function localeFallbacks(locale: string, fallback?: string): string[];
declare function translationCoverage(i18n: ResolvedI18n): Record<string, {
    translated: number;
    total: number;
    percentage: number;
    missing: string[];
    extra: string[];
}>;
interface LocaleFormatter {
    number(value: number, options?: Intl.NumberFormatOptions): string;
    currency(value: number, currency: string, options?: Omit<Intl.NumberFormatOptions, "style" | "currency">): string;
    date(value: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
    relative(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string;
    list(values: string[], options?: Intl.ListFormatOptions): string;
}
declare function createLocaleFormatter(locale: string, timeZone?: string, calendar?: string): LocaleFormatter;
/** ICU-style plural/select templates with exact values and recursive interpolation. */
declare function formatMessage(template: string, params: Record<string, string | number>, locale: string): string;

/**
 * @wrnexus/i18n — deterministic locale loading, fallback resolution, SSR
 * translation markers, browser translation helpers, and UI language controls.
 */

type Messages = Record<string, unknown>;
interface LocaleLoadOptions {
    /** Throw on invalid JSON instead of warning and continuing. */
    strict?: boolean;
    /** Maximum JSON file size. Default 1 MiB. */
    maxFileBytes?: number;
}
interface I18nCookieConfig {
    name?: string;
    maxAge?: number;
    path?: string;
    sameSite?: "Strict" | "Lax" | "None";
    secure?: boolean;
}
interface I18nConfig {
    default?: string;
    locales?: string[];
    /** Human-readable locale names used by package language controls. */
    labels?: Record<string, string>;
    /** Per-locale fallback override. Example: `{ "fr-CA": ["fr", "en"] }`. */
    fallbacks?: Record<string, string[]>;
    /** Locale direction overrides. Arabic/Hebrew/Persian/Urdu are RTL automatically. */
    direction?: Record<string, "ltr" | "rtl">;
    cookie?: I18nCookieConfig;
    strict?: boolean;
}
interface ResolvedI18n {
    default: string;
    langs: string[];
    messages: Record<string, Messages>;
    fallbacks: Record<string, string[]>;
    direction: Record<string, "ltr" | "rtl">;
    labels: Record<string, string>;
    cookie: Required<I18nCookieConfig>;
}
declare const LANG_COOKIE = "wire-lang";
declare const I18N_JS_HREF = "/__wrnexus/i18n.js";
declare function normalizeLocale(locale: string): string;
/**
 * Load both supported layouts:
 * - `locales/en.json`
 * - `locales/en/common.json`, `locales/en/auth.json`
 *
 * Namespaced files become `messages.en.common` and `messages.en.auth`.
 */
declare function loadLocales(dir: string, options?: LocaleLoadOptions): Record<string, Messages>;
declare function localeDirection(locale: string, overrides?: Record<string, "ltr" | "rtl">): "ltr" | "rtl";
declare function resolveI18n(messages: Record<string, Messages>, config?: I18nConfig): ResolvedI18n;
declare function lookupMessage(messages: Messages | undefined, key: string): string | undefined;
declare function interpolate(message: string, params?: Record<string, string | number>): string;
declare function translationChain(i18n: ResolvedI18n, locale: string): string[];
declare function makeT(i18n: ResolvedI18n, lang: string): TFunction;
/** Deeply apply tenant-specific translations without mutating the shared locale bundle. */
declare function withTenantMessages(i18n: ResolvedI18n, overrides: Record<string, Messages>): ResolvedI18n;
/** Load only common and route-specific messages for one locale. */
declare function loadRouteMessages(directory: string, locale: string, route: string): Messages;
declare function parseAcceptLanguage(value: string | null): string[];
declare function resolveLang(i18n: ResolvedI18n, cookieValue: string | undefined, acceptLanguage: string | null): string;
declare function translateHtml(html: string, t: TFunction): string;
declare function renderI18nData(i18n: ResolvedI18n, lang: string): string;
declare const I18N_RUNTIME: string;

export { type ExtractedTranslationKey, I18N_JS_HREF, I18N_RUNTIME, type I18nConfig, type I18nCookieConfig, LANG_COOKIE, type LocaleFormatter, type LocaleLoadOptions, type Messages, type ResolvedI18n, auditLocaleKeys, createLocaleFormatter, createPseudoLocale, extractTranslationKeys, extractTranslationKeysFromFiles, flattenMessageKeys, flattenMessages, formatCurrency, formatDate, formatMessage, formatNumber, formatRelativeTime, interpolate, loadLocales, loadRouteMessages, localeDirection, localeFallbacks, lookupMessage, makeT, normalizeLocale, parseAcceptLanguage, plural, pseudoLocalize, renderI18nData, resolveI18n, resolveLang, translateHtml, translationChain, translationCoverage, withTenantMessages };
```

---

## @wrnexus/identity

Documentation URL: https://wrnexusjs.dev/packages/identity

# @wrnexus/identity

Enterprise identity and governance for WRNexusJS: OIDC discovery, signed SAML adapter flows,
LDAP/Active Directory synchronization adapters, SCIM provisioning, scoped API keys, service
accounts, approval workflows, consent history, retention, subject export/deletion and audit.

The package complements `@wrnexus/auth` (passkeys, MFA, devices, sessions, OAuth and audited
impersonation) and `@wrnexus/authz` (RBAC, ABAC and policy decisions). Protocol-specific SAML and
directory parsing is supplied through adapters so applications can select a maintained vendor SDK
without weakening framework validation, replay protection or governance auditing.

### Exported TypeScript declarations

```ts
interface OidcMetadata {
    issuer: string;
    authorization_endpoint: string;
    token_endpoint: string;
    userinfo_endpoint?: string;
    jwks_uri: string;
    scopes_supported?: string[];
}
declare function discoverOidc(issuer: string, options?: {
    fetch?: typeof fetch;
}): Promise<OidcMetadata>;
declare function oidcAuthorizationUrl(metadata: OidcMetadata, input: {
    clientId: string;
    redirectUri: string;
    state: string;
    nonce: string;
    codeChallenge: string;
    scopes?: string[];
}): string;
interface EnterpriseIdentity {
    externalId: string;
    username: string;
    displayName?: string;
    email?: string;
    groups: string[];
    active: boolean;
    attributes?: Record<string, unknown>;
}
interface SamlAssertion {
    id: string;
    issuer: string;
    audience: string;
    recipient: string;
    expiresAt: number;
    identity: EnterpriseIdentity;
}
interface SamlAdapter {
    createLoginRequest(input: {
        requestId: string;
        callbackUrl: string;
        relayState: string;
    }): Promise<string> | string;
    verifySignedResponse(response: string): Promise<SamlAssertion>;
}
interface ReplayStore {
    consume(id: string, expiresAt: number): Promise<boolean>;
}
declare function memoryReplayStore(now?: () => number): ReplayStore;
declare function createSamlFederation(options: {
    adapter: SamlAdapter;
    issuer: string;
    audience: string;
    recipient: string;
    replayStore?: ReplayStore;
    now?: () => number;
}): {
    login: (input: {
        requestId: string;
        callbackUrl: string;
        relayState: string;
    }) => Promise<string> | string;
    callback(encodedResponse: string): Promise<EnterpriseIdentity>;
};
interface DirectoryAdapter {
    kind: "ldap" | "active-directory";
    search(input: {
        baseDn: string;
        filter: string;
        attributes: string[];
        signal?: AbortSignal;
    }): Promise<EnterpriseIdentity[]>;
    authenticate?(username: string, password: string, signal?: AbortSignal): Promise<EnterpriseIdentity | null>;
}
declare function syncDirectory(adapter: DirectoryAdapter, options: {
    baseDn: string;
    filter?: string;
    attributes?: string[];
    signal?: AbortSignal;
    upsert: (identity: EnterpriseIdentity) => void | Promise<void>;
    disableMissing?: (externalIds: string[]) => void | Promise<void>;
}): Promise<{
    provider: "ldap" | "active-directory";
    synchronized: number;
}>;
interface ScimUser extends EnterpriseIdentity {
    id: string;
    /** RFC 7643 field accepted at the HTTP boundary. */
    userName?: string;
    schemas?: string[];
}
interface ScimStore {
    list(): Promise<ScimUser[]>;
    get(id: string): Promise<ScimUser | null>;
    create(user: Omit<ScimUser, "id">): Promise<ScimUser>;
    update(id: string, user: Partial<ScimUser>): Promise<ScimUser | null>;
    delete(id: string): Promise<boolean>;
}
declare function memoryScimStore(): ScimStore;
declare function createScimHandler(options: {
    store: ScimStore;
    bearerToken: string;
    basePath?: string;
    maxBodyBytes?: number;
}): (request: Request) => Promise<Response>;
interface MachineCredential {
    id: string;
    ownerId: string;
    kind: "api-key" | "service-account";
    name: string;
    scopes: string[];
    secretHash: string;
    createdAt: number;
    expiresAt?: number;
    revokedAt?: number;
}
declare function createMachineIdentityManager(now?: () => number): {
    issue(input: {
        ownerId: string;
        name: string;
        scopes: string[];
        kind?: MachineCredential["kind"];
        expiresAt?: number;
    }): Promise<{
        secret: string;
        credential: {
            secretHash: string;
            id: string;
            ownerId: string;
            kind: "api-key" | "service-account";
            name: string;
            scopes: string[];
            createdAt: number;
            expiresAt?: number;
            revokedAt?: number;
        };
    }>;
    authenticate(secret: string, requiredScope?: string): Promise<{
        secretHash: string;
        id: string;
        ownerId: string;
        kind: "api-key" | "service-account";
        name: string;
        scopes: string[];
        createdAt: number;
        expiresAt?: number;
        revokedAt?: number;
    } | null>;
    revoke(id: string): boolean;
    list(ownerId: string): {
        secretHash: string;
        id: string;
        ownerId: string;
        kind: "api-key" | "service-account";
        name: string;
        scopes: string[];
        createdAt: number;
        expiresAt?: number;
        revokedAt?: number;
    }[];
};
interface GovernanceEvent {
    id: string;
    type: string;
    subjectId: string;
    actorId?: string;
    createdAt: number;
    data?: Record<string, unknown>;
}
declare function createGovernance(options?: {
    now?: () => number;
    audit?: (event: GovernanceEvent) => void | Promise<void>;
    exportSubject?: (subjectId: string) => unknown | Promise<unknown>;
    deleteSubject?: (subjectId: string) => void | Promise<void>;
}): {
    consent(subjectId: string, purpose: string, granted: boolean, version: string): Promise<{
        granted: boolean;
        version: string;
        at: number;
    }>;
    consents(subjectId: string): {
        [k: string]: {
            granted: boolean;
            version: string;
            at: number;
        };
    };
    request(subjectId: string, action: "export" | "delete"): Promise<{
        id: `${string}-${string}-${string}-${string}-${string}`;
        subjectId: string;
        action: "export" | "delete";
        status: "pending";
        requestedAt: number;
    }>;
    decide(id: string, actorId: string, approved: boolean): Promise<{
        decision: {
            status: "approved" | "rejected";
            decidedAt: number;
            decidedBy: string;
            id: string;
            subjectId: string;
            action: "export" | "delete";
            requestedAt: number;
        };
        result: unknown;
    }>;
    enforceRetention(records: Array<{
        subjectId: string;
        createdAt: number;
    }>, maxAgeMs: number, remove: (record: {
        subjectId: string;
        createdAt: number;
    }) => void | Promise<void>): Promise<number>;
};

export { type DirectoryAdapter, type EnterpriseIdentity, type GovernanceEvent, type MachineCredential, type OidcMetadata, type ReplayStore, type SamlAdapter, type SamlAssertion, type ScimStore, type ScimUser, createGovernance, createMachineIdentityManager, createSamlFederation, createScimHandler, discoverOidc, memoryReplayStore, memoryScimStore, oidcAuthorizationUrl, syncDirectory };
```

---

## @wrnexus/image

Documentation URL: https://wrnexusjs.dev/packages/image

# @wrnexus/image

Secure responsive-image planning, loader adapters, picture sources, preload hints, placeholders, and performance auditing for WRNexusJS.

Build-time conversion is available through `optimizeImage`. It normalizes and
bounds width/format variants, prevents variant explosions, writes deterministic
filenames, and returns a manifest with dimensions and byte sizes:

```ts
import { optimizeImage } from "@wrnexus/image";

const manifest = await optimizeImage("public/hero.jpg", {
  outputDir: "public/generated/images",
  widths: [480, 960, 1440],
  formats: ["avif", "webp"],
  quality: 80,
});
```

Install the optional `sharp` peer (`bun add sharp`) for the default AVIF/WebP
processor. Build systems can instead supply an `ImageProcessor` adapter, which
also makes transformation pipelines deterministic in tests.

## Helper API

```ts
import {
  createResponsiveImage,
  createPicture,
  createCdnImageLoader,
  createPathImageLoader,
  createBlurPlaceholder,
  imagePreload,
  auditImage,
} from "@wrnexus/image";

const loader = createCdnImageLoader("https://images.example.com/transform");
const picture = createPicture({
  src: "/hero.jpg",
  alt: "Product dashboard",
  width: 1600,
  height: 900,
  widths: [480, 768, 1200, 1600],
  formats: ["avif", "webp"],
  sizes: "(max-width: 768px) 100vw, 1200px",
  fetchPriority: "high",
  loader,
});
```

Remote loaders require HTTPS. Source URLs are validated, dimensions and quality are bounded, placeholder colors are restricted to safe CSS colors, and preload attributes are escaped.

## Components

Enable `imagePlugin()` and use:

- `<OptimizedImage />`
- `<Picture />`
- `<ImageCard />`

The package-owned blocks compose `@wrnexus/ui` where a complete UI block is appropriate while keeping the low-level image element lightweight.

### Exported TypeScript declarations

```ts
export { ImagePluginOptions, imageComponentsDir, default as imagePlugin } from './plugin.js';
import '@wrnexus/plugin';

interface ImageProcessorResult {
    data: Uint8Array;
    width: number;
    height: number;
}
interface ImageProcessor {
    transform(input: string, options: {
        width: number;
        format: Exclude<ImageFormat, "original">;
        quality: number;
    }): Promise<ImageProcessorResult>;
}
interface OptimizeImageOptions {
    outputDir: string;
    widths: number[];
    formats?: Array<Exclude<ImageFormat, "original">>;
    quality?: number;
    maxVariants?: number;
    processor?: ImageProcessor;
}
interface OptimizedImageVariant {
    path: string;
    width: number;
    height: number;
    format: Exclude<ImageFormat, "original">;
    bytes: number;
}
interface OptimizedImageManifest {
    source: string;
    variants: OptimizedImageVariant[];
}
declare function optimizeImage(input: string, options: OptimizeImageOptions): Promise<OptimizedImageManifest>;

type ImageFormat = "avif" | "webp" | "jpeg" | "png" | "original";
interface ImageLoaderInput {
    src: string;
    width: number;
    quality?: number;
    format?: ImageFormat;
}
type ImageLoader = (input: ImageLoaderInput) => string;
interface ImagePolicy {
    remoteHosts?: string[];
    allowedProtocols?: string[];
    maxWidth?: number;
    maxQuality?: number;
}
interface ResponsiveImageOptions extends ImagePolicy {
    src: string;
    alt: string;
    width: number;
    height: number;
    widths?: number[];
    sizes?: string;
    quality?: number;
    format?: ImageFormat;
    loading?: "eager" | "lazy";
    fetchPriority?: "high" | "low" | "auto";
    decoding?: "async" | "sync" | "auto";
    loader?: ImageLoader;
    class?: string;
}
interface ResponsiveImageAttributes {
    src: string;
    srcset?: string;
    sizes?: string;
    alt: string;
    width: string;
    height: string;
    loading: "eager" | "lazy";
    decoding: "async" | "sync" | "auto";
    fetchpriority?: "high" | "low" | "auto";
    class?: string;
}
interface ImageAuditInput {
    src: string;
    width?: number;
    height?: number;
    renderedWidth?: number;
    bytes?: number;
    loading?: string;
    fetchPriority?: string;
    isLcp?: boolean;
}
interface ImageAuditIssue {
    code: string;
    severity: "error" | "warning" | "info";
    message: string;
}
declare const defaultImageLoader: ImageLoader;
declare function createResponsiveImage(options: ResponsiveImageOptions): ResponsiveImageAttributes;
declare function auditImage(input: ImageAuditInput): ImageAuditIssue[];
interface PictureSource {
    type: string;
    srcset: string;
    sizes?: string;
}
interface PicturePlan {
    image: ResponsiveImageAttributes;
    sources: PictureSource[];
}
declare function normalizeImageWidths(widths: readonly number[], options?: {
    min?: number;
    max?: number;
}): number[];
declare function createCdnImageLoader(baseUrl: string, options?: {
    sourceParam?: string;
    widthParam?: string;
    qualityParam?: string;
    formatParam?: string;
}): ImageLoader;
declare function createPathImageLoader(prefix?: string): ImageLoader;
declare function createPicture(options: ResponsiveImageOptions & {
    formats?: ImageFormat[];
}): PicturePlan;
declare function createBlurPlaceholder(options?: {
    width?: number;
    height?: number;
    color?: string;
    accent?: string;
}): string;
declare function imagePreload(image: ResponsiveImageAttributes, options?: {
    as?: string;
    type?: string;
    crossOrigin?: "anonymous" | "use-credentials";
}): string;
declare function imageCacheKey(input: ImageLoaderInput): string;

export { type ImageAuditInput, type ImageAuditIssue, type ImageFormat, type ImageLoader, type ImageLoaderInput, type ImagePolicy, type ImageProcessor, type ImageProcessorResult, type OptimizeImageOptions, type OptimizedImageManifest, type OptimizedImageVariant, type PicturePlan, type PictureSource, type ResponsiveImageAttributes, type ResponsiveImageOptions, auditImage, createBlurPlaceholder, createCdnImageLoader, createPathImageLoader, createPicture, createResponsiveImage, defaultImageLoader, imageCacheKey, imagePreload, normalizeImageWidths, optimizeImage };
```

---

## @wrnexus/jwt

Documentation URL: https://wrnexusjs.dev/packages/jwt

# @wrnexus/jwt

> Dependency-free JSON Web Tokens (HS256) via Web Crypto, plus a bearer-token auth middleware for WrNexus.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

`@wrnexus/jwt` signs and verifies stateless JSON Web Tokens using the **HS256**
(HMAC-SHA-256) algorithm. It has no runtime dependencies — signing and
verification are implemented directly on the standard **Web Crypto** API
(`crypto.subtle`), which Bun provides natively. It runs server-side and pairs
with the session-based auth in `@wrnexus/core`, giving you a stateless option
for API and mobile clients. Reach for it when you need bearer-token auth rather
than cookie sessions.

## Installation

```bash
bun add @wrnexus/jwt
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

## API

Single entry point (`@wrnexus/jwt`). All functions are async and return Promises.

| Export                                  | Kind      | Description                                                     |
| --------------------------------------- | --------- | --------------------------------------------------------------- |
| `signJwt(payload, secret, options?)`    | function  | Sign claims into an HS256 token string.                         |
| `verifyJwt<T>(token, secret, options?)` | function  | Verify a token and return its claims, or throw.                 |
| `jwtAuth(options)`                      | function  | Middleware that verifies a bearer JWT and sets `ctx.user`.      |
| `JwtError`                              | class     | Error thrown on any signature/payload/expiry failure.           |
| `JwtClaims`                             | interface | Claims shape (`sub`, `iat`, `exp`, `nbf`, plus arbitrary keys). |
| `SignOptions`                           | interface | Options for `signJwt`.                                          |
| `JwtAuthOptions`                        | interface | Options for `jwtAuth`.                                          |

### `signJwt(payload, secret, options?)`

```ts
function signJwt(payload: JwtClaims, secret: string, options?: SignOptions): Promise<string>;
```

Signs `payload` with `secret` using HS256 and returns the encoded token
(`header.body.signature`). An `iat` (issued-at) claim is always added.

`SignOptions`:

- `expiresIn?: number` — seconds until expiry; sets the `exp` claim.
- `now?: number` — override the issued-at time (seconds), useful for testing.

### `verifyJwt<T>(token, secret, options?)`

```ts
function verifyJwt<T extends JwtClaims = JwtClaims>(
  token: string,
  secret: string,
  options?: { now?: number },
): Promise<T>;
```

Verifies the HS256 signature and returns the decoded claims typed as `T`.
Throws `JwtError` when the token is malformed, the signature is invalid, the
payload is not valid JSON, the token is expired (`exp`), or not yet valid
(`nbf`). Pass `now` (seconds) to override the reference time for the `exp`/`nbf`
checks.

### `jwtAuth(options)`

```ts
function jwtAuth(options: JwtAuthOptions): Middleware;
```

Returns a WrNexus `Middleware` that reads a token, verifies it, and assigns the
claims to `ctx.user`.

`JwtAuthOptions`:

- `secret: string` — the HMAC secret used to verify tokens.
- `getToken?: (ctx: Context) => string | undefined` — how to extract the token.
  Defaults to reading `Authorization: Bearer <token>`.
- `required?: boolean` — when `true` (default), a missing or invalid token
  responds with `401 { ok: false, error: "Unauthorized" }`. When `false`,
  requests pass through and `ctx.user` is only set if a valid token is present.

## Usage

```ts
import { signJwt, verifyJwt, jwtAuth, JwtError } from "@wrnexus/jwt";

const secret = process.env.JWT_SECRET!;

// Sign a token that expires in one hour
const token = await signJwt({ sub: user.id, role: "admin" }, secret, {
  expiresIn: 3600,
});

// Verify it later
try {
  const claims = await verifyJwt<{ sub: string; role: string }>(token, secret);
  console.log(claims.sub, claims.role);
} catch (err) {
  if (err instanceof JwtError) {
    // invalid signature, expired, malformed, etc.
  }
}
```

Protecting routes with the middleware:

```ts
import { jwtAuth } from "@wrnexus/jwt";

// Require a valid bearer token; ctx.user holds the verified claims
app.use(jwtAuth({ secret: process.env.JWT_SECRET! }));

// Optional auth — populate ctx.user when present, but don't 401
app.use(jwtAuth({ secret: process.env.JWT_SECRET!, required: false }));
```

## Requirements / Notes

- **Bun-only.** Uses the standard Web Crypto API (`crypto.subtle.importKey`,
  `sign`, `verify`) plus `btoa`/`atob` and `TextEncoder`/`TextDecoder` — all
  provided by Bun. No third-party crypto dependency.
- **Algorithm:** HS256 (HMAC with SHA-256) only. Asymmetric algorithms (RS/ES)
  are not supported.
- Integrates with [`@wrnexus/core`](../core) for `Context`, `Middleware`, and
  `ctx.user`; it complements the framework's cookie/session auth with a
  stateless bearer-token flow for API and mobile clients.

## Access, refresh, scope, and cookie helpers

```ts
import {
  createAccessToken,
  createRefreshToken,
  verifyAccessToken,
  verifyRefreshToken,
  extractBearerToken,
  requireScopes,
  jwtCookie,
} from "@wrnexus/jwt";
```

The helpers add explicit `type: "access" | "refresh"` claims, scope checks, refresh-token family metadata, no-store token responses, and secure cookie defaults. `__Host-` cookies are rejected unless they use `Path=/` and `Secure`; `SameSite=None` is rejected without `Secure`.

## 0.8 helper kit

```ts
import {
  createTokenPair,
  verifyAccessToken,
  verifyRefreshToken,
  extractBearerToken,
  readJwtCookie,
  jwtCookie,
  clearJwtCookie,
  requireScopes,
} from "@wrnexus/jwt";

const pair = await createTokenPair(user.id, {
  accessSecret: process.env.JWT_ACCESS_SECRET!,
  refreshSecret: process.env.JWT_REFRESH_SECRET!,
  scopes: ["profile:read"],
  family: sessionFamily,
});
```

The helper kit validates `__Host-` cookie invariants, cookie names and paths, `SameSite=None` security, typed access/refresh token types, scope requirements, and no-store token responses.
In addition to local HS256 secrets/keyrings, the package verifies standards-based
RS256 tokens through bounded remote JWKS caches:

```ts
import { createRemoteJwks, verifyJwtWithJwks } from "@wrnexus/jwt";

const jwks = createRemoteJwks("https://issuer.example/.well-known/jwks.json");
const claims = await verifyJwtWithJwks(token, jwks, {
  issuer: "https://issuer.example",
  audience: "my-api",
  maxAge: 300,
});
```

JWKS URLs must use HTTPS. Responses have key-count/byte limits, accept only
RS256 signing RSA keys, deduplicate concurrent refreshes, cache imported public
keys, and force an immediate refresh for an unknown `kid` so issuer rotation
does not wait for cache expiry. Never use decoded-but-unverified claims for an
authorization decision.

### Exported TypeScript declarations

```ts
import { Context, Middleware } from '@wrnexus/core';

interface JwtKey {
    id: string;
    secret: string;
    active?: boolean;
}
interface JwtKeyring {
    active(): JwtKey;
    resolve(id: string): JwtKey | undefined;
    keys(): JwtKey[];
}
declare function decodeJwt(token: string): {
    header: Record<string, unknown>;
    claims: JwtClaims;
};
declare function createJwtKeyring(keys: JwtKey[]): JwtKeyring;
declare function signWithKeyring(claims: JwtClaims, keyring: JwtKeyring, options?: SignOptions): Promise<string>;
declare function verifyWithKeyring<T extends JwtClaims = JwtClaims>(token: string, keyring: JwtKeyring, options?: VerifyOptions): Promise<T>;

interface AccessTokenClaims extends JwtClaims {
    sub: string;
    type: "access";
    scopes?: string[];
}
interface RefreshTokenClaims extends JwtClaims {
    sub: string;
    type: "refresh";
    family?: string;
}
declare function extractBearerToken(value: Headers | Request | Context | string | null | undefined): string | undefined;
declare function tryVerifyJwt<T extends JwtClaims = JwtClaims>(token: string | undefined, secret: string, options?: VerifyOptions): Promise<T | null>;
declare function assertJwtClaims<T extends JwtClaims>(claims: T, requirements?: {
    subject?: boolean;
    type?: string;
    required?: string[];
}): T;
declare function tokenScopes(claims: JwtClaims): string[];
declare function hasScopes(claims: JwtClaims, required: readonly string[], mode?: "all" | "any"): boolean;
declare function requireScopes(required: readonly string[], mode?: "all" | "any"): Middleware;
declare function createAccessToken(subject: string, secret: string, options?: Omit<SignOptions, "expiresIn"> & {
    expiresIn?: number;
    scopes?: string[];
    claims?: JwtClaims;
}): Promise<string>;
declare function createRefreshToken(subject: string, secret: string, options?: Omit<SignOptions, "expiresIn"> & {
    expiresIn?: number;
    family?: string;
    claims?: JwtClaims;
}): Promise<string>;
declare function verifyAccessToken(token: string, secret: string, options?: VerifyOptions): Promise<AccessTokenClaims>;
declare function verifyRefreshToken(token: string, secret: string, options?: VerifyOptions): Promise<RefreshTokenClaims>;
declare function readJwtCookie(value: Headers | Request | string | null | undefined, name?: string): string | undefined;
declare function jwtCookie(token: string, options?: {
    name?: string;
    maxAge?: number;
    secure?: boolean;
    sameSite?: "Strict" | "Lax" | "None";
    path?: string;
}): string;
declare function clearJwtCookie(options?: Omit<Parameters<typeof jwtCookie>[1], "maxAge">): string;
interface JwtTokenPair {
    accessToken: string;
    refreshToken: string;
    tokenType: "Bearer";
    expiresIn: number;
}
declare function createTokenPair(subject: string, input: {
    accessSecret: string;
    refreshSecret?: string;
    accessExpiresIn?: number;
    refreshExpiresIn?: number;
    scopes?: string[];
    family?: string;
    accessOptions?: Omit<SignOptions, "expiresIn">;
    refreshOptions?: Omit<SignOptions, "expiresIn">;
}): Promise<JwtTokenPair>;
declare function jwtResponse(accessToken: string, input?: {
    refreshToken?: string;
    expiresIn?: number;
    tokenType?: string;
    scope?: string[];
}): Response;

interface RemoteJwksOptions {
    fetch?: typeof fetch;
    cacheTtlMs?: number;
    maxKeys?: number;
    maxBytes?: number;
    now?: () => number;
}
interface RemoteJwks {
    resolve(kid: string, alg: string): Promise<CryptoKey>;
    refresh(): Promise<void>;
    clear(): void;
    stats(): {
        fetches: number;
        hits: number;
        keys: number;
        expiresAt: number;
    };
}
declare function createRemoteJwks(url: string, options?: RemoteJwksOptions): RemoteJwks;
declare function verifyJwtWithJwks<T extends JwtClaims = JwtClaims>(token: string, jwks: RemoteJwks, options?: VerifyOptions): Promise<T>;

/**
 * @wrnexus/jwt — dependency-free JSON Web Tokens (HS256) via WebCrypto, plus a
 * bearer-token auth middleware. Pairs with the session auth in @wrnexus/core for
 * stateless (API/mobile) authentication.
 *
 *   const token = await signJwt({ sub: user.id, role: "admin" }, secret, { expiresIn: 3600 });
 *   const claims = await verifyJwt(token, secret); // throws JwtError if invalid/expired
 */

declare class JwtError extends Error {
    constructor(message: string);
}
interface JwtClaims {
    /** Subject (user id). */
    sub?: string;
    /** Issued-at (seconds). */
    iat?: number;
    /** Expiry (seconds). */
    exp?: number;
    /** Not-before (seconds). */
    nbf?: number;
    [key: string]: unknown;
}
interface SignOptions {
    /** Seconds until expiry (sets `exp`). */
    expiresIn?: number;
    /** Override issued-at (seconds). */
    now?: number;
    issuer?: string;
    audience?: string | string[];
    jwtId?: string;
    /** Key identifier placed in the protected header. */
    keyId?: string;
}
interface VerifyOptions {
    now?: number;
    clockTolerance?: number;
    issuer?: string;
    audience?: string | string[];
    maxAge?: number;
}
/** Sign a payload into a JWT (HS256). */
declare function signJwt(payload: JwtClaims, secret: string, options?: SignOptions): Promise<string>;
/** Verify a JWT and return its claims. Throws `JwtError` on any failure. */
declare function verifyJwt<T extends JwtClaims = JwtClaims>(token: string, secret: string, options?: VerifyOptions): Promise<T>;
interface JwtAuthOptions {
    secret: string;
    /** Where to read the token. Default: `Authorization: Bearer <token>`. */
    getToken?: (ctx: Context) => string | undefined;
    /** Reject unauthenticated requests with 401. Default true. */
    required?: boolean;
}
/**
 * Middleware that verifies a bearer JWT and sets `ctx.user` to its claims.
 * When `required` (default), a missing/invalid token gets a 401.
 */
declare function jwtAuth(options: JwtAuthOptions): Middleware;

export { type AccessTokenClaims, type JwtAuthOptions, type JwtClaims, JwtError, type JwtKey, type JwtKeyring, type JwtTokenPair, type RefreshTokenClaims, type RemoteJwks, type RemoteJwksOptions, type SignOptions, type VerifyOptions, assertJwtClaims, clearJwtCookie, createAccessToken, createJwtKeyring, createRefreshToken, createRemoteJwks, createTokenPair, decodeJwt, extractBearerToken, hasScopes, jwtAuth, jwtCookie, jwtResponse, readJwtCookie, requireScopes, signJwt, signWithKeyring, tokenScopes, tryVerifyJwt, verifyAccessToken, verifyJwt, verifyJwtWithJwks, verifyRefreshToken, verifyWithKeyring };
```

---

## @wrnexus/language-server

Documentation URL: https://wrnexusjs.dev/packages/language-server

# @wrnexus/language-server

Editor-neutral Language Server Protocol support for `.wrn` files. It uses the canonical
`@wrnexus/syntax` parser, diagnostics, accessibility rules, and formatter.

```bash
bunx wrnexus-language-server --stdio
```

Capabilities include syntax, accessibility and TypeScript expression diagnostics, formatting,
completion, hover, document symbols, go-to-definition, references, rename, and quick fixes.
The custom `wrnexus/virtualDocument` request returns the mapped TypeScript representation of an
open `.wrn` document for editor TypeScript plugins and safe refactoring tools. Any LSP 3.x client can launch the
stdio command. Example Neovim configuration:

```lua
vim.lsp.start({
  name = "wrnexus",
  cmd = { "bunx", "wrnexus-language-server", "--stdio" },
  root_dir = vim.fs.root(0, { "wrnexus.config.ts", "package.json", ".git" }),
})
```

JetBrains users can register the same command through an LSP client/plugin. The server
does not require VS Code and never reads environment secrets or sends source over a
network.

### Exported TypeScript declarations

```ts
interface WorkspaceCompletionItem {
    label: string;
    kind: number;
    detail: string;
    insertText?: string;
    data?: Record<string, unknown>;
}
declare function workspaceCompletionItems(root: string): WorkspaceCompletionItem[];
declare function clearWorkspaceIndexCache(root?: string): void;
declare function extractComponentRefactor(document: TextDocument, range: Range, name: string): {
    documentChanges: ({
        kind: string;
        uri: string;
        textDocument?: undefined;
        edits?: undefined;
    } | {
        textDocument: {
            uri: string;
            version: number | null;
        };
        edits: {
            range: Range;
            newText: string;
        }[];
        kind?: undefined;
        uri?: undefined;
    })[];
};
declare function htmlToWrn(html: string, name?: string): string;

interface Position {
    line: number;
    character: number;
}
interface Range {
    start: Position;
    end: Position;
}
interface TextDocument {
    uri: string;
    text: string;
    version?: number;
}
declare const WRN_COMPLETIONS: readonly ["page", "component", "layout", "props", "outputs", "state", "computed", "effect", "watch", "lifecycle", "load", "action", "api", "realtime", "view", "style", "runtime", "hydrate"];
declare function offsetAt(text: string, position: Position): number;
declare function positionAt(text: string, requestedOffset: number): Position;
declare function wordAt(text: string, position: Position): {
    word: string;
    range: Range;
} | null;
declare function documentDiagnostics(document: TextDocument): {
    range: {
        start: {
            line: number;
            character: number;
        };
        end: {
            line: number;
            character: number;
        };
    };
    severity: number;
    code: string;
    source: string;
    message: string;
}[];
/** TypeScript representation consumed by editor TypeScript plugins and safe refactoring tools. */
declare function virtualTypeScriptDocument(document: TextDocument): {
    uri: string;
    languageId: "typescript";
    text: string;
    mappings: Array<{
        virtualStartLine: number;
        virtualEndLine: number;
        sourceStartLine: number;
        sourceStartColumn: number;
    }>;
};
declare function formatDocument(document: TextDocument, tabSize?: number, insertSpaces?: boolean): {
    range: {
        start: {
            line: number;
            character: number;
        };
        end: Position;
    };
    newText: string;
}[];
declare function documentSymbols(document: TextDocument): {
    name: string;
    kind: number;
    range: {
        start: Position;
        end: Position;
    };
    selectionRange: {
        start: Position;
        end: Position;
    };
}[];
declare function symbolLocations(document: TextDocument, position: Position): {
    uri: string;
    range: {
        start: Position;
        end: Position;
    };
}[];
declare function definitionLocation(document: TextDocument, position: Position): {
    uri: string;
    range: {
        start: Position;
        end: Position;
    };
} | null;
declare function hover(document: TextDocument, position: Position): {
    contents: {
        kind: string;
        value: string;
    };
    range: Range;
} | null;
declare function completionItems(): {
    label: "page" | "component" | "layout" | "props" | "outputs" | "state" | "computed" | "effect" | "watch" | "lifecycle" | "load" | "action" | "api" | "realtime" | "view" | "style" | "runtime" | "hydrate";
    kind: number;
    detail: string;
}[];

export { type Position, type Range, type TextDocument, WRN_COMPLETIONS, type WorkspaceCompletionItem, clearWorkspaceIndexCache, completionItems, definitionLocation, documentDiagnostics, documentSymbols, extractComponentRefactor, formatDocument, hover, htmlToWrn, offsetAt, positionAt, symbolLocations, virtualTypeScriptDocument, wordAt, workspaceCompletionItems };
```

---

## @wrnexus/mcp

Documentation URL: https://wrnexusjs.dev/packages/mcp

# @wrnexus/mcp

Editor-neutral Model Context Protocol server for AI development tools.

```bash
bunx wrnexus-mcp --root=.
```

It exposes current routes, components with props/events, database schema files, compiler diagnostics,
runtime errors, dev-server health, framework documentation and installed packages. Files are resolved
inside the configured application root and returned as bounded structured JSON.

### Exported TypeScript declarations

```ts
interface McpTool {
    name: string;
    description: string;
    inputSchema: {
        type: "object";
        properties?: Record<string, unknown>;
        additionalProperties?: boolean;
    };
}
interface McpServerOptions {
    maxFiles?: number;
    maxFileBytes?: number;
    fetch?: typeof fetch;
    devServerUrl?: string;
    runtimeErrors?: () => unknown[] | Promise<unknown[]>;
}
interface McpServer {
    tools(): McpTool[];
    call(name: string, args?: Record<string, unknown>): Promise<unknown>;
    handle(message: unknown): Promise<Record<string, unknown> | null>;
}
declare function createFrameworkMcpServer(appRoot: string, options?: McpServerOptions): McpServer;

export { type McpServer, type McpServerOptions, type McpTool, createFrameworkMcpServer };
```

---

## @wrnexus/mobile

Documentation URL: https://wrnexusjs.dev/packages/mobile

# @wrnexus/mobile

> SSR-safe access to Capacitor plugins from WRNexusJS browser code.

## Overview

`@wrnexus/mobile` keeps optional native imports out of server rendering while giving
browser-owned modules one consistent registry for Capacitor plugins. During SSR,
`mobile.isNative()` is `false` and `mobile.platform()` is `"web"`.

## Installation

Install a plugin through the WRNexusJS CLI so the web and native projects stay aligned:

```bash
wrnexus mobile add @capacitor/camera @capacitor/haptics
```

## Usage

### Register and invoke a Capacitor plugin

Import Capacitor packages only from browser-owned code, never from API routes or SSR
helpers.

```ts
import { Camera, CameraResultType } from "@capacitor/camera";
import { mobile } from "@wrnexus/mobile";

mobile.registerPlugin("Camera", Camera);

export async function takePhoto() {
  if (!mobile.isNative()) return null;
  return mobile.invoke("Camera", "getPhoto", {
    quality: 85,
    resultType: CameraResultType.Uri,
  });
}
```

### Provide a browser fallback

`whenNative` runs the first callback only in a Capacitor WebView and can return a
web/SSR-safe fallback everywhere else.

```ts
import { Haptics, ImpactStyle } from "@capacitor/haptics";
import { mobile } from "@wrnexus/mobile";

mobile.registerPlugin("Haptics", Haptics);

export const confirmAction = () =>
  mobile.whenNative(
    () => mobile.invoke("Haptics", "impact", { style: ImpactStyle.Medium }),
    () => navigator.vibrate?.(30),
  );
```

### Read an optional plugin without throwing

```ts
import type { NetworkPlugin } from "@capacitor/network";
import { mobile } from "@wrnexus/mobile";

const network = mobile.plugin<NetworkPlugin>("Network");
const status = network ? await network.getStatus() : { connected: true, connectionType: "unknown" };
```

## API

- `registerPlugin(name, instance)` registers a browser-imported plugin.
- `plugin(name)` returns a plugin or `undefined`; `requirePlugin(name)` throws when absent.
- `invoke(plugin, method, options?)` calls a registered method and returns its result.
- `whenNative(native, fallback?)` selects native behavior without breaking SSR.
- `isNative()` and `platform()` report the current Capacitor environment.

Unavailable required plugins throw `MobileUnavailableError` with an actionable message.

The package also provides portable application-facing primitives:

- `listenDeepLinks` normalizes initial and live links with an allowed-scheme list.
- `PushNotifications` performs permission gating and validates registrations.
- `SecureStorage` namespaces and validates keys over an application-supplied encrypted
  Keychain/Keystore adapter; it does not mislabel browser `localStorage` as secure.
- `OfflineQueue` persists bounded sync batches through a pluggable durable store.

## Requirements / Notes

- Capacitor plugin imports must remain in browser-owned modules.
- `@wrnexus/mobile` re-exports `native` from `@wrnexus/native` for applications that
  prefer the higher-level cross-platform capability API.

### Exported TypeScript declarations

```ts
export { native } from '@wrnexus/native';

interface DeepLink {
    url: URL;
    path: string;
    query: URLSearchParams;
}
declare function parseDeepLink(value: string, schemes?: string[]): DeepLink | null;
interface DeepLinkSource {
    current?(): Promise<string | undefined>;
    subscribe(listener: (url: string) => void): void | (() => void);
}
/** Normalize initial and live native links and ignore malformed/unapproved schemes. */
declare function listenDeepLinks(source: DeepLinkSource, listener: (link: DeepLink) => void, schemes?: string[]): () => void;
interface PushRegistration {
    token: string;
    platform?: string;
}
interface PushAdapter {
    permission(): Promise<"granted" | "denied" | "prompt" | "unavailable">;
    requestPermission?(): Promise<"granted" | "denied">;
    register(): Promise<PushRegistration>;
    subscribe?(listener: (notification: unknown) => void): () => void;
}
declare class PushNotifications {
    private readonly adapter;
    constructor(adapter: PushAdapter);
    register(): Promise<PushRegistration>;
    subscribe(listener: (notification: unknown) => void): () => void;
}
interface SecureStorageAdapter {
    get(key: string): Promise<string | null>;
    set(key: string, value: string): Promise<void>;
    remove(key: string): Promise<void>;
}
declare class SecureStorage {
    #private;
    private readonly adapter;
    private readonly namespace;
    constructor(adapter: SecureStorageAdapter, namespace?: string);
    get(key: string): Promise<string | null>;
    set(key: string, value: string): Promise<void>;
    remove(key: string): Promise<void>;
}
interface OfflineTask<T = unknown> {
    id: string;
    type: string;
    payload: T;
    createdAt: number;
    attempts: number;
}
interface OfflineTaskStore {
    load(): Promise<OfflineTask[]>;
    save(tasks: OfflineTask[]): Promise<void>;
}
declare function memoryOfflineTaskStore(): OfflineTaskStore;
declare class OfflineQueue {
    #private;
    private readonly store;
    constructor(store?: OfflineTaskStore);
    process<T>(type: string, handler: (payload: T) => Promise<void>): void;
    add<T>(type: string, payload: T): Promise<OfflineTask<T>>;
    sync(limit?: number): Promise<{
        completed: number;
        failed: number;
    }>;
    size(): Promise<number>;
}
interface MobileEnvironment {
    platform: string;
    native: boolean;
    online: boolean;
    userAgent?: string;
}
declare function mobileEnvironment(): MobileEnvironment;

/** @wrnexus/mobile — SSR-safe access to Capacitor's native bridge. */

type MobilePlatform = "ios" | "android" | "web" | string;
interface CapacitorBridge {
    isNativePlatform?: () => boolean;
    getPlatform?: () => MobilePlatform;
    Plugins?: Record<string, unknown>;
}
declare class MobileUnavailableError extends Error {
    constructor(message?: string);
}
/** Register a plugin imported by browser-only application code. */
declare function registerPlugin<T extends object>(name: string, instance: T): T;
/** True only inside a native Capacitor iOS or Android WebView. SSR-safe. */
declare function isNative(): boolean;
/** Current Capacitor platform, falling back to `web` during SSR and in browsers. */
declare function platform(): MobilePlatform;
/** Return an injected Capacitor plugin, or undefined when it is unavailable. */
declare function plugin<T extends object>(name: string): T | undefined;
/** Require an installed native plugin and produce a useful error when absent. */
declare function requirePlugin<T extends object>(name: string): T;
/** Invoke a plugin method without importing native code into an SSR module. */
declare function invoke<TResult = unknown>(pluginName: string, method: string, options?: unknown): Promise<TResult>;
/** Run native behavior when available, with an optional SSR/web fallback. */
declare function whenNative<T>(native: () => T | Promise<T>, fallback?: () => T | Promise<T>): Promise<T | undefined>;
declare const mobile: {
    isNative: typeof isNative;
    platform: typeof platform;
    registerPlugin: typeof registerPlugin;
    plugin: typeof plugin;
    requirePlugin: typeof requirePlugin;
    invoke: typeof invoke;
    whenNative: typeof whenNative;
};

export { type CapacitorBridge, type DeepLink, type DeepLinkSource, type MobileEnvironment, type MobilePlatform, MobileUnavailableError, OfflineQueue, type OfflineTask, type OfflineTaskStore, type PushAdapter, PushNotifications, type PushRegistration, SecureStorage, type SecureStorageAdapter, invoke, isNative, listenDeepLinks, memoryOfflineTaskStore, mobile, mobileEnvironment, parseDeepLink, platform, plugin, registerPlugin, requirePlugin, whenNative };
```

---

## @wrnexus/native

Documentation URL: https://wrnexusjs.dev/packages/native

# @wrnexus/native

> Cross-platform capabilities for browsers, Capacitor WebViews, and compiled native apps.

## Overview

`@wrnexus/native` exposes capabilities by name so application code can ask what the
current platform supports before presenting an action. Browser capabilities use Web
APIs; mobile capabilities use installed Capacitor plugins. `platform()` returns
`"server"` during SSR, `"browser"` on the web, and the Capacitor platform in a native
WebView.

## Installation

```bash
bun add @wrnexus/native
```

## Usage

### Share a page when the platform supports it

```ts
import { native } from "@wrnexus/native";

export async function shareCurrentPage() {
  if (!native.supports("share")) return false;
  await native.run("share", {
    title: document.title,
    url: location.href,
  });
  return true;
}
```

### Register an application-specific capability

`register` returns an unregister function, which is useful for tests and temporary
feature modules.

```ts
import { native } from "@wrnexus/native";

const unregister = native.register("orders.scan", {
  browser: {
    supported: () => typeof window !== "undefined",
    run: async ({ orderId }: { orderId: string }) => {
      const code = window.prompt(`Scan code for order ${orderId}`);
      return { code };
    },
  },
});

const result = await native.run<{ code: string | null }>("orders.scan", { orderId: "ord_42" });
unregister();
```

### Target browser or mobile behavior explicitly

```ts
import { native } from "@wrnexus/native";

const canUseMobileCamera = native.supports("camera", "mobile");
const position = await native.run(
  "geolocation",
  { enableHighAccuracy: true },
  { target: "browser" },
);
```

## API

- `supports(name, target?)` checks availability without running the capability.
- `run(name, options?, runOptions?)` executes it or rejects with `NativeUnavailableError`.
- `register(name, capability)` adds or overrides a capability and returns cleanup.
- `registered()` lists capability names; `clearRegistry()` resets the registry.
- `isMobile()` and `platform()` report the current target safely during SSR.

Built-ins include `camera`, `clipboard.write`, `share`, `geolocation`, `network`,
`haptics`, storage, filesystem, notifications, and device information.

`defineNativeManifest` declares required capabilities and typed permissions, while
`PermissionManager` normalizes permission query/request flows across platform adapters.

## Requirements / Notes

Use `supports()` before showing optional controls. Mobile capabilities require their
matching Capacitor plugins to be installed and registered by the application.

### Exported TypeScript declarations

```ts
import { N as NativePlatform, a as NativeCapability, b as NativeRunOptions, c as NativeTarget } from './types-CDShWg0i.js';
export { d as NativeAdapter, e as NativeBrowserRuntime } from './types-CDShWg0i.js';
export { browserCapabilities } from './browser.js';
export { mobileCapabilities } from './mobile.js';

declare class NativeUnavailableError extends Error {
    constructor(message: string);
}
declare function isMobile(): boolean;
declare function platform(): NativePlatform;
declare function register<TOptions = unknown, TResult = unknown>(name: string, capability: NativeCapability<TOptions, TResult>): () => void;
declare function registered(): string[];
declare function supports(name: string, target?: NativeTarget): boolean;
declare function run<TResult = unknown>(name: string, options?: unknown, runOptions?: NativeRunOptions): Promise<TResult>;
declare function clearRegistry(): void;

type NativePermission = "camera" | "geolocation" | "microphone" | "notifications" | "photos" | "storage" | (string & {});
interface NativeCapabilityManifestEntry {
    name: string;
    description?: string;
    permissions?: NativePermission[];
    targets?: NativeTarget[];
    optional?: boolean;
}
interface NativeCapabilityManifest {
    name: string;
    version?: string;
    capabilities: NativeCapabilityManifestEntry[];
}
declare function defineNativeManifest<T extends NativeCapabilityManifest>(manifest: T): T;
declare function inspectNativeCapabilities(target?: NativeTarget): Array<{
    name: string;
    supported: boolean;
}>;
declare function missingNativeCapabilities(manifest: NativeCapabilityManifest, target?: NativeTarget): NativeCapabilityManifestEntry[];
interface PermissionAdapter {
    query(name: NativePermission): Promise<"granted" | "denied" | "prompt" | "unavailable">;
    request?(name: NativePermission): Promise<"granted" | "denied">;
}
declare class PermissionManager {
    private readonly adapter;
    constructor(adapter: PermissionAdapter);
    query(name: NativePermission): Promise<"denied" | "granted" | "prompt" | "unavailable">;
    ensure(name: NativePermission): Promise<boolean>;
}

declare const native: {
    isMobile: typeof isMobile;
    platform: typeof platform;
    register: typeof register;
    registered: typeof registered;
    run: typeof run;
    supports: typeof supports;
};

export { NativeCapability, type NativeCapabilityManifest, type NativeCapabilityManifestEntry, type NativePermission, NativePlatform, NativeRunOptions, NativeTarget, NativeUnavailableError, type PermissionAdapter, PermissionManager, clearRegistry, defineNativeManifest, inspectNativeCapabilities, isMobile, missingNativeCapabilities, native, platform, register, registered, run, supports };
```

---

## @wrnexus/oauth

Documentation URL: https://wrnexusjs.dev/packages/oauth

# @wrnexus/oauth

> Dependency-free OAuth 2.0 sign-in for any provider, with PKCE and presets for Google, GitHub, and Discord.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

`@wrnexus/oauth` implements the OAuth 2.0 Authorization Code flow (with PKCE) for
server-side sign-in. It ships ready-made provider presets and a `defineProvider`
helper for custom providers, then gives you two flow functions — `startAuth`
(build the redirect) and `completeAuth` (exchange the code and fetch the user's
profile). It has no runtime dependencies: it uses the platform `fetch` and
WebCrypto only. Pairs naturally with `@wrnexus/core`'s `logIn` to establish a
session once you have a normalized profile.

## Installation

```bash
bun add @wrnexus/oauth
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

## API

### Providers

Each preset takes `ProviderCredentials` and returns an `OAuthProvider`.

```ts
interface ProviderCredentials {
  clientId: string;
  clientSecret: string;
  scopes?: string[]; // override the preset's default scopes
}
```

| Export                   | Default scopes               | Notes                                                              |
| ------------------------ | ---------------------------- | ------------------------------------------------------------------ |
| `google(creds)`          | `openid`, `email`, `profile` | Sets `access_type: offline` for refresh tokens.                    |
| `github(creds)`          | `read:user`, `user:email`    | Maps `name` (falls back to `login`) and `avatar_url`.              |
| `discord(creds)`         | `identify`, `email`          | Builds the avatar CDN URL from the user id + hash.                 |
| `defineProvider(config)` | —                            | Pass a full `OAuthProvider` to define a custom OAuth 2.0 provider. |

An `OAuthProvider` describes the endpoints, scopes, credentials, optional extra
authorize params, and a `mapProfile` normalizer:

```ts
interface OAuthProvider {
  name: string;
  authorizeUrl: string;
  tokenUrl: string;
  userInfoUrl: string;
  scopes: string[];
  clientId: string;
  clientSecret: string;
  authorizeParams?: Record<string, string>; // e.g. access_type, prompt
  mapProfile: (raw: Record<string, unknown>) => OAuthProfile;
}
```

### Flow

#### `startAuth(provider, options): Promise<StartAuthResult>`

Builds the authorize redirect URL with a generated PKCE challenge and CSRF
`state`. Store the returned `state` and `verifier` (session/cookie), then 302 the
user to `url`.

```ts
interface StartAuthOptions {
  redirectUri: string;
  state?: string; // reuse a state instead of generating one
  params?: Record<string, string>; // extra authorize params, merged last
}

interface StartAuthResult {
  url: string; // authorize URL to redirect to
  state: string; // CSRF state — verify on callback
  verifier: string; // PKCE code verifier — pass to completeAuth
}
```

#### `completeAuth(provider, options): Promise<{ tokens, profile }>`

On the callback: exchanges the authorization `code` for tokens, then fetches and
normalizes the user profile. Convenience wrapper over `exchangeCode` +
`fetchProfile`.

```ts
interface CompleteAuthOptions {
  code: string;
  redirectUri: string;
  verifier?: string; // the PKCE verifier from startAuth
  fetch?: typeof fetch; // inject a fetch implementation (tests)
}
```

#### Lower-level helpers

| Export                                   | Signature                 | Purpose                                                         |
| ---------------------------------------- | ------------------------- | --------------------------------------------------------------- |
| `exchangeCode(provider, options)`        | `→ Promise<OAuthTokens>`  | Exchange an authorization code for tokens.                      |
| `fetchProfile(provider, tokens, fetch?)` | `→ Promise<OAuthProfile>` | Fetch + normalize the user's profile.                           |
| `randomToken(bytes?)`                    | `→ string`                | Random URL-safe token (default 32 bytes) for `state`/verifiers. |

### Types

```ts
interface OAuthTokens {
  access_token: string;
  token_type?: string;
  refresh_token?: string;
  expires_in?: number;
  id_token?: string;
  scope?: string;
}

interface OAuthProfile {
  id: string;
  email?: string;
  name?: string;
  avatar?: string;
  raw: Record<string, unknown>;
}
```

## Usage

```ts
import { google, startAuth, completeAuth } from "@wrnexus/oauth";
import { logIn } from "@wrnexus/core";

const provider = google({
  clientId: process.env.GOOGLE_CLIENT_ID!,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
});

const redirectUri = "https://example.com/auth/callback";

// 1. Kick off sign-in: redirect the user to the provider.
async function beginLogin(ctx) {
  const { url, state, verifier } = await startAuth(provider, { redirectUri });
  // Persist state + verifier in the session, then redirect.
  ctx.session.set("oauth_state", state);
  ctx.session.set("oauth_verifier", verifier);
  return Response.redirect(url, 302);
}

// 2. Handle the callback.
async function handleCallback(ctx, code: string, state: string) {
  if (state !== ctx.session.get("oauth_state")) throw new Error("bad state");

  const { profile } = await completeAuth(provider, {
    code,
    redirectUri,
    verifier: ctx.session.get("oauth_verifier"),
  });

  logIn(ctx, { id: profile.id, email: profile.email });
}
```

Custom provider with `defineProvider`:

```ts
import { defineProvider, startAuth } from "@wrnexus/oauth";

const gitlab = defineProvider({
  name: "gitlab",
  authorizeUrl: "https://gitlab.com/oauth/authorize",
  tokenUrl: "https://gitlab.com/oauth/token",
  userInfoUrl: "https://gitlab.com/api/v4/user",
  scopes: ["read_user"],
  clientId: process.env.GITLAB_CLIENT_ID!,
  clientSecret: process.env.GITLAB_CLIENT_SECRET!,
  mapProfile: (raw) => ({
    id: String(raw.id),
    email: raw.email as string | undefined,
    name: raw.name as string | undefined,
    avatar: raw.avatar_url as string | undefined,
    raw,
  }),
});
```

## Requirements / Notes

- **Bun-only.** Relies on the global `fetch` and WebCrypto (`crypto.getRandomValues`,
  `crypto.subtle.digest`) — no other runtime dependencies.
- The flow is stateless by design: you are responsible for storing `state` and
  `verifier` between `startAuth` and `completeAuth` (session or signed cookie).
- Pairs with [`@wrnexus/core`](../core) — feed the normalized `OAuthProfile` into
  `logIn` to establish a session.
  OIDC integrations can combine strict discovery with the rotating JWKS verifier:

```ts
import { createRemoteJwks } from "@wrnexus/jwt";
import { discoverOidc, verifyOidcIdToken } from "@wrnexus/oauth";

const metadata = await discoverOidc("https://issuer.example");
const jwks = createRemoteJwks(metadata.jwks_uri);
const claims = await verifyOidcIdToken(idToken, {
  issuer: metadata.issuer,
  clientId: "client-id",
  jwks,
  nonce: expectedNonce,
  accessToken,
});
```

Discovery requires an exact normalized issuer and HTTPS endpoints without URL
credentials/fragments. ID-token verification checks the RS256 signature,
expiry/not-before, issuer, audience, required OIDC claims, nonce, multi-audience
`azp`, optional token age, and optional `at_hash` binding.

### Exported TypeScript declarations

```ts
import { JwtClaims, RemoteJwks } from '@wrnexus/jwt';

interface OAuthStateRecord {
    state: string;
    verifier: string;
    redirectUri: string;
    returnTo?: string;
    expiresAt: number;
}
interface OAuthStateStore {
    set(record: OAuthStateRecord): Promise<void>;
    consume(state: string): Promise<OAuthStateRecord | null>;
}
declare function memoryOAuthStateStore(now?: () => number): OAuthStateStore;
declare function createOAuthState(store: OAuthStateStore, input: Omit<OAuthStateRecord, "state" | "expiresAt"> & {
    ttlMs?: number;
}): Promise<OAuthStateRecord>;
declare function refreshOAuthTokens(provider: OAuthProvider, refreshToken: string, fetchImpl?: typeof fetch): Promise<OAuthTokens>;
interface OidcDiscovery {
    issuer: string;
    authorization_endpoint: string;
    token_endpoint: string;
    userinfo_endpoint?: string;
    jwks_uri: string;
    revocation_endpoint?: string;
}
declare function discoverOidc(issuer: string, fetchImpl?: typeof fetch): Promise<OidcDiscovery>;
interface OidcIdTokenClaims extends JwtClaims {
    sub: string;
    iss: string;
    aud: string | string[];
    exp: number;
    iat: number;
    nonce?: string;
    azp?: string;
    at_hash?: string;
}
interface VerifyOidcIdTokenOptions {
    issuer: string;
    clientId: string;
    jwks: RemoteJwks;
    nonce?: string;
    accessToken?: string;
    now?: number;
    clockTolerance?: number;
    maxAge?: number;
}
declare function validateOidcClaims(claims: JwtClaims, options: Pick<VerifyOidcIdTokenOptions, "clientId" | "nonce">): asserts claims is OidcIdTokenClaims;
declare function verifyOidcIdToken(token: string, options: VerifyOidcIdTokenOptions): Promise<OidcIdTokenClaims>;
declare function validateOAuthReturnTo(value: string | undefined, origin: string, fallback?: string): string;

/**
 * @wrnexus/oauth — OAuth 2.0 sign-in with any provider. Ships presets for Google,
 * GitHub, and Discord, and `defineProvider` for a custom one. Dependency-free
 * (uses `fetch` + WebCrypto for PKCE). Pairs with @wrnexus/core's `logIn`.
 *
 *   const provider = google({ clientId, clientSecret });
 *   // 1. send the user to the provider:
 *   const { url, state, verifier } = await startAuth(provider, { redirectUri });
 *   // (store `state` + `verifier` in the session, then 302 to `url`)
 *   // 2. on the callback:
 *   const { profile } = await completeAuth(provider, { code, redirectUri, verifier });
 *   logIn(ctx, { id: profile.id, email: profile.email });
 */
interface OAuthTokens {
    access_token: string;
    token_type?: string;
    refresh_token?: string;
    expires_in?: number;
    id_token?: string;
    scope?: string;
}
interface OAuthProfile {
    id: string;
    email?: string;
    name?: string;
    avatar?: string;
    raw: Record<string, unknown>;
}
interface OAuthProvider {
    name: string;
    authorizeUrl: string;
    tokenUrl: string;
    userInfoUrl: string;
    scopes: string[];
    clientId: string;
    clientSecret: string;
    /** Extra params for the authorize request (e.g. `access_type`, `prompt`). */
    authorizeParams?: Record<string, string>;
    /** Normalize the provider's raw userinfo into an OAuthProfile. */
    mapProfile: (raw: Record<string, unknown>) => OAuthProfile;
}
interface ProviderCredentials {
    clientId: string;
    clientSecret: string;
    scopes?: string[];
}
type FetchLike = typeof fetch;
declare function google(creds: ProviderCredentials): OAuthProvider;
declare function github(creds: ProviderCredentials): OAuthProvider;
declare function discord(creds: ProviderCredentials): OAuthProvider;
/** Define a custom OAuth2 provider. */
declare function defineProvider(config: OAuthProvider): OAuthProvider;
/** A random URL-safe token (for `state` and the PKCE verifier). */
declare function randomToken(bytes?: number): string;
interface StartAuthOptions {
    redirectUri: string;
    /** Provide to reuse a state (else one is generated). */
    state?: string;
    /** Extra authorize params (merged over the provider's). */
    params?: Record<string, string>;
}
interface StartAuthResult {
    /** The full authorize URL to redirect the user to. */
    url: string;
    /** CSRF state — store it (session/cookie) and verify on callback. */
    state: string;
    /** PKCE code verifier — store it and pass to `completeAuth`. */
    verifier: string;
}
/** Build the authorize redirect (with PKCE + state). */
declare function startAuth(provider: OAuthProvider, options: StartAuthOptions): Promise<StartAuthResult>;
interface CompleteAuthOptions {
    code: string;
    redirectUri: string;
    /** The PKCE verifier from `startAuth`. */
    verifier?: string;
    /** Inject a fetch implementation (tests). */
    fetch?: FetchLike;
}
/** Exchange the authorization code for tokens, then fetch the user profile. */
declare function completeAuth(provider: OAuthProvider, options: CompleteAuthOptions): Promise<{
    tokens: OAuthTokens;
    profile: OAuthProfile;
}>;
/** Exchange an authorization code for tokens. */
declare function exchangeCode(provider: OAuthProvider, options: CompleteAuthOptions): Promise<OAuthTokens>;
/** Fetch + normalize the user's profile from the provider. */
declare function fetchProfile(provider: OAuthProvider, tokens: OAuthTokens, fetchImpl?: FetchLike): Promise<OAuthProfile>;

export { type CompleteAuthOptions, type OAuthProfile, type OAuthProvider, type OAuthStateRecord, type OAuthStateStore, type OAuthTokens, type OidcDiscovery, type OidcIdTokenClaims, type ProviderCredentials, type StartAuthOptions, type StartAuthResult, type VerifyOidcIdTokenOptions, completeAuth, createOAuthState, defineProvider, discord, discoverOidc, exchangeCode, fetchProfile, github, google, memoryOAuthStateStore, randomToken, refreshOAuthTokens, startAuth, validateOAuthReturnTo, validateOidcClaims, verifyOidcIdToken };
```

---

## @wrnexus/observability

Documentation URL: https://wrnexusjs.dev/packages/observability

# @wrnexus/observability

Open-standard traces, metrics, logs, health checks, Web Vitals, error reporting and profiling.

Use `createOperationTracer()` for `database`, `cache`, `queue`, `realtime`, `server-action` or custom `application` spans. Export through OTLP, Prometheus, Zipkin/Jaeger, or the Sentry-compatible error reporter; Grafana can consume the Prometheus or OTLP signals.

Privacy-conscious counters, gauges, histograms, HTTP middleware, Web Vitals ingestion, browser collection, and exporter adapters. Request bodies and user identifiers are not collected by default.

```ts
export default {
  observability: { enabled: true, serverTiming: true, sampleRate: 0.1, webVitals: true },
};
```

## Traces, correlated logs, and OTLP

```ts
import {
  createOtlpMetricExporter,
  createOtlpTraceExporter,
  createStructuredLogger,
  metricsMiddleware,
  traceMiddleware,
} from "@wrnexus/observability";

const traces = createOtlpTraceExporter("https://collector.example/v1/traces", {
  serviceName: "checkout",
  headers: { authorization: `Bearer ${process.env.OTLP_TOKEN}` },
});

export const tracing = traceMiddleware({
  serviceName: "checkout",
  sampleRate: 0.1,
  exporter: traces,
  onExportError(error) {
    console.error("trace export failed", error);
  },
});

export const metrics = metricsMiddleware();
export const metricExporter = createOtlpMetricExporter("https://collector.example/v1/metrics", {
  serviceName: "checkout",
});

export const logger = createStructuredLogger({ service: "checkout" });
// Request middleware can create a correlated child from ctx.locals.
logger
  .child({
    traceId: ctx.locals.traceId,
    spanId: ctx.locals.spanId,
    requestId: ctx.locals.requestId,
  })
  .info("order accepted", { orderId });
```

The tracing middleware accepts and validates W3C `traceparent`, creates a child server span,
stores correlation identifiers in `ctx.locals`, installs the framework tracer on `ctx.tracer`,
and returns `traceparent` plus `x-request-id`. Export failures are isolated from application
responses when `onExportError` is configured.

## Liveness and readiness

```ts
import { HealthRegistry } from "@wrnexus/core";
import { createLivenessHandler, createReadinessHandler } from "@wrnexus/observability";

const health = new HealthRegistry();
health.register("database", async () =>
  (await db.ping()) ? { status: "up" } : { status: "down" },
);

export const live = createLivenessHandler();
export const ready = createReadinessHandler(health);
```

Liveness reports whether the process can answer requests. Readiness returns HTTP 503 when a
registered dependency is down. Dependency messages and details are hidden unless
`exposeDetails: true` is explicitly selected for a trusted endpoint.

### Exported TypeScript declarations

```ts
export { MetricLabels, MetricPoint, MetricsRegistry } from './metrics.js';
export { MetricExporter, MetricsMiddlewareOptions, WebVitalRecord, WebVitalsHandlerOptions, createHttpMetricExporter, createOtlpMetricExporter, createWebVitalsHandler, defaultMetrics, metricsMiddleware } from './server.js';
export { WebVitalsClientOptions, webVitalsClient } from './client.js';
export { S as SpanExporter, a as SpanRecord, T as TraceContext, b as TraceMiddlewareOptions, c as createOtlpTraceExporter, f as formatTraceparent, p as parseTraceparent, t as traceMiddleware } from './trace-CvRlPzhE.js';
export { HealthHandlerOptions, createLivenessHandler, createReadinessHandler } from './health.js';
export { LogLevel, LogRecord, StructuredLogger, StructuredLoggerOptions, createStructuredLogger } from './logging.js';
export { ErrorReporter, FrameworkSpanKind, LogExporter, OperationTracer, createJaegerExporter, createOperationTracer, createOtlpLogExporter, createPerformanceProfiler, createPrometheusPushExporter, createSentryCompatibleReporter, createZipkinExporter, renderPrometheus } from './integrations.js';
import '@wrnexus/core';
```

---

## @wrnexus/playground

Documentation URL: https://wrnexusjs.dev/packages/playground

# @wrnexus/playground

A deployable, shareable `.wrn` playground with diagnostics, generated JavaScript,
safe SSR-shaped HTML, sandboxed preview, reactive/UI examples, and version adapters.

Run `wrnexus playground`, or deploy `createPlaygroundHandler()`.

### Exported TypeScript declarations

```ts
interface PlaygroundCompilation {
    source: string;
    generated: string;
    client: string;
    html: string;
    interactiveHtml: string;
    diagnostics: Array<{
        code: string;
        message: string;
        severity: string;
    }>;
    version: string;
}
interface PlaygroundVersionAdapter {
    version: string;
    compile(source: string): Promise<Omit<PlaygroundCompilation, "source" | "version">>;
}
declare function compilePlayground(source: string, version?: string): PlaygroundCompilation;
declare function encodePlaygroundShare(source: string): string;
declare function decodePlaygroundShare(value: string): string;
declare function comparePlaygroundVersions(source: string, adapters: PlaygroundVersionAdapter[]): Promise<{
    current: PlaygroundCompilation;
    comparisons: {
        generated: string;
        client: string;
        html: string;
        interactiveHtml: string;
        diagnostics: Array<{
            code: string;
            message: string;
            severity: string;
        }>;
        version: string;
    }[];
}>;
declare function createPlaygroundHandler(options?: {
    versions?: PlaygroundVersionAdapter[];
    examples?: Record<string, string>;
}): (request: Request) => Promise<Response>;

export { type PlaygroundCompilation, type PlaygroundVersionAdapter, comparePlaygroundVersions, compilePlayground, createPlaygroundHandler, decodePlaygroundShare, encodePlaygroundShare };
```

---

## @wrnexus/plugin

Documentation URL: https://wrnexusjs.dev/packages/plugin

# @wrnexus/plugin

## Least-privilege package permissions

Package manifests declare every framework capability they register:

```json
{
  "wrnexus": {
    "permissions": ["routes", "migrations"],
    "routes": [{ "kind": "api", "path": "/api/example", "entry": "./route.ts" }]
  }
}
```

Applications can enable fail-closed grants:

```ts
export default {
  pluginPermissions: {
    enforce: true,
    grants: { "example-plugin": ["routes"] },
  },
};
```

Discovery rejects used-but-undeclared capabilities with
`WRN-PLUGIN-PERMISSION-UNDECLARED` and ungranted capabilities with
`WRN-PLUGIN-PERMISSION-DENIED`. Permissions cover components, browser runtime,
assets, styles, routes, middleware, migrations, config, transforms,
diagnostics/tooling, and server/build hooks.

## Compatibility matrices

Manifests can add `compatibility: { bunMin: "1.3.0", os: ["linux",
"darwin"] }` alongside `runtimes` and `requires`. Use
`testPluginCompatibility(manifest, targets)` in a package test to exercise the
complete support matrix. Runtime discovery enforces the same Bun minimum, OS,
runtime, and capability declarations used by the test kit.

Deterministic WRNexusJS plugin contracts for configuration, AST/code transforms,
diagnostics, development servers, production builds, and DevToolbar extensions.

Use `definePlugin()` and declare `enforce`, `before`, or `after` when ordering matters.
Duplicate names and dependency cycles are rejected.

## Complete lifecycle and contributions

Plugins may implement `setup`, `configure`, `configResolved`, `transformAst`,
`transformCode`, `diagnostics`, `routes`, `configureServer`, `buildStart`,
`buildEnd`, `render`, `deploy`, `shutdown`, and `hmrUpdate`. The runner preserves
resolved plugin order for every hook and executes `setup` exactly once.

In addition to components, routes, middleware, assets, styles, runtimes, and
migrations, plugins can contribute `directives`, `cliCommands`,
`virtualModules`, `deploymentAdapters`, `configSchemas`, `documentation`, and
`typeDefinitions`. Names are collision checked. Configuration schemas run after
configuration resolution, CLI commands are callable as normal `wrnexus`
commands, directives participate in AST transformation, and production builds
materialize virtual modules and invoke matching contributed adapters.

### Exported TypeScript declarations

```ts
export { PageAst, WrnDiagnostic } from '@wrnexus/syntax';
import { WrnexusPackageManifest, WrnexusPlugin, PluginInput, PluginContext, PluginRunner } from './types.js';
export { ClientRuntimeDefinition, ClientRuntimeInject, ClientRuntimeLoad, ClientRuntimeType, PackageAssetDefinition, PackageMigrationDefinition, PackagePluginManifest, PackageRouteDefinition, PackageStyleDefinition, PluginCliCommand, PluginCommand, PluginConfigSchema, PluginContributions, PluginDeploymentAdapter, PluginDevToolbarPanel, PluginDirective, PluginOrder, PluginPermission, PluginVirtualModule, TransformContext } from './types.js';
export { assertContributionId, contentTypeForPath, defaultClientRuntimePath, defaultPackageAssetPath, definePackageManifest, normalizeClientRuntime, normalizePackageAsset, validateStyleIds } from './manifest.js';
export { DiscoverPluginOptions, discoverPlugins } from './discovery.js';

interface PluginCompatibilityTarget {
    runtime: "bun" | "node" | "edge" | "worker" | "service-worker" | "browser";
    version?: string;
    os?: "win32" | "linux" | "darwin" | string;
    capabilities?: readonly string[];
}
interface PluginCompatibilityResult {
    target: PluginCompatibilityTarget;
    ok: boolean;
    issues: Array<{
        code: "WRN-PLUGIN-MATRIX-RUNTIME" | "WRN-PLUGIN-MATRIX-VERSION" | "WRN-PLUGIN-MATRIX-OS" | "WRN-PLUGIN-MATRIX-CAPABILITY";
        message: string;
    }>;
}
declare function testPluginCompatibility(manifest: WrnexusPackageManifest, targets: readonly PluginCompatibilityTarget[]): PluginCompatibilityResult[];

declare function definePlugin(plugin: WrnexusPlugin): WrnexusPlugin;
declare function flattenPlugins(input: PluginInput, output?: WrnexusPlugin[]): WrnexusPlugin[];

/** Resolve plugin order deterministically and reject duplicates/cycles. */
declare function resolvePlugins(input: PluginInput): WrnexusPlugin[];
declare function createPluginRunner(input: PluginInput, context: PluginContext): PluginRunner;

export { type PluginCompatibilityResult, type PluginCompatibilityTarget, PluginContext, PluginInput, PluginRunner, WrnexusPackageManifest, WrnexusPlugin, createPluginRunner, definePlugin, flattenPlugins, resolvePlugins, testPluginCompatibility };
```

---

## @wrnexus/pubsub

Documentation URL: https://wrnexusjs.dev/packages/pubsub

# @wrnexus/pubsub

> Topic-based publish/subscribe with a pluggable driver — in-process by default, Redis for cross-process messaging.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

`@wrnexus/pubsub` is a small server-side pub/sub bus. You publish messages to a
topic and subscribe with topic patterns; handlers fire for matching topics. The
default driver keeps everything in-process, and you can swap in the Redis driver
(`@wrnexus/pubsub/redis`) to fan messages out across processes or hosts. It also
backs `@wrnexus/core`'s realtime bridge for horizontal scaling.

## Installation

```bash
bun add @wrnexus/pubsub
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

## API

### `createPubSub(driver?): PubSub`

Creates a bus over a driver. Defaults to `memoryDriver()` (in-process).

```ts
interface PubSub {
  publish<T = unknown>(topic: string, message: T): Promise<void>;
  subscribe<T = unknown>(pattern: string, handler: Handler<T>): () => void;
  close(): Promise<void>;
}

type Handler<T = unknown> = (message: T, topic: string) => void | Promise<void>;
```

- `publish(topic, message)` — resolves once the driver and in-memory async handlers finish.
- `subscribe(pattern, handler)` — returns an unsubscribe function.
- `close()` — idempotently rejects new work, clears local subscriptions, and closes the driver.

### Pattern matching

Subscription patterns match in three ways:

- **Exact** — `"order:created"` matches only that topic.
- **Prefix** — `"order:*"` matches any topic starting with `"order:"`.
- **Everything** — `"*"` matches all topics.

### `memoryDriver(): PubSubDriver`

The default in-process driver. Handlers are invoked synchronously (fire-and-forget
for async handlers) whenever a published topic matches a registered pattern.

```ts
interface PubSubDriver {
  publish(topic: string, message: unknown): void | Promise<void>;
  subscribe(pattern: string, handler: Handler): () => void;
}
```

### `@wrnexus/pubsub/redis` — `redisDriver(url?)`

A cross-process driver backed by Redis. It speaks RESP over a raw TCP socket via
`Bun.connect`, so it adds **no npm dependency**. `url` defaults to `$REDIS_URL`,
then `redis://localhost:6379`. The URL may carry a password and a database index
(e.g. `redis://:secret@host:6379/2`).

```ts
function redisDriver(url?: string, options?: RedisDriverOptions): PubSubDriver & { close(): void };
```

- Exact topics use Redis `SUBSCRIBE`; wildcard patterns (`ns:*`, `*`) use
  `PSUBSCRIBE`, whose glob semantics line up with this library's matching.
- Messages are JSON-stringified on publish and `JSON.parse`d on receipt; a payload
  that isn't valid JSON is delivered as the raw string.
- `close()` tears down both the subscriber and publisher connections.
- Lost sockets reconnect with bounded exponential backoff and active subscriptions
  are replayed. `maxPending` bounds unavailable-connection writes (default 1000);
  `reconnectDelayMs` and `reconnectMaxDelayMs` tune recovery (100ms/5000ms).

### RESP codec (internal)

`redis.ts` uses a minimal RESP implementation exported from `resp.ts`
(`encodeCommand`, `parseReply`, `concat`, and the `RespValue` type). These are
implementation details of the Redis driver, not part of the public package entry.

## Usage

In-process (default):

```ts
import { createPubSub } from "@wrnexus/pubsub";

const bus = createPubSub();

const off = bus.subscribe("order:*", (msg, topic) => {
  console.log(topic, msg);
});

await bus.publish("order:created", { id: 7 });

off(); // unsubscribe
```

Cross-process with Redis:

```ts
import { createPubSub } from "@wrnexus/pubsub";
import { redisDriver } from "@wrnexus/pubsub/redis";

const driver = redisDriver("redis://localhost:6379");
const bus = createPubSub(driver);

bus.subscribe("order:*", (msg, topic) => {
  // received on any app process subscribed to this pattern
});

await bus.publish("order:created", { id: 7 });

// on shutdown (also closes the driver)
await bus.close();
```

## Requirements / Notes

- **Bun-only.** The Redis driver depends on `Bun.connect`; it throws
  `redisDriver requires the Bun runtime (Bun.connect).` outside Bun. The default
  in-memory driver has no runtime dependencies.
- The Redis driver reads `REDIS_URL` from the environment when no `url` is passed.
- Backs [`@wrnexus/core`](../core)'s realtime bridge for horizontal scaling.
- No external npm dependencies — the Redis client is a self-contained RESP codec.

### Exported TypeScript declarations

```ts
import { Context } from '@wrnexus/core';
import { SubjectContext } from '@wrnexus/rpc';

interface MessageEnvelope<T = unknown> {
    id: string;
    topic: string;
    data: T;
    timestamp: number;
    attempts: number;
}
interface ResilientPubSubOptions {
    retries?: number;
    retryDelayMs?: number;
    onError?: (error: unknown, envelope: MessageEnvelope) => void;
}
declare function createResilientPubSub(driver: PubSubDriver, options?: ResilientPubSubOptions): PubSub;
interface PresenceMember {
    id: string;
    metadata?: Record<string, unknown>;
    joinedAt: number;
    expiresAt: number;
}
declare class PresenceChannel {
    #private;
    private readonly ttlMs;
    private readonly now;
    constructor(ttlMs?: number, now?: () => number);
    touch(id: string, metadata?: Record<string, unknown>): PresenceMember;
    leave(id: string): boolean;
    list(): PresenceMember[];
    prune(): number;
}

interface SubjectPubSub {
    publish<T>(ctx: Context, topic: string, message: T): Promise<void>;
    subscribe<T>(pattern: string, handler: (message: T, topic: string, subject?: SubjectContext) => void | Promise<void>): () => void;
}
/**
 * Authenticated pub/sub envelope. The token uses a fixed, purpose-specific
 * audience; subscribers verify it before exposing the message to a handler.
 */
declare function subjectPubSub(bus: PubSub): SubjectPubSub;

/**
 * @wrnexus/pubsub — topic-based publish/subscribe with a pluggable driver.
 * The default is in-process; swap in a Redis/NATS driver for cross-instance
 * messaging (it also backs @wrnexus/core's realtime bridge).
 *
 *   const bus = createPubSub();
 *   const off = bus.subscribe("order:*", (msg, topic) => {...});
 *   await bus.publish("order:created", { id: 7 });
 *
 * Subscriptions match exact topics, "ns:*" prefixes, and "*" (everything).
 */
type Handler<T = unknown> = (message: T, topic: string) => void | Promise<void>;
interface PubSubDriver {
    publish(topic: string, message: unknown): void | Promise<void>;
    subscribe(pattern: string, handler: Handler): () => void;
    close?(): void | Promise<void>;
}
interface PubSub {
    publish<T = unknown>(topic: string, message: T): Promise<void>;
    subscribe<T = unknown>(pattern: string, handler: Handler<T>): () => void;
    /** Stop new work, remove subscriptions, and close the backing driver. */
    close(): Promise<void>;
}
/** In-process pub/sub driver (default). */
declare function memoryDriver(): PubSubDriver;
/** Create a pub/sub bus over a driver (in-memory by default). */
declare function createPubSub(driver?: PubSubDriver): PubSub;

interface NatsClient {
    publish(subject: string, data: Uint8Array): void | Promise<void>;
    subscribe(subject: string, handler: (data: Uint8Array, subject: string) => void): () => void;
    close?(): void | Promise<void>;
}
declare function natsDriver(client: NatsClient): PubSubDriver;
interface KafkaClient {
    publish(topic: string, value: string): void | Promise<void>;
    subscribe(pattern: string, handler: (value: string, topic: string) => void): () => void;
    close?(): void | Promise<void>;
}
/** Kafka adapter contract; consumer-group/rebalance policy remains owned by the selected client. */
declare function kafkaDriver(client: KafkaClient): PubSubDriver;

export { type Handler, type KafkaClient, type MessageEnvelope, type NatsClient, PresenceChannel, type PresenceMember, type PubSub, type PubSubDriver, type ResilientPubSubOptions, type SubjectPubSub, createPubSub, createResilientPubSub, kafkaDriver, memoryDriver, natsDriver, subjectPubSub };
```

---

## @wrnexus/pwa

Documentation URL: https://wrnexusjs.dev/packages/pwa

# @wrnexus/pwa

Official PWA primitives for manifests, service workers, offline pages and precaching, runtime
caching, background synchronization, push notifications, install/update events, offline mutation
stores, and conflict resolution. `createOfflineQueue()` accepts a durable IndexedDB-style store and
retries requests with stable idempotency headers.

### Exported TypeScript declarations

```ts
interface IndexedDbMigration {
    version: number;
    migrate(db: IDBDatabase, transaction: IDBTransaction): void;
}
declare function openPwaDatabase(name: string, migrations: IndexedDbMigration[], factory?: IDBFactory): Promise<IDBDatabase>;
declare function indexedDbOfflineQueueStore(db: IDBDatabase, storeName?: string): OfflineQueueStore;
declare const offlineQueueMigration: IndexedDbMigration;
interface StoredPushSubscription {
    id: string;
    userId: string;
    endpoint: string;
    expirationTime?: number | null;
    keys: {
        p256dh: string;
        auth: string;
    };
    createdAt: number;
}
interface PushSubscriptionStore {
    put(value: StoredPushSubscription): Promise<void>;
    remove(id: string): Promise<void>;
    list(userId: string): Promise<StoredPushSubscription[]>;
}
declare function memoryPushSubscriptionStore(): PushSubscriptionStore;
interface PushSqlClient {
    query<T = Record<string, unknown>>(sql: string, parameters?: unknown[]): Promise<{
        rows: T[];
    }>;
}
declare function postgresPushSubscriptionStore(db: PushSqlClient): PushSubscriptionStore;
declare const POSTGRES_PUSH_SUBSCRIPTION_SCHEMA = "CREATE TABLE IF NOT EXISTS wrnexus_push_subscriptions (id text PRIMARY KEY,user_id text NOT NULL,endpoint text NOT NULL,expiration_time bigint,p256dh text NOT NULL,auth text NOT NULL,created_at bigint NOT NULL); CREATE INDEX IF NOT EXISTS wrnexus_push_user ON wrnexus_push_subscriptions (user_id);";
declare function createPushSubscriptionService(store: PushSubscriptionStore, options?: {
    now?: () => number;
    maxPerUser?: number;
}): {
    subscribe(userId: string, value: {
        endpoint: string;
        expirationTime?: number | null;
        keys: {
            p256dh: string;
            auth: string;
        };
    }): Promise<{
        keys: {
            p256dh: string;
            auth: string;
        };
        createdAt: number;
        endpoint: string;
        expirationTime?: number | null;
        id: string;
        userId: string;
    }>;
    unsubscribe: (id: string) => Promise<void>;
    list: (userId: string) => Promise<StoredPushSubscription[]>;
};
declare function renderOfflineQueueReview(items: OfflineMutation[], conflicts?: Array<{
    id: string;
    message: string;
}>): string;
declare const PWA_REVIEW_RUNTIME = "document.addEventListener(\"click\",event=>{const button=event.target.closest(\"[data-pwa-retry],[data-pwa-remove],[data-pwa-client],[data-pwa-server]\");if(!button)return;const action=button.hasAttribute(\"data-pwa-retry\")?\"retry\":button.hasAttribute(\"data-pwa-remove\")?\"remove\":button.hasAttribute(\"data-pwa-client\")?\"client\":\"server\";const id=button.getAttribute(\"data-pwa-\"+action);dispatchEvent(new CustomEvent(\"wrnexus:pwa-review\",{detail:{action,id}}))});";

type RuntimeCacheStrategy = "network-first" | "cache-first" | "stale-while-revalidate";
interface RuntimeCacheRule {
    pattern: string;
    strategy: RuntimeCacheStrategy;
    cacheName?: string;
    methods?: string[];
}
interface ServiceWorkerOptions {
    cacheName?: string;
    offlineUrl?: string;
    startUrl?: string;
    cacheUrls?: string[];
    runtimeCaching?: RuntimeCacheRule[];
    backgroundSyncTag?: string;
}
interface WebManifestOptions {
    name: string;
    shortName?: string;
    description?: string;
    id?: string;
    startUrl?: string;
    scope?: string;
    display?: "standalone" | "fullscreen" | "minimal-ui" | "browser";
    themeColor?: string;
    backgroundColor?: string;
    icons?: Array<{
        src: string;
        sizes: string;
        type?: string;
        purpose?: string;
    }>;
    shortcuts?: unknown[];
    screenshots?: unknown[];
    categories?: string[];
    lang?: string;
}
declare function createWebManifest(options: WebManifestOptions): {
    id: string;
    name: string;
    short_name: string;
    description: string | undefined;
    start_url: string;
    scope: string;
    display: "standalone" | "fullscreen" | "minimal-ui" | "browser";
    theme_color: string;
    background_color: string;
    icons: {
        src: string;
        sizes: string;
        type?: string;
        purpose?: string;
    }[];
    shortcuts: unknown[];
    screenshots: unknown[];
    categories: string[];
    lang: string;
};
declare function generateServiceWorker(options?: ServiceWorkerOptions): string;
interface OfflineMutation<T = unknown> {
    id: string;
    createdAt: number;
    updatedAt: number;
    endpoint: string;
    method: string;
    payload: T;
    attempts: number;
}
interface OfflineQueueStore {
    list(): Promise<OfflineMutation[]>;
    put(item: OfflineMutation): Promise<void>;
    remove(id: string): Promise<void>;
}
declare function memoryOfflineQueueStore(): OfflineQueueStore;
type ConflictResolution<T> = {
    action: "client" | "server" | "merge";
    value: T;
};
declare function resolveOfflineConflict<T extends object>(client: T, server: T, strategy?: "client-wins" | "server-wins" | "last-write-wins" | ((client: T, server: T) => T)): ConflictResolution<T>;
declare function createOfflineQueue(options?: {
    store?: OfflineQueueStore;
    fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
    maxItems?: number;
    now?: () => number;
}): {
    enqueue<T>(input: Omit<OfflineMutation<T>, "id" | "createdAt" | "updatedAt" | "attempts">): Promise<OfflineMutation<T>>;
    list: () => Promise<OfflineMutation<unknown>[]>;
    sync(): Promise<{
        id: string;
        ok: boolean;
        status?: number;
    }[]>;
    remove: (id: string) => Promise<void>;
};
declare function pwaClientRuntime(serviceWorkerUrl?: string): string;
declare function subscribeToPush(registration: ServiceWorkerRegistration, publicKey: Uint8Array): Promise<PushSubscription>;

export { type ConflictResolution, type IndexedDbMigration, type OfflineMutation, type OfflineQueueStore, POSTGRES_PUSH_SUBSCRIPTION_SCHEMA, PWA_REVIEW_RUNTIME, type PushSqlClient, type PushSubscriptionStore, type RuntimeCacheRule, type RuntimeCacheStrategy, type ServiceWorkerOptions, type StoredPushSubscription, type WebManifestOptions, createOfflineQueue, createPushSubscriptionService, createWebManifest, generateServiceWorker, indexedDbOfflineQueueStore, memoryOfflineQueueStore, memoryPushSubscriptionStore, offlineQueueMigration, openPwaDatabase, postgresPushSubscriptionStore, pwaClientRuntime, renderOfflineQueueReview, resolveOfflineConflict, subscribeToPush };
```

---

## @wrnexus/queue

Documentation URL: https://wrnexusjs.dev/packages/queue

# @wrnexus/queue

> A background job queue with delays, retries + exponential backoff, recurring jobs, and concurrent workers.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

`@wrnexus/queue` is a server-side in-process job queue. You register named
workers, enqueue jobs (optionally delayed or recurring), and let the queue poll
and run them on a timer — with per-job retry limits and doubling backoff between
attempts. The default store lives in memory; the design allows a pluggable driver
to back it with Redis/SQL for durability across restarts. Reach for it when you
need to defer work (emails, webhooks, cleanup) off the request path without a
heavyweight external broker. Tests can drive it deterministically via `drain()`.

## Installation

```bash
bun add @wrnexus/queue
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

## API

The package exports a single factory plus its supporting types.

### `createQueue(options?): Queue`

Creates a new queue instance.

```ts
function createQueue(options?: QueueOptions): Queue;
```

#### `QueueOptions`

| Option        | Type                                 | Default    | Description                                              |
| ------------- | ------------------------------------ | ---------- | -------------------------------------------------------- |
| `maxAttempts` | `number`                             | `3`        | Default max attempts per job before it is dead-lettered. |
| `backoffMs`   | `number`                             | `1000`     | Base retry backoff in ms; doubles per attempt.           |
| `pollMs`      | `number`                             | `250`      | Poll interval used once `start()` is called (ms).        |
| `onFailed`    | `(job: Job, error: unknown) => void` | —          | Called when a job exhausts its attempts.                 |
| `concurrency` | `number`                             | unlimited  | Maximum jobs claimed by one `drain()` call.              |
| `capacity`    | `number`                             | unlimited  | Maximum queued plus active jobs before adds reject.      |
| `now`         | `() => number`                       | `Date.now` | Clock injection for deterministic tests.                 |

### `Queue`

The object returned by `createQueue`.

| Method     | Signature                                                      | Description                                                    |
| ---------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
| `add`      | `add<T>(name, data: T, options?: AddOptions): Promise<Job<T>>` | Enqueue a job under a worker name. Returns the created job.    |
| `process`  | `process<T>(name, handler: JobHandler<T>): void`               | Register the worker that runs jobs of the given name.          |
| `drain`    | `drain(now?: number): Promise<number>`                         | Run every job whose `runAt ≤ now`, once. Returns how many ran. |
| `start`    | `start(): void`                                                | Begin polling every `pollMs`. No-op if already started.        |
| `stop`     | `stop(): void`                                                 | Stop the poll timer.                                           |
| `shutdown` | `shutdown({ force? }): Promise<void>`                          | Stop accepting jobs and await active work; force aborts it.    |
| `size`     | `size(): number`                                               | Number of jobs currently queued.                               |
| `get/list` | `get(id)` / `list(name?)`                                      | Inspect defensive copies of pending jobs.                      |
| `cancel`   | `cancel(id): boolean`                                          | Remove queued work or abort an active handler.                 |
| `failed`   | `failed(): Job[]`                                              | Inspect exhausted jobs in the dead-letter collection.          |
| `retry`    | `retry(id): Promise<boolean>`                                  | Reset and requeue a dead-lettered job.                         |

#### `AddOptions`

| Option           | Type     | Description                                                                |
| ---------------- | -------- | -------------------------------------------------------------------------- |
| `delayMs`        | `number` | Delay before the job becomes runnable (ms).                                |
| `maxAttempts`    | `number` | Max attempts before dead-lettering. Defaults to the queue's `maxAttempts`. |
| `repeat`         | `number` | Re-enqueue this job this many ms after each successful run (recurring).    |
| `priority`       | `number` | Higher values are selected first among due jobs.                           |
| `idempotencyKey` | `string` | Return the matching pending job instead of enqueueing a duplicate.         |

#### `JobHandler<T>`

```ts
type JobHandler<T = unknown> = (
  job: Job<T>,
  context: { signal: AbortSignal },
) => void | Promise<void>;
```

#### `Job<T>`

```ts
interface Job<T = unknown> {
  id: string; // e.g. "job_1"
  name: string;
  data: T;
  attempts: number;
  maxAttempts: number;
  runAt: number; // epoch ms; job runs when now ≥ runAt
  repeat?: number; // if set, re-enqueue this many ms after each success
}
```

## Usage

Register workers, enqueue jobs, then start the poller:

```ts
import { createQueue } from "@wrnexus/queue";

const queue = createQueue({ maxAttempts: 3, backoffMs: 1000 });

// Register a worker for the "email" job name.
queue.process<{ to: string }>("email", async (job) => {
  await send(job.data.to);
});

// Enqueue a delayed job with up to 3 attempts.
await queue.add("email", { to: "a@b.com" }, { delayMs: 5000, maxAttempts: 3 });

queue.start(); // begin polling; queue.stop() to halt
```

Use `context.signal` in network/database calls so forced shutdown and active
cancellation finish promptly. For process termination, prefer
`await queue.shutdown()`; use `{ force: true }` only after your grace period.

### Durable queue

`createDurableQueue({ store })` retains jobs until their handler succeeds and
supports atomic leases when a driver implements `QueueStore.claim`. It exposes
the same cancellation/shutdown behavior plus `list`, `failed`, and `retry`.
The included `memoryQueueStore()` is useful for tests; production Redis/SQL
drivers should make `claim()` atomic to prevent two workers executing one job.

### Recurring jobs

Pass `repeat` to re-enqueue a job a fixed interval after each successful run:

```ts
queue.process("heartbeat", async () => ping());
await queue.add("heartbeat", {}, { repeat: 60_000 }); // runs ~every minute
```

### Handling permanent failures

When a job's `attempts` reaches `maxAttempts`, it is dropped and `onFailed`
fires instead of retrying:

```ts
const queue = createQueue({
  onFailed: (job, error) => {
    console.error(`job ${job.id} (${job.name}) gave up`, error);
  },
});
```

### Deterministic testing

Instead of `start()`, inject a clock and drive the queue with `drain()`:

```ts
let clock = 0;
const queue = createQueue({ now: () => clock });

queue.process("task", async () => {
  /* ... */
});
await queue.add("task", {}, { delayMs: 5000 });

clock = 5000;
const ran = await queue.drain(); // => 1
```

### Durable workflows and approvals

`createWorkflowEngine(store)` executes dependency-ordered steps and persists every transition,
result, progress update, failure, cancellation, and approval record. Approval steps pause safely
and can resume after a process restart because the snapshot lives in the supplied `WorkflowStore`.

```ts
const workflow = defineDurableWorkflow({
  name: "publish-report",
  steps: [
    { name: "build", run: buildReport },
    { name: "approve", dependsOn: ["build"], approval: true, run: (report) => report },
    { name: "publish", dependsOn: ["approve"], run: publishReport },
  ],
});

const run = await engine.start(workflow, input);
await engine.approve(workflow, run.id, "approve", currentUser.id);
```

Use `memoryWorkflowStore()` for tests. Production stores implement the small `get`, `put`, and
`list` contract using the same transactional database or durable service as the application.

## Retry & backoff behavior

- On a thrown handler error, the job is retried while `attempts < maxAttempts`.
- The next `runAt` is set to `now + backoffMs * 2^(attempts - 1)` (exponential
  backoff): with `backoffMs: 1000` the delays are 1s, 2s, 4s, …
- A job whose worker name has no registered handler stays queued until one is
  registered (it is not counted as runnable by `drain`).
- `drain` is re-entrant-safe: overlapping calls are skipped while one is running.

## Requirements / Notes

- **Bun-only** runtime (Node is not supported), consistent with the rest of the
  WrNexus framework. The queue itself relies only on standard timers
  (`setInterval`/`clearInterval`) and has no runtime dependencies.
- The default store is in-process, so queued jobs do not survive a restart; a
  pluggable driver is intended for backing it with Redis/SQL for durability.
- Works alongside `@wrnexus/core` for offloading work from the request path.

### Exported TypeScript declarations

```ts
import { ExecutionContext, Context } from '@wrnexus/core';
import { SubjectContext } from '@wrnexus/rpc';

/**
 * Persistence contract for the durable queue.
 *
 * Distributed drivers should implement `claim()` atomically and exclude leased
 * jobs from `due()` until their lease expires. Calling `put()` must replace the
 * stored record and release any previous lease for that job.
 */
interface QueueStore {
    put(job: Job): Promise<void>;
    get(id: string): Promise<Job | null>;
    remove(id: string): Promise<void>;
    due(now: number, limit: number): Promise<Job[]>;
    list(name?: string): Promise<Job[]>;
    claim?(id: string, worker: string, leaseUntil: number): Promise<boolean>;
}
declare function memoryQueueStore(): QueueStore;
interface DurableQueueOptions {
    store?: QueueStore;
    workerId?: string;
    maxAttempts?: number;
    concurrency?: number;
    capacity?: number;
    leaseMs?: number;
    backoff?: (attempt: number) => number;
    now?: () => number;
    onDeadLetter?: (job: Job, error: unknown) => void | Promise<void>;
    context?: (job: Job, signal: AbortSignal) => ExecutionContext;
}
interface DurableQueue {
    add<T>(name: string, data: T, options?: AddOptions): Promise<Job<T>>;
    process<T>(name: string, handler: JobHandler<T>): void;
    drain(): Promise<number>;
    cancel(id: string): Promise<boolean>;
    shutdown(options?: {
        force?: boolean;
    }): Promise<void>;
    list(name?: string): Promise<Job[]>;
    failed(): Job[];
    retry(id: string): Promise<boolean>;
}
declare function createDurableQueue(options?: DurableQueueOptions): DurableQueue;

interface RedisQueueClient {
    get(key: string): Promise<string | null>;
    set(key: string, value: string, options?: {
        NX?: boolean;
        PX?: number;
    }): Promise<unknown>;
    del(...keys: string[]): Promise<unknown>;
    zadd(key: string, score: number, member: string): Promise<unknown>;
    zrem(key: string, member: string): Promise<unknown>;
    zrangebyscore(key: string, min: number, max: number, options?: {
        limit: [number, number];
    }): Promise<string[]>;
    smembers(key: string): Promise<string[]>;
    sadd(key: string, member: string): Promise<unknown>;
    srem(key: string, member: string): Promise<unknown>;
}
/** Redis-backed queue store using only the common client command surface. */
declare function redisQueueStore(client: RedisQueueClient, prefix?: string): QueueStore;
interface SqlQueueClient {
    query<T = Record<string, unknown>>(sql: string, parameters?: unknown[]): Promise<{
        rows: T[];
    }>;
}
/** PostgreSQL store with atomic SKIP LOCKED leasing and JSON payloads. */
declare function postgresQueueStore(db: SqlQueueClient, table?: string): QueueStore;
declare const POSTGRES_QUEUE_SCHEMA = "CREATE TABLE IF NOT EXISTS wrnexus_jobs (\n  id text PRIMARY KEY, name text NOT NULL, payload jsonb NOT NULL, run_at bigint NOT NULL,\n  priority integer NOT NULL DEFAULT 0, lease_owner text, lease_until bigint\n); CREATE INDEX IF NOT EXISTS wrnexus_jobs_due ON wrnexus_jobs (run_at, priority DESC);";

interface ScheduledJob<T = unknown> {
    name: string;
    data: T;
    everyMs: number;
    options?: AddOptions;
}
interface QueueScheduler {
    start(): void;
    stop(): void;
    tick(now?: number): Promise<number>;
    snapshot(): {
        running: boolean;
        schedules: number;
        nextRuns: Record<string, number>;
    };
}
/** Restart-safe scheduler when used with a durable queue and stable idempotency buckets. */
declare function createQueueScheduler(queue: DurableQueue, schedules: ScheduledJob[], options?: {
    pollMs?: number;
    now?: () => number;
}): QueueScheduler;
declare function addBatch<T>(queue: DurableQueue, name: string, values: T[], options?: AddOptions): Promise<Job<T>[]>;
interface QueueDashboardSnapshot {
    generatedAt: number;
    pending: number;
    failed: number;
    byName: Record<string, number>;
    oldestRunAt?: number;
}
declare function queueDashboardSnapshot(queue: DurableQueue): Promise<QueueDashboardSnapshot>;
declare function renderQueueDashboard(snapshot: QueueDashboardSnapshot): string;
/** Long-running scheduler/worker loop suitable for a dedicated process or container. */
declare function runQueueDaemon(queue: DurableQueue, scheduler: QueueScheduler, options?: {
    signal?: AbortSignal;
    pollMs?: number;
    onError?: (error: unknown) => void;
}): Promise<void>;

type WorkflowStatus = "pending" | "running" | "waiting-approval" | "completed" | "failed" | "cancelled";
interface WorkflowStep$1<I = unknown, O = unknown> {
    name: string;
    dependsOn?: string[];
    approval?: boolean;
    run(input: I, context: WorkflowRunContext): O | Promise<O>;
}
interface WorkflowRunContext {
    workflowId: string;
    step: string;
    results: Readonly<Record<string, unknown>>;
    signal: AbortSignal;
    progress(value: number, message?: string): void;
}
interface WorkflowSnapshot {
    id: string;
    name: string;
    status: WorkflowStatus;
    input: unknown;
    results: Record<string, unknown>;
    completed: string[];
    waitingFor?: string;
    progress: number;
    message?: string;
    error?: string;
    updatedAt: number;
}
interface WorkflowStore {
    get(id: string): Promise<WorkflowSnapshot | null>;
    put(snapshot: WorkflowSnapshot): Promise<void>;
    list(): Promise<WorkflowSnapshot[]>;
}
declare function memoryWorkflowStore(): WorkflowStore;
interface WorkflowDefinition<I = unknown> {
    name: string;
    steps: WorkflowStep$1<any, any>[];
    /** Compile-time input marker; definitions do not store runtime input values. */
    readonly __input?: I;
}
interface WorkflowEngine {
    start<I>(definition: WorkflowDefinition<I>, input: I, id?: string): Promise<WorkflowSnapshot>;
    resume<I>(definition: WorkflowDefinition<I>, id: string): Promise<WorkflowSnapshot>;
    approve<I>(definition: WorkflowDefinition<I>, id: string, step: string, actor: string): Promise<WorkflowSnapshot>;
    cancel(id: string): Promise<boolean>;
    get(id: string): Promise<WorkflowSnapshot | null>;
    list(): Promise<WorkflowSnapshot[]>;
}
declare function createWorkflowEngine(store?: WorkflowStore): WorkflowEngine;
declare function defineDurableWorkflow<I>(definition: WorkflowDefinition<I>): WorkflowDefinition<I>;

interface SubjectEnvelope<T> {
    payload: T;
    identity?: string;
}
interface SubjectJob<T> extends Omit<Job<SubjectEnvelope<T>>, "data"> {
    data: T;
    subject?: SubjectContext;
}
interface SubjectQueue {
    add<T>(ctx: Context, name: string, data: T, options?: AddOptions): Promise<Job<SubjectEnvelope<T>>>;
    process<T>(name: string, handler: (job: SubjectJob<T>, context: {
        signal: AbortSignal;
    }) => void | Promise<void>): void;
}
/** Queue adapter that persists a signed end-user context alongside job data. */
/** Works with both the in-memory Queue and createDurableQueue(). */
declare function subjectQueue(queue: Queue | DurableQueue): SubjectQueue;

/**
 * @wrnexus/queue — a background job queue with delays, retries + backoff, and
 * concurrent workers. The default store is in-process; a pluggable driver lets
 * you back it with Redis/SQL for durability across restarts.
 *
 *   const queue = createQueue();
 *   queue.process("email", async (job) => { await send(job.data); });
 *   await queue.add("email", { to: "a@b.com" }, { delayMs: 5000, maxAttempts: 3 });
 *   queue.start();                 // begin polling; queue.stop() to halt
 *
 * Tests can drive it deterministically with `await queue.drain(now)`.
 */
interface Job<T = unknown> {
    id: string;
    name: string;
    data: T;
    attempts: number;
    maxAttempts: number;
    runAt: number;
    /** If set, re-enqueue this job this many ms after each successful run. */
    repeat?: number;
    priority: number;
    idempotencyKey?: string;
    createdAt: number;
}
interface JobContext {
    /** Aborted when an active job is cancelled or the queue is force-stopped. */
    signal: AbortSignal;
    /** The same trusted context shape used by HTTP, actions, realtime and webhooks. */
    execution: ExecutionContext;
}
type JobHandler<T = unknown> = (job: Job<T>, context: JobContext) => void | Promise<void>;
interface AddOptions {
    /** Delay before the job becomes runnable (ms). */
    delayMs?: number;
    /** Max attempts before it's dead-lettered. Default from queue options. */
    maxAttempts?: number;
    /** Re-enqueue this job this many ms after each successful run (recurring). */
    repeat?: number;
    /** Higher-priority jobs run first when multiple jobs are due. */
    priority?: number;
    /** Prevent duplicate queued work with the same stable key. */
    idempotencyKey?: string;
}
interface QueueOptions {
    /** Default max attempts per job. Default 3. */
    maxAttempts?: number;
    /** Base retry backoff (ms); doubles per attempt. Default 1000. */
    backoffMs?: number;
    /** Poll interval when started (ms). Default 250. */
    pollMs?: number;
    /** Called when a job exhausts its attempts. */
    onFailed?: (job: Job, error: unknown) => void;
    /** Maximum jobs executed in one drain. Default: unlimited. */
    concurrency?: number;
    /** Maximum queued + active jobs. Adds reject once this limit is reached. */
    capacity?: number;
    /** Clock injection (tests). Default Date.now. */
    now?: () => number;
    context?: (job: Job, signal: AbortSignal) => ExecutionContext;
}
interface Queue {
    add<T>(name: string, data: T, options?: AddOptions): Promise<Job<T>>;
    process<T>(name: string, handler: JobHandler<T>): void;
    /** Run every job whose runAt ≤ now, once. Returns how many ran. */
    drain(now?: number): Promise<number>;
    start(): void;
    stop(): void;
    /** Stop accepting work and wait for active handlers (or abort them). */
    shutdown(options?: {
        force?: boolean;
    }): Promise<void>;
    size(): number;
    get(id: string): Job | undefined;
    list(name?: string): Job[];
    cancel(id: string): boolean;
    failed(): Job[];
    retry(id: string): Promise<boolean>;
}
interface JobDefinition<I> {
    name: string;
    options?: Omit<AddOptions, "idempotencyKey">;
    run: JobHandler<I>;
}
declare function defineJob<I>(definition: JobDefinition<I>): JobDefinition<I>;
interface WorkflowStep<I, O> {
    name: string;
    run(input: I): O | Promise<O>;
}
declare function defineWorkflow<T>(name: string, steps: Array<WorkflowStep<any, any>>): {
    name: string;
    steps: WorkflowStep<any, any>[];
    run(input: T): Promise<unknown>;
};
declare function cronToInterval(cron: string): number;
declare function createQueue(options?: QueueOptions): Queue;

export { type AddOptions, type DurableQueue, type DurableQueueOptions, type Job, type JobContext, type JobDefinition, type JobHandler, POSTGRES_QUEUE_SCHEMA, type Queue, type QueueDashboardSnapshot, type QueueOptions, type QueueScheduler, type QueueStore, type RedisQueueClient, type ScheduledJob, type SqlQueueClient, type SubjectJob, type SubjectQueue, type WorkflowDefinition, type WorkflowEngine, type WorkflowRunContext, type WorkflowSnapshot, type WorkflowStatus, type WorkflowStep, type WorkflowStore, addBatch, createDurableQueue, createQueue, createQueueScheduler, createWorkflowEngine, cronToInterval, defineDurableWorkflow, defineJob, defineWorkflow, memoryQueueStore, memoryWorkflowStore, postgresQueueStore, queueDashboardSnapshot, redisQueueStore, renderQueueDashboard, runQueueDaemon, subjectQueue };
```

---

## @wrnexus/reactive

Documentation URL: https://wrnexusjs.dev/packages/reactive

# @wrnexus/reactive

> Tiny, type-safe reactive primitives (signals) with zero dependencies.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

`@wrnexus/reactive` is the seed of WrNexus's reactivity layer: a minimal `signal`
primitive that holds a value, notifies subscribers when it changes, and hands back
an unsubscribe function. It is deliberately small and framework-agnostic — it powers
nothing on its own, but is shaped so client islands (and later the `.wrn` compiler's
`state` blocks) can build reactive bindings on top of it. Reach for it when you need
observable state without pulling in a full reactivity library.

## Installation

```bash
bun add @wrnexus/reactive
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

## API

The package has a single entry point (`.`) exporting one function and three types.

### `signal<T>(initial: T): Signal<T>`

Creates a reactive signal seeded with `initial`. Returns a `Signal<T>`:

| Member      | Signature                          | Description                                                                                              |
| ----------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `get`       | `(): T`                            | Read the current value.                                                                                  |
| `set`       | `(next: T): void`                  | Write a new value. Subscribers run **only when the value actually changes** (compared with `Object.is`). |
| `update`    | `(fn: (current: T) => T): void`    | Apply a function to the current value; equivalent to `set(fn(get()))`.                                   |
| `subscribe` | `(fn: Subscriber<T>): Unsubscribe` | Register a subscriber; returns a function that removes it.                                               |

### Types

```ts
type Subscriber<T> = (value: T) => void;
type Unsubscribe = () => void;

interface Signal<T> {
  get(): T;
  set(next: T): void;
  update(fn: (current: T) => T): void;
  subscribe(fn: Subscriber<T>): Unsubscribe;
}
```

Notes on semantics:

- **No-op updates are skipped.** `set` compares the incoming value to the current
  one with `Object.is`; identical values do not notify subscribers.
- **Safe unsubscribe during notification.** Subscribers are iterated over a copy of
  the subscriber set, so a subscriber may call its own (or another's) unsubscribe
  while a notification is in flight.

## Usage

```ts
import { signal } from "@wrnexus/reactive";

const count = signal(0);

count.get(); // 0

// Subscribe; the returned function unsubscribes.
const off = count.subscribe((value) => {
  console.log("count is now", value);
});

count.set(1); // logs: count is now 1
count.set(1); // no-op — value unchanged, no notification
count.update((n) => n + 1); // logs: count is now 2

off(); // stop listening
count.set(3); // nothing logged
```

Typed signals infer `T` from the initial value, or can be annotated explicitly:

```ts
import { signal, type Signal } from "@wrnexus/reactive";

const user: Signal<{ name: string } | null> = signal(null);
user.set({ name: "Ada" });
```

## Requirements / Notes

- **Bun-only.** Distributed as TypeScript source (`main`/`exports` point at
  `src/index.ts`); consume it under Bun, which runs `.ts` directly.
- **Zero dependencies.** The only runtime API used is the standard `Object.is`.
- Foundational primitive for WrNexus client islands and the forthcoming `.wrn`
  compiler `state` blocks.

### Exported TypeScript declarations

```ts
/**
 * Fine-grained reactive primitives shared by server utilities and client code.
 * Updates are synchronous by default and coalesced inside `batch()`.
 */
type Subscriber<T> = (value: T, previous?: T) => void;
type Unsubscribe = () => void;
type Cleanup = () => void;
interface Signal<T> {
    get(): T;
    set(next: T): void;
    update(fn: (current: T) => T): void;
    subscribe(fn: Subscriber<T>): Unsubscribe;
}
interface ReadonlySignal<T> {
    get(): T;
    subscribe(fn: Subscriber<T>): Unsubscribe;
}
/** Coalesce every signal notification made by `fn` into one flush. */
declare function batch<T>(fn: () => T): T;
/** Read reactive values without recording dependencies. */
declare function untrack<T>(fn: () => T): T;
declare function signal<T>(initial: T): Signal<T>;
/**
 * Run a dependency-tracked side effect. Dependencies are rebuilt after every
 * execution, preventing stale subscriptions when conditional reads change.
 */
declare function effect(run: () => void | Cleanup): Cleanup;
/** Create a lazily readable derived signal with automatic dependency tracking. */
declare function computed<T>(read: () => T): ReadonlySignal<T>;

interface WatchOptions<T> {
    immediate?: boolean;
    equals?: (left: T, right: T) => boolean;
}
declare function watch<T>(read: () => T, listener: (value: T, previous: T | undefined) => void | Cleanup, options?: WatchOptions<T>): Cleanup;
type ResourceStatus = "idle" | "pending" | "success" | "error";
interface Resource<T> {
    data: ReadonlySignal<T | undefined>;
    error: ReadonlySignal<unknown>;
    status: ReadonlySignal<ResourceStatus>;
    loading: ReadonlySignal<boolean>;
    run(): Promise<T | undefined>;
    abort(reason?: unknown): void;
    reset(): void;
}
interface ResourceOptions<T> {
    initial?: T;
    immediate?: boolean;
    keepPrevious?: boolean;
}
declare function resource<T>(loader: (signal: AbortSignal) => Promise<T>, options?: ResourceOptions<T>): Resource<T>;
interface ReactiveScope {
    add(cleanup: Cleanup): Cleanup;
    dispose(): void;
    readonly disposed: boolean;
}
declare function createScope(): ReactiveScope;

interface HistorySignal<T> extends Signal<T> {
    undo(): boolean;
    redo(): boolean;
    canUndo(): boolean;
    canRedo(): boolean;
    clearHistory(): void;
}
declare function historySignal<T>(initial: T, options?: {
    limit?: number;
    equals?: (left: T, right: T) => boolean;
}): HistorySignal<T>;
interface UrlStateOptions<T> {
    url?: URL;
    parameter: string;
    parse?: (value: string | null) => T;
    serialize?: (value: T) => string | null;
    replace?: (url: URL) => void;
}
declare function urlSignal<T>(initial: T, options: UrlStateOptions<T>): Signal<T>;
interface ReactiveContext<T> {
    provide<R>(value: T, run: () => R): R;
    use(): T;
}
declare function createContextProvider<T>(defaultValue?: T): ReactiveContext<T>;
declare function mountPortal(content: Node | string, target: Element): () => void;
declare function transition(update: () => void, options?: {
    className?: string;
    target?: Element;
    durationMs?: number;
    signal?: AbortSignal;
}): Promise<void>;
interface TimelineStep {
    durationMs: number;
    delayMs?: number;
    easing?: (progress: number) => number;
    update(progress: number): void;
}
interface AnimationTimeline {
    play(options?: {
        reverse?: boolean;
        signal?: AbortSignal;
    }): Promise<void>;
    cancel(reason?: unknown): void;
    readonly running: boolean;
}
declare function createTimeline(steps: TimelineStep[], options?: {
    now?: () => number;
    frame?: (callback: () => void) => unknown;
}): AnimationTimeline;

export { type AnimationTimeline, type Cleanup, type HistorySignal, type ReactiveContext, type ReactiveScope, type ReadonlySignal, type Resource, type ResourceOptions, type ResourceStatus, type Signal, type Subscriber, type TimelineStep, type Unsubscribe, type UrlStateOptions, type WatchOptions, batch, computed, createContextProvider, createScope, createTimeline, effect, historySignal, mountPortal, resource, signal, transition, untrack, urlSignal, watch };
```

---

## @wrnexus/realtime

Documentation URL: https://wrnexusjs.dev/packages/realtime

# @wrnexus/realtime

Typed rooms, secure message envelopes, browser room helpers, presence utilities, and complete realtime UI blocks for WRNexusJS.

## Server rooms

```ts
import { defineRoom } from "@wrnexus/realtime";

export default defineRoom("support", {
  async authorize(context) {
    return Boolean(context.user);
  },
  message(client, message) {
    client.broadcast(message);
  },
});
```

The package re-exports the hardened realtime registry from `@wrnexus/core`, including authentication, origin checks, quotas, message-size limits, schema validation hooks, and room authorization.

## Messages and browser helpers

```ts
import {
  createRealtimeMessage,
  createPresenceEvent,
  createTypingEvent,
  connectRoom,
  sendRoomMessage,
} from "@wrnexus/realtime";

const room = connectRoom("support", { query: { ticket: "T-100" } });
sendRoomMessage(
  room,
  createRealtimeMessage({
    type: "message",
    room: "support",
    data: { text: "Hello" },
  }),
);
```

Message IDs use Web Crypto. A runtime without secure randomness must provide an explicit message ID.

## Components

Enable `realtimePlugin()` and use:

- `<RealtimeRoom />`
- `<RealtimeMessageBubble />`
- `<MessageComposer />`
- `<RoomStatus />`
- `<RoomMeta />`
- `<PresenceList />`
- `<TypingIndicator />`

These package-owned blocks compose existing `@wrnexus/ui` components such as `Card`, `Alert`, `Avatar`, `Badge`, `Button`, `Input`, and `ChatBubble`.

Incoming messages can be bounded and constrained:

```ts
const message = parseRealtimeMessage(rawMessage, {
  maxBytes: 64 * 1024,
  maxDepth: 12,
  allowedTypes: ["message", "typing", "presence"],
  room: "support",
});
```

The parser rejects oversized payloads, circular/unsupported values, unsafe object keys, invalid message types, invalid room names, and room mismatches.

## Replay, acknowledgements, SSE, and monitoring

`createRealtimeHistory()` keeps a bounded sequenced log per room. Clients acknowledge a
sequence and `resume(room, clientId)` returns only missed events. A snapshot exposes room,
message, acknowledgement, and sequence counts for monitoring without exposing payloads.

```ts
const history = createRealtimeHistory({ limitPerRoom: 100 });
const entry = history.publish("support", message);
history.acknowledge("support", clientId, entry.sequence);
const missed = history.resume("support", clientId);
```

`realtimeSseResponse(stream, signal)` converts the same sequenced envelope into standards-based
Server-Sent Events with event IDs, event types, JSON data, cancellation, and no-cache headers.

### Exported TypeScript declarations

```ts
export { RawSocket, RealtimeBridge, RealtimeBus, RealtimeConnectMeta, RealtimeEnvelope, RealtimeHandler, RealtimeRegistry, RealtimeRegistryOptions, RealtimeSecurityOptions, RealtimeSocket, Room, RoomAuthInfo, RoomClient, RoomDefinition, RoomHandlers, Target, bridgeRealtime, createRealtimeRegistry, defineRoom, isRoomDefinition } from '@wrnexus/core';
export { RealtimePluginOptions, realtimeComponentsDir, default as realtimePlugin } from './plugin.js';
import '@wrnexus/plugin';

type RealtimeMessageType = string;
interface RealtimeMessage<T = unknown> {
    id: string;
    type: RealtimeMessageType;
    room?: string;
    senderId?: string;
    senderName?: string;
    sentAt: string;
    data: T;
    meta?: Record<string, string | number | boolean | null>;
}
interface CreateRealtimeMessageOptions<T> {
    id?: string;
    type: string;
    room?: string;
    senderId?: string;
    senderName?: string;
    sentAt?: string | Date;
    data: T;
    meta?: Record<string, string | number | boolean | null>;
}
interface ParseRealtimeMessageOptions {
    maxBytes?: number;
    maxDepth?: number;
    allowedTypes?: readonly string[];
    room?: string;
}
interface RealtimePresence {
    userId: string;
    name?: string;
    avatar?: string;
    status?: "online" | "away" | "busy" | "offline";
    joinedAt?: string;
    lastSeenAt?: string;
    meta?: Record<string, string | number | boolean | null>;
}
interface RealtimeRoomMeta {
    id: string;
    name: string;
    description?: string;
    memberCount?: number;
    onlineCount?: number;
    createdAt?: string;
    updatedAt?: string;
    private?: boolean;
    tags?: string[];
}
declare function assertRealtimeRoomName(name: string): string;
declare function createRealtimeMessage<T>(options: CreateRealtimeMessageOptions<T>): RealtimeMessage<T>;
declare function isRealtimeMessage(value: unknown): value is RealtimeMessage;
declare function parseRealtimeMessage<T = unknown>(value: string | unknown, options?: ParseRealtimeMessageOptions): RealtimeMessage<T>;
declare function createPresenceEvent(action: "join" | "leave" | "update", presence: RealtimePresence, room?: string): RealtimeMessage<{
    action: "join" | "leave" | "update";
    presence: RealtimePresence;
}>;
declare function createTypingEvent(userId: string, typing: boolean, options?: {
    room?: string;
    name?: string;
}): RealtimeMessage<{
    userId: string;
    name?: string;
    typing: boolean;
}>;
declare function roomMemberSummary(members: readonly RealtimePresence[]): {
    total: number;
    online: number;
    away: number;
    busy: number;
};

interface BrowserRoomConnection {
    readonly name: string;
    send(message: unknown): BrowserRoomConnection;
    on(type: string | ((message: unknown) => void), callback?: (message: unknown) => void): BrowserRoomConnection;
    close(): void;
}
interface WrnexusRealtimeWindow extends Window {
    wire?: {
        room?: (name: string, query?: string) => BrowserRoomConnection;
    };
}
declare function roomQuery(params: Record<string, string | number | boolean | null | undefined>): string;
declare function connectRoom(name: string, options?: {
    query?: Record<string, string | number | boolean | null | undefined>;
    window?: WrnexusRealtimeWindow;
}): BrowserRoomConnection;
declare function sendRoomMessage<T>(room: BrowserRoomConnection, message: RealtimeMessage<T> | T): BrowserRoomConnection;

interface SequencedRealtimeMessage<T = unknown> {
    sequence: number;
    message: RealtimeMessage<T>;
}
interface RealtimeHistorySnapshot {
    rooms: number;
    messages: number;
    acknowledgements: number;
    oldestSequence?: number;
    latestSequence?: number;
}
interface RealtimeHistoryOptions {
    limitPerRoom?: number;
    maxClients?: number;
}
interface RealtimeHistory {
    publish<T>(room: string, message: RealtimeMessage<T>): SequencedRealtimeMessage<T>;
    replay(room: string, afterSequence?: number, limit?: number): SequencedRealtimeMessage[];
    acknowledge(room: string, clientId: string, sequence: number): void;
    acknowledged(room: string, clientId: string): number;
    resume(room: string, clientId: string, limit?: number): SequencedRealtimeMessage[];
    snapshot(): RealtimeHistorySnapshot;
    clear(room?: string): void;
}
declare function createRealtimeHistory(options?: RealtimeHistoryOptions): RealtimeHistory;
declare function createAcknowledgement(room: string, sequence: number, clientId: string): RealtimeMessage<{
    sequence: number;
    clientId: string;
}>;
declare function realtimeSseResponse(stream: ReadableStream<SequencedRealtimeMessage>, signal?: AbortSignal): Response;

interface DatabaseChange<T = unknown> {
    table: string;
    operation: "insert" | "update" | "delete";
    key?: string | number;
    record?: T;
    occurredAt: number;
}
interface DatabaseChangeSource {
    subscribe(handler: (change: DatabaseChange) => void | Promise<void>): () => void;
}
declare function databaseChangeFeed(source: DatabaseChangeSource, publish: (topic: string, change: DatabaseChange) => void | Promise<void>, options?: {
    prefix?: string;
    allowTables?: string[];
}): () => void;
interface FileStreamFrame {
    streamId: string;
    index: number;
    total: number;
    bytes: Uint8Array;
}
declare function frameFileStream(streamId: string, bytes: Uint8Array, options?: {
    chunkBytes?: number;
    maxBytes?: number;
}): FileStreamFrame[];
declare function createFileStreamReceiver(options?: {
    maxBytes?: number;
    maxStreams?: number;
}): {
    accept(frame: FileStreamFrame): Uint8Array | null;
    snapshot: () => {
        activeStreams: number;
        bufferedBytes: number;
    };
};

export { type BrowserRoomConnection, type CreateRealtimeMessageOptions, type DatabaseChange, type DatabaseChangeSource, type FileStreamFrame, type ParseRealtimeMessageOptions, type RealtimeHistory, type RealtimeHistoryOptions, type RealtimeHistorySnapshot, type RealtimeMessage, type RealtimeMessageType, type RealtimePresence, type RealtimeRoomMeta, type SequencedRealtimeMessage, type WrnexusRealtimeWindow, assertRealtimeRoomName, connectRoom, createAcknowledgement, createFileStreamReceiver, createPresenceEvent, createRealtimeHistory, createRealtimeMessage, createTypingEvent, databaseChangeFeed, frameFileStream, isRealtimeMessage, parseRealtimeMessage, realtimeSseResponse, roomMemberSummary, roomQuery, sendRoomMessage };
```

---

## @wrnexus/router

Documentation URL: https://wrnexusjs.dev/packages/router

# @wrnexus/router

> File-based router that maps an `app/` directory onto route tables and matches request paths against them.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

`@wrnexus/router` scans an application's `app/` directory once at startup and builds route tables for pages, API endpoints, realtime channels, middleware, server-rendered `.wrn` components, layouts, and validation schemas. It also compiles URL patterns (`/users/[id]`) into RegExps and matches request paths against them. Request input is never turned into a file path, which makes the router immune to path traversal. This is a server-side package used by the WrNexus runtime to resolve incoming requests, plus a codegen helper for compile-time typed links.

## Installation

```bash
bun add @wrnexus/router
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

## Directory conventions

The router maps files under `appDir` onto routes:

```
app/pages/index.tsx        -> GET /
app/pages/about.tsx        -> GET /about
app/pages/users/[id].tsx   -> GET /users/:id
app/api/hello.ts           -> /api/hello
app/realtime/chat.ts       -> /realtime/chat
app/pages/*.wrn  (api)    -> embedded /api/* routes
app/pages/*.wrn  (rt)     -> embedded /realtime/* routes
app/middleware/*.ts        -> global middleware (alphabetical)
app/components/*.wrn      -> server-rendered components (by basename)
app/layouts/*.wrn         -> named page layouts
app/schemas/*.ts           -> validation schemas
```

Allowed route extensions are `.ts`, `.tsx`, and `.wrn`. Dotfiles and underscore-prefixed files are ignored. A trailing `index` segment is dropped from the route. `.wrn` pages may embed `api` and `realtime` blocks, which the router extracts and mounts under `/api/*` and `/realtime/*`.

## API

### `buildRouter(appDir, opts?): Router`

Scan an app directory and build all route tables.

```ts
function buildRouter(appDir: string, opts?: RouterOptions): Router;

interface RouterOptions {
  /** Extra dirs scanned for `.wrn` components (e.g. `@wrnexus/ui`), before
   *  `app/components`, so an app component of the same name wins. */
  componentDirs?: string[];
}
```

The returned `Router` exposes the built tables plus per-kind matchers:

```ts
interface Router {
  pages: Route[];
  api: Route[];
  realtime: Route[];
  /** Absolute paths of middleware modules, in execution order (alphabetical). */
  middlewareFiles: string[];
  /** Server-rendered `.wrn` components, mounted via `data-component`. */
  components: ComponentRef[];
  /** Named page layouts (`app/layouts/<name>.wrn`); a page picks one via `layout`. */
  layouts: ComponentRef[];
  /** Validation schemas (`app/schemas/<name>.ts`) shared by API + forms. */
  schemas: ComponentRef[];
  matchPage(pathname: string): RouteMatch | null;
  matchApi(pathname: string): RouteMatch | null;
  matchRealtime(pathname: string): RouteMatch | null;
}

interface ComponentRef {
  /** Validated component name (matches a `data-component` attribute). */
  name: string;
  /** Absolute path to the component's `.wrn` module. */
  file: string;
}
```

Component, layout, and schema names are validated with `isSafeIslandName` from `@wrnexus/core`; unsafe names are skipped with a warning. Realtime channel names are validated the same way.

### Route matching

| Export                | Signature                                                   | Description                                                                                                         |
| --------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `compileRoutePattern` | `(raw: string) => Pick<Route, "regex" \| "paramNames">`     | Compile a `/users/[id]` pattern into a RegExp (with optional trailing slash) plus ordered param names.              |
| `matchRoute`          | `(routes: Route[], pathname: string) => RouteMatch \| null` | Return the first route whose regex matches; captured params are `decodeURIComponent`-decoded.                       |
| `sortRoutes`          | `(routes: Route[]) => Route[]`                              | Order routes so static routes win over dynamic ones (fewer params first), then longer/more specific patterns first. |

```ts
interface Route {
  raw: string; // e.g. "/users/[id]"
  file: string; // absolute path to the handling module
  regex: RegExp; // compiled matcher
  paramNames: string[]; // ordered dynamic param names
}

interface RouteMatch {
  route: Route;
  params: Record<string, string>;
}
```

### Typed-routes codegen

```ts
function generateRoutesFile(pages: Route[]): string;
```

Emits the source for `app/routes.gen.ts`: a `Routes` map (each page path → its `[param]` types), a `RoutePath` union, and an `href()` builder that fills params and rejects unknown paths at compile time. Entries are de-duplicated and sorted by path.

### Re-exports

`Middleware` (the type from `@wrnexus/core`) is re-exported for callers that load middleware modules themselves.

## Usage

```ts
import { buildRouter } from "@wrnexus/router";

const router = buildRouter("./app", {
  componentDirs: ["./node_modules/@wrnexus/ui/components"],
});

// Resolve an incoming request.
const match = router.matchPage("/users/42");
if (match) {
  console.log(match.route.file); // absolute path to the page module
  console.log(match.params); // { id: "42" }
}

const api = router.matchApi("/api/hello");
const rt = router.matchRealtime("/realtime/chat");
```

Generating the typed-routes file (as `wrnexus dev` does):

```ts
import { generateRoutesFile } from "@wrnexus/router";
import { writeFileSync } from "node:fs";

const router = buildRouter("./app");
writeFileSync("./app/routes.gen.ts", generateRoutesFile(router.pages));
```

```ts
// Then, in app code, links are checked at compile time:
import { href } from "./routes.gen.ts";

href("/users/[id]", { id: "42" }); // "/users/42"
href("/about"); // "/about"
href("/nope"); // type error: unknown path
```

Lower-level pattern matching, if you need it directly:

```ts
import { compileRoutePattern, matchRoute, sortRoutes, type Route } from "@wrnexus/router";

const { regex, paramNames } = compileRoutePattern("/posts/[slug]");
const routes = sortRoutes([{ raw: "/posts/[slug]", file: "…", regex, paramNames }]);
const m = matchRoute(routes, "/posts/hello"); // { route, params: { slug: "hello" } }
```

## Requirements / Notes

- Scanning uses `node:fs` (`existsSync`, `readdirSync`, `statSync`) and `node:path` — runs under Bun.
- Depends on [`@wrnexus/compiler`](../compiler) to `parse` `.wrn` pages and extract embedded `api` / `realtime` blocks.
- Depends on [`@wrnexus/core`](../core) for `isSafeIslandName` (name validation) and the `Middleware` type.
- Missing route directories are tolerated — a route kind you don't use simply yields an empty table.

### Exported TypeScript declarations

```ts
export { Middleware } from '@wrnexus/core';

/**
 * Route compilation + matching.
 *
 * Supported segments:
 *   [id]          required parameter
 *   [id?]         optional parameter
 *   [[id]]        optional parameter (directory-friendly form)
 *   [...slug]     required catch-all
 *   [[...slug]]   optional catch-all
 */
interface RouteParam {
    name: string;
    optional: boolean;
    catchAll: boolean;
}
interface Route {
    /** The human-readable route pattern, e.g. `/users/[id]`. */
    raw: string;
    /** Absolute path to the module that handles this route. */
    file: string;
    /** Compiled matcher. */
    regex: RegExp;
    /** Ordered names of dynamic params captured by `regex`. */
    paramNames: string[];
    /** Rich parameter metadata. Optional for compatibility with old manifests. */
    paramMeta?: RouteParam[];
}
interface RouteMatch {
    route: Route;
    params: Record<string, string>;
}
/** Return parameter metadata without requiring callers to inspect the regex. */
declare function getRouteParams(raw: string): RouteParam[];
/** Compile a WRNexus route pattern into a RegExp + parameter metadata. */
declare function compileRoutePattern(raw: string): Pick<Route, "regex" | "paramNames" | "paramMeta">;
/**
 * Order routes so static and constrained routes win over optional/catch-all
 * routes. The ordering remains deterministic for identical specificity.
 */
declare function sortRoutes(routes: Route[]): Route[];
/** Find duplicate URL patterns before request handling starts. */
declare function findRouteConflicts(routes: Route[]): Array<{
    raw: string;
    files: string[];
}>;
/** Find the first route whose pattern matches `pathname`. */
declare function matchRoute(routes: Route[], pathname: string): RouteMatch | null;

/**
 * Typed-routes codegen. From the scanned page routes, emit `app/routes.gen.ts`
 * with a `Routes` map (path -> param types) and an `href()` builder.
 */

declare function generateRoutesFile(pages: Route[]): string;

interface NamedRoute extends Route {
    name: string;
    metadata?: Record<string, unknown>;
}
interface RouteManifestEntry {
    name: string;
    path: string;
    file: string;
    params: ReturnType<typeof getRouteParams>;
    metadata?: Record<string, unknown>;
}
declare function routeName(raw: string): string;
declare function nameRoutes(routes: readonly Route[], metadata?: Record<string, Record<string, unknown>>): NamedRoute[];
declare function createRouteManifest(routes: readonly NamedRoute[]): RouteManifestEntry[];
declare function routeUrl(route: Pick<NamedRoute, "raw" | "paramMeta">, params?: Record<string, string | number | Array<string | number> | null | undefined>, query?: Record<string, string | number | boolean | null | undefined>): string;
declare function findNamedRoute(routes: readonly NamedRoute[], name: string): NamedRoute;

/**
 * @wrnexus/router — file-based router.
 *
 * Maps the `app/` directory onto route tables:
 *   app/pages/index.tsx     -> GET /
 *   app/pages/about.tsx     -> GET /about
 *   app/pages/users/[id].tsx-> GET /users/:id
 *   app/api/hello.ts        -> /api/hello
 *   app/realtime/chat.ts    -> /realtime/chat
 *   app/pages/*.wrn api      -> embedded /api/* routes
 *   app/pages/*.wrn realtime -> embedded /realtime/* routes
 *   app/middleware/*.ts     -> global middleware (alphabetical)
 *   app/components/*.wrn   -> server-rendered components (by declaration),
 *                              mounted in a page via data-component="<name>"
 */

interface ComponentRef {
    /** Validated component name (matches a `data-component` attribute). */
    name: string;
    /** Absolute path to the component's `.wrn` module. */
    file: string;
}
interface Router {
    pages: Route[];
    api: Route[];
    realtime: Route[];
    /** Absolute paths of middleware modules, in execution order. */
    middlewareFiles: string[];
    /** Server-rendered `.wrn` components, mounted via `data-component`. */
    components: ComponentRef[];
    /** Named page layouts (`app/layouts/<name>.wrn`); a page picks one via `layout`. */
    layouts: ComponentRef[];
    /** Typed global/page stores discovered under `app/stores/`. */
    stores: ComponentRef[];
    /** Validation schemas (`app/schemas/<name>.ts`) shared by API + forms. */
    schemas: ComponentRef[];
    /** Authorization declarations (`app/authz/<name>.ts`) merged into the catalog. */
    authz: ComponentRef[];
    /** Service implementations (`app/services/<name>.ts`) mounted for inter-app calls. */
    services: ComponentRef[];
    matchPage(pathname: string): RouteMatch | null;
    matchApi(pathname: string): RouteMatch | null;
    matchRealtime(pathname: string): RouteMatch | null;
}
interface ExternalRouteDefinition {
    kind: "page" | "api" | "realtime";
    path: string;
    entry: string;
    name?: string;
}
interface RouterOptions {
    /**
     * Extra directories to scan for `.wrn` components (e.g. `@wrnexus/ui`).
     * Scanned before `app/components`, so an app component of the same name wins.
     */
    componentDirs?: string[];
    /** Package-owned routes registered by the plugin contribution system. */
    externalRoutes?: ExternalRouteDefinition[];
    /** Package-owned middleware executed before app/middleware. */
    middlewareFiles?: string[];
}
/**
 * Convert a scanned file's relative path into a URL route pattern.
 *  - strips the extension
 *  - drops a trailing `index` segment
 *  - prefixes with `prefix` (e.g. "/api")
 */
declare function fileToRoute(rel: string, prefix?: string): string;
/** Scan an app directory and build all route tables. */
declare function buildRouter(appDir: string, opts?: RouterOptions): Router;

export { type ComponentRef, type ExternalRouteDefinition, type NamedRoute, type Route, type RouteManifestEntry, type RouteMatch, type Router, type RouterOptions, buildRouter, compileRoutePattern, createRouteManifest, fileToRoute, findNamedRoute, findRouteConflicts, generateRoutesFile, getRouteParams, matchRoute, nameRoutes, routeName, routeUrl, sortRoutes };
```

---

## @wrnexus/rpc

Documentation URL: https://wrnexusjs.dev/packages/rpc

# `@wrnexus/rpc`

Define a service contract in a shared workspace package, then import that same contract from the caller and callee.

```ts
import {
  defineService,
  implement,
  inProcessTransport,
  procedure,
  serviceClient,
} from "@wrnexus/rpc";
import { v } from "@wrnexus/validation";

const greeter = defineService({
  name: "greeter",
  procedures: {
    greet: procedure
      .input(v.object({ name: v.string() }))
      .output<{ message: string }>()
      .build(),
  },
});

const service = implement(
  greeter,
  { greet: async ({ name }) => ({ message: `Hello, ${name}` }) },
  { selfApp: "greeter" },
);

const client = serviceClient(greeter, {
  app: "greeter",
  transport: inProcessTransport({
    "greeter/greet": (input, identity) => service.invoke("greet", input, identity),
  }),
});
await client.greet({ name: "Ada" });
```

Service files default-export `implement(...)` from `app/services`. The development server mounts them under the private `/__wrnexus/rpc` prefix.

Pass `{ as: ctx }` to `serviceClient` to propagate the subject. The signed token contains only subject and tenant identifiers; permissions are always checked by the callee. Set `WRNEXUS_RPC_SECRET` in every app, use at least 32 characters, and never reuse the session secret.

Calls time out by default. Retrying is intentionally deferred; when introduced, only procedures marked `.idempotent()` may be retried.

## Deployment requirement: apps must be unreachable except through the gateway

`/__wrnexus/rpc/*` is authenticated by TWO signals together: a marker header
(`x-wrnexus-internal: 1`) AND the absence of any `X-Forwarded-*` header. The
WrNexus gateway satisfies this by construction — it strips any inbound
marker header from the public request, and it always adds `X-Forwarded-*`
when proxying to an app. A direct loopback call from a sibling app process
carries the marker and no forwarded headers, so it passes; anything that
came through the gateway carries forwarded headers, so it's rejected even if
it also carries the marker.

**This check only works if the app process is unreachable except through the
gateway.** If an app's port is exposed directly, or if a reverse proxy sits
in front of it WITHOUT setting `X-Forwarded-*` (a bare `proxy_pass` with no
`proxy_set_header X-Forwarded-For`/`X-Forwarded-Host`/`X-Forwarded-Proto`),
then an external caller can set the marker header itself, arrive with no
forwarded headers, and reach `/__wrnexus/rpc/*` as if it were an internal
call — bypassing the gateway's edge block entirely.

Requirements for any deployment:

- App processes must bind to a private/loopback interface and be reachable
  ONLY through the gateway (or an equivalent trusted front door) — never
  exposed directly to the internet or an untrusted network.
- Any reverse proxy placed in front of an app (nginx, a load balancer, etc.)
  MUST set `X-Forwarded-For`, `X-Forwarded-Host`, and `X-Forwarded-Proto` on
  every request it forwards. Omitting these silently reopens the private RPC
  namespace to anyone who can reach the proxy.

### Exported TypeScript declarations

```ts
import { ObjectSchema } from '@wrnexus/validation';
import { Context } from '@wrnexus/core';

/** Extract the validated value type from a `v.object(...)` schema. */
type InferInput<S> = S extends ObjectSchema<infer T> ? T : never;
/**
 * One callable procedure on a service. `input` is validated on the callee
 * before the handler runs; `permission` is enforced there too.
 */
/** Structural shape of a validation schema, so ProcedureDef needs no generic. */
interface InputSchema {
    parse(value: Record<string, unknown>): {
        ok: boolean;
        value: unknown;
        errors: Record<string, string>;
    };
}
interface ProcedureDef<Input = unknown, Output = unknown> {
    input?: InputSchema;
    /** Permission the callee checks before invoking the handler. */
    permission?: string;
    /** Only idempotent procedures are ever retried. */
    idempotent?: boolean;
    /** Type-only markers; never present at runtime. */
    readonly __input?: Input;
    readonly __output?: Output;
}
/**
 * A procedure map with its element types erased. The `any` is deliberate and
 * confined to this alias: the phantom `__input`/`__output` markers make
 * ProcedureDef invariant, so no narrower erasure accepts a real contract.
 * (No eslint-disable needed — `no-explicit-any` is off repo-wide, and a
 * redundant directive is itself a lint warning.)
 */
type AnyProcedures = Record<string, ProcedureDef<any, any>>;
interface ServiceContract<Procedures extends AnyProcedures = AnyProcedures> {
    /** Stable service id, used in the mounted path. */
    name: string;
    procedures: Procedures;
}
type InferProcedureInput<P> = P extends ProcedureDef<infer I, unknown> ? I : never;
type InferProcedureOutput<P> = P extends ProcedureDef<unknown, infer O> ? O : never;
/** What a transport returns: either a value or a structured failure. */
type ServiceResult<T = unknown> = {
    ok: true;
    value: T;
} | {
    ok: false;
    code: string;
    message: string;
    retryable: boolean;
};

declare const RPC_ERROR_CODES: {
    /** The request never reached a handler: connection, timeout, 5xx. */
    readonly transport: "RPC_TRANSPORT";
    /** Input failed the contract's schema. */
    readonly invalid: "RPC_INVALID";
    /** The callee's permission check refused. */
    readonly denied: "RPC_DENIED";
    /** No such service or procedure on the callee. */
    readonly unknown: "RPC_UNKNOWN";
    /** The handler threw or returned a failure. */
    readonly handler: "RPC_HANDLER";
    /** Identity token missing, malformed, expired, or for another audience. */
    readonly identity: "RPC_IDENTITY";
    /**
     * The callee answered, but not with a ServiceResult — a proxy's HTML error
     * page, a truncated body, an unexpected shape. Distinct from `transport`:
     * something DID respond, so retrying returns the same thing.
     */
    readonly malformed: "RPC_MALFORMED";
};
type RpcErrorCode = (typeof RPC_ERROR_CODES)[keyof typeof RPC_ERROR_CODES];
/**
 * 5xx, 429 and 408 mean "the callee could not answer, try later". Any other
 * 4xx is the callee saying no — retrying just repeats the same rejection.
 *
 * The range is bounded on BOTH sides deliberately: an unbounded `>= 500`
 * puts a garbage status like 1000 in the retryable bucket, and this function
 * is the sole gate the client and HTTP transport trust for retry safety.
 * An out-of-range value must fail closed, i.e. not retryable.
 */
declare function isRetryableStatus(status: number): boolean;
declare function success<T>(value: T): ServiceResult<T>;
declare function failure(code: string, message: string): ServiceResult<never>;
interface ToResultOptions {
    /** Include the original message. Off by default: it may name internals. */
    exposeMessage?: boolean;
}
declare class ServiceError extends Error {
    readonly code: string;
    readonly retryable: boolean;
    /**
     * `retryable` defaults to the code-derived value for callers that
     * construct a `ServiceError` directly. Pass it explicitly when relaying a
     * wire result: the transport already computed the authoritative value
     * (e.g. a bounded HTTP-status check), and recomputing it here from the
     * code alone would silently flip it — `RPC_TRANSPORT` derives to `true`,
     * even for a non-retryable 403.
     */
    constructor(code: string, message: string, retryable?: boolean);
    /**
     * Convert to a wire result. The message is replaced unless explicitly
     * exposed: a handler's error text routinely names tables, hosts, or
     * credentials, and this value crosses an app boundary.
     */
    toResult(options?: ToResultOptions): ServiceResult<never>;
}

/**
 * Fluent, IMMUTABLE builder: every method returns a new builder, so a shared
 * base can be branched without one branch mutating another.
 */
declare class ProcedureBuilder<Input, Output> {
    private readonly def;
    private constructor();
    static create(): ProcedureBuilder<void, void>;
    input<S extends ObjectSchema<object>>(schema: S): ProcedureBuilder<InferInput<S>, Output>;
    output<T>(): ProcedureBuilder<Input, T>;
    permission(id: string): ProcedureBuilder<Input, Output>;
    /** Mark safe to retry. Anything not marked is never retried. */
    idempotent(): ProcedureBuilder<Input, Output>;
    build(): ProcedureDef<Input, Output>;
}
declare const procedure: ProcedureBuilder<void, void>;
declare function defineService<Procedures extends AnyProcedures>(def: {
    name: string;
    procedures: Procedures;
}): ServiceContract<Procedures>;

/** Header the identity token travels in. */
declare const RPC_IDENTITY_HEADER = "x-wrnexus-rpc-identity";
interface SubjectContext {
    subjectId: string;
    tenantId?: string;
    /**
     * The app that CLAIMS to have minted the token. Self-asserted: the signing
     * secret is workspace-wide, so any app can set this to any name. Useful for
     * logs and tracing; NEVER an authorization input.
     */
    callerApp: string;
}
interface ExportOptions {
    ttlSeconds?: number;
}
interface ImportOptions {
    /**
     * Reject a token older than this regardless of its own `exp`, so a caller
     * that mints with a huge ttlSeconds cannot create a long-lived
     * impersonation credential the callee will honour. Defaults to 300s.
     */
    maxAgeSeconds?: number;
}
/**
 * The workspace-wide RPC signing secret.
 *
 * Deliberately separate from the session secret: reusing that would make a
 * leaked RPC token a session-forgery primitive. All workspace apps share this
 * secret, so they form ONE trust boundary — any app can mint a token naming
 * any user, and compromising the lowest-privilege app compromises identity
 * across all of them.
 */
declare function rpcSecret(): string;
/**
 * Mint a short-lived token naming the current subject, addressed to one app.
 *
 * Carries `sub` and `tenant` ONLY. Roles are deliberately absent: every app
 * shares the PermissionStore, so the callee resolves them itself, which makes
 * a stale or forged privilege claim impossible by construction.
 *
 * Returns undefined for an anonymous request — there is no identity to carry.
 */
declare function exportSubjectContext(ctx: Context, targetApp: string, options?: ExportOptions): Promise<string | undefined>;
/**
 * Verify a token addressed to THIS app and return the subject it names.
 *
 * `selfApp` is the audience check: it is what stops app B replaying a token it
 * received from A against a third app C.
 */
declare function importSubjectContext(token: string, selfApp: string, options?: ImportOptions): Promise<SubjectContext>;

interface RpcTarget {
    app: string;
    service: string;
    procedure: string;
}
interface CallOptions {
    signal?: AbortSignal;
    identity?: string;
    /** Supplied from the declared procedure; only these calls may be retried. */
    idempotent?: boolean;
}
interface Transport {
    call(target: RpcTarget, payload: unknown, options: CallOptions): Promise<ServiceResult>;
}
interface RetryTransportOptions {
    /** Retries after the initial attempt. Default: 2. */
    retries?: number;
    /** Initial exponential-backoff delay in milliseconds. Default: 50. */
    backoffMs?: number;
    /** Consecutive retryable failures before the target circuit opens. Default: 3. */
    circuitFailureThreshold?: number;
    /** How long an open circuit rejects calls before one probe is allowed. Default: 5s. */
    circuitCooldownMs?: number;
    now?: () => number;
    sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
}
/**
 * Add bounded retry and a per-procedure circuit breaker to any transport.
 * The idempotency bit comes from the contract and is not caller-controlled.
 */
declare function retryingTransport(base: Transport, options?: RetryTransportOptions): Transport;
type InProcessHandler = (payload: unknown, identity?: string) => Promise<ServiceResult> | ServiceResult;
/** Direct transport for tests and local integration harnesses. */
declare function inProcessTransport(handlers: Record<string, InProcessHandler>): Transport;

interface HandlerContext {
    subject?: SubjectContext;
}
type ServiceHandlers<Procedures extends AnyProcedures> = {
    [K in keyof Procedures]: (input: InferProcedureInput<Procedures[K]>, ctx: HandlerContext) => Promise<InferProcedureOutput<Procedures[K]>> | InferProcedureOutput<Procedures[K]>;
};
interface ImplementOptions {
    selfApp: string;
    checkPermission?: (permission: string, subject?: SubjectContext) => Promise<boolean> | boolean;
}
interface ServiceImplementation<Procedures extends AnyProcedures = AnyProcedures> {
    contract: ServiceContract<Procedures>;
    invoke(procedure: string, payload: unknown, identity?: string): Promise<ServiceResult>;
}
declare function implement<Procedures extends AnyProcedures>(contract: ServiceContract<Procedures>, handlers: ServiceHandlers<Procedures>, options: ImplementOptions): ServiceImplementation<Procedures>;

interface ServiceClientOptions {
    app?: string;
    transport: Transport;
    as?: Context;
    timeoutMs?: number;
}
type ServiceClient<Procedures extends AnyProcedures> = {
    [K in keyof Procedures]: (input: InferProcedureInput<Procedures[K]>) => Promise<InferProcedureOutput<Procedures[K]>>;
};
declare function serviceClient<Procedures extends AnyProcedures>(contract: ServiceContract<Procedures>, options: ServiceClientOptions): ServiceClient<Procedures>;

declare const RPC_PATH_PREFIX = "/__wrnexus/rpc";
declare const RPC_INTERNAL_HEADER = "x-wrnexus-internal";
declare function rpcPath(service: string, procedure: string): string;
/**
 * Resolve the origin an RPC call to `app` should target.
 *
 * Prefer `WRNEXUS_INTERNAL_ORIGINS` (loopback origins the gateway hands each
 * child before spawning it) over `appOrigin`, which resolves the app's
 * PUBLIC origin. The public origin is the wrong target for RPC: the gateway
 * unconditionally 404s the reserved `/__wrnexus/rpc` prefix on anything that
 * arrives at a public origin — that block is the whole point, it is what
 * keeps inter-app calls off the public internet. Falling back to `appOrigin`
 * when no internal-origin map is present keeps single-app and test setups
 * (which only set `WRNEXUS_WORKSPACE_ORIGINS`) working.
 */
declare function resolveAppOrigin(app: string): string;
interface HttpTransportOptions {
    resolveOrigin?: (app: string) => string;
    fetch?: typeof fetch;
}
declare function httpTransport(options?: HttpTransportOptions): Transport;

declare const RPC_STREAM_PATH_PREFIX = "/__wrnexus/rpc-stream";
declare function rpcStreamPath(service: string, procedure: string): string;
interface StreamImplementation<Procedures extends AnyProcedures = AnyProcedures> {
    contract: ServiceContract<Procedures>;
    stream(procedure: string, payload: unknown, identity?: string): AsyncIterable<unknown>;
    streamOptions: Required<Pick<StreamImplementOptions, "maxFrameBytes" | "heartbeatMs">>;
    metrics: StreamMetrics;
}
type StreamHandlers<Procedures extends AnyProcedures> = {
    [K in keyof Procedures]: (input: InferProcedureInput<Procedures[K]>, ctx: {
        subject?: SubjectContext;
    }) => AsyncIterable<InferProcedureOutput<Procedures[K]>>;
};
interface StreamImplementOptions {
    selfApp: string;
    checkPermission?: (permission: string, subject?: SubjectContext) => Promise<boolean> | boolean;
    /** Maximum serialized SSE data frame. Default: 64 KiB. */
    maxFrameBytes?: number;
    /** Emit an SSE comment while idle. Default: 15 seconds. */
    heartbeatMs?: number;
}
interface StreamMetricsSnapshot {
    started: number;
    completed: number;
    failed: number;
    active: number;
}
declare class StreamMetrics {
    private started;
    private completed;
    private failed;
    private active;
    begin(): void;
    complete(): void;
    fail(): void;
    snapshot(): StreamMetricsSnapshot;
}
/** Define an authenticated, validated stream endpoint. */
declare function implementStream<Procedures extends AnyProcedures>(contract: ServiceContract<Procedures>, handlers: StreamHandlers<Procedures>, options: StreamImplementOptions): StreamImplementation<Procedures>;
interface StreamClientOptions {
    app?: string;
    as?: Context;
    fetch?: typeof fetch;
    signal?: AbortSignal;
    /** Reject oversized server frames before parsing. Default: 64 KiB. */
    maxFrameBytes?: number;
}
type StreamClient<Procedures extends AnyProcedures> = {
    [K in keyof Procedures]: (input: InferProcedureInput<Procedures[K]>) => AsyncIterable<InferProcedureOutput<Procedures[K]>>;
};
declare function streamClient<Procedures extends AnyProcedures>(contract: ServiceContract<Procedures>, options?: StreamClientOptions): StreamClient<Procedures>;

export { type AnyProcedures, type CallOptions, type ExportOptions, type HandlerContext, type HttpTransportOptions, type ImplementOptions, type ImportOptions, type InProcessHandler, type InferInput, type InferProcedureInput, type InferProcedureOutput, type InputSchema, ProcedureBuilder, type ProcedureDef, RPC_ERROR_CODES, RPC_IDENTITY_HEADER, RPC_INTERNAL_HEADER, RPC_PATH_PREFIX, RPC_STREAM_PATH_PREFIX, type RetryTransportOptions, type RpcErrorCode, type RpcTarget, type ServiceClient, type ServiceClientOptions, type ServiceContract, ServiceError, type ServiceHandlers, type ServiceImplementation, type ServiceResult, type StreamClient, type StreamClientOptions, type StreamHandlers, type StreamImplementOptions, type StreamImplementation, StreamMetrics, type StreamMetricsSnapshot, type SubjectContext, type ToResultOptions, type Transport, defineService, exportSubjectContext, failure, httpTransport, implement, implementStream, importSubjectContext, inProcessTransport, isRetryableStatus, procedure, resolveAppOrigin, retryingTransport, rpcPath, rpcSecret, rpcStreamPath, serviceClient, streamClient, success };
```

---

## @wrnexus/security

Documentation URL: https://wrnexusjs.dev/packages/security

# @wrnexus/security

Secure-by-default utilities for WRNexusJS: bounded HTML-safe serialization, prototype-pollution rejection, URL policy, secure cookies, request hardening, security presets, and SSRF-safe remote fetches.

```ts
import { safeFetch, securityPreset, setSecureCookie } from "@wrnexus/security";

export default { security: securityPreset("strict") };
const response = await safeFetch(remoteUrl, { allowedHosts: ["api.example.com"] });
setSecureCookie(ctx, "__Host-session", sessionId);
```

### Exported TypeScript declarations

```ts
export { S as SafeFetchOptions, a as SafeUrlPolicy, i as isPrivateAddress, b as isSafeUrl, s as safeFetch, c as sanitizeUrl, v as validateUrl } from './fetch-DzQ8J9S2.js';
export { SecureSerializeOptions, secureJsonStringify, serializeForHtml } from './serialization.js';
import { CookieOptions, Context, RequestLimitsConfig, Middleware, SecurityConfig } from '@wrnexus/core';
export { TrustedHtmlPolicy, TrustedHtmlValue, createTrustedHtml, isTrustedHtml, unwrapTrustedHtml } from './trusted-html.js';

declare class SecurityError extends Error {
    readonly code: string;
    readonly status: number;
    constructor(code: string, message: string, status?: number, options?: ErrorOptions);
}

interface SafeObjectOptions {
    maxDepth?: number;
    maxKeys?: number;
    allowInstances?: boolean;
}
declare function isDangerousObjectKey(key: string): boolean;
declare function assertSafeObject(value: unknown, options?: SafeObjectOptions): void;
declare function safeMerge<T extends Record<string, unknown>>(target: T, ...sources: Array<Record<string, unknown> | undefined | null>): T;

interface SecureCookieOptions extends CookieOptions {
    hostOnly?: boolean;
}
declare function secureCookieOptions(ctx: Pick<Context, "url">, options?: SecureCookieOptions): CookieOptions;
declare function setSecureCookie(ctx: Pick<Context, "url" | "cookies">, name: string, value: string, options?: SecureCookieOptions): void;

type RequestHardeningOptions = RequestLimitsConfig;
declare function requestHardening(options?: RequestHardeningOptions): Middleware;

type SecurityPreset = "balanced" | "strict" | "api";
declare function securityPreset(preset?: SecurityPreset): SecurityConfig;

export { type RequestHardeningOptions, type SafeObjectOptions, type SecureCookieOptions, SecurityError, type SecurityPreset, assertSafeObject, isDangerousObjectKey, requestHardening, safeMerge, secureCookieOptions, securityPreset, setSecureCookie };
```

---

## @wrnexus/ssr

Documentation URL: https://wrnexusjs.dev/packages/ssr

# @wrnexus/ssr

> Server-side rendering: wraps a page's HTML body in a complete HTML document with a metadata-driven `<head>`.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

Pages in WrNexus return an HTML string for the body. `@wrnexus/ssr` takes that body and produces a full HTML document — building the `<head>` from page metadata and global SEO defaults, resolving canonical/Open Graph/Twitter tags, and injecting module preloads and `<script type="module">` tags. It is deliberately server-only: nothing in this package touches the DOM or ships to the browser, keeping server code genuinely server-only. Reach for it on the server when turning a rendered page body into a response document.

## Installation

```bash
bun add @wrnexus/ssr
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

## API

The package has a single export.

### `renderDocument(opts: RenderOptions): string`

Renders a complete HTML document as a string, beginning with `<!doctype html>`. All metadata is HTML-escaped (via `escapeHtml` from `@wrnexus/core`), so a malicious title or description cannot break out of its element or attribute. The body is placed inside `<div id="app">`.

#### `RenderOptions`

| Field          | Type        | Description                                                                                                                                    |
| -------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `meta`         | `PageMeta`  | Page metadata for the document head (required).                                                                                                |
| `body`         | `string`    | Rendered HTML for the body, placed inside `#app` (required).                                                                                   |
| `seo`          | `SeoConfig` | Global SEO defaults, typically from `wrnexus.config.ts`.                                                                                       |
| `url`          | `URL`       | Current request URL, used to resolve canonical/Open Graph URLs.                                                                                |
| `scripts`      | `string[]`  | URLs of `<script type="module">` tags to load (e.g. per-island chunks or the reactive runtime). Each also gets a `<link rel="modulepreload">`. |
| `defaultTitle` | `string`    | Default document title used when `meta.title` is absent.                                                                                       |
| `extraHead`    | `string`    | Raw HTML injected at the end of `<head>` (trusted, framework-controlled — not escaped).                                                        |
| `extraBody`    | `string`    | Raw HTML injected at the end of `<body>` (trusted, framework-controlled — not escaped).                                                        |
| `htmlAttrs`    | `string`    | Attributes for the `<html>` element, e.g. ` data-theme="dark"` (trusted).                                                                      |

`PageMeta` and `SeoConfig` come from `@wrnexus/core`. `PageMeta` is an alias of `SeoConfig`, whose fields are all optional:

```ts
type SeoConfig = {
  title?: string;
  titleTemplate?: string; // e.g. "%s — My Site"; %s is replaced with the page title
  description?: string;
  canonical?: string;
  canonicalBase?: string; // origin used to absolutize canonical/image URLs
  robots?: string;
  keywords?: string | string[];
  image?: string;
  siteName?: string;
  type?: string; // Open Graph type; defaults to "website"
  locale?: string;
  twitterCard?: string; // defaults to "summary"
  twitterSite?: string;
  themeColor?: string;
};
```

#### Metadata resolution

`renderDocument` merges page metadata (`meta`) over global defaults (`seo`), field by field, so per-page values win. Notable behavior:

- **Title**: uses `meta.title`, else `seo.title`, else `defaultTitle`, else `"WrNexus"`. When the page sets its own title and `seo.titleTemplate` contains `%s`, the template is applied.
- **Canonical / image URLs**: resolved against `canonicalBase` (or the request `url`'s origin) into absolute URLs when possible.
- **Keywords**: an array is joined with `", "`.
- **Emitted tags**: `<title>`, and as applicable `description`, `robots`, `keywords`, `theme-color`, and `canonical` link, plus Open Graph (`og:title`, `og:description`, `og:type`, `og:url`, `og:site_name`, `og:locale`, `og:image`) and Twitter (`twitter:card`, `twitter:title`, `twitter:description`, `twitter:image`, `twitter:site`) meta tags. The document always includes `charset`, `viewport`, and a `/favicon.ico` icon link.

## Usage

### Render an SEO-ready application page

```ts
import { renderDocument } from "@wrnexus/ssr";

const html = renderDocument({
  meta: {
    title: "About Us",
    description: "Learn more about our team.",
  },
  seo: {
    titleTemplate: "%s — Acme",
    siteName: "Acme",
    canonicalBase: "https://acme.example",
    twitterSite: "@acme",
  },
  url: new URL("https://acme.example/about"),
  body: "<h1>About Us</h1>",
  scripts: ["/_wire/runtime.js", "/_wire/islands/about.js"],
  htmlAttrs: ' data-theme="dark"',
});

return new Response(html, {
  headers: { "content-type": "text/html; charset=utf-8" },
});
```

The produced document has `<title>About Us — Acme</title>`, the SEO/Open Graph/Twitter tags derived from the merged metadata, a `modulepreload` link and module `<script>` for each entry in `scripts`, and the body wrapped in `<div id="app">`.

### Add trusted framework assets and boot data

Use `extraHead` and `extraBody` only for HTML generated by your application or the
framework. User-provided values belong in `meta`, where they are escaped.

```ts
const html = renderDocument({
  meta: { title: "Dashboard", robots: "noindex" },
  body: dashboardHtml,
  url: ctx.url,
  extraHead: '<link rel="stylesheet" href="/_wrnexus/admin.css">',
  extraBody: `<script type="application/json" id="boot">${JSON.stringify(bootData).replaceAll("<", "\\u003c")}</script>`,
});

return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } });
```

## Requirements / Notes

- **Server-only.** This module never imports or touches the DOM and is safe to keep out of client bundles.
- **Depends on [`@wrnexus/core`](../core)** for `escapeHtml` and the `PageMeta` / `SeoConfig` types.
- **Bun-only** — like the rest of WrNexus, this package targets the Bun runtime (Node is not supported).

### Exported TypeScript declarations

```ts
import { PageMeta, SeoConfig } from '@wrnexus/core';
export { RpcContext, RpcHandlerOptions, RpcManifestContract, RpcParameterContract, RpcRequestPayload, createRpcHandler } from './rpc.js';
export { disposeRequestStores, renderStoreHydration, requestStoreContainer } from './store-context.js';
import '@wrnexus/store';

/**
 * @wrnexus/ssr — server-side rendering.
 *
 * Pages return an HTML string for the body; this module wraps that body in a
 * full document with a `<head>` built from page metadata. It is intentionally
 * isolated from any client runtime: nothing here touches the DOM or ships to
 * the browser, which keeps "server-only code" genuinely server-only.
 */

interface ScriptAsset {
    src: string;
    /** Module scripts are the default for backward compatibility. */
    type?: "module" | "classic";
    async?: boolean;
    defer?: boolean;
    integrity?: string;
    crossOrigin?: "anonymous" | "use-credentials";
    nonce?: string;
    attributes?: Record<string, string | boolean>;
}
type RenderScript = string | ScriptAsset;
interface PartialPrerenderResult {
    shell: string;
    regions: Array<{
        id: string;
        html: string;
    }>;
}
/** Extract compiler-emitted dynamic regions into a cacheable static shell. */
declare function partialPrerender(html: string, startIndex?: number): PartialPrerenderResult;
/** Stream the static shell first, followed by inert region templates for client insertion. */
declare function streamPartialDocument(result: PartialPrerenderResult, nonce?: string): ReadableStream<Uint8Array>;
interface RenderOptions {
    /** Page metadata for the document head. */
    meta: PageMeta;
    /** Global SEO defaults from `wrnexus.config.ts`. */
    seo?: SeoConfig;
    /** Current request URL, used to resolve canonical/Open Graph URLs. */
    url?: URL;
    /** Rendered HTML for the body (placed inside `#app`). */
    body: string;
    /**
     * URLs of `<script type="module">` tags to load (e.g. per-island chunks or
     * the reactive runtime). Only the scripts a page actually needs are passed.
     */
    scripts?: RenderScript[];
    /** Optional default document title used when meta.title is absent. */
    defaultTitle?: string;
    /** Raw HTML injected at the end of `<head>` (trusted, framework-controlled). */
    extraHead?: string;
    /** CSP nonce applied to framework-promoted `.wrn` style blocks. */
    styleNonce?: string;
    /** Raw HTML injected at the end of `<body>` (trusted, framework-controlled). */
    extraBody?: string;
    /** Attributes for the `<html>` element, e.g. ` data-theme="dark"` (trusted). */
    htmlAttrs?: string;
    /**
     * Optional application-authored full document shell. It must contain
     * `<html>`, `<head>`, and `<body>`. Framework metadata, assets, and scripts
     * are merged into it instead of wrapping the rendered body again.
     */
    documentTemplate?: string;
}
/**
 * Render a complete HTML document.
 *
 * Metadata is HTML-escaped so a malicious title/description can never break
 * out of its element or attribute.
 */
declare function renderDocument(opts: RenderOptions): string;
interface ExtractedWrnexusStyles {
    html: string;
    styles: string;
}
/**
 * Promote compiler-emitted `.wrn` style blocks out of rendered markup and into
 * the document head. They are emitted after global stylesheets, deduplicated by
 * stable id, ordered layout -> page -> component, and nonce-tagged for CSP.
 */
declare function extractWrnexusStyles(html: string, nonce?: string): ExtractedWrnexusStyles;
interface StreamRenderOptions extends Omit<RenderOptions, "body"> {
    body: string | Promise<string> | AsyncIterable<string>;
}
/**
 * Stream a complete document while preserving the exact head/body contract of
 * `renderDocument`. Async iterables can flush a shell, primary content, and
 * slower fragments without buffering the entire route.
 */
declare function renderDocumentStream(opts: StreamRenderOptions): ReadableStream<Uint8Array>;
declare function streamDocumentResponse(opts: StreamRenderOptions, init?: ResponseInit): Response;

export { type PartialPrerenderResult, type RenderOptions, type RenderScript, type ScriptAsset, type StreamRenderOptions, extractWrnexusStyles, partialPrerender, renderDocument, renderDocumentStream, streamDocumentResponse, streamPartialDocument };
```

---

## @wrnexus/store

Documentation URL: https://wrnexusjs.dev/packages/store

# @wrnexus/store

Typed global and page-scoped WRNexusJS stores with runtime-specific state, computed values, actions, lifecycle hooks, persistence, SSR isolation, and HMR support.

Use `defineStore()` to declare a store and `createStoreContainer()` to create an isolated request or browser container. Store definitions are framework helpers and do not require UI components.

### Exported TypeScript declarations

```ts
import { StoreMutation, StoreFunction, StoreDefinition, StoreInstance, StoreCombinedState } from './types.js';
export { PersistenceStorage, StoreActionContext, StoreActionDefinition, StoreInstanceCore, StoreKind, StoreLifecycleContext, StorePersistenceConfig, StoreRuntime } from './types.js';

declare function defineStore<S extends object, C extends object = Record<string, never>, A extends Record<string, StoreFunction> = Record<string, StoreFunction>, CS extends object = Record<string, never>, SS extends object = Record<string, never>>(definition: StoreDefinition<S, C, A, CS, SS>): StoreDefinition<S, C, A, CS, SS>;
interface StoreContainerOptions {
    runtime: "server" | "client";
    request?: unknown;
    routeId?: string;
    hydration?: Record<string, unknown>;
    onMutation?: (mutation: StoreMutation) => void;
}
declare class StoreContainer {
    readonly runtime: "server" | "client";
    readonly request?: unknown;
    readonly routeId?: string;
    private readonly instances;
    private readonly hydration;
    private readonly onMutation?;
    private readonly lastMutations;
    constructor(options: StoreContainerOptions);
    use<S extends object, C extends object, A extends Record<string, StoreFunction>, CS extends object, SS extends object>(definition: StoreDefinition<S, C, A, CS, SS>): Promise<StoreInstance<StoreCombinedState<S, CS, SS>, C, A>>;
    serialize(): Record<string, unknown>;
    inspect(): Array<{
        name: string;
        kind: "global" | "page";
        state: Readonly<Record<string, unknown>>;
        computed: Readonly<Record<string, unknown>>;
        lastAction?: string;
        changed: string[];
        hydrationSource: "server" | "persistence" | "initial";
    }>;
    hotUpdate<S extends object, C extends object, A extends Record<string, StoreFunction>, CS extends object, SS extends object>(definition: StoreDefinition<S, C, A, CS, SS>): Promise<{
        preserved: string[];
        reset: string[];
    }>;
    disposePageStores(): Promise<void>;
    dispose(): Promise<void>;
}
declare function createStoreContainer(options: StoreContainerOptions): StoreContainer;
declare function createStoreInstance<S extends object, C extends object, A extends Record<string, StoreFunction>, CS extends object, SS extends object>(definition: StoreDefinition<S, C, A, CS, SS>, options: Omit<StoreContainerOptions, "hydration"> & {
    hydration?: unknown;
}): StoreInstance<StoreCombinedState<S, CS, SS>, C, A>;

export { StoreCombinedState, StoreContainer, type StoreContainerOptions, StoreDefinition, StoreFunction, StoreInstance, StoreMutation, createStoreContainer, createStoreInstance, defineStore };
```

---

## @wrnexus/styles

Documentation URL: https://wrnexusjs.dev/packages/styles

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

### Exported TypeScript declarations

```ts
import { PerformanceBudgets, SeoConfig, SecurityConfig } from '@wrnexus/core';
import { PluginInput, PluginPermission } from '@wrnexus/plugin';
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 both `<html data-theme="...">` and
 * `<html data-accent="...">` from cookies, so the correct theme and accent are
 * present before the first paint. The reserved token key `color-scheme` is
 * emitted as the native CSS property instead of a custom property.
 */
type ThemeSemanticColor = "primary" | "secondary" | "info" | "success" | "warning" | "danger" | "error";
type ThemeToken = "color-scheme" | "color-bg" | "color-background" | "color-foreground" | "color-surface" | "color-surface-2" | "color-surface-raised" | "color-surface-muted" | "color-surface-soft" | "color-text" | "color-text-muted" | "color-text-subtle" | "color-muted" | "color-border" | "color-border-strong" | "color-code-background" | "color-code-surface" | "color-code-text" | "color-code-muted" | "color-code-border" | `color-${ThemeSemanticColor}` | `color-${ThemeSemanticColor}-${"hover" | "active" | "contrast" | "soft" | "muted" | "text"}` | `color-on-${"primary" | "secondary"}` | "radius" | "radius-sm" | "shadow-1" | "shadow-sm" | "shadow-md" | "shadow-lg" | "space-section" | "space-section-sm" | "container-max" | "font-sans";
/** Known tokens get autocomplete while applications may add namespaced custom tokens. */
type ThemeTokens = Partial<Record<ThemeToken, string>> & Record<string, string>;
declare function defineThemeTokens<T extends ThemeTokens>(tokens: T): T;
declare function themeVar(token: ThemeToken, fallback?: string): string;
declare const THEME_PALETTE_NAMES: readonly ["blue", "indigo", "violet", "emerald", "cyan", "rose", "amber", "slate"];
type ThemePaletteName = (typeof THEME_PALETTE_NAMES)[number];
/** Required semantic colors for a custom application palette. */
interface CustomThemePalette {
    primary: string;
    primaryHover: string;
    primaryContrast: string;
    secondary: string;
    secondaryHover: string;
    secondaryContrast: string;
    info: string;
    success: string;
    warning: string;
    danger: string;
}
interface ThemeAccentConfig {
    /**
     * Accent used when no `wire-accent` cookie is present.
     *
     * - Omitted: use the named `palette`, or `blue` when no palette is configured.
     * - `false`: keep the configured base palette until the user explicitly picks an accent.
     */
    default?: ThemePaletteName | false;
    /** Runtime-selectable accent names. Defaults to every built-in THEME_PALETTE. */
    options?: ThemePaletteName[];
}
interface ThemeConfig {
    /** Built-in palette name, or a complete custom semantic color palette. */
    palette?: ThemePaletteName | CustomThemePalette;
    /** Runtime accent/palette switcher configuration. */
    accent?: ThemeAccentConfig;
    /** 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>;
    defaultAccent?: ThemePaletteName;
    accentNames: ThemePaletteName[];
}
/** Cookies used by the SSR renderer and client runtime. */
declare const THEME_COOKIE = "wire-theme";
declare const ACCENT_COOKIE = "wire-accent";
declare const THEME_CSS_HREF = "/__wrnexus/theme.css";
declare const THEME_CSS_PREFIX = "/__wrnexus/theme/";
declare const THEME_JS_HREF = "/__wrnexus/theme.js";
/**
 * Single source of truth for both configured palettes and runtime accents.
 * Do not create a second hard-coded ACCENTS map in the browser runtime.
 */
declare const THEME_PALETTES: Record<ThemePaletteName, CustomThemePalette>;
/** 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;
/** Pick a valid accent name from a cookie value, falling back to the configured default. */
declare function resolveAccentName(cookieValue: string | undefined, theme: ResolvedTheme): ThemePaletteName | undefined;
/** URL for the small stylesheet containing only one active theme/accent pair. */
declare function activeThemeCssHref(themeName: string, accentName?: string): string;
/** Render only the tokens needed for the current SSR-selected theme and accent. */
declare function renderActiveThemeCss(theme: ResolvedTheme, themeName: string, accentName?: string): string;
/**
 * Generate the theme stylesheet.
 *
 * Theme selectors are emitted first. Accent selectors are emitted afterwards,
 * so a selected accent consistently overrides every semantic palette token,
 * including soft/muted/text variants, before the first paint.
 */
declare function renderThemeCss(theme: ResolvedTheme): string;
/**
 * Generate the client theme runtime. It exposes `window.wireTheme` and
 * `window.wireAccent`, and binds theme/accent controls.
 *
 * The runtime changes only data attributes and cookies. It never writes inline
 * CSS variables and never uses localStorage, so CSS and SSR remain the single
 * source of truth.
 */
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[];
};

declare const CURRENT_COMPATIBILITY_DATE = "2026-08-02";
declare const CURRENT_FRAMEWORK_BEHAVIOUR = 1;
interface CompatibilityPolicy {
    compatibilityDate?: string;
    frameworkBehaviour?: number;
}
interface CompatibilityReport {
    configuredDate?: string;
    effectiveDate: string;
    currentDate: string;
    configuredBehaviour?: number;
    effectiveBehaviour: number;
    currentBehaviour: number;
    needsUpgrade: boolean;
    future: boolean;
    messages: string[];
}
declare function isCompatibilityDate(value: string): boolean;
declare function resolveCompatibility(policy: CompatibilityPolicy): CompatibilityReport;

/**
 * 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;
    /** Original application entry when a package-aware wrapper was generated. */
    originalEntryPath?: string | null;
    /** Package component/style directories that processors should scan. */
    sources?: string[];
    /** Package-owned CSS entries automatically imported into the application bundle. */
    entries?: string[];
    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;
    /** Include the complete @wrnexus/ui component catalog stylesheet. Default true. */
    includeUi?: boolean;
    /**
     * 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>;
    /** Automatically append package scan sources to custom processor input. Default true. */
    includePackageSources?: boolean;
    /** Production defaults to throw; development defaults to best-effort fallback. */
    failureMode?: "throw" | "fallback";
}
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 WrNexus 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;
    /** Ordered URL rules for runtime caching. Patterns are regular-expression source strings. */
    runtimeCaching?: Array<{
        pattern: string;
        strategy: "network-first" | "cache-first" | "stale-while-revalidate";
        cacheName?: string;
        methods?: string[];
    }>;
    /** Background Sync tag used by the offline mutation queue. */
    backgroundSyncTag?: string;
}
type DevToolbarPosition = "bottom-center" | "bottom-left" | "bottom-right";
interface DevToolbarConfig {
    enabled?: boolean;
    position?: DevToolbarPosition;
    defaultOpen?: boolean;
    keyboardShortcut?: string;
    scanOnNavigation?: boolean;
    scanOnHmr?: boolean;
    openEditor?: boolean;
    editor?: string;
    rules?: Partial<Record<string, boolean>>;
    severity?: Partial<Record<string, "error" | "warning" | "info" | "suggestion">>;
    ignoredRules?: string[];
    ignoredPaths?: string[];
    slowRequestMs?: number;
    largeImageBytes?: number;
    veryLargeImageBytes?: number;
}
interface ExperimentalConfig {
    serverComponents?: boolean;
    streaming?: boolean;
    partialHydration?: boolean;
    typedRpc?: boolean;
    pluginTransforms?: boolean;
    [feature: string]: boolean | undefined;
}
interface PerformanceConfig {
    budgets?: PerformanceBudgets;
    /** `warn` reports budget violations; `error` fails production builds. */
    enforcement?: "off" | "warn" | "error";
    analyze?: boolean;
}
interface ObservabilityConfig {
    enabled?: boolean;
    serviceName?: string;
    serverTiming?: boolean;
    sampleRate?: number;
    exporter?: "console" | "otlp" | "none";
    endpoint?: string;
    /** Collect privacy-preserving Core Web Vitals from real browsers. */
    webVitals?: boolean;
    /** Same-origin endpoint receiving Web Vitals. */
    webVitalsEndpoint?: string;
}
interface TenancyConfig {
    mode?: "subdomain" | "domain" | "path" | "custom";
    required?: boolean;
    rootDomains?: string[];
    pathPrefix?: string;
}
interface BuildConfig {
    cache?: boolean;
    cacheDir?: string;
    sourceMaps?: boolean;
    report?: boolean;
    adapter?: "bun" | "node" | "static" | "serverless" | "edge" | string;
}
interface NavigationConfig {
    /**
     * `auto` (default) omits navigation JavaScript from fully static pages and
     * progressively enhances routes that already need browser behavior.
     * `client` always enhances same-origin links with in-place page swaps.
     * `document` keeps normal browser navigation so every route performs a fresh
     * server-rendered document request.
     */
    mode?: "auto" | "client" | "document";
}
interface ImportsConfig {
    mode?: "legacy" | "compatible" | "explicit";
    autoImport?: boolean;
    aliases?: Record<string, string>;
}
interface TypesConfig {
    strict?: boolean;
    noImplicitAny?: boolean;
    strictNullChecks?: boolean;
    checkTemplates?: boolean;
    checkComponentProps?: boolean;
    generateDeclarations?: boolean;
    globalTypes?: string;
}
interface FunctionsConfig {
    legacyDefaultRuntime?: "current" | "client" | "server" | "shared";
}
interface StoresConfig {
    strictMutations?: boolean;
    persistence?: boolean;
}
interface CompatibilityConfig {
    legacyEmit?: boolean;
    legacyEventProps?: boolean;
    legacyComponentDiscovery?: boolean;
    stringLayouts?: boolean;
}
interface AppConfig extends CompatibilityPolicy {
    /** Ordered reusable configuration layers; the application always has final precedence. */
    extends?: string | string[];
    /** Compiler/dev/build plugins, resolved in deterministic pre/normal/post order. */
    plugins?: PluginInput;
    /** Optional least-privilege enforcement for automatically discovered packages. */
    pluginPermissions?: {
        enforce?: boolean;
        grants?: Record<string, PluginPermission[]>;
    };
    /** WRN v0.6 explicit import and compatibility resolution. */
    imports?: ImportsConfig;
    /** TypeScript-backed .wrn type checking and declaration generation. */
    types?: TypesConfig;
    /** Legacy function runtime behavior for existing applications. */
    functions?: FunctionsConfig;
    /** Typed global/page store behavior. */
    stores?: StoresConfig;
    /** Temporary v0.5 syntax compatibility switches. */
    compatibility?: CompatibilityConfig;
    /** Opt-in APIs that are not yet covered by stable compatibility guarantees. */
    experimental?: ExperimentalConfig;
    /** Route and asset budgets plus build analyzer behavior. */
    performance?: PerformanceConfig;
    /** Request tracing, Server-Timing, and exporter configuration. */
    observability?: ObservabilityConfig;
    /** First-class tenant resolution defaults. */
    tenancy?: TenancyConfig;
    /** Build cache, source map, report, and deployment adapter settings. */
    build?: BuildConfig;
    /** Page navigation strategy. Defaults to progressive client navigation. */
    navigation?: NavigationConfig;
    /** Development-only page diagnostics toolbar. Enabled by default in development. */
    devToolbar?: boolean | DevToolbarConfig;
    /** 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[];
        labels?: Record<string, string>;
        fallbacks?: Record<string, string[]>;
        direction?: Record<string, "ltr" | "rtl">;
        cookie?: {
            name?: string;
            maxAge?: number;
            path?: string;
            sameSite?: "Strict" | "Lax" | "None";
            secure?: boolean;
        };
        strict?: boolean;
    };
    /** 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;
interface ResolvedConfigLayers {
    config: AppConfig;
    sources: string[];
}
declare function resolveConfigLayers(appRoot: string, application: AppConfig): Promise<ResolvedConfigLayers>;
/** 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>;
interface ConfigIssue {
    path: string;
    severity: "error" | "warning";
    message: string;
}
declare function defineConfig(config: AppConfig): AppConfig;
declare function validateAppConfig(config: AppConfig): ConfigIssue[];
interface ExplainedConfig {
    profile: string;
    config: AppConfig;
    issues: ConfigIssue[];
    sources: string[];
}
declare function explainAppConfig(appRoot: string, profile?: string): Promise<ExplainedConfig>;
/** 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>;

interface CssTokenAudit {
    declared: string[];
    used: string[];
    missing: string[];
    unused: string[];
}
/** Audit framework design-token declarations and var() references. */
declare function auditWireTokens(css: string): CssTokenAudit;
interface StyleSource {
    path: string;
    reason?: string;
}
/** Normalize/dedupe Tailwind scan sources without allowing line injection. */
declare function normalizeStyleSources(values: readonly (string | StyleSource)[]): StyleSource[];
declare function tailwindSourceDirectives(values: readonly (string | StyleSource)[]): string;
interface ContrastResult {
    ratio: number;
    level: "fail" | "aa-large" | "aa" | "aaa";
}
declare function contrast(foreground: string, background: string): ContrastResult | null;
interface CssPerformanceAuditIssue {
    code: string;
    severity: "error" | "warning" | "info";
    message: string;
    line?: number;
}
/** Detect CSS patterns that commonly increase style, paint, or compositing cost. */
declare function auditCssPerformance(source: string): CssPerformanceAuditIssue[];

/**
 * @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.
 *
 * Development can fall back to best-effort CSS. Production throws by default so
 * a deployment cannot silently ship unprocessed Tailwind/PostCSS directives.
 */
declare function renderStyles(ctx: StyleProcessContext, styles?: StylesConfig): Promise<string>;

export { ACCENT_COOKIE, type AppConfig, type BuildConfig, CURRENT_COMPATIBILITY_DATE, CURRENT_FRAMEWORK_BEHAVIOUR, type CompatibilityPolicy, type CompatibilityReport, type ConfigIssue, type ContrastResult, type CssPerformanceAuditIssue, type CssTokenAudit, type CustomThemePalette, DEFAULT_THEMES, type DevToolbarConfig, type ExperimentalConfig, type ExplainedConfig, type FontConfig, type FontDisplay, type GoogleFont, type LocalFontFace, type MobileConfig, type Mode, type NavigationConfig, type ObservabilityConfig, type PerformanceConfig, type PwaConfig, type ResolvedConfigLayers, type ResolvedTheme, type StyleProcessContext, type StyleSource, type StylesConfig, type Mode as StylesMode, THEME_COOKIE, THEME_CSS_HREF, THEME_CSS_PREFIX, THEME_JS_HREF, THEME_PALETTES, THEME_PALETTE_NAMES, type TenancyConfig, type ThemeAccentConfig, type ThemeConfig, type ThemePaletteName, type ThemeSemanticColor, type ThemeToken, type ThemeTokens, activeThemeCssHref, auditCssPerformance, auditWireTokens, bundleCss, contrast, defineConfig, defineThemeTokens, explainAppConfig, findStyleEntry, fontCspSources, headToString, isCompatibilityDate, loadAppConfig, loadEnv, loadRawConfig, normalizeStyleSources, renderActiveThemeCss, renderFontHead, renderProductionFontHead, renderStyles, renderThemeCss, renderThemeRuntime, resolveAccentName, resolveCompatibility, resolveConfigLayers, resolveProfile, resolveThemeConfig, resolveThemeName, tailwindSourceDirectives, themeVar, validateAppConfig };
```

---

## @wrnexus/syntax

Documentation URL: https://wrnexusjs.dev/packages/syntax

# @wrnexus/syntax

Canonical WRN lexer, parser, AST, language metadata, source positions, and stable
diagnostics. Framework tooling should import this package instead of implementing a
separate `.wrn` parser.

See `docs/WRN-LANGUAGE-SPEC-1.0.md` in the WRNexusJS repository.

### Exported TypeScript declarations

```ts
export { LexError, Lexer } from './tokenizer.js';
export { FormatWrnOptions, formatWrn } from './formatter.js';
export { A as ActionBlock, a as ApiBlock, b as Attr, C as ComputedDecl, D as DataApiBlock, c as DataMode, E as EffectBlock, d as EventDecl, F as FunctionParameterDecl, e as FunctionRuntime, L as LifecycleBlock, f as LoadBlock, M as ModeFunctionsBlock, O as OutputDecl, P as PageAst, g as ParseError, h as PersistDecl, i as PropDecl, R as RealtimeBlock, j as RealtimeHandler, k as RuntimeFunctionDecl, S as SeoBlock, l as StateDecl, m as StateRuntime, n as StoreKind, o as StoreLifecycleDecl, p as StructuredImportDecl, V as VOID_ELEMENTS, q as ViewNode, W as WatchBlock, r as parse, s as parseComputedDeclarations, t as parseHtmlView, u as parseOutputs, v as parsePersist, w as parseRuntimeFunctions, x as parseStateDeclarations, y as parseStoreLifecycle, z as parseStructuredImports, B as stripRuntimeFunctionModifiers } from './parser-CTLO4mcT.js';
export { RuntimeType, eraseFunctionTypes, inferredRuntimeType, runtimeTypeOf, validateTypedInitializer } from './types.js';
import { WrnDiagnostic } from './diagnostics.js';
export { DiagnoseOptions, WrnDiagnosticSeverity, WrnSourcePosition, assertValidAst, classifyParseError, containsReadonlyPropMutation, diagnose, diagnosticFromError, formatDiagnostic, isHydrationStrategy, isRuntimeTarget, positionAt } from './diagnostics.js';
export { WRN_DIAGNOSTIC_CODES, WRN_HYDRATION_STRATEGIES, WRN_LANGUAGE_VERSION, WRN_ROOT_KINDS, WRN_ROOT_MEMBERS, WRN_RUNTIME_TARGETS, WrnHydrationStrategy, WrnRootKind, WrnRootMember, WrnRuntimeTarget } from './spec.js';

/** Current stable syntax contract. Bump only when parsers/codegen need migration. */
declare const WRN_SYNTAX_VERSION: "0.4";
type WrnSyntaxFeature = "typed-declarations" | "layouts" | "server-client-blocks" | "effects" | "watch" | "lifecycle" | "embedded-api" | "realtime" | "runtime-markers";
declare const WRN_SYNTAX_FEATURES: Readonly<Record<WrnSyntaxFeature, boolean>>;
interface SourceRange {
    start: number;
    end: number;
}
declare function createSourceRange(start: number, end: number): SourceRange;
declare function sliceSource(source: string, range: SourceRange): string;
declare function diagnosticSummary(diagnostics: readonly WrnDiagnostic[]): {
    errors: number;
    warnings: number;
    info: number;
    codes: Record<string, number>;
};
declare function supportsSyntaxFeature(feature: string): feature is WrnSyntaxFeature;

export { type SourceRange, WRN_SYNTAX_FEATURES, WRN_SYNTAX_VERSION, WrnDiagnostic, type WrnSyntaxFeature, createSourceRange, diagnosticSummary, sliceSource, supportsSyntaxFeature };
```

---

## @wrnexus/test

Documentation URL: https://wrnexusjs.dev/packages/test

# @wrnexus/test

> Testing utilities for WrNexus apps — component rendering, reactive-DOM mounting, route handler calls, and a full in-process app harness, plus a one-import re-export of `bun:test`.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

`@wrnexus/test` is the server-side test toolkit you reach for when writing tests
for a WrNexus app. It runs under `bun test` (invoked via `wrnexus test`) and gives
you a single import surface: the `bun:test` primitives (`test`, `expect`, `mock`,
…) re-exported alongside WrNexus-aware helpers that compile `.wrn` components,
hydrate server HTML in a DOM, invoke API route handlers, and boot the real app on
an ephemeral port for integration tests.

## Installation

```bash
bun add @wrnexus/test
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

## API

### Re-exported test primitives

For one-import DX, the following are re-exported straight from `bun:test`:

`test`, `expect`, `describe`, `it`, `beforeEach`, `afterEach`, `beforeAll`,
`afterAll`, `mock`, `spyOn`.

`createContext` is also re-exported from `@wrnexus/core`.

### `renderComponent(source, props?)`

```ts
function renderComponent(source: string, props?: Record<string, unknown>): Promise<string>;
```

Compiles a `.wrn` component `source` string (via `@wrnexus/compiler`) and renders
it to an HTML string with the given `props`. Throws if the compiled module has no
`render` export.

### `mountHtml(html)`

```ts
function mountHtml(html: string): {
  document: Document;
  window: unknown;
  querySelector: (sel: string) => Element | null;
  querySelectorAll: (sel: string) => Element[];
};
```

Mounts server-rendered `html` in a `happy-dom` window with the reactive runtime
hydrated, so you can test `data-scope` / `data-text` / `data-for` / `data-show`
behaviour. Returns the window plus `document` and query helpers; assert on those.

> `happy-dom` is loaded lazily (via `require`), so importing this package never
> requires it unless you actually call `mountHtml`.

### `callRoute(handler, request)`

```ts
function callRoute(
  handler: (ctx: Context) => Response | Promise<Response>,
  request: Request,
): Promise<Response>;
```

Calls an API route `handler` with a `Context` built from a `Request` (using
`createContext`). Returns the handler's `Response`.

### `createHarness(projectRoot, options?)`

```ts
function createHarness(projectRoot: string, options?: HarnessOptions): Promise<Harness>;

interface HarnessOptions {
  /** Config/env profile. Default "test". */
  profile?: string;
}

interface Harness {
  /** Base URL of the ephemeral test server. */
  url: string;
  /** Fetch a path on the app (relative to `url`). */
  fetch(path: string, init?: RequestInit): Promise<Response>;
  /** The scanned router (pages/api/realtime/components). */
  router: unknown;
  /** Stop the server. */
  close(): void;
}
```

Boots the app at `projectRoot` on an ephemeral port (`port: 0`) for integration
tests covering pages, API routes, middleware, and the full request pipeline. Loads
env and app config for the given `profile` (default `"test"`) so it picks up your
test database/env. The server runs in `development` mode with HMR disabled.
Remember to `await app.close()` when done.

## Usage

The CLI supports focused suites by file or directory convention:

```bash
wrnexus test unit                 # *.unit.test.ts or test/unit/**
wrnexus test component            # *.component.test.ts or test/component/**
wrnexus test api                  # *.api.test.ts or test/api/**
wrnexus test accessibility        # *.a11y.test.ts / *.accessibility.test.ts
wrnexus test performance          # *.performance.test.ts / *.benchmark.test.ts
wrnexus test browser              # Playwright project when configured
wrnexus test visual               # Playwright tests tagged @visual
```

Pass the application directory after the level, for example
`wrnexus test component examples/basic-app`. A focused command fails clearly when no matching
suite exists instead of silently running unrelated tests.

```ts
import { test, expect, renderComponent, mountHtml, createHarness } from "@wrnexus/test";

test("counter renders its label", async () => {
  const html = await renderComponent(SRC, { start: 3, label: "Hits" });
  expect(html).toContain("Hits");
});

test("reactive scope hydrates", () => {
  const { querySelector } = mountHtml(serverHtml);
  expect(querySelector("[data-text]")?.textContent).toBe("3");
});

test("home page responds", async () => {
  const app = await createHarness("examples/basic-app");
  const res = await app.fetch("/");
  expect(res.status).toBe(200);
  await app.close();
});
```

Calling an API route handler directly:

```ts
import { test, expect, callRoute } from "@wrnexus/test";
import { GET } from "../app/api/health.ts";

test("health endpoint", async () => {
  const res = await callRoute(GET, new Request("http://test/api/health"));
  expect(res.status).toBe(200);
});
```

## Requirements / Notes

- **Bun-only.** Runs under `bun test` (via `wrnexus test`); uses Bun's module
  loading and the `bun:test` runtime.
- `mountHtml` requires **`happy-dom`** to be available in the workspace (loaded
  lazily; it's a dev dependency, not a runtime dependency of this package).
- Works with the rest of the WrNexus toolchain:
  [`@wrnexus/compiler`](../compiler) (compiles `.wrn` sources),
  [`@wrnexus/core`](../core) (`Context` / `createContext`),
  [`@wrnexus/csr`](../csr) (reactive runtime for `mountHtml`),
  [`@wrnexus/dev-server`](../dev-server) (`startServer` behind `createHarness`),
  and [`@wrnexus/styles`](../styles) (config/env/profile loading for the harness).

### Exported TypeScript declarations

```ts
import { ProblemDetails, Context } from '@wrnexus/core';
export { createContext } from '@wrnexus/core';
export { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock, spyOn, test } from 'bun:test';

interface TestRequestOptions extends Omit<RequestInit, "body"> {
    body?: BodyInit | Record<string, unknown> | URLSearchParams | FormData | null;
    baseUrl?: string;
}
/** Build a web-standard Request with convenient JSON/FormData handling. */
declare function testRequest(path?: string, options?: TestRequestOptions): Request;
/** Create a complete Context suitable for middleware and route unit tests. */
declare function testContext(path?: string, options?: TestRequestOptions): Context;
interface JsonResponse<T> {
    response: Response;
    body: T;
}
declare function readJsonResponse<T = unknown>(response: Response): Promise<JsonResponse<T>>;
declare function expectProblem(response: Response, status?: number): Promise<ProblemDetails>;
interface Deferred<T> {
    promise: Promise<T>;
    resolve(value: T | PromiseLike<T>): void;
    reject(reason?: unknown): void;
}
declare function deferred<T>(): Deferred<T>;
interface WaitForOptions {
    timeoutMs?: number;
    intervalMs?: number;
    signal?: AbortSignal;
}
/** Poll a condition without depending on fake timers or a browser runtime. */
declare function waitFor(condition: () => boolean | Promise<boolean>, options?: WaitForOptions): Promise<void>;
declare class MemoryCookieJar {
    #private;
    apply(response: Response): void;
    header(): string;
    request(path: string, options?: TestRequestOptions): Request;
    clear(): void;
}

interface TransactionalDatabase {
    tx<T>(callback: (transaction: TransactionalDatabase) => Promise<T>): Promise<T>;
}
/** Run test work in a real transaction and always force rollback. */
declare function withDatabaseRollback<T>(db: TransactionalDatabase, run: (transaction: TransactionalDatabase) => T | Promise<T>): Promise<T>;
declare function createFactory<T extends Record<string, unknown>>(build: (sequence: number) => T): {
    build(overrides?: Partial<T>): T;
    buildMany(count: number, overrides?: Partial<T>): T[];
    reset(): void;
};
interface BrowserArtifactPage {
    screenshot(options: {
        path: string;
        fullPage?: boolean;
    }): Promise<unknown>;
    context(): {
        tracing?: {
            stop(options: {
                path: string;
            }): Promise<unknown>;
        };
    };
}
declare function captureBrowserArtifacts(page: BrowserArtifactPage, testName: string, options?: {
    root?: string;
    screenshot?: boolean;
    trace?: boolean;
}): Promise<{
    screenshot?: string;
    trace?: string;
}>;

/**
 * @wrnexus/test — testing utilities for WrNexus apps. Runs on `bun test` (via
 * `wrnexus test`). Import everything from one place:
 *
 *   import { test, expect, renderComponent, mountHtml, createHarness } from "@wrnexus/test";
 *
 *   test("counter renders its label", async () => {
 *     const html = await renderComponent(SRC, { start: 3, label: "Hits" });
 *     expect(html).toContain("Hits");
 *   });
 *
 *   test("home page responds", async () => {
 *     const app = await createHarness("examples/basic-app");
 *     const res = await app.fetch("/");
 *     expect(res.status).toBe(200);
 *     await app.close();
 *   });
 */

/** Compile a `.wrn` component source + render it to HTML with the given props. */
declare function renderComponent(source: string, props?: Record<string, unknown>): Promise<string>;
/**
 * Mount server-rendered HTML in a happy-dom window with the reactive runtime
 * hydrated, so you can test `data-scope`/`data-text`/`data-for`/`data-show`
 * behaviour. Returns the window; assert on `win.document`.
 */
declare function mountHtml(html: string): {
    document: Document;
    window: unknown;
    querySelector: (sel: string) => Element | null;
    querySelectorAll: (sel: string) => Element[];
};
/** Call an API route handler with a `Context` built from a Request. */
declare function callRoute(handler: (ctx: Context) => Response | Promise<Response>, request: Request): Promise<Response>;
interface Harness {
    /** Base URL of the ephemeral test server. */
    url: string;
    /** Fetch a path on the app (relative to `url`). */
    fetch(path: string, init?: RequestInit): Promise<Response>;
    /** The scanned router (pages/api/realtime/components). */
    router: unknown;
    /** Stop the server. */
    close(): void;
}
interface HarnessOptions {
    /** Config/env profile. Default "test". */
    profile?: string;
}
/**
 * Boot the app on an ephemeral port for integration tests (pages, API routes,
 * middleware, the full pipeline). Uses the "test" profile by default so it picks
 * up your test database/env. Remember to `await app.close()`.
 */
declare function createHarness(projectRoot: string, options?: HarnessOptions): Promise<Harness>;

export { type BrowserArtifactPage, type Deferred, type Harness, type HarnessOptions, type JsonResponse, MemoryCookieJar, type TestRequestOptions, type TransactionalDatabase, type WaitForOptions, callRoute, captureBrowserArtifacts, createFactory, createHarness, deferred, expectProblem, mountHtml, readJsonResponse, renderComponent, testContext, testRequest, waitFor, withDatabaseRollback };
```

---

## @wrnexus/tracking

Documentation URL: https://wrnexusjs.dev/packages/tracking

# @wrnexus/tracking

> Error tracking for WrNexus apps: capture exceptions manually or via middleware and fan them out to pluggable sinks.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

`@wrnexus/tracking` is a small, server-side error-capture layer. You create a
tracker with one or more **sinks**, then feed it errors — either manually with
`tracker.capture(err, context)` or automatically by mounting `tracker.middleware()`
in your request pipeline. A `consoleSink` is included; forwarding to Sentry,
Datadog, or any other backend is just a matter of writing a tiny sink. Reach for
it when you want a single, sink-agnostic place to route application errors. Sinks
run best-effort — a throwing sink never breaks the request.

## Installation

```bash
bun add @wrnexus/tracking
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

## API

### `createTracker(options?): Tracker`

Creates a tracker. `TrackerOptions`:

| Option       | Type                                        | Description                                                                 |
| ------------ | ------------------------------------------- | --------------------------------------------------------------------------- |
| `sinks`      | `ErrorSink[]`                               | Initial sinks to fan events out to. Defaults to `[]`.                       |
| `now`        | `() => number`                              | Clock used for `event.timestamp` (epoch ms). Defaults to `Date.now`.        |
| `beforeSend` | `(event: ErrorEvent) => ErrorEvent \| null` | Scrub/enrich an event before it reaches any sink. Return `null` to drop it. |

The returned `Tracker`:

| Member       | Signature                                                              | Description                                                                                                                                                                          |
| ------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `capture`    | `(error: unknown, context?: Record<string, unknown>) => Promise<void>` | Normalizes any thrown value into an `Error`, builds an `ErrorEvent`, runs `beforeSend`, then dispatches to all sinks. Non-`Error` values are wrapped in an `Error` named `NonError`. |
| `addSink`    | `(sink: ErrorSink) => void`                                            | Registers an additional sink at runtime.                                                                                                                                             |
| `middleware` | `() => Middleware`                                                     | Returns a WrNexus `Middleware` that captures any error thrown downstream, then re-throws it so the framework's error handler still produces the response.                            |

The middleware attaches this context to captured events:

```ts
{ method: ctx.req.method, path: ctx.url.pathname, requestId: ctx.locals.requestId }
```

### `consoleSink: ErrorSink`

A built-in sink that logs a compact one-line message via `console.error`, e.g.
`[error] TypeError: cannot read x {"userId":42}`.

### Types

```ts
interface ErrorEvent {
  error: Error;
  context: Record<string, unknown>; // request info, user id, tags…
  timestamp: number; // epoch ms
}

interface ErrorSink {
  name?: string;
  capture(event: ErrorEvent): void | Promise<void>;
}
```

## Usage

Manual capture:

```ts
import { createTracker, consoleSink } from "@wrnexus/tracking";

const tracker = createTracker({ sinks: [consoleSink] });

try {
  await doWork();
} catch (err) {
  await tracker.capture(err, { userId: 42, op: "doWork" });
  throw err;
}
```

As request middleware:

```ts
import { createTracker, consoleSink } from "@wrnexus/tracking";

const tracker = createTracker({ sinks: [consoleSink] });

app.use(tracker.middleware()); // captures + re-throws downstream errors
```

A custom sink with `beforeSend` scrubbing:

```ts
import { createTracker, type ErrorSink } from "@wrnexus/tracking";

const sentrySink: ErrorSink = {
  name: "sentry",
  async capture(event) {
    await Sentry.captureException(event.error, { extra: event.context });
  },
};

const tracker = createTracker({
  sinks: [sentrySink],
  beforeSend(event) {
    delete event.context.password; // scrub secrets
    return event; // return null to drop the event entirely
  },
});

tracker.addSink(anotherSink); // add more sinks later
```

## Requirements / Notes

- Runs on **Bun** only (Node is not supported).
- Peer package: [`@wrnexus/core`](../core) — the `Context` and `Middleware` types
  used by `tracker.middleware()` come from there.
- Sink dispatch is fire-and-forget-safe: all sinks run via `Promise.all`, and a
  sink that throws is swallowed so it can never break the app.

### Exported TypeScript declarations

```ts
import { Middleware } from '@wrnexus/core';

type TelemetryKind = "error" | "event" | "metric" | "span";
interface TelemetryEnvelope {
    id: string;
    kind: TelemetryKind;
    name: string;
    timestamp: number;
    traceId?: string;
    userId?: string;
    tenantId?: string;
    attributes: Record<string, unknown>;
    payload?: unknown;
}
interface TelemetrySink {
    name?: string;
    send(events: TelemetryEnvelope[]): void | Promise<void>;
}
interface TelemetryPipeline {
    emit(input: Omit<TelemetryEnvelope, "id" | "timestamp"> & Partial<Pick<TelemetryEnvelope, "id" | "timestamp">>): Promise<void>;
    flush(): Promise<void>;
    close(): Promise<void>;
    size(): number;
}
interface TelemetryPipelineOptions {
    sinks: TelemetrySink[];
    batchSize?: number;
    flushIntervalMs?: number;
    maxQueueSize?: number;
    sampleRate?: number;
    beforeSend?: (event: TelemetryEnvelope) => TelemetryEnvelope | null;
    onSinkError?: (error: unknown, sink: TelemetrySink, events: readonly TelemetryEnvelope[]) => void | Promise<void>;
    now?: () => number;
    random?: () => number;
}
declare function createTelemetryPipeline(options: TelemetryPipelineOptions): TelemetryPipeline;
declare const telemetryConsoleSink: TelemetrySink;

/**
 * @wrnexus/tracking — error tracking with pluggable sinks. Capture exceptions
 * manually or via middleware, and fan them out to any sink (console by default;
 * write a small sink to forward to Sentry/Datadog/etc.).
 *
 *   const tracker = createTracker({ sinks: [consoleSink] });
 *   app-middleware: tracker.middleware()   // captures + re-throws request errors
 *   tracker.capture(err, { userId });      // manual
 */

interface ErrorEvent {
    error: Error;
    /** Arbitrary structured context (request info, user id, tags…). */
    context: Record<string, unknown>;
    /** Epoch ms. */
    timestamp: number;
}
interface ErrorSink {
    name?: string;
    capture(event: ErrorEvent): void | Promise<void>;
}
interface Tracker {
    capture(error: unknown, context?: Record<string, unknown>): Promise<void>;
    addSink(sink: ErrorSink): void;
    /** Middleware that captures errors thrown downstream, then re-throws them. */
    middleware(): Middleware;
}
interface TrackerOptions {
    sinks?: ErrorSink[];
    now?: () => number;
    /** Scrub/enrich an event before it hits sinks (return null to drop it). */
    beforeSend?: (event: ErrorEvent) => ErrorEvent | null;
}
/** A sink that logs a compact one-line error to the console. */
declare const consoleSink: ErrorSink;
declare function createTracker(options?: TrackerOptions): Tracker;

export { type ErrorEvent, type ErrorSink, type TelemetryEnvelope, type TelemetryKind, type TelemetryPipeline, type TelemetryPipelineOptions, type TelemetrySink, type Tracker, type TrackerOptions, consoleSink, createTelemetryPipeline, createTracker, telemetryConsoleSink };
```

---

## @wrnexus/typecheck

Documentation URL: https://wrnexusjs.dev/packages/typecheck

# @wrnexus/typecheck

Static type checking for `.wrn` declarations, props, state, outputs, functions, stores, and generated virtual TypeScript files.

The package is compiler tooling rather than a browser UI package, so its public kit consists of programmatic typecheck helpers and diagnostics.

### Exported TypeScript declarations

```ts
import { PageAst } from '@wrnexus/syntax';
export { componentContract, storeContract } from './contracts.js';
export { findAppRoot, loadApplicationTypes } from './project.js';

interface WrnTypeDiagnostic {
    code: string;
    category: "error" | "warning" | "info";
    message: string;
    file: string;
    line: number;
    column: number;
    length: number;
    expected?: string;
    received?: string;
    hint?: string;
    related?: {
        file: string;
        line: number;
        column: number;
        message: string;
    };
}
interface TypecheckOptions {
    filePath?: string;
    appRoot?: string;
    strict?: boolean;
    noImplicitAny?: boolean;
    strictNullChecks?: boolean;
    checkRuntimeBoundaries?: boolean;
}
interface SourceMapping {
    virtualStartLine: number;
    virtualEndLine: number;
    sourceStartLine: number;
    sourceStartColumn: number;
}
interface VirtualTypeScriptModule {
    ast: PageAst;
    fileName: string;
    code: string;
    mappings: SourceMapping[];
}
declare function virtualTypeScriptModule(source: string, filePath?: string, appRoot?: string): VirtualTypeScriptModule;
declare function checkWrnSource(source: string, options?: TypecheckOptions): WrnTypeDiagnostic[];
declare function checkWrnFile(filePath: string, options?: Omit<TypecheckOptions, "filePath">): WrnTypeDiagnostic[];

export { type TypecheckOptions, type VirtualTypeScriptModule, type WrnTypeDiagnostic, checkWrnFile, checkWrnSource, virtualTypeScriptModule };
```

---

## @wrnexus/ui

Documentation URL: https://wrnexusjs.dev/packages/ui

# @wrnexus/ui

> First-party Wire UI component library — a set of themeable `.wrn` components plus a single tokenized stylesheet.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

`@wrnexus/ui` ships a library of server-rendered `.wrn` components (layout, form
controls, and feedback UI) together with one themeable stylesheet, `ui.css`. The
components are **auto-discovered** by the framework router — you don't import them
in code. Once the package's component directory is on the router's scan path, you
mount any component in a page with `data-component="<name>"`. Every visual is
driven by `var(--wire-*)` theme tokens, so components restyle instantly when the
theme changes. The tiny JS surface (`src/index.ts`) exists only so the toolchain
(CLI build + dev server) can locate the component directory and stylesheet.

The complete PDF-aligned catalog currently contains **85 components**. The
generated `COMPONENTS.md` and `component-reference.json` files document every
mount name, prop, inferred type, default/required status, slot, event, category,
and source file directly from the packaged `.wrn` source.

## Installation

```bash
bun add @wrnexus/ui
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

In practice you rarely install this directly: `@wrnexus/cli` and
`@wrnexus/dev-server` already depend on it and wire it into the router for you
(see [Auto-discovery](#auto-discovery)).

## Components

Components live as `.wrn` files under `packages/ui/components/`. The canonical mount
name comes from the component declaration (for example, `component Button` mounts as
`data-component="Button"`). Component lookup is case-insensitive, so existing lowercase
mounts continue to work. Each component accepts a `class` prop (appended to its root
element), and most render their body from either a named prop or the default slot.

### Layout

| Name        | Purpose                            | Key props   |
| ----------- | ---------------------------------- | ----------- |
| `container` | Max-width centered content wrapper | `class`     |
| `stack`     | Vertical column with gap           | `gap` (0–8) |
| `hstack`    | Horizontal row with gap            | `gap` (0–8) |
| `grid`      | CSS grid container                 | see source  |
| `divider`   | Horizontal rule                    | `class`     |
| `spacer`    | Flexible/empty spacing element     | see source  |

### Core / feedback

| Name           | Purpose                                              | Key props                                                                                       |
| -------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `button`       | Button                                               | `label`, `variant` (`default`\|`primary`\|`danger`\|`ghost`), `size` (`sm`\|`md`\|`lg`), `type` |
| `input`        | Text input                                           | see source                                                                                      |
| `textarea`     | Multi-line input                                     | see source                                                                                      |
| `checkbox`     | Checkbox                                             | see source                                                                                      |
| `badge`        | Small status badge                                   | `label`, `variant`                                                                              |
| `alert`        | Callout box                                          | `variant` (`info`\|`success`\|`danger`\|`warning`), `title`, `message`                          |
| `card`         | Padded, bordered surface                             | `class`                                                                                         |
| `avatar`       | User avatar                                          | see source                                                                                      |
| `spinner`      | Loading indicator                                    | see source                                                                                      |
| `disclosure`   | Expandable details/summary                           | see source                                                                                      |
| `theme-toggle` | Theme switch button (binds `data-wire-theme-toggle`) | `label`                                                                                         |

### Additional controls & data display

Also shipped: `select`, `radio`, `switch`, `progress`, `tag`, `skeleton`,
`tooltip`, `table`, `FAQAccordion`, `AnnouncementBar`, and `BackToTop`.

The PDF-defined minimum release and essential build-first set also includes
typed typography, form primitives, loading actions, combobox and multi-select,
time/date-time and recurring schedule controls, confirmation dialogs, data
tables, filters, desktop/mobile navigation, mega menus, marketing/product/legal
page shells, product and metric cards, FAQ composition, pricing comparison,
SDK tabs, legal navigation, and cookie preferences.

`Seo` and `StructuredData` remain framework/page concerns rather than body
components: use the native page `seo { ... }` block and document-head APIs so
metadata is emitted in `<head>` instead of invalid component markup.

The authoritative, always-current list is `uiComponentNames()` (below), which reads
the component directory at runtime.

For the full catalog, see [`COMPONENTS.md`](./COMPONENTS.md). The machine-readable
equivalent is exported as `@wrnexus/ui/component-reference.json`.

## API

The JS module (`@wrnexus/ui`) exposes five helpers used by the build tooling to
locate the component assets. There is no component code to import — the components
are `.wrn` files rendered server-side.

| Export             | Signature                  | Returns                                                                                    |
| ------------------ | -------------------------- | ------------------------------------------------------------------------------------------ |
| `uiComponentsDir`  | `() => string`             | Absolute path to the `.wrn` component directory (feed to `buildRouter`'s `componentDirs`). |
| `uiCssPath`        | `() => string`             | Absolute path to `ui.css`.                                                                 |
| `uiCss`            | `() => string`             | The `ui.css` file contents (all `.wire-*` classes, themed via tokens).                     |
| `uiComponentNames` | `() => string[]`           | Sorted list of declared built-in component names.                                          |
| `uiComponentPath`  | `(name: string) => string` | Absolute source path for a declared component name or case-insensitive alias.              |

### `./ui.css` asset export

`package.json` also exposes the raw stylesheet as a subpath asset:

```json
"exports": {
  ".": "./src/index.ts",
  "./ui.css": "./ui.css"
}
```

The framework serves this stylesheet once at `/__wrnexus/ui.css`, so pages get all
component styles from a single request.

### Component styles and motion

Components own their BEM styles in local `style {}` blocks and do not require a
Tailwind scan. `ui.css` supplies only global tokens, resets, and shared
primitives. Applications can still use Tailwind independently in their own
source files.

The shared stylesheet gives all component boundaries consistent, GPU-friendly
entry and interaction motion. Override `--wire-motion-fast`,
`--wire-motion-base`, `--wire-motion-slow`, `--wire-ease-standard`, or
`--wire-ease-emphasized` to tune it. Hover lift is limited to precise pointing
devices and `prefers-reduced-motion` is honored automatically.

### Using the selected theme in application UI

The active theme and palette are not limited to `@wrnexus/ui` components. The
framework exposes the resolved values as semantic CSS custom properties, so
pages and custom `.wrn` components can use the same contract:

```css
.account-card {
  background: var(--wire-color-surface);
  color: var(--wire-color-text);
  border: 1px solid var(--wire-color-border);
}

.account-card__action {
  background: var(--wire-color-primary);
  color: var(--wire-color-primary-contrast);
}
```

Stable no-spacing helper classes are also available: `wire-bg-page`,
`wire-bg-surface`, `wire-bg-surface-2`, `wire-bg-primary`, `wire-bg-secondary`,
`wire-text`, `wire-text-muted`, `wire-text-primary`, `wire-text-success`,
`wire-text-warning`, `wire-text-danger`, and `wire-border`.

Tailwind-authored custom markup can continue using the palette families already
used by packaged components. `indigo-*` and `violet-*` resolve to primary,
`blue-*` to info, `emerald-*`/`green-*` to success, `amber-*` to warning, and
`red-*`/`rose-*` to danger. These aliases live at `:root`, so they work outside
a `[data-component]` boundary too.

## Usage

### Auto-discovery

The router scans extra `componentDirs` (in addition to the app's own
`app/components`) and keys components by name. Library dirs are scanned **first**
and `app/components` **last**, so an app component of the same name shadows the
library's. The CLI build (`@wrnexus/cli`) and dev server (`@wrnexus/dev-server`)
both wire the UI directory in for you:

```ts
import { buildRouter } from "@wrnexus/router";
import { uiComponentsDir } from "@wrnexus/ui";

const router = buildRouter(appDir, { componentDirs: [uiComponentsDir()] });
```

### Mounting components in a page

Once discovered, mount any component by name via `data-component`. Quoted
attributes (other than `data-component`) become string props:

```html
<div data-component="card">
  <div data-component="badge" label="New"></div>
  <button data-component="button" label="Save" variant="primary" size="lg"></button>
  <div data-component="alert" variant="success" title="Done" message="Saved."></div>
</div>
```

## Overrides

Ways to customize the components, in increasing order of power:

1. **Theme tokens** — override CSS custom properties such as `--wire-color-primary`,
   `--wire-color-surface`, `--wire-radius-sm`, etc. Every component style resolves
   through `var(--wire-*)`, so changing a token restyles everything instantly
   (including across theme switches).
2. **App CSS** — redefine a `.wire-*` class in your own stylesheet, which is loaded
   after `ui.css` and therefore wins.
3. **`class` prop** — pass a `class` prop to a component; it is appended to the
   component's root element, letting you add per-instance classes without touching
   the base styles.
4. **`wrnexus eject <name>`** — copy the component's `.wrn` source into your
   `app/components`, where (because app components shadow library ones) you fully
   own and can edit it. Use `uiComponentNames()` for the list of ejectable names.

## Requirements / Notes

- **Bun-only** — the package uses standard fs/path/url APIs but is published and
  consumed within the Bun-native WrNexus toolchain (Node is not supported).
- Peer packages: components are discovered and rendered by
  [`@wrnexus/router`](../router) (via `componentDirs`) and served by
  [`@wrnexus/dev-server`](../dev-server) / built by [`@wrnexus/cli`](../cli).
- Depends on [`@wrnexus/core`](../core) (`dependencies`).
- `theme-toggle` relies on the framework's theme runtime, which binds the
  `data-wire-theme-toggle` attribute — no per-component JS is required.

### Exported TypeScript declarations

```ts
export {  }
```

---

## @wrnexus/uploader

Documentation URL: https://wrnexusjs.dev/packages/uploader

# @wrnexus/uploader

Config-driven file uploads + serving for [WrNexus](https://www.npmjs.com/org/wrnexus). Declare
named **storage stores** (local disk or any S3-compatible backend) in `wrnexus.config.ts`, upload
with one function call, drop a drag-and-drop widget on a page, and serve files back — public or
private. Zero external dependencies (S3 is signed with a built-in AWS SigV4 implementation, like the
rest of the framework).

## Usage

### Configure local and S3 stores

```ts
// wrnexus.config.ts
import type { AppConfig } from "@wrnexus/styles";

const config: AppConfig = {
  storage: {
    default: "public",
    stores: {
      // Local disk, world-readable — served by the framework with a 1-year cache.
      public: {
        driver: "local",
        dir: "uploads/public", // relative to the app root (dev) / cwd (prod)
        access: "public",
        maxBytes: 10_000_000,
        accept: ["image/*", ".pdf"], // MIME, "type/*" wildcards, or ".ext"
      },
      // Private S3 (works with AWS, Cloudflare R2, Backblaze B2, MinIO, DO Spaces).
      docs: {
        driver: "s3",
        access: "private",
        bucket: "my-bucket",
        region: "auto",
        endpoint: "https://<acct>.r2.cloudflarestorage.com",
        accessKeyId: process.env.S3_KEY!,
        secretAccessKey: process.env.S3_SECRET!,
      },
    },
  },
};
export default config;
```

### Upload from an API route or server function

```ts
// app/api/upload.ts — one-liner
import { handleUpload } from "@wrnexus/uploader";
export const POST = handleUpload({ store: "public" });
// → { ok: true, files: [{ key, url, name, type, size }] }
```

```ts
// or drive it yourself, anywhere you have the request
import { upload, getStore } from "@wrnexus/uploader";
const { files } = await upload("docs", ctx.req, { prefix: "invoices" });
await getStore("docs").driver.delete(files[0].key);
```

Uploads are validated (size + type), stored under a random, collision-proof, path-safe key
(the client filename is never used as a path), and — for public stores — returned with a servable
`url`.

### Add a client upload widget

Drop the element anywhere; the runtime (drag-and-drop, per-file progress, success/failed states) is
auto-injected on pages that contain `data-uploader`:

```html
<div
  data-uploader="public"
  data-endpoint="/api/upload"
  data-accept="image/*"
  data-max="10000000"
  data-multiple
></div>
```

Or via the first-party UI component:

```html
<div
  data-component="file-upload"
  store="public"
  endpoint="/api/upload"
  accept="image/*"
  multiple="true"
></div>
```

It dispatches bubbling events you can listen for:

- `wrnexus:upload` — `detail: { file, result: { key, url, name, size, type } }`
- `wrnexus:upload-error` — `detail: { file, error }`

### Serve private files behind application authentication

- **Public + local** → served automatically at `/__wrnexus/uploads/<store>/<key>` (immutable cache).
- **Public + S3** → `url` is the bucket/CDN URL directly.
- **Private** (any driver) → mount a route and gate it with your auth middleware:

```ts
// app/api/files/[key].ts
import { serveFromStore } from "@wrnexus/uploader";
export const GET = serveFromStore("docs"); // your middleware decides who gets in
```

## API

Uploads can participate in security and media pipelines without changing storage drivers. Pass a
`scan` hook to reject malware/DLP findings before storage, and `afterStore` to enqueue image/video
processing or indexing. If post-processing throws, WRNexus deletes the newly written object so a
partially accepted upload is never left behind.

| Export                                  | What                                                            |
| --------------------------------------- | --------------------------------------------------------------- |
| `handleUpload(opts)`                    | POST route handler → JSON `{ ok, files }`                       |
| `upload(store, req, opts)`              | Parse + validate + store; returns `{ files }`                   |
| `serveFromStore(store)`                 | Route handler that streams an object back (gate it for private) |
| `getStore(name?)` / `hasStorage(name?)` | Reach a store's `driver` (`put`/`get`/`delete`/`publicUrl`)     |
| `configureStorage(config, root)`        | Build the registry (the framework calls this at startup)        |
| `s3Driver` / `localDriver` / `signS3`   | Lower-level building blocks                                     |

## Notes

- Uploads count against the server's `maxBodyBytes`; per-file limits use each store's `maxBytes`.
- SigV4 signing is implemented from scratch (no `@aws-sdk`); tested against local S3 semantics.
  Live AWS/R2 connectivity depends on your credentials + bucket policy.
- v1 buffers each file in memory up to its size cap (fine for images/docs up to tens of MB).

## Helper and component kit

Use `formatFileSize`, `uploadAccept`, `uploadedFileMap`, `uploaderAttributes`, and `assertUploadedFiles` to keep upload forms and server validation consistent.

Enable `uploaderPlugin()` for:

- `<UploadDropzone />`
- `<UploadStatus />`

The complete blocks compose `Card`, `Alert`, and `Badge` from `@wrnexus/ui`; the specialized upload runtime remains responsible for the native file input and secure transport behavior.
Large files can use `createResumableUploadManager`. Sessions are bounded and
expiring; chunks may arrive out of order, carry SHA-256 checksums, and are
idempotent when retried. Conflicting retries reject, and the object is assembled
only after every exact-sized chunk is present.

```ts
const uploads = createResumableUploadManager({
  driver: getStore("documents").driver,
  sessions: redisUploadSessionStore,
  chunkSize: 5 * 1024 * 1024,
  maxBytes: 500 * 1024 * 1024,
  accept: ["application/pdf"],
});

const session = await uploads.create({ name: "report.pdf", size, type });
await uploads.uploadChunk(session.id, index, bytes, sha256);
```

The included memory session store is intended for one-process apps and tests.
Multi-instance production deployments should implement `ResumableSessionStore`
with shared durable storage and atomic session updates, and periodically call
`prune()` for abandoned uploads.

### Exported TypeScript declarations

```ts
import { Context } from '@wrnexus/core';
export { UploaderPluginOptions, uploaderComponentsDir, default as uploaderPlugin } from './plugin.js';
import '@wrnexus/plugin';

/**
 * Storage driver contract + config types.
 *
 * A `StorageDriver` is the low-level object store (local disk, S3, …). It knows
 * how to put/get/delete raw bytes under a key — nothing about HTTP, multipart
 * parsing, validation, or URLs. The registry (`client.ts`) builds one driver per
 * configured store and the upload layer (`upload.ts`) drives them. This mirrors
 * `@wrnexus/db`'s driver/adapter split.
 */
/** Whether a store's objects are world-readable or served behind app auth. */
type StoreAccess = "public" | "private";
/** An object read back from a store. */
interface StoredObject {
    /** Object bytes as a web stream (preferred) or a buffer. */
    body: ReadableStream<Uint8Array> | Uint8Array;
    /** MIME type to serve with. */
    contentType: string;
    /** Size in bytes, when known. */
    size?: number;
}
/** Metadata passed alongside the bytes on `put`. */
interface PutMeta {
    contentType: string;
    /** Original client filename (informational only — NEVER used as a path). */
    filename?: string;
}
/** The low-level object store. Implementations: `adapters/local.ts`, `adapters/s3.ts`. */
interface StorageDriver {
    /** Persist `data` under `key` (overwrites). */
    put(key: string, data: Uint8Array, meta: PutMeta): Promise<void>;
    /** Fetch an object, or `null` if it doesn't exist. */
    get(key: string): Promise<StoredObject | null>;
    /** Remove an object. No error if it's already gone. */
    delete(key: string): Promise<void>;
    /**
     * A directly-servable absolute URL for a PUBLIC object (e.g. an S3/CDN URL), or
     * `null` when the framework should serve it (local public stores). Private
     * stores always return `null`.
     */
    publicUrl(key: string): string | null;
}
/** Local-disk store. `dir` is resolved against the app root when relative. */
interface LocalStoreConfig {
    driver: "local";
    access: StoreAccess;
    /** Directory the files live under (e.g. "uploads/public"). */
    dir: string;
    /** Reject files larger than this many bytes (per file). */
    maxBytes?: number;
    /** Allowed types: MIME (`"image/*"`, `"application/pdf"`) and/or extensions (`".pdf"`). */
    accept?: string[];
}
/** S3 / S3-compatible store (AWS, Cloudflare R2, Backblaze B2, MinIO, DO Spaces). */
interface S3StoreConfig {
    driver: "s3";
    access: StoreAccess;
    bucket: string;
    region: string;
    accessKeyId: string;
    secretAccessKey: string;
    /**
     * Custom endpoint for non-AWS services, e.g.
     * `https://<acct>.r2.cloudflarestorage.com`. Omit for AWS S3.
     */
    endpoint?: string;
    /** Force path-style URLs (`/bucket/key`). Defaults on for custom endpoints. */
    forcePathStyle?: boolean;
    /** Public base URL for `publicUrl()` (a CDN or public bucket domain). */
    publicBaseUrl?: string;
    maxBytes?: number;
    accept?: string[];
}
type StoreConfig = LocalStoreConfig | S3StoreConfig;
/** The `storage` block in `wrnexus.config.ts`. */
interface StorageConfig {
    /** Name of the store used when a call omits one. Defaults to the first store. */
    default?: string;
    /** Named stores, reached with `getStore("<name>")` / `upload("<name>", …)`. */
    stores: Record<string, StoreConfig>;
}

/**
 * Process-wide store registry, configured once at server startup from the
 * `storage` block in `wrnexus.config.ts` (mirrors `@wrnexus/db`'s registry).
 * Handlers then call `getStore("<name>")` — or omit the name for the default.
 */

interface Store {
    name: string;
    access: StoreAccess;
    driver: StorageDriver;
    config: StoreConfig;
}
/** Build a driver per configured store. Safe to call again (fully replaces). */
declare function configureStorage(config: StorageConfig | undefined, appRoot: string): void;
/** Whether the default (or a named) store is configured. */
declare function hasStorage(name?: string): boolean;
/** The default store, or a named one. Throws if it isn't configured. */
declare function getStore(name?: string): Store;
/** Names of all configured stores. */
declare function storeNames(): string[];

/**
 * The HTTP-facing upload + serve layer: parse multipart requests, validate,
 * store, and serve files back. Built on the store registry (`client.ts`).
 */

/** Reserved prefix the framework serves PUBLIC local objects from. */
declare const UPLOADS_PREFIX = "/__wrnexus/uploads/";
interface UploadedFile {
    /** Storage key — pass to `getStore().driver.get/delete` or a serve route. */
    key: string;
    /** A servable URL for public objects, or `null` for private stores. */
    url: string | null;
    /** Original (sanitized) client filename, for display. */
    name: string;
    type: string;
    size: number;
}
interface UploadOptions {
    /** Only read files from this form field (default: every file field). */
    field?: string;
    /** Override the store's `maxBytes`. */
    maxBytes?: number;
    /** Override the store's `accept` list. */
    accept?: string[];
    /** Key prefix, e.g. `"avatars"` → keys become `avatars/<yyyy>/<mm>/<rand>.<ext>`. */
    prefix?: string;
    /** Virus/DLP/content scanner invoked before bytes enter storage. Throw or return unsafe to reject. */
    scan?: (file: UploadScanInput) => UploadScanResult | Promise<UploadScanResult>;
    /** Image/video/indexing hook invoked after storage. Failure removes the just-written object. */
    afterStore?: (file: UploadedFile & {
        bytes: Uint8Array;
        store: Store;
    }) => void | Promise<void>;
}
interface UploadScanInput {
    name: string;
    type: string;
    size: number;
    bytes: Uint8Array;
    store: Store;
}
interface UploadScanResult {
    safe: boolean;
    reason?: string;
    scanner?: string;
}
/** A 4xx-carrying error so `handleUpload` can map it to a status. */
declare class UploadError extends Error {
    readonly status: number;
    constructor(message: string, status?: number);
}
/** The servable URL for a stored object (public → URL, private → null). */
declare function storedUrl(store: Store, key: string): string | null;
/**
 * Read multipart file(s) from a request and store them. Throws `UploadError`
 * (4xx) on validation failures. Call it directly, or use `handleUpload`.
 */
declare function upload(storeName: string | undefined, req: Request, opts?: UploadOptions): Promise<{
    files: UploadedFile[];
}>;
/**
 * Ready-made POST handler:
 *
 *   // app/api/upload.ts
 *   export const POST = handleUpload({ store: "public" });
 *
 * Returns `{ ok:true, files:[…] }` on success, or `{ ok:false, error }` with a
 * 4xx/5xx status.
 */
declare function handleUpload(opts?: UploadOptions & {
    store?: string;
}): (ctx: Context) => Promise<Response>;
/**
 * Serve an object from a store as a route handler — mount it behind your auth
 * middleware to gate PRIVATE files:
 *
 *   // app/api/files/[key].ts
 *   export const GET = serveFromStore("docs");
 *
 * Reads the key from `ctx.params.key` (or `ctx.params.path`); it may contain `/`.
 */
declare function serveFromStore(storeName?: string, opts?: {
    param?: string;
}): (ctx: Context) => Promise<Response>;
/**
 * Framework asset hook: serve PUBLIC local objects at
 * `/__wrnexus/uploads/<store>/<key>`. Returns `null` for anything it doesn't
 * own (unknown/private/S3-backed store) so the caller falls through. Wired into
 * the dev + prod asset servers.
 */
declare function serveStoredFile(pathname: string): Promise<Response | null>;

/**
 * Client runtime for `<div data-uploader>` elements — drag-and-drop + file
 * input, per-file progress bars, and success/failed states. Injected by
 * `collectScripts` only on pages that contain `data-uploader` (same mechanism as
 * `validate.js`). Self-contained: it injects its own themed stylesheet (using
 * `--wire-*` tokens) and posts each file via XHR so upload progress is live.
 *
 * Markup it enhances (also a valid no-JS `<form>` fallback if you wrap it):
 *   <div data-uploader="public" data-endpoint="/api/upload"
 *        data-accept="image/*" data-max="10000000" data-multiple></div>
 *
 * Events dispatched on the element (bubble):
 *   wrnexus:upload        detail: { file, result: { key, url, name, size, type } }
 *   wrnexus:upload-error  detail: { file, error }
 *
 * NOTE: written with single/double quotes + string concatenation only — no
 * backticks and no ${...}, so it embeds safely in the exported template string.
 */
declare const UPLOAD_JS_HREF = "/__wrnexus/uploader.js";
declare const UPLOAD_RUNTIME = "\n(function () {\n  if (typeof document === \"undefined\") return;\n  var CSRF_COOKIE = \"wire-csrf\";\n\n  var CSS =\n    \".wire-uploader{display:block}\" +\n    \".wire-uploader-zone{display:flex;align-items:center;justify-content:center;text-align:center;\" +\n      \"min-height:8rem;padding:1.25rem;border:2px dashed var(--wire-border,#cbd5e1);border-radius:12px;\" +\n      \"background:var(--wire-surface,transparent);color:var(--wire-muted,#64748b);cursor:pointer;\" +\n      \"transition:border-color .15s ease,background-color .15s ease;position:relative}\" +\n    \".wire-uploader-zone:hover,.wire-uploader-zone:focus-visible{border-color:var(--wire-brand,#3f7dff);outline:none}\" +\n    \".wire-uploader-zone.is-drag{border-color:var(--wire-brand,#3f7dff);background:color-mix(in oklab,var(--wire-brand,#3f7dff) 8%,transparent)}\" +\n    \".wire-uploader-prompt{font-size:.9rem;pointer-events:none}\" +\n    \".wire-uploader-input{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer}\" +\n    \".wire-uploader-list{list-style:none;margin:.75rem 0 0;padding:0;display:flex;flex-direction:column;gap:.5rem}\" +\n    \".wire-uploader-item{display:grid;grid-template-columns:1fr auto;gap:.15rem .75rem;align-items:center;\" +\n      \"font-size:.82rem;padding:.5rem .7rem;border:1px solid var(--wire-border,#e2e8f0);border-radius:8px}\" +\n    \".wire-uploader-name{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--wire-text,#0f172a)}\" +\n    \".wire-uploader-meta{color:var(--wire-muted,#94a3b8);font-variant-numeric:tabular-nums}\" +\n    \".wire-uploader-bar{grid-column:1/-1;height:5px;border-radius:999px;background:var(--wire-border,#e2e8f0);overflow:hidden}\" +\n    \".wire-uploader-fill{height:100%;width:0;border-radius:999px;background:var(--wire-brand,#3f7dff);transition:width .15s ease}\" +\n    \".wire-uploader-status{grid-column:1/-1;font-size:.75rem;color:var(--wire-muted,#94a3b8);font-variant-numeric:tabular-nums}\" +\n    \".wire-uploader-item.is-done .wire-uploader-fill{background:var(--wire-success,#16a34a)}\" +\n    \".wire-uploader-item.is-done .wire-uploader-status{color:var(--wire-success,#16a34a)}\" +\n    \".wire-uploader-item.is-error .wire-uploader-fill{background:var(--wire-danger,#dc2626)}\" +\n    \".wire-uploader-item.is-error .wire-uploader-status{color:var(--wire-danger,#dc2626)}\";\n\n  function injectCss() {\n    if (document.getElementById(\"wire-uploader-css\")) return;\n    var s = document.createElement(\"style\");\n    s.id = \"wire-uploader-css\";\n    s.textContent = CSS;\n    document.head.appendChild(s);\n  }\n\n  function cookie(name) {\n    var m = document.cookie.match(new RegExp(\"(?:^|; )\" + name + \"=([^;]*)\"));\n    return m ? decodeURIComponent(m[1]) : \"\";\n  }\n  function el(tag, cls, text) {\n    var e = document.createElement(tag);\n    if (cls) e.className = cls;\n    if (text != null) e.textContent = text;\n    return e;\n  }\n  function fmt(n) {\n    if (n < 1024) return n + \" B\";\n    if (n < 1048576) return (n / 1024).toFixed(1) + \" KB\";\n    return (n / 1048576).toFixed(1) + \" MB\";\n  }\n  function accepts(accept, file) {\n    var list = (accept || \"\").split(\",\").map(function (s) { return s.trim().toLowerCase(); }).filter(Boolean);\n    if (!list.length) return true;\n    var type = (file.type || \"\").toLowerCase();\n    var name = (file.name || \"\").toLowerCase();\n    var ext = name.indexOf(\".\") >= 0 ? name.slice(name.lastIndexOf(\".\")) : \"\";\n    return list.some(function (rule) {\n      if (rule.charAt(0) === \".\") return rule === ext;\n      if (rule.slice(-2) === \"/*\") return type.indexOf(rule.slice(0, -1)) === 0;\n      return rule === type;\n    });\n  }\n\n  function setup(root) {\n    if (root.__wrnexusUploader) return;\n    root.__wrnexusUploader = true;\n\n    var endpoint = root.getAttribute(\"data-endpoint\") || \"/api/upload\";\n    var multipleAttr = root.getAttribute(\"data-multiple\");\n    var multiple = root.hasAttribute(\"data-multiple\") && multipleAttr !== \"false\";\n    var accept = root.getAttribute(\"data-accept\") || \"\";\n    var maxBytes = parseInt(root.getAttribute(\"data-max\") || \"0\", 10) || 0;\n    var field = root.getAttribute(\"data-field\") || (multiple ? \"files\" : \"file\");\n    var invalidate = String(root.getAttribute(\"data-invalidate\") || \"\")\n      .split(\",\")\n      .map(function (tag) { return tag.trim(); })\n      .filter(Boolean);\n    var promptText = root.getAttribute(\"data-label\") || \"Drag files here or click to browse\";\n\n    root.classList.add(\"wire-uploader\");\n    var zone = el(\"div\", \"wire-uploader-zone\");\n    zone.setAttribute(\"role\", \"button\");\n    zone.setAttribute(\"tabindex\", \"0\");\n    zone.appendChild(el(\"div\", \"wire-uploader-prompt\", promptText));\n    var input = document.createElement(\"input\");\n    input.type = \"file\";\n    input.className = \"wire-uploader-input\";\n    if (multiple) input.multiple = true;\n    if (accept) input.accept = accept;\n    zone.appendChild(input);\n    var listEl = el(\"ul\", \"wire-uploader-list\");\n    root.appendChild(zone);\n    root.appendChild(listEl);\n\n    zone.addEventListener(\"keydown\", function (e) {\n      if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); input.click(); }\n    });\n    [\"dragenter\", \"dragover\"].forEach(function (ev) {\n      zone.addEventListener(ev, function (e) { e.preventDefault(); zone.classList.add(\"is-drag\"); });\n    });\n    [\"dragleave\", \"drop\"].forEach(function (ev) {\n      zone.addEventListener(ev, function (e) { e.preventDefault(); zone.classList.remove(\"is-drag\"); });\n    });\n    zone.addEventListener(\"drop\", function (e) {\n      if (e.dataTransfer && e.dataTransfer.files) handle(e.dataTransfer.files);\n    });\n    input.addEventListener(\"change\", function () {\n      if (input.files) handle(input.files);\n      input.value = \"\";\n    });\n\n    function handle(files) {\n      var arr = Array.prototype.slice.call(files);\n      if (!multiple) arr = arr.slice(0, 1);\n      arr.forEach(uploadOne);\n    }\n\n    function row(file) {\n      var li = el(\"li\", \"wire-uploader-item\");\n      li.appendChild(el(\"span\", \"wire-uploader-name\", file.name));\n      li.appendChild(el(\"span\", \"wire-uploader-meta\", fmt(file.size)));\n      var bar = el(\"div\", \"wire-uploader-bar\");\n      var fill = el(\"div\", \"wire-uploader-fill\");\n      bar.appendChild(fill);\n      li.appendChild(bar);\n      var status = el(\"span\", \"wire-uploader-status\", \"\");\n      li.appendChild(status);\n      listEl.appendChild(li);\n      return { li: li, fill: fill, status: status };\n    }\n\n    function uploadOne(file) {\n      var ui = row(file);\n      if (maxBytes && file.size > maxBytes) return fail(ui, \"Too large (max \" + fmt(maxBytes) + \")\", file);\n      if (!accepts(accept, file)) return fail(ui, \"Type not allowed\", file);\n\n      var fd = new FormData();\n      fd.append(field, file, file.name);\n      var xhr = new XMLHttpRequest();\n      xhr.open(\"POST\", endpoint, true);\n      var token = cookie(CSRF_COOKIE);\n      if (token) xhr.setRequestHeader(\"x-csrf-token\", token);\n      xhr.upload.addEventListener(\"progress\", function (e) {\n        if (e.lengthComputable) {\n          var pct = Math.round((e.loaded / e.total) * 100);\n          ui.fill.style.width = pct + \"%\";\n          ui.status.textContent = pct + \"%\";\n        }\n      });\n      xhr.addEventListener(\"load\", function () {\n        var data = null;\n        try { data = JSON.parse(xhr.responseText); }\n        catch (error) { console.warn(\"[wrnexus:uploader] upload response was not valid JSON\", error); }\n        if (xhr.status >= 200 && xhr.status < 300 && data && data.ok) {\n          done(ui, (data.files && data.files[0]) || null, file);\n        } else {\n          fail(ui, (data && data.error) || (\"Upload failed (\" + xhr.status + \")\"), file);\n        }\n      });\n      xhr.addEventListener(\"error\", function () { fail(ui, \"Network error\", file); });\n      xhr.send(fd);\n    }\n\n    function done(ui, info, file) {\n      ui.li.classList.remove(\"is-error\");\n      ui.li.classList.add(\"is-done\");\n      ui.fill.style.width = \"100%\";\n      ui.status.textContent = \"\\u2713 Uploaded\";\n      root.dispatchEvent(new CustomEvent(\"wrnexus:upload\", { bubbles: true, detail: { file: file, result: info } }));\n      if (invalidate.length) {\n        window.dispatchEvent(new CustomEvent(\"wrnexus:cache:invalidate\", {\n          detail: { tags: invalidate, source: \"uploader\", file: file, result: info }\n        }));\n      }\n    }\n    function fail(ui, msg, file) {\n      ui.li.classList.add(\"is-error\");\n      ui.status.textContent = \"\\u2717 \" + msg;\n      root.dispatchEvent(new CustomEvent(\"wrnexus:upload-error\", { bubbles: true, detail: { file: file, error: msg } }));\n    }\n  }\n\n  function init() {\n    injectCss();\n    var nodes = document.querySelectorAll(\"[data-uploader]\");\n    for (var i = 0; i < nodes.length; i++) setup(nodes[i]);\n  }\n  if (document.readyState === \"loading\") document.addEventListener(\"DOMContentLoaded\", init);\n  else init();\n})();\n";

/**
 * Local-disk storage driver. Files live under a configured directory; keys map
 * to relative paths inside it. Path traversal is rejected — a key can never
 * escape the base dir.
 */

declare function localDriver(config: LocalStoreConfig, appRoot: string): StorageDriver;

/**
 * S3 (and S3-compatible) storage driver — zero deps, SigV4-signed `fetch`.
 * Works with AWS S3, Cloudflare R2, Backblaze B2, MinIO, DigitalOcean Spaces.
 *
 * Path-style vs virtual-hosted: AWS defaults to virtual-hosted
 * (`bucket.s3.region.amazonaws.com`); custom endpoints (R2/MinIO) default to
 * path-style (`endpoint/bucket/key`). Override with `forcePathStyle`.
 */

declare function s3Driver(config: S3StoreConfig): StorageDriver;

/**
 * AWS Signature Version 4 for S3 requests — zero external deps, built on
 * `node:crypto` + `fetch`. Matches the framework's zero-dep ethos (like
 * `@wrnexus/ai`) and works with any S3-compatible service (AWS, Cloudflare R2,
 * Backblaze B2, MinIO, DigitalOcean Spaces).
 *
 * Reference: docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
 */
/** Hex-encoded SHA-256 of a payload. */
declare function sha256Hex(data: Uint8Array | string): string;
/**
 * Percent-encode an S3 object key for the request path. Every character except
 * the RFC 3986 unreserved set is encoded; `/` between segments is preserved.
 */
declare function encodeKey(key: string): string;
interface SignInput {
    method: string;
    host: string;
    /** Canonical URI — already `%`-encoded, begins with `/`. */
    path: string;
    region: string;
    accessKeyId: string;
    secretAccessKey: string;
    /** Hex SHA-256 of the body, or `"UNSIGNED-PAYLOAD"`. */
    payloadHash: string;
    /** Extra headers to sign (e.g. `content-type`). `host`/`x-amz-*` are added here. */
    headers?: Record<string, string>;
    date: Date;
    service?: string;
}
/**
 * Compute the signed header set for an S3 request. Returns the headers to send
 * (lowercased names, including `authorization`, `host`, `x-amz-date`,
 * `x-amz-content-sha256`).
 */
declare function signS3(input: SignInput): Record<string, string>;

/**
 * Minimal extension ↔ MIME mapping + `accept` matching. Zero-dep: just a table
 * big enough for the common upload types (images, docs, media, archives).
 */
/** Lowercased extension WITHOUT the dot (e.g. "png"), or "" if none. */
declare function extOf(name: string): string;
/** MIME type for a filename/key by its extension, or a safe default. */
declare function contentTypeOf(name: string, fallback?: string): string;
/** The conventional extension for a MIME type, or "" (used to name S3 keys). */
declare function extForType(type: string): string;
/**
 * Does `file` (its MIME `type` + `name`) satisfy an `accept` list? Each accept
 * entry is a MIME type (`"image/png"`), a wildcard MIME (`"image/*"`), or a
 * dotted extension (`".pdf"`). An empty/omitted list accepts everything.
 */
declare function accepts(accept: string[] | undefined, file: {
    type: string;
    name: string;
}): boolean;

interface UploadPolicy {
    maxBytes?: number;
    accept?: string[];
    requireChecksum?: boolean;
    filenamePattern?: RegExp;
}
interface UploadInspection {
    filename: string;
    contentType: string;
    size: number;
    sha256: string;
    extension: string;
}
declare class UploadPolicyError extends Error {
    readonly code: string;
    constructor(message: string, code: string);
}
declare function safeObjectKey(filename: string, prefix?: string): string;
declare function inspectUpload(filename: string, bytes: Uint8Array, declaredType?: string): Promise<UploadInspection>;
declare function enforceUploadPolicy(inspection: UploadInspection, policy: UploadPolicy, expectedChecksum?: string): void;
declare function sniffContentType(bytes: Uint8Array): string | null;
interface SignedFileToken {
    store: string;
    key: string;
    expiresAt: number;
    disposition?: "inline" | "attachment";
}
declare function createSignedFileToken(input: SignedFileToken, secret: string): Promise<string>;
declare function verifySignedFileToken(token: string, secret: string, now?: number): Promise<SignedFileToken | null>;

declare function formatFileSize(bytes: number, locale?: string): string;
declare function uploadAccept(value: string | readonly string[]): string;
declare function uploadedFileMap(files: readonly UploadedFile[]): Record<string, UploadedFile>;
declare function uploaderAttributes(options?: {
    store?: string;
    endpoint?: string;
    accept?: string | readonly string[];
    maxBytes?: number;
    multiple?: boolean;
    field?: string;
    label?: string;
}): Record<string, string | boolean>;
declare function assertUploadedFiles(files: readonly UploadedFile[], options?: {
    min?: number;
    max?: number;
}): readonly UploadedFile[];

interface ResumableUploadSession {
    id: string;
    key: string;
    name: string;
    type: string;
    size: number;
    chunkSize: number;
    totalChunks: number;
    createdAt: number;
    expiresAt: number;
    chunks: Record<number, Uint8Array>;
    digests: Record<number, string>;
}
interface ResumableSessionStore {
    get(id: string): Promise<ResumableUploadSession | null>;
    put(session: ResumableUploadSession): Promise<void>;
    delete(id: string): Promise<void>;
    list(): Promise<ResumableUploadSession[]>;
}
declare function memoryResumableSessionStore(): ResumableSessionStore;
interface ResumableUploadManagerOptions {
    driver: StorageDriver;
    sessions?: ResumableSessionStore;
    maxBytes?: number;
    chunkSize?: number;
    maxSessions?: number;
    ttlMs?: number;
    accept?: string[];
    prefix?: string;
    publicUrl?: (key: string) => string | null;
    now?: () => number;
}
interface CreateResumableUpload {
    name: string;
    type?: string;
    size: number;
    chunkSize?: number;
}
interface ResumableChunkResult {
    receivedChunks: number;
    totalChunks: number;
    complete: boolean;
    file?: UploadedFile;
}
interface ResumableUploadManager {
    create(input: CreateResumableUpload): Promise<ResumableUploadSession>;
    uploadChunk(id: string, index: number, data: Uint8Array, sha256?: string): Promise<ResumableChunkResult>;
    status(id: string): Promise<{
        received: number[];
        totalChunks: number;
        expiresAt: number;
    } | null>;
    cancel(id: string): Promise<boolean>;
    prune(): Promise<number>;
}
declare function createResumableUploadManager(options: ResumableUploadManagerOptions): ResumableUploadManager;

interface QuotaUsage {
    owner: string;
    bytes: number;
    objects: number;
    updatedAt: number;
}
interface QuotaStore {
    get(owner: string): Promise<QuotaUsage>;
    reserve(owner: string, bytes: number, limits: {
        bytes: number;
        objects?: number;
    }): Promise<boolean>;
    release(owner: string, bytes: number): Promise<void>;
}
declare function memoryQuotaStore(): QuotaStore;
interface QuotaSqlClient {
    query<T = any>(sql: string, parameters?: unknown[]): Promise<{
        rows: T[];
    }>;
}
/** PostgreSQL quota accounting using a single atomic conditional upsert. */
declare function postgresQuotaStore(db: QuotaSqlClient, table?: string): QuotaStore;
declare const POSTGRES_QUOTA_SCHEMA = "CREATE TABLE IF NOT EXISTS wrnexus_storage_quota (owner text PRIMARY KEY, bytes bigint NOT NULL DEFAULT 0, objects integer NOT NULL DEFAULT 0, updated_at bigint NOT NULL);";
interface MultipartObjectClient {
    create(key: string, meta: PutMeta): Promise<string>;
    uploadPart(uploadId: string, key: string, part: number, bytes: Uint8Array): Promise<string>;
    complete(uploadId: string, key: string, parts: Array<{
        part: number;
        etag: string;
    }>): Promise<void>;
    abort(uploadId: string, key: string): Promise<void>;
}
declare function multipartUpload(client: MultipartObjectClient, key: string, bytes: Uint8Array, meta: PutMeta, options?: {
    partBytes?: number;
    concurrency?: number;
}): Promise<void>;
interface TemporaryObject {
    key: string;
    expiresAt: number;
}
declare function createTemporaryObjectCleaner(driver: StorageDriver, options?: {
    now?: () => number;
    limit?: number;
}): {
    track(key: string, ttlMs: number): void;
    cleanup(at?: number): Promise<number>;
    snapshot: () => {
        tracked: number;
        nextExpiry: number | undefined;
    };
};
interface VideoTranscodeOptions {
    format: "mp4" | "webm";
    width?: number;
    height?: number;
    videoBitrateKbps?: number;
}
declare function ffmpegVideoTranscoder(options?: {
    executable?: string;
    spawn?: (args: string[]) => {
        exited: Promise<number>;
    };
}): (input: string, output: string, config: VideoTranscodeOptions) => Promise<void>;

export { type CreateResumableUpload, type LocalStoreConfig, type MultipartObjectClient, POSTGRES_QUOTA_SCHEMA, type PutMeta, type QuotaSqlClient, type QuotaStore, type QuotaUsage, type ResumableChunkResult, type ResumableSessionStore, type ResumableUploadManager, type ResumableUploadManagerOptions, type ResumableUploadSession, type S3StoreConfig, type SignedFileToken, type StorageConfig, type StorageDriver, type Store, type StoreAccess, type StoreConfig, type StoredObject, type TemporaryObject, UPLOADS_PREFIX, UPLOAD_JS_HREF, UPLOAD_RUNTIME, UploadError, type UploadInspection, type UploadOptions, type UploadPolicy, UploadPolicyError, type UploadScanInput, type UploadScanResult, type UploadedFile, type VideoTranscodeOptions, accepts, assertUploadedFiles, configureStorage, contentTypeOf, createResumableUploadManager, createSignedFileToken, createTemporaryObjectCleaner, encodeKey, enforceUploadPolicy, extForType, extOf, ffmpegVideoTranscoder, formatFileSize, getStore, handleUpload, hasStorage, inspectUpload, localDriver, memoryQuotaStore, memoryResumableSessionStore, multipartUpload, postgresQuotaStore, s3Driver, safeObjectKey, serveFromStore, serveStoredFile, sha256Hex, signS3, sniffContentType, storeNames, storedUrl, upload, uploadAccept, uploadedFileMap, uploaderAttributes, verifySignedFileToken };
```

---

## @wrnexus/validation

Documentation URL: https://wrnexusjs.dev/packages/validation

# @wrnexus/validation

> One fluent schema, validated on the server (API bodies, env vars) and mirrored to an eval-free browser validator for forms.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Boundary contracts

Use `ContractRegistry` with `defineContract` or `defineEvent` to publish the
same schema descriptors for APIs, actions, webhooks, realtime, queues, cron,
pub/sub, plugins, configuration, and environment variables.

```ts
import { ContractRegistry, defineEvent, v } from "@wrnexus/validation";

export const contracts = new ContractRegistry().register(
  defineEvent({
    name: "user.created",
    version: 1,
    consumers: ["notification-worker", "audit-service"],
    payload: v.object({ userId: v.string().uuid(), createdAt: v.string().date() }),
  }),
);
```

Export the registry from `app/contracts.ts`, then accept a baseline with
`wrnexus contracts snapshot`. CI can run `wrnexus contracts check`; removed
contracts/fields, required-field additions, type changes, narrowed enums, and
tighter validation fail with stable `WRN-CONTRACT-*` diagnostics and list known
consumers. A generated `wrnexus.contracts.json` can be used instead of a module.

## Overview

Define a schema once with the fluent `v` builder, then reuse it in three places: `.parse()` runs server-side and returns coerced values plus per-field errors; `.describe()` emits a plain-JSON `SchemaDescriptor` that the browser runtime interprets (no `eval`, no bundled validator); and helpers like `parseBody` and `parseEnv` wire schemas straight into API routes and startup config. The server rule logic (`applyRule`/`checkField`) and the client runtime (`VALIDATE_RUNTIME`) mirror each other exactly, so a form validates identically in both places. Schemas are conventionally kept in `app/schemas/`.

## Installation

```bash
bun add @wrnexus/validation
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

## API

### The `v` builder

```ts
import { v } from "@wrnexus/validation";
```

| Factory            | Returns         | Field methods                                                                                                       |
| ------------------ | --------------- | ------------------------------------------------------------------------------------------------------------------- |
| `v.string()`       | `StringSchema`  | `email()`, `url()`, `uuid()`, `date()`, `length(n)`, `oneOf(string[])`, `pattern(re)`, `trim()`, `min(n)`, `max(n)` |
| `v.number()`       | `NumberSchema`  | `integer()`, `positive()`, `oneOf(number[])`, `min(n)`, `max(n)`                                                    |
| `v.boolean()`      | `BooleanSchema` | (base methods only)                                                                                                 |
| `v.object(fields)` | `ObjectSchema`  | `parse(input)`, `describe()`                                                                                        |

Every field schema is chainable and shares these base methods:

- `min(n, message?)` / `max(n, message?)` — for strings, bounds the length; for numbers, bounds the value.
- `required(message?)` — require a non-empty value and optionally replace the default `"Required"` message on both server and browser validation.
- `optional()` — an empty/missing value passes instead of erroring `"Required"`.
- `label(text)` — human label carried into the descriptor.
- `default(value)` — value substituted when the field is absent (implies `optional`).
- `refine(fn, message?)` — **server-only** predicate. `fn` returns `true` (ok), `false` (use `message`), or a `string` (that error). Not serialized to the client.

Each string rule accepts an optional trailing `message` to override the default error text.

### `ObjectSchema`

```ts
schema.parse(input: unknown): ParseResult
schema.describe(): SchemaDescriptor
```

`parse` coerces each field (strings stay strings, `v.number()` runs `Number()`, `v.boolean()` treats `true` / `"true"` / `"on"` as true), applies its rules and refinements, fills in `default()` values, and returns:

```ts
interface ParseResult<T = Record<string, unknown>> {
  ok: boolean; // true when errors is empty
  value: T; // coerced values (present pass or fail)
  errors: Record<string, string>; // field name → first failing message
}
```

`describe()` returns the JSON bridge for the client:

```ts
interface SchemaDescriptor {
  type: "object";
  fields: Record<string, FieldDescriptor>;
}
interface FieldDescriptor {
  type: "string" | "number" | "boolean";
  optional?: boolean;
  label?: string;
  trim?: boolean; // strings only
  rules: RuleDescriptor[];
}
```

### Rules and coercion

`RuleDescriptor` is a discriminated union of the serializable rules — `min`, `max`, `length`, `email`, `url`, `uuid`, `date`, `oneOf`, `pattern`, `integer`. Two exported functions apply them and are shared by the server (the client runtime reimplements the same logic):

- `applyRule(type, rule, value): string | null` — validate one already-coerced value against one rule.
- `checkField(desc, raw): { value, error }` — coerce and validate one field. Empty input (`undefined`/`null`/`""`) is `"Required"` unless `optional`. Strings with `trim` are trimmed first. Numbers that fail `Number()` yield `"Must be a number"`.

Notes on specific rules: `email`/`url`/`uuid` test built-in regexes; `date` uses `Date.parse`; `pattern` reconstructs a `RegExp` from its `source`/`flags` and passes silently if the pattern is invalid; `integer` requires `Number.isInteger`; `positive()` is implemented as `min(Number.MIN_VALUE)`.

### API helpers

```ts
invalid(errors: Record<string, string>): Response   // ready 400 { ok:false, errors }

parseBody<T>(schema, req):
  Promise<{ ok: true; value: T } | { ok: false; response: Response }>
```

`parseBody` reads the request body from JSON, `application/x-www-form-urlencoded`, or `multipart/form-data`, validates it, and on failure hands back a ready 400 `Response`.

### Environment config

```ts
parseEnv<T>(schema: ObjectSchema, source?): T
```

Validates env vars (from `Bun.env`, falling back to `process.env`) against a schema and coerces them (`PORT` → number, `DEBUG` → boolean). On any problem it throws **one** error listing every offending variable, so misconfiguration fails fast at startup.

### Client runtime (from `runtime.ts`)

```ts
renderSchemasScript(descriptors: Record<string, SchemaDescriptor>): string
VALIDATE_RUNTIME: string
```

- `renderSchemasScript` produces `window.__wireSchemas = { name: descriptor, … };` to inline in the page.
- `VALIDATE_RUNTIME` is a self-contained, eval-free IIFE string. Injected as a `<script>`, it binds every `form[data-schema]` and validates on submit and blur, writing messages into `[data-error="<field>"]` elements and toggling `aria-invalid` / `.wire-invalid`. On a valid submit it `fetch`es the form `action` as JSON (attaching the `wire-csrf` cookie as an `x-csrf-token` header), then follows `data-redirect` / a `redirect` in the response, surfaces server-side field errors, and fires `wire:success` / `wire:error` events. It exposes `window.__wireValidate.init(root)` and self-initializes on `DOMContentLoaded`.

## Usage

Define a schema and validate an API body:

```ts
import { v, parseBody } from "@wrnexus/validation";

export const signupSchema = v.object({
  email: v.string().required("Enter your email address").trim().email(),
  password: v.string().required("Enter your password").min(8).max(200),
  age: v.number().integer().min(13).max(120).optional(),
  role: v.string().oneOf(["user", "admin"]).default("user"),
  agree: v.boolean(),
});

// inside a route handler
const result = await parseBody(signupSchema, req);
if (!result.ok) return result.response; // ready 400 with field errors
const { email, password, role } = result.value;
```

Server-only refinement:

```ts
const schema = v.object({
  username: v
    .string()
    .min(3)
    .refine((name) => !RESERVED.has(String(name)), "That name is taken"),
});
```

Validate environment at startup:

```ts
import { v, parseEnv } from "@wrnexus/validation";

export const env = parseEnv(
  v.object({
    DATABASE_URL: v.string().min(1),
    PORT: v.number().integer().default(3000),
    DEBUG: v.boolean().optional(),
  }),
);
// throws one readable error listing every bad variable if misconfigured
```

Wire the same schema into the browser:

```ts
import { renderSchemasScript, VALIDATE_RUNTIME } from "@wrnexus/validation";
import { signupSchema } from "./app/schemas/signup.ts";

const head = `<script>${renderSchemasScript({ signup: signupSchema.describe() })}</script>
<script>${VALIDATE_RUNTIME}</script>`;
// render a <form data-schema="signup"> with [data-error="email"] etc.
```

## Requirements / Notes

- **Bun-only.** `parseEnv` reads `Bun.env` (falling back to `process.env`); `parseBody` and `invalid` use the Web `Request`/`Response` APIs that back `Bun.serve`.
- Refinements (`refine`) run only server-side and are never serialized — client and server agree on every other rule because both interpret the same `RuleDescriptor` list.
- No runtime dependencies. Ships as TypeScript source (`src/index.ts`) executed directly by Bun.
- Pairs with the WrNexus server (`@wrnexus/core`) for route handlers and the SSR layer that injects `renderSchemasScript` / `VALIDATE_RUNTIME`.

## Helper and component kit

The public helper API includes `parseOrThrow`, `ValidationError`, `validationResponse`, `firstValidationError`, `validationSummary`, and `schemaFieldNames`.

Schema output is inferred automatically by `ObjectSchema`, `parseOrThrow`, `parseBody`, `parseEnv`, and `asyncSchema`. Use `InferSchema<typeof schema>` when a named output type is useful:

```ts
const accountSchema = v.object({
  email: v.string().email(),
  attempts: v.number().integer(),
});

type AccountInput = InferSchema<typeof accountSchema>;
const account = parseOrThrow(accountSchema, input);
// account.email: string
// account.attempts: number
```

Enable `validationPlugin()` for:

- `<ValidationSummary />`
- `<FieldError />`

The summary block composes `Alert` from `@wrnexus/ui`, while `FieldError` remains a lightweight accessible field-level primitive.
Schemas can drive external contracts without maintaining a second definition:

```ts
import {
  localizeDescriptor,
  openApiRequestBody,
  parseDescriptor,
  toJsonSchema,
} from "@wrnexus/validation";

const jsonSchema = toJsonSchema(contactSchema, {
  id: "urn:example:contact",
  title: "Contact request",
});
const requestBody = openApiRequestBody(contactSchema);

const mr = localizeDescriptor(contactSchema, (key, params) =>
  translations.t(`validation.${key}`, params),
);
const result = parseDescriptor(mr, input);
```

JSON Schema output targets draft 2020-12, closes unknown object properties, and
maps lengths/ranges/formats/enums/patterns/integer rules. OpenAPI request bodies
reuse the same properties. Localized descriptors preserve explicit custom
messages and fill default required, type-coercion, and rule messages; the same
descriptor is consumable by server parsing and the eval-free browser runtime.

### Exported TypeScript declarations

```ts
export { ValidationPluginOptions, validationComponentsDir, default as validationPlugin } from './plugin.js';
import '@wrnexus/plugin';

/**
 * Client-side validation. `renderSchemasScript` bakes the discovered schema
 * descriptors into `window.__wireSchemas`; `VALIDATE_RUNTIME` is a generic,
 * eval-free validator that reads them and validates every `form[data-schema]`
 * on submit and blur, writing messages into `[data-error="<field>"]` elements.
 * The rule logic mirrors `checkField`/`applyRule` in index.ts.
 */

/** `window.__wireSchemas = { name: descriptor, ... }` for the client validator. */
declare function renderSchemasScript(descriptors: Record<string, SchemaDescriptor>): string;
declare const VALIDATE_RUNTIME: string;

interface AsyncValidationContext<T> {
    value: T;
    addIssue(field: keyof T | string, message: string): void;
    signal?: AbortSignal;
}
type AsyncRefinement<T> = (context: AsyncValidationContext<T>) => void | Promise<void>;
declare class AsyncObjectSchema<T extends object = Record<string, unknown>> {
    #private;
    readonly base: ObjectSchema<T>;
    constructor(base: ObjectSchema<T>);
    refine(refinement: AsyncRefinement<T>): this;
    describe(): SchemaDescriptor;
    parse(input: unknown, signal?: AbortSignal): Promise<ParseResult<T>>;
}
declare function asyncSchema<T extends object>(schema: ObjectSchema<T>): AsyncObjectSchema<T>;
declare function parseBodyAsync<T extends object>(schema: AsyncObjectSchema<T>, request: Request, signal?: AbortSignal): Promise<{
    ok: true;
    value: T;
} | {
    ok: false;
    response: Response;
}>;
interface OpenApiSchema {
    type: "object";
    properties: Record<string, Record<string, unknown>>;
    required?: string[];
}
declare function schemaToOpenApi(schema: ObjectSchema | AsyncObjectSchema): OpenApiSchema;
declare function mergeValidationResults<T>(...results: ParseResult<T>[]): ParseResult<T>;

declare class ValidationError<T = Record<string, unknown>> extends Error {
    readonly result: ParseResult<T>;
    constructor(result: ParseResult<T>);
}
declare function parseOrThrow<T extends object>(schema: ObjectSchema<T>, input: unknown): T;
declare function validationResponse(result: ParseResult, options?: {
    successStatus?: number;
    failureStatus?: number;
}): Response;
declare function firstValidationError(errors: Record<string, string>): string | null;
declare function validationSummary(errors: Record<string, string>): Array<{
    field: string;
    message: string;
}>;
declare function schemaFieldNames(schema: ObjectSchema | SchemaDescriptor): string[];

interface JsonSchemaDocument {
    $schema: "https://json-schema.org/draft/2020-12/schema";
    $id?: string;
    title?: string;
    type: "object";
    properties: Record<string, Record<string, unknown>>;
    required?: string[];
    additionalProperties: false;
}
declare function toJsonSchema(schema: ObjectSchema | SchemaDescriptor, options?: {
    id?: string;
    title?: string;
}): JsonSchemaDocument;
declare function openApiRequestBody(schema: ObjectSchema | SchemaDescriptor, options?: {
    description?: string;
    required?: boolean;
    contentTypes?: string[];
}): {
    required: boolean;
    content: {
        [k: string]: {
            schema: {
                $id?: string;
                title?: string;
                type: "object";
                properties: Record<string, Record<string, unknown>>;
                required?: string[];
                additionalProperties: false;
            };
        };
    };
    description?: string | undefined;
};
type ValidationMessageKey = "required" | "number" | `rule.${RuleDescriptor["kind"]}`;
type ValidationMessageTranslator = (key: ValidationMessageKey, params: Record<string, unknown>) => string;
declare function localizeDescriptor(schema: ObjectSchema | SchemaDescriptor, translate: ValidationMessageTranslator): SchemaDescriptor;
declare function parseDescriptor<T = Record<string, unknown>>(descriptor: SchemaDescriptor, source: Record<string, unknown>): ParseResult<T>;

type ContractKind = "api" | "action" | "webhook" | "realtime" | "queue" | "cron" | "pubsub" | "plugin" | "config" | "env";
interface ContractDefinition<T extends object = Record<string, unknown>> {
    kind: ContractKind;
    name: string;
    version: number;
    payload: ObjectSchema<T> | SchemaDescriptor;
    consumers?: string[];
    description?: string;
}
interface ContractRecord {
    kind: ContractKind;
    name: string;
    version: number;
    payload: SchemaDescriptor;
    consumers: string[];
    description?: string;
}
interface ContractSnapshot {
    format: 1;
    contracts: ContractRecord[];
}
interface ContractIssue {
    code: "WRN-CONTRACT-REMOVED" | "WRN-CONTRACT-FIELD-REMOVED" | "WRN-CONTRACT-FIELD-REQUIRED" | "WRN-CONTRACT-FIELD-TYPE" | "WRN-CONTRACT-RULE-TIGHTENED";
    contract: string;
    field?: string;
    message: string;
    consumers: string[];
}
declare function defineContract<T extends object>(definition: ContractDefinition<T>): ContractDefinition<T>;
declare function defineEvent<T extends object>(definition: Omit<ContractDefinition<T>, "kind"> & {
    kind?: "realtime" | "pubsub";
}): ContractDefinition<T>;
declare class ContractRegistry {
    private readonly records;
    register<T extends object>(definition: ContractDefinition<T>): this;
    snapshot(): ContractSnapshot;
}
declare function checkContractCompatibility(previous: ContractSnapshot, current: ContractSnapshot): ContractIssue[];

/**
 * @wrnexus/validation — one schema, validated on the server (API) and the browser
 * (forms). A schema is a fluent builder; `.parse()` runs server-side and returns
 * coerced values + field errors, while `.describe()` emits a JSON descriptor the
 * eval-free client validator interprets. Define schemas once in `app/schemas/`.
 */
type RuleDescriptor = {
    kind: "min";
    n: number;
    message?: string;
} | {
    kind: "max";
    n: number;
    message?: string;
} | {
    kind: "length";
    n: number;
    message?: string;
} | {
    kind: "email";
    message?: string;
} | {
    kind: "url";
    message?: string;
} | {
    kind: "uuid";
    message?: string;
} | {
    kind: "date";
    message?: string;
} | {
    kind: "oneOf";
    values: (string | number)[];
    message?: string;
} | {
    kind: "pattern";
    source: string;
    flags?: string;
    message?: string;
} | {
    kind: "integer";
    message?: string;
};
interface FieldDescriptor {
    type: "string" | "number" | "boolean" | "unknown";
    optional?: boolean;
    /** Message used when a required field is empty. Defaults to "Required". */
    requiredMessage?: string;
    /** Message used when coercion to the declared type fails. */
    typeMessage?: string;
    label?: string;
    /** Trim string input before validating. */
    trim?: boolean;
    rules: RuleDescriptor[];
}
interface SchemaDescriptor {
    type: "object";
    fields: Record<string, FieldDescriptor>;
}
interface ParseResult<T = Record<string, unknown>> {
    ok: boolean;
    /** Coerced values (present whether or not validation passed). */
    value: T;
    /** Field name → message, only for fields that failed. */
    errors: Record<string, string>;
}
/**
 * Apply one rule to an already-coerced value. Shared by the server; the client
 * runtime (runtime.ts) mirrors this exactly. Returns an error message or null.
 */
declare function applyRule(type: string, rule: RuleDescriptor, value: unknown): string | null;
/** Coerce + validate one field against its descriptor. */
declare function checkField(desc: FieldDescriptor, raw: unknown): {
    value: unknown;
    error: string | null;
};
/** A server-only refinement (a predicate that can't be serialized to the client). */
type Refinement = {
    fn: (value: unknown) => boolean | string;
    message?: string;
};
declare abstract class FieldSchema {
    abstract readonly type: "string" | "number" | "boolean" | "unknown";
    protected _optional: boolean;
    protected _requiredMessage?: string;
    protected _label?: string;
    protected _default?: unknown;
    protected rules: RuleDescriptor[];
    protected refinements: Refinement[];
    optional(): this;
    /** Require a non-empty value and optionally replace the default message. */
    required(message?: string): this;
    label(label: string): this;
    /** Value used when the field is absent (implies optional). */
    default(value: unknown): this;
    min(n: number, message?: string): this;
    max(n: number, message?: string): this;
    /**
     * Custom SERVER-side validation. `fn` returns true (ok), false (use `message`),
     * or a string (that error). Not mirrored to the client validator.
     */
    refine(fn: (value: unknown) => boolean | string, message?: string): this;
    getDefault(): unknown;
    runRefinements(value: unknown): string | null;
    describe(): FieldDescriptor;
}
declare class StringSchema<TValue extends string = string> extends FieldSchema {
    /** Type-only marker used to preserve literal unions through schema inference. */
    readonly __value: TValue;
    readonly type: "string";
    private _trim;
    email(message?: string): this;
    url(message?: string): this;
    uuid(message?: string): this;
    date(message?: string): this;
    length(n: number, message?: string): this;
    oneOf<const TValues extends readonly string[]>(values: TValues, message?: string): StringSchema<TValues extends readonly [string, ...string[]] ? TValues[number] : TValue>;
    trim(): this;
    pattern(re: RegExp, message?: string): this;
    describe(): FieldDescriptor;
}
declare class NumberSchema<TValue extends number = number> extends FieldSchema {
    /** Type-only marker used to preserve numeric literal unions through schema inference. */
    readonly __value: TValue;
    readonly type: "number";
    integer(message?: string): this;
    positive(message?: string): this;
    oneOf<const TValues extends readonly number[]>(values: TValues, message?: string): NumberSchema<TValues extends readonly [number, ...number[]] ? TValues[number] : TValue>;
}
declare class BooleanSchema extends FieldSchema {
    readonly type: "boolean";
}
declare class UnknownSchema extends FieldSchema {
    readonly type: "unknown";
}
type AnyFieldSchema = StringSchema<string> | NumberSchema<number> | BooleanSchema | UnknownSchema;
/** Infer the runtime value produced by a field schema. */
type InferFieldValue<TField extends FieldSchema> = TField extends StringSchema<infer TValue> ? TValue : TField extends NumberSchema<infer TValue> ? TValue : TField extends BooleanSchema ? boolean : unknown;
/** Infer the validated object produced by a field map. */
type InferObjectFields<TFields extends Record<string, FieldSchema>> = {
    [K in keyof TFields]: InferFieldValue<TFields[K]>;
};
/** Infer the object value produced by an object schema. */
type InferSchema<TSchema extends ObjectSchema> = TSchema extends ObjectSchema<infer TValue> ? TValue : never;
declare class ObjectSchema<TValue extends object = Record<string, unknown>> {
    private readonly fields;
    /** Type-only marker used by helper functions to infer validated output. */
    readonly __output: TValue;
    constructor(fields: Record<string, FieldSchema>);
    /** Return a defensive copy of the schema fields. */
    getFields(): Readonly<Record<string, FieldSchema>>;
    /** Create a new schema with fields added or replaced. The original is unchanged. */
    extend<TFields extends Record<string, FieldSchema>>(fields: TFields): ObjectSchema<Omit<TValue, keyof TFields> & InferObjectFields<TFields>>;
    /** Create a new schema containing fields from both schemas. */
    merge<TOther extends object>(schema: ObjectSchema<TOther>): ObjectSchema<TValue & TOther>;
    /** Validate an input object; returns coerced values + per-field errors. */
    parse(input: unknown): ParseResult<TValue>;
    describe(): SchemaDescriptor;
}
/** The fluent schema builder. */
declare const v: {
    string: () => StringSchema<string>;
    number: () => NumberSchema<number>;
    boolean: () => BooleanSchema;
    unknown: () => UnknownSchema;
    object: <TFields extends Record<string, FieldSchema>>(fields: TFields) => ObjectSchema<InferObjectFields<TFields>>;
};
/**
 * Validate environment variables against a schema at startup. Values are read
 * from `Bun.env` / `process.env` by default and coerced by the schema (so
 * `PORT` becomes a number, `DEBUG` a boolean). On any problem it throws ONE
 * readable error listing every offending variable, so misconfiguration fails
 * fast with an actionable message instead of surfacing deep inside the app.
 *
 *   export const env = parseEnv(v.object({
 *     DATABASE_URL: v.string().min(1),
 *     PORT: v.number(),
 *   }));
 */
declare function parseEnv<T extends object>(schema: ObjectSchema<T>, source?: Record<string, string | undefined>): T;
/** A 400 response carrying field errors, for API routes. */
declare function invalid(errors: Record<string, string>): Response;
/**
 * Parse a request's JSON body against a schema. On failure returns
 * `{ ok: false, response }` (a ready 400); on success `{ ok: true, value }`.
 */
declare function parseBody<T extends object>(schema: ObjectSchema<T>, req: Request): Promise<{
    ok: true;
    value: T;
} | {
    ok: false;
    response: Response;
}>;

export { type AnyFieldSchema, AsyncObjectSchema, type AsyncRefinement, type AsyncValidationContext, BooleanSchema, type ContractDefinition, type ContractIssue, type ContractKind, type ContractRecord, ContractRegistry, type ContractSnapshot, type FieldDescriptor, FieldSchema, type InferFieldValue, type InferObjectFields, type InferSchema, type JsonSchemaDocument, NumberSchema, ObjectSchema, type OpenApiSchema, type ParseResult, type RuleDescriptor, type SchemaDescriptor, StringSchema, UnknownSchema, VALIDATE_RUNTIME, ValidationError, type ValidationMessageKey, type ValidationMessageTranslator, applyRule, asyncSchema, checkContractCompatibility, checkField, defineContract, defineEvent, firstValidationError, invalid, localizeDescriptor, mergeValidationResults, openApiRequestBody, parseBody, parseBodyAsync, parseDescriptor, parseEnv, parseOrThrow, renderSchemasScript, schemaFieldNames, schemaToOpenApi, toJsonSchema, v, validationResponse, validationSummary };
```

# Complete @wrnexus/ui component reference and source contracts

## Accordion

Showcase: https://component.wrnexusjs.dev/
Mount: <Accordion /> (legacy: data-component="Accordion")
Category: base
Purpose: Theme-aware, responsive accordion component.
Props: size: string = "default", color: string = "primary", variant: string = "default", class: string = "", id: string = "accordion", items: unknown[] = [], defaultOpen: unknown[] = [], multiple: boolean = false, alwaysOpen: boolean = false, disabled: boolean = false, indicator: string = "plus", indicatorPosition: string = "start", showIndicator: boolean = true, bordered: boolean = false, separated: boolean = false, flush: boolean = false, contentItalic: boolean = false
Slots: none
Events: change, open, close

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component Accordion {
  outputs {
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
    size: string = "default"
    color: string = "primary"
    variant: string = "default"
    class: string = ""

    id: string = "accordion"
    items: unknown[] = []
    defaultOpen: unknown[] = []
    multiple: boolean = false
    alwaysOpen: boolean = false
    disabled: boolean = false

    indicator: string = "plus"
    indicatorPosition: string = "start"
    showIndicator: boolean = true
    bordered: boolean = false
    separated: boolean = false
    flush: boolean = false
    contentItalic: boolean = false

}

  state openValues = defaultOpen

  functions {
    shared function itemValue(item, index) {
      return item.value !== undefined && item.value !== ""
        ? String(item.value)
        : String(index)
    }

    shared function nestedValue(parent, parentIndex, item, index) {
      return itemValue(parent, parentIndex) + "." + itemValue(item, index)
    }

    shared function isOpen(value) {
      return openValues.includes(value)
    }

    shared function allowsMultiple() {
      return multiple || alwaysOpen
    }

    // Named rather than computed: an output is resolved as a property name, so
    // output[eventName] would not reach a parent binding.
    client function dispatchAccordionEvent(sourceEvent, eventName, value, item, payload) {
      payload = {
        component: "Accordion",
        value: value,
        item: item,
        open: isOpen(value),
        openValues: openValues
      }

      if (eventName === "open") {
        output.open(payload)
      } else if (eventName === "close") {
        output.close(payload)
      } else {
        output.change(payload)
      }
    }

    client function toggleItem(sourceEvent, value, item, wasOpen) {
      if (disabled || item.disabled) {
        return
      }

      wasOpen = isOpen(value)

      if (wasOpen) {
        openValues = openValues.filter((entry) => entry !== value)
      } else if (allowsMultiple()) {
        openValues = openValues.concat([value])
      } else {
        openValues = [value]
      }

      dispatchAccordionEvent(
        sourceEvent,
        wasOpen ? "close" : "open",
        value,
        item
      )
      dispatchAccordionEvent(sourceEvent, "change", value, item)
    }

  }

  view {
    <div
      {...attrs}
      id="{id}"
      data-wrn-accordion
      data-variant="{variant}"
      data-indicator="{indicator}"
      data-multiple="{allowsMultiple() ? 'true' : 'false'}"
      class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--accordion wire-next--accordion-{variant} {bordered ? 'wire-next--accordion-bordered' : ''} {separated ? 'wire-next--accordion-separated' : ''} {flush ? 'wire-next--accordion-flush' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
    >
      {#each items as item, index}
        <section
          class="wire-next__accordion-item"
          data-open="{isOpen(itemValue(item, index)) ? 'true' : 'false'}"
          data-disabled="{disabled || item.disabled ? 'true' : 'false'}"
        >
          <h3 class="wire-next__accordion-heading">
            <button
              type="button"
              id="{id}-trigger-{index}"
              aria-expanded="{isOpen(itemValue(item, index)) ? 'true' : 'false'}"
              aria-controls="{id}-panel-{index}"
              disabled="{disabled || item.disabled}"
              @click="toggleItem(event, itemValue(item, index), item)"
            >
              {#if showIndicator && indicatorPosition === "start"}
                <span class="wire-next__accordion-indicator" aria-hidden="true">
                  {#if indicator === "chevron"}
                    <span class="icon-[lucide--chevron-down]"></span>
                  {:else}
                    <span>+</span>
                  {/if}
                </span>
              {/if}
              <span>{item.label || item.title}</span>
              {#if showIndicator && indicatorPosition === "end"}
                <span class="wire-next__accordion-indicator" aria-hidden="true">
                  {#if indicator === "chevron"}
                    <span class="icon-[lucide--chevron-down]"></span>
                  {:else}
                    <span>+</span>
                  {/if}
                </span>
              {/if}
            </button>
          </h3>

          <div
            id="{id}-panel-{index}"
            class="wire-next__accordion-panel"
            role="region"
            aria-labelledby="{id}-trigger-{index}"
            aria-hidden="{isOpen(itemValue(item, index)) ? 'false' : 'true'}"
          >
            <div>
              <div class="wire-next__accordion-content {contentItalic ? 'wire-next__accordion-content-italic' : ''}">
                {#if item.content}<p>{item.content}</p>{/if}

                {#if item.children && item.children.length > 0}
                  <div class="wire-next__accordion-nested">
                    {#each item.children as child, childIndex}
                      <section
                        class="wire-next__accordion-item"
                        data-open="{isOpen(nestedValue(item, index, child, childIndex)) ? 'true' : 'false'}"
                      >
                        <h4 class="wire-next__accordion-heading">
                          <button
                            type="button"
                            id="{id}-trigger-{index}-{childIndex}"
                            aria-expanded="{isOpen(nestedValue(item, index, child, childIndex)) ? 'true' : 'false'}"
                            aria-controls="{id}-panel-{index}-{childIndex}"
                            disabled="{disabled || child.disabled}"
                            @click="toggleItem(event, nestedValue(item, index, child, childIndex), child)"
                          >
                            {#if showIndicator}
                              <span class="wire-next__accordion-indicator" aria-hidden="true">
                                {#if indicator === "chevron"}
                                  <span class="icon-[lucide--chevron-down]"></span>
                                {:else}
                                  <span>+</span>
                                {/if}
                              </span>
                            {/if}
                            <span>{child.label || child.title}</span>
                          </button>
                        </h4>
                        <div
                          id="{id}-panel-{index}-{childIndex}"
                          class="wire-next__accordion-panel"
                          role="region"
                          aria-labelledby="{id}-trigger-{index}-{childIndex}"
                          aria-hidden="{isOpen(nestedValue(item, index, child, childIndex)) ? 'false' : 'true'}"
                        >
                          <div>
                            <div class="wire-next__accordion-content {contentItalic ? 'wire-next__accordion-content-italic' : ''}">
                              <p>{child.content}</p>
                            </div>
                          </div>
                        </div>
                      </section>
                    {/each}
                  </div>
                {/if}
              </div>
            </div>
          </div>
        </section>
      {/each}
    </div>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--accordion {
      --wire-accordion-accent: var(--wire-component-color);
      display: grid;
      width: 100%;
      color: var(--wire-color-text);
    }

    .wire-next__accordion-item {
      min-width: 0;
    }

    .wire-next__accordion-heading {
      margin: 0;
      font: inherit;
    }

    .wire-next__accordion-heading > button {
      display: flex;
      width: 100%;
      align-items: center;
      gap: 0.75rem;
      padding: 0.85rem 0;
      border: 0;
      background: transparent;
      color: var(--wire-color-text);
      font: inherit;
      font-size: 0.88rem;
      font-weight: 700;
      text-align: left;
      cursor: pointer;
    }

    .wire-next__accordion-heading > button > span:not(.wire-next__accordion-indicator) {
      min-width: 0;
      flex: 1;
    }

    .wire-next__accordion-heading > button:focus-visible {
      border-radius: var(--wire-radius-sm);
      outline: 2px solid var(--wire-accordion-accent);
      outline-offset: 2px;
    }

    .wire-next__accordion-heading > button:disabled {
      cursor: not-allowed;
      opacity: 0.5;
    }

    .wire-next__accordion-indicator {
      display: inline-flex;
      width: 1rem;
      height: 1rem;
      flex: 0 0 1rem;
      align-items: center;
      justify-content: center;
      color: var(--wire-color-muted);
      font-size: 1rem;
      font-weight: 500;
      line-height: 1;
      transition:
        color var(--wire-motion-fast) var(--wire-ease-standard),
        transform var(--wire-motion-fast) var(--wire-ease-standard);
    }

    .wire-next__accordion-indicator > span {
      display: inline-flex;
      align-items: center;
      justify-content: center;
    }

    .wire-next__accordion-indicator > [class*="icon-["] {
      width: 0.95rem;
      height: 0.95rem;
    }

    .wire-next__accordion-item[data-open="true"]
      > .wire-next__accordion-heading
      .wire-next__accordion-indicator {
      color: var(--wire-accordion-accent);
      transform: rotate(45deg);
    }

    .wire-next--accordion[data-indicator="chevron"]
      .wire-next__accordion-item[data-open="true"]
      > .wire-next__accordion-heading
      .wire-next__accordion-indicator {
      transform: rotate(180deg);
    }

    .wire-next__accordion-item[data-open="true"] > .wire-next__accordion-heading > button {
      color: var(--wire-accordion-accent);
    }

    .wire-next__accordion-panel {
      display: grid;
      grid-template-rows: 0fr;
      opacity: 0;
      transition:
        grid-template-rows var(--wire-motion-base) var(--wire-ease-standard),
        opacity var(--wire-motion-fast) var(--wire-ease-standard);
    }

    .wire-next__accordion-item[data-open="true"] > .wire-next__accordion-panel {
      grid-template-rows: 1fr;
      opacity: 1;
    }

    .wire-next__accordion-panel > div {
      overflow: hidden;
    }

    .wire-next__accordion-content {
      padding: 0 0 1rem 1.75rem;
      color: var(--wire-color-muted);
      font-size: 0.86rem;
      line-height: 1.65;
    }

    .wire-next__accordion-content > p {
      color: inherit;
    }

    .wire-next__accordion-content-italic > p::first-line {
      font-style: italic;
    }

    .wire-next__accordion-nested {
      display: grid;
      margin-top: 0.5rem;
    }

    .wire-next__accordion-nested .wire-next__accordion-content {
      padding-left: 1.5rem;
    }

    .wire-next--accordion-bordered {
      overflow: hidden;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-md);
      background: var(--wire-color-surface);
    }

    .wire-next--accordion-bordered > .wire-next__accordion-item + .wire-next__accordion-item {
      border-top: 1px solid var(--wire-color-border);
    }

    .wire-next--accordion-bordered
      > .wire-next__accordion-item
      > .wire-next__accordion-heading
      > button {
      padding-inline: 1rem;
    }

    .wire-next--accordion-bordered
      > .wire-next__accordion-item
      > .wire-next__accordion-panel
      .wire-next__accordion-content {
      padding-inline: 1rem;
    }

    .wire-next--accordion-separated {
      gap: 0.5rem;
    }

    .wire-next--accordion-separated > .wire-next__accordion-item {
      overflow: hidden;
      border: 1px solid transparent;
      border-radius: var(--wire-radius-md);
      background: var(--wire-color-surface-2);
    }

    .wire-next--accordion-separated > .wire-next__accordion-item[data-open="true"] {
      border-color: var(--wire-color-border);
    }

    .wire-next--accordion-separated
      > .wire-next__accordion-item
      > .wire-next__accordion-heading
      > button,
    .wire-next--accordion-separated
      > .wire-next__accordion-item
      > .wire-next__accordion-panel
      .wire-next__accordion-content {
      padding-inline: 1rem;
    }

    .wire-next--accordion-flush {
      border-radius: 0;
      background: transparent;
      box-shadow: none;
    }

    @media (prefers-reduced-motion: reduce) {
    .wire-next__accordion-panel,
      .wire-next__accordion-indicator {
        transition: none;
      }
    }
  }
}
```

---

## AdvancedSelect

Showcase: https://component.wrnexusjs.dev/
Mount: <AdvancedSelect /> (legacy: data-component="AdvancedSelect")
Category: advanced-forms
Purpose: Theme-aware, responsive advanced select component.
Props: size: string = "default", color: string = "primary", label: string = "Advanced Select", name: string = "", value: string = "", values: unknown[] = [], options: unknown[] = [], groups: unknown[] = [], placeholder: string = "Select an option", placeholderIcon: string = "", searchPlaceholder: string = "Search options…", multiple: boolean = false, searchable: boolean = true, defaultOpen: boolean = false, clearable: boolean = true, allowEmpty: boolean = true, tags: boolean = false, disabled: boolean = false, required: boolean = false, invalid: boolean = false, validationMessage: string = "", helpText: string = "", loading: boolean = false, loadingLabel: string = "Loading options…", emptyLabel: string = "No options found", selectedOptionsLabel: string = "Selected options", clearLabel: string = "Clear selection", createLabel: string = "Create", loadMoreLabel: string = "Load more", searchMode: string = "contains", searchFields: string = "label,description", minSearchLength: number = 0, searchResultLimit: number = 0, maxSelections: number = 0, showCounter: boolean = false, counterTemplate: string = "{selected} selected", optionTemplate: string = "default", selectedTemplate: string = "default", closeOnSelect: boolean = true, scrollToSelected: boolean = true, fixed: boolean = false, placement: string = "bottom", remote: boolean = false, remoteUrl: string = "", remoteQueryParam: string = "q", remoteDebounce: number = 250, remoteAutoLoad: boolean = true, infinite: boolean = false, hasMore: boolean = false, page: number = 1, class: string = ""
Slots: none
Events: search, select, change, clear, open, close, load, error

### Complete .wrn source contract

```wrn
import SelectStyles from "../styles/SelectStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component AdvancedSelect {
  outputs {
    search(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    load(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
  }

  props {
    size: string = "default"
    color: string = "primary"
    label: string = "Advanced Select"
    name: string = ""
    value: string = ""
    values: unknown[] = []
    options: unknown[] = []
    groups: unknown[] = []
    placeholder: string = "Select an option"
    placeholderIcon: string = ""
    searchPlaceholder: string = "Search options…"
    multiple: boolean = false
    searchable: boolean = true
    defaultOpen: boolean = false
    clearable: boolean = true
    allowEmpty: boolean = true
    tags: boolean = false
    disabled: boolean = false
    required: boolean = false
    invalid: boolean = false
    validationMessage: string = ""
    helpText: string = ""
    loading: boolean = false
    loadingLabel: string = "Loading options…"
    emptyLabel: string = "No options found"
    selectedOptionsLabel: string = "Selected options"
    clearLabel: string = "Clear selection"
    createLabel: string = "Create"
    loadMoreLabel: string = "Load more"
    searchMode: string = "contains"
    searchFields: string = "label,description"
    minSearchLength: number = 0
    searchResultLimit: number = 0
    maxSelections: number = 0
    showCounter: boolean = false
    counterTemplate: string = "{selected} selected"
    optionTemplate: string = "default"
    selectedTemplate: string = "default"
    closeOnSelect: boolean = true
    scrollToSelected: boolean = true
    fixed: boolean = false
    placement: string = "bottom"
    remote: boolean = false
    remoteUrl: string = ""
    remoteQueryParam: string = "q"
    remoteDebounce: number = 250
    remoteAutoLoad: boolean = true
    infinite: boolean = false
    hasMore: boolean = false
    page: number = 1
    class: string = ""
  }

  state open = defaultOpen
  state query: string = ""
  state activeIndex: number = -1
  state selectedValue = value
  state selectedValues = values

  functions {
    shared function allOptions() {
      return [...groups.flatMap((group) => group.options || []), ...options]
    }

    shared function searchableText(option) {
      return ((option.label || "") + " " + (option.description || "")).toLowerCase()
    }

    shared function matches(option) {
      if (!query || query.length < Number(minSearchLength || 0)) {
        return true
      }
      if (searchMode === "startsWith") {
        return searchableText(option).startsWith(query.toLowerCase())
      }
      if (searchMode === "exact") {
        return searchableText(option) === query.toLowerCase()
      }
      return searchableText(option).includes(query.toLowerCase())
    }

    shared function matchingOptions(list) {
      return list.filter((option) => matches(option))
    }

    shared function visibleOptions(list) {
      if (searchResultLimit > 0) {
        return matchingOptions(list).slice(0, Number(searchResultLimit))
      }
      return matchingOptions(list)
    }

    shared function flatVisibleOptions() {
      return visibleOptions(allOptions())
    }

    shared function isSelected(option) {
      return (
        multiple
        ? selectedValues.includes(option.value)
        : selectedValue === option.value
      )
    }

    shared function selectedOptions() {
      return allOptions().filter((option) => isSelected(option))
    }

    shared function selectedCount() {
      return multiple ? selectedValues.length : selectedValue ? 1 : 0
    }

    shared function counterText() {
      return (
        maxSelections
        ? selectedCount() + " / " + maxSelections + " selected"
        : selectedCount() + " selected"
      )
    }

    shared function selectedText() {
      return (
        multiple
        ? selectedValues.join(", ")
        : selectedOptions().length
          ? selectedOptions()[0].label
          : ""
      )
    }

    shared function triggerText() {
      return selectedCount() ? selectedText() : placeholder
    }

    shared function canSelect(option) {
      if (option.disabled) {
        return false
      }
      if (!multiple || isSelected(option)) {
        return true
      }
      if (maxSelections <= 0) {
        return true
      }
      return selectedCount() < maxSelections
    }

    shared function chooseSingle(option) {
      if (!canSelect(option)) {
        return
      }
      selectedValue = option.value
      query = ""
      if (closeOnSelect) {
        open = false
      }
    }

    shared function chooseMultiple(option) {
      if (!canSelect(option)) {
        return
      }
      if (isSelected(option)) {
        selectedValues = selectedValues.filter((item) => item !== option.value)
      } else {
        selectedValues = selectedValues.concat([option.value])
      }
      query = ""
    }

    shared function chooseOption(option) {
      if (multiple) {
        chooseMultiple(option)
        return
      }
      chooseSingle(option)
    }

    client function clearSelection(event) {
      event.stopPropagation()
      selectedValue = ""
      selectedValues = []
      query = ""
      open = false
    }

    shared function toggle() {
      if (disabled) {
        return;
      };
      open = !open;
      activeIndex = open && flatVisibleOptions().length ? 0 : -1;
    }

    shared function moveActive(direction) {
      if (!flatVisibleOptions().length) {
        return
      }
      activeIndex = (activeIndex + direction + flatVisibleOptions().length) % flatVisibleOptions().length
    }

    client function handleKeydown(event) {
      if (disabled) {
        return
      }
      if (event.key === "ArrowDown") {
        event.preventDefault()
        if (!open) {
          open = true
        }
        moveActive(1)
      } else if (event.key === "ArrowUp") {
        event.preventDefault()
        if (!open) {
          open = true
        }
        moveActive(-1)
      } else if (event.key === "Enter" || event.key === " ") {
        event.preventDefault()
        if (!open) {
          open = true
        } else if (activeIndex >= 0) {
          chooseOption(flatVisibleOptions()[activeIndex])
        }
      } else if (event.key === "Escape") {
        open = false
      } else if (event.key === "Home" && open) {
        event.preventDefault()
        activeIndex = 0
      } else if (event.key === "End" && open) {
        event.preventDefault()
        activeIndex = flatVisibleOptions().length - 1
      }
    }

    shared function optionIndex(option) {
      return flatVisibleOptions().findIndex((item) => item.value === option.value)
    }
  }

  view {
    <div
      class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--advanced-select {open ? 'wire-next--open' : ''} {fixed ? 'wire-next--advanced-select-fixed' : ''} {invalid ? 'wire-next--invalid' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
      data-placement="{placement}"
      data-search-mode="{searchMode}"
      data-remote="{remote ? 'true' : 'false'}"
      data-remote-url="{remoteUrl}"
      data-remote-query-param="{remoteQueryParam}"
      data-remote-debounce="{remoteDebounce}"
      data-remote-auto-load="{remoteAutoLoad ? 'true' : 'false'}"
      data-infinite="{infinite ? 'true' : 'false'}"
      data-page="{page}"
      data-wrn-select
      @focusout="if (!event.currentTarget.contains(event.relatedTarget)) { open = false }"
    >
      <div class="wire-next__row">
        <label id="{name}-label" for="{name}-trigger">{label}</label>
        <small data-show="showCounter" data-text="counterText()">{counterText()}</small>
      </div>

      <div class="wire-next__select-control">
        <button
          {...attrs}
          id="{name}-trigger"
          type="button"
          class="wire-next__select-trigger"
          role="combobox"
          aria-haspopup="listbox"
          aria-expanded="{open}"
          aria-controls="{name}-listbox"
          aria-labelledby="{name}-label"
          aria-invalid="{invalid}"
          disabled="{disabled}"
          @click="toggle()"
          @keydown="handleKeydown(event)"
        >
          <span class="wire-next__select-value">
            <span data-show="!multiple" data-text="triggerText()">{triggerText()}</span>
            <span
              data-show="multiple && selectedTemplate === 'count' && selectedCount() > 0"
              data-text="selectedCount() + ' selected'"
            >{selectedCount()} selected</span>
            <span
              data-show="multiple && selectedTemplate === 'text' && selectedCount() > 0"
              data-text="selectedText()"
            >{selectedText()}</span>
            <span
              class="wire-next__select-tags"
              aria-label="{selectedOptionsLabel}"
              data-show="multiple && selectedTemplate !== 'count' && selectedTemplate !== 'text' && selectedCount() > 0"
            >
              {#each allOptions() as option}
                <span data-show="isSelected(option)">
                  {#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
                  {#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
                  {#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
                  {option.label}
                </span>
              {/each}
            </span>
            <span data-show="multiple && selectedCount() === 0">
              {#if placeholderIcon}<i class="{placeholderIcon}" aria-hidden="true"></i>{/if}
              <span class="wire-next__placeholder">{placeholder}</span>
            </span>
          </span>
          <span
            class="wire-next__select-chevron icon-[lucide--chevrons-up-down]"
            aria-hidden="true"
          ></span>
        </button>

        <button
          type="button"
          class="wire-next__clear-select"
          data-show="clearable && allowEmpty && selectedCount() > 0 && !disabled"
          aria-label="{clearLabel}"
          title="{clearLabel}"
          @click="clearSelection(event)"
        >
          <span class="icon-[lucide--x]" aria-hidden="true"></span>
        </button>
      </div>

      <div
        class="wire-next__select-dropdown {fixed ? 'wire-next__select-dropdown--fixed' : ''}"
        data-show="open"
        style="{open ? '' : 'display: none'}"
      >
          {#if searchable}
            <label class="wire-next__select-search">
              <span class="wire-visually-hidden">{searchPlaceholder}</span>
              <span class="wire-next__search-icon" aria-hidden="true">⌕</span>
              <input
                type="search"
                value="{query}"
                placeholder="{searchPlaceholder}"
                autocomplete="off"
                @input="query = event.target.value; activeIndex = flatVisibleOptions().length ? 0 : -1"
                @keydown="handleKeydown(event)"
              />
            </label>
          {/if}

          <div
            class="wire-next__select-message"
            data-show="remote && query.length !== 0 && query.length < minSearchLength"
          >Enter at least {minSearchLength} characters.</div>
          <div class="wire-next__select-message" data-show="loading" role="status">
            <i class="wire-spinner wire-spinner--inline" aria-hidden="true"></i>
            {loadingLabel}
          </div>
            <div
              id="{name}-listbox"
              class="wire-next__select-list"
              data-show="(!remote || (query.length === 0 && remoteAutoLoad) || query.length >= minSearchLength) && !loading"
              role="listbox"
              aria-multiselectable="{multiple}"
            >
              {#each groups as group}
                {#if visibleOptions(group.options || []).length}
                  <div class="wire-next__option-group" role="group" aria-label="{group.label}">
                    <div class="wire-next__group-label">{group.label}</div>
                    {#each group.options || [] as option}
                      <button
                        type="button"
                        class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
                        data-option-value="{option.value}"
                        data-show="matches(option)"
                        role="option"
                        aria-selected="{isSelected(option)}"
                        disabled="{!canSelect(option)}"
                        @mouseenter="activeIndex = optionIndex(option)"
                        @click="chooseOption(option)"
                      >
                        {#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
                        {#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
                        {#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
                        <span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
                        <i
                          class="wire-next__check icon-[lucide--check]"
                          data-show="isSelected(option)"
                          aria-hidden="true"
                        ></i>
                      </button>
                    {/each}
                  </div>
                {/if}
              {/each}

              {#each options as option}
                <button
                  type="button"
                  class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
                  data-option-value="{option.value}"
                  data-show="matches(option)"
                  role="option"
                  aria-selected="{isSelected(option)}"
                  disabled="{!canSelect(option)}"
                  @mouseenter="activeIndex = optionIndex(option)"
                  @click="chooseOption(option)"
                >
                  {#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
                  {#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
                  {#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
                  <span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
                  <i
                    class="wire-next__check icon-[lucide--check]"
                    data-show="isSelected(option)"
                    aria-hidden="true"
                  ></i>
                </button>
              {/each}

              {#if !loading && !flatVisibleOptions().length}
                <div class="wire-next__select-message">{emptyLabel}</div>
              {/if}
            </div>

          {#if tags && query && !allOptions().some((option) => option.label.toLowerCase() === query.toLowerCase())}
            <button type="button" class="wire-next__create-option">{createLabel} “{query}”</button>
          {/if}
          <button
            type="button"
            class="wire-next__load-more"
            data-wrn-select-load-more
            data-show="infinite && hasMore"
          >{loadMoreLabel}</button>
      </div>

      <input
        type="hidden"
        name="{name}"
        value="{multiple ? selectedValues.join(',') : selectedValue}"
        required="{required}"
        aria-describedby="{validationMessage ? `${name}-validation` : helpText ? `${name}-help` : ''}"
      />

      {#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
      <small
        id="{name}-validation"
        class="wire-next__validation"
        data-error="{name}"
      >{validationMessage}</small>
    </div>
    <SelectStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next__row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
    }

    .wire-next__select-search {
      position: relative;
      display: block;
    }

    .wire-next__select-search input {
      padding-left: 2.25em;
    }

    .wire-next__select-tags {
      display: flex;
      flex-wrap: wrap;
      gap: 0.4rem;
    }

    .wire-next__select-tags > span {
      display: inline-flex;
      align-items: center;
      gap: 0.35rem;
      padding: 0.3rem 0.55rem;
      border-radius: 999px;
      color: var(--wire-component-color);
      background: color-mix(in srgb, var(--wire-component-color) 12%, transparent);
      font-size: 0.75em;
      font-weight: 700;
    }

    .wire-next__select-tags img {
      width: 1.25rem;
      height: 1.25rem;
      border-radius: 50%;
      object-fit: cover;
    }

    .wire-next__select-value {
      display: flex;
      min-width: 0;
      align-items: center;
      gap: 0.55em;
    }

    .wire-next__placeholder {
      overflow: hidden;
      color: var(--wire-color-muted);
      text-overflow: ellipsis;
      white-space: nowrap;
    }
  }
}
```

---

## Alert

Showcase: https://component.wrnexusjs.dev/
Mount: <Alert /> (legacy: data-component="Alert")
Category: base
Purpose: Theme-aware, responsive alert component.
Props: size: string = "default", color: string = "info", variant: string = "soft", class: string = "", radius: string = "md", shadow: string = "sm", title: string = "Alert", description: string = "", items: unknown[] = [], actions: unknown[] = [], showIcon: boolean = false, icon: string = "", dismissible: boolean = false, dismissLabel: string = "Dismiss alert", role: string = "alert", live: string = "polite", linkLabel: string = "", linkHref: string = "", actionLabel: string = "", actionHref: string = "", compact: boolean = false
Slots: none
Events: dismiss, action

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component Alert {
  outputs {
    dismiss(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    action(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
    size: string = "default"
    color: string = "info"
    variant: string = "soft"
    class: string = ""
    radius: string = "md"
    shadow: string = "sm"

    title: string = "Alert"
    description: string = ""
    items: unknown[] = []
    actions: unknown[] = []

    showIcon: boolean = false
    icon: string = ""
    dismissible: boolean = false
    dismissLabel: string = "Dismiss alert"
    role: string = "alert"
    live: string = "polite"

    linkLabel: string = ""
    linkHref: string = ""
    actionLabel: string = ""
    actionHref: string = ""
    compact: boolean = false

  }

  state visible: boolean = true

  functions {
    // The output name has to be written out rather than computed: outputs are
    // resolved as named properties, so a dynamic key would not reach a parent
    // binding. Only two names exist here, so a branch is honest and cheap.
    client function dispatchAlertEvent(sourceEvent, eventName, action, payload) {
      payload = {
        component: "Alert",
        title: title,
        color: color,
        variant: variant,
        action: action
      }

      if (eventName === "dismiss") {
        output.dismiss(payload)
      } else {
        output.action(payload)
      }
    }

    client function dismissAlert(sourceEvent) {
      visible = false
      dispatchAlertEvent(sourceEvent, "dismiss", null)
    }

    client function selectAction(sourceEvent, action) {
      dispatchAlertEvent(sourceEvent, "action", action)
    }
  }

  view {
    <section
      {...attrs}
      data-wrn-alert
      data-color="{color}"
      data-variant="{variant}"
      data-radius="{radius}"
      data-shadow="{shadow}"
      data-show="visible"
      aria-hidden="{visible ? 'false' : 'true'}"
      role="{role}"
      aria-live="{live}"
      class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--alert wire-next--alert-{variant} {compact ? 'wire-next--alert-compact' : ''} {class}"
    >
      {#if showIcon}
        <span class="wire-next__alert-icon" aria-hidden="true">
          {#if icon}
            <span class="{icon}"></span>
          {:else if color === "success"}
            <span class="icon-[lucide--circle-check]"></span>
          {:else if color === "danger"}
            <span class="icon-[lucide--circle-x]"></span>
          {:else if color === "warning"}
            <span class="icon-[lucide--triangle-alert]"></span>
          {:else}
            <span class="icon-[lucide--info]"></span>
          {/if}
        </span>
      {/if}

      <div class="wire-next__alert-body">
        {#if title}<strong class="wire-next__alert-title">{title}</strong>{/if}
        {#if description}<p>{description}</p>{/if}

        {#if items.length > 0}
          <ul>
            {#each items as item}<li>{item.label || item}</li>{/each}
          </ul>
        {/if}

        {#if actions.length > 0 || actionLabel || linkLabel}
          <div class="wire-next__alert-actions">
            {#each actions as item}
              <a
                href="{item.href || '#'}"
                data-variant="{item.variant || 'link'}"
                @click="selectAction(event, item)"
              >{item.label}</a>
            {/each}
            {#if actionLabel}
              <a
                href="{actionHref || '#'}"
                data-variant="action"
                @click="selectAction(event, { label: actionLabel, href: actionHref })"
              >{actionLabel}</a>
            {/if}
            {#if linkLabel}
              <a
                href="{linkHref || '#'}"
                data-variant="link"
                @click="selectAction(event, { label: linkLabel, href: linkHref })"
              >{linkLabel}</a>
            {/if}
          </div>
        {/if}
      </div>

      {#if dismissible}
        <button
          type="button"
          class="wire-next__alert-dismiss"
          aria-label="{dismissLabel}"
          @click="dismissAlert(event)"
        >
          <span class="icon-[lucide--x]" aria-hidden="true"></span>
        </button>
      {/if}
    </section>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--alert-soft {
      border-color: color-mix(in srgb, var(--wire-alert-color) 45%, var(--wire-color-border));
      background: var(--wire-alert-soft);
    }
    .wire-next--alert-soft .wire-next__alert-title,
    .wire-next--alert-soft .wire-next__alert-icon,
    .wire-next--alert-soft .wire-next__alert-actions a {
      color: var(--wire-alert-color);
    }
    .wire-next--alert-solid {
      border-color: var(--wire-alert-color);
      background: var(--wire-alert-color);
      color: white;
    }
    .wire-next--alert.wire-next--alert-solid
      :is(
        .wire-next__alert-title,
        .wire-next__alert-body > p,
        .wire-next__alert-body li,
        .wire-next__alert-actions a,
        .wire-next__alert-icon
      ) {
      color: currentColor;
    }
    .wire-next--alert-solid[data-color="light"] {
      color: #18181b;
    }
    .wire-next--alert-solid[data-color="warning"] {
      color: #18181b;
    }
    .wire-next--alert-bordered {
      border-color: var(--wire-alert-color);
      background: var(--wire-color-surface);
    }
    .wire-next--alert-bordered .wire-next__alert-title,
    .wire-next--alert-bordered .wire-next__alert-icon,
    .wire-next--alert-bordered .wire-next__alert-actions a {
      color: var(--wire-alert-color);
    }
    .wire-next--alert-accent {
      border-color: color-mix(in srgb, var(--wire-alert-color) 35%, var(--wire-color-border));
      border-left: 4px solid var(--wire-alert-color);
      background: var(--wire-alert-soft);
    }
    .wire-next--alert-accent .wire-next__alert-title,
    .wire-next--alert-accent .wire-next__alert-icon,
    .wire-next--alert-accent .wire-next__alert-actions a {
      color: var(--wire-alert-color);
    }

    .wire-next--alert {
      --wire-alert-color: var(--wire-component-color);
      --wire-alert-soft: color-mix(in srgb, var(--wire-alert-color) 16%, var(--wire-color-surface));
      display: grid;
      grid-template-columns: auto minmax(0, 1fr) auto;
      gap: 0.8rem;
      width: 100%;
      padding: 1rem;
      border: 1px solid transparent;
      border-radius: var(--wire-radius, 0.75rem);
      color: var(--wire-color-text);
    }

    .wire-next--alert[data-radius="none"] {
      border-radius: 0;
    }

    .wire-next--alert[data-radius="sm"] {
      border-radius: var(--wire-radius-sm, 0.375rem);
    }

    .wire-next--alert[data-radius="lg"] {
      border-radius: calc(var(--wire-radius, 0.75rem) * 1.35);
    }

    .wire-next--alert[data-radius="xl"] {
      border-radius: calc(var(--wire-radius, 0.75rem) * 1.75);
    }

    .wire-next--alert[data-shadow="none"] {
      box-shadow: none;
    }

    .wire-next--alert[data-shadow="sm"] {
      box-shadow: 0 6px 16px color-mix(in srgb, #000 14%, transparent);
    }

    .wire-next--alert[data-shadow="md"] {
      box-shadow:
        0 2px 6px color-mix(in srgb, #000 10%, transparent),
        0 12px 28px color-mix(in srgb, #000 18%, transparent);
    }

    .wire-next--alert[data-shadow="lg"] {
      box-shadow:
        0 4px 10px color-mix(in srgb, #000 12%, transparent),
        0 20px 48px color-mix(in srgb, #000 24%, transparent);
    }

    .wire-next--alert[data-color="secondary"] {
      --wire-alert-color: #737373;
    }

    .wire-next--alert[data-color="success"] {
      --wire-alert-color: #0f9f8f;
    }

    .wire-next--alert[data-color="danger"] {
      --wire-alert-color: #dc3545;
    }

    .wire-next--alert[data-color="warning"] {
      --wire-alert-color: #eab308;
    }

    .wire-next--alert[data-color="info"] {
      --wire-alert-color: #2563eb;
    }

    .wire-next--alert[data-color="dark"] {
      --wire-alert-color: #27272a;
    }

    .wire-next--alert[data-color="light"] {
      --wire-alert-color: #f4f4f5;
    }

    .wire-next--alert-compact {
      padding: 0.75rem 0.9rem;
    }

    .wire-next--alert-compact:not(:has(.wire-next__alert-icon)):not(:has(.wire-next__alert-dismiss)) {
      grid-template-columns: minmax(0, 1fr);
    }

    .wire-next--alert-compact .wire-next__alert-body {
      display: block;
    }

    .wire-next--alert-compact .wire-next__alert-title {
      margin-right: 0.25rem;
    }

    .wire-next--alert-compact .wire-next__alert-body > p {
      display: inline;
    }

    .wire-next__alert-icon {
      display: inline-grid;
      width: 1.25rem;
      height: 1.25rem;
      place-items: center;
      margin-top: 0.05rem;
      color: var(--wire-alert-color);
    }

    .wire-next__alert-icon > span {
      width: 1rem;
      height: 1rem;
    }

    .wire-next__alert-body {
      display: grid;
      min-width: 0;
      gap: 0.35rem;
    }

    .wire-next__alert-title {
      font-size: 0.88rem;
      line-height: 1.45;
    }

    .wire-next__alert-body > p,
    .wire-next__alert-body li {
      color: inherit;
      font-size: 0.8rem;
      line-height: 1.55;
    }

    .wire-next__alert-body > ul {
      display: grid;
      gap: 0.25rem;
      margin: 0.15rem 0 0;
      padding-left: 1.25rem;
    }

    .wire-next__alert-actions {
      display: flex;
      flex-wrap: wrap;
      gap: 0.5rem 1rem;
      margin-top: 0.35rem;
    }

    .wire-next__alert-actions a {
      color: var(--wire-alert-color);
      font-size: 0.78rem;
      font-weight: 700;
      text-decoration: none;
    }

    .wire-next__alert-actions a:hover {
      text-decoration: underline;
    }

    .wire-next__alert-actions a[data-variant="action"] {
      padding: 0.35rem 0.6rem;
      border-radius: var(--wire-radius-sm);
      background: var(--wire-alert-color);
      color: white;
      text-decoration: none;
    }

    .wire-next__alert-dismiss {
      display: inline-grid;
      width: 1.75rem;
      height: 1.75rem;
      place-items: center;
      padding: 0;
      border: 0;
      border-radius: var(--wire-radius-sm);
      background: transparent;
      color: currentColor;
      cursor: pointer;
    }

    .wire-next__alert-dismiss:hover {
      background: color-mix(in srgb, currentColor 10%, transparent);
    }

    .wire-next__alert-dismiss:focus-visible {
      outline: 2px solid currentColor;
      outline-offset: 2px;
    }

    @media (max-width: 36rem) {
    .wire-next--alert {
        grid-template-columns: auto minmax(0, 1fr);
      }
    .wire-next__alert-dismiss {
        grid-column: 2;
        grid-row: 1;
        justify-self: end;
      }
    }
  }
}
```

---

## AnnouncementBar

Showcase: https://component.wrnexusjs.dev/
Mount: <AnnouncementBar /> (legacy: data-component="AnnouncementBar")
Category: marketing
Purpose: Publish a responsive notice with badge, icon, supporting copy, action, dismiss behavior, and width controls.
Props: badge: string = "", badgeIcon: string = "", message: string = "Announcement", description: string = "", icon: string = "icon-[lucide--megaphone]", actionLabel: string = "", actionHref: string = "", actionIcon: string = "", dismissible: boolean = false, dismissLabel: string = "Dismiss announcement", sticky: boolean = false, compact: boolean = false, size: string = "default", width: string = "default", color: string = "primary", variant: string = "soft", role: string = "status", live: string = "polite", class: string = ""
Slots: none
Events: dismiss

### Complete .wrn source contract

```wrn
component AnnouncementBar {
  outputs {
    dismiss(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
badge: string = ""
    badgeIcon: string = ""
    message: string = "Announcement"
    description: string = ""
    icon: string = "icon-[lucide--megaphone]"
    actionLabel: string = ""
    actionHref: string = ""
    actionIcon: string = ""
    dismissible: boolean = false
    dismissLabel: string = "Dismiss announcement"
    sticky: boolean = false
    compact: boolean = false
    size: string = "default"
    width: string = "default"
    color: string = "primary"
    variant: string = "soft"
    role: string = "status"
    live: string = "polite"
    class: string = ""
  }

  state dismissed: boolean = false

  view {
    <aside
      {...attrs}
      data-ui-component="AnnouncementBar"
      data-size='{size}'
      data-width='{width}'
      data-color='{color}'
      data-variant='{variant}'
      data-compact='{compact}'
      data-sticky='{sticky}'
      data-show='{!dismissed}'
      aria-hidden='{dismissed ? "true" : "false"}'
      role='{role}'
      aria-live='{live}'
      class='wire-announcement {class}'
    >
      <div class="wire-announcement__surface">
        <span
          class="wire-announcement__accent"
          aria-hidden="true"
        ></span>

        <span
          class="wire-announcement__glow wire-announcement__glow--one"
          aria-hidden="true"
        ></span>

        <span
          class="wire-announcement__glow wire-announcement__glow--two"
          aria-hidden="true"
        ></span>

        <div class="wire-announcement__inner">
          <div class="wire-announcement__content">
            {#if icon}
              <span class="wire-announcement__icon">
                <span
                  class='{icon}'
                  aria-hidden="true"
                ></span>
              </span>
            {/if}

            <div class="wire-announcement__copy">
              <div class="wire-announcement__headline">
                {#if badge}
                  <span class="wire-announcement__badge">
                    {#if badgeIcon}
                      <span
                        class='{badgeIcon}'
                        aria-hidden="true"
                      ></span>
                    {/if}

                    <span>{badge}</span>
                  </span>
                {/if}

                <p class="wire-announcement__message">
                  {message}
                </p>
              </div>

              {#if description && !compact}
                <p class="wire-announcement__description">
                  {description}
                </p>
              {/if}
            </div>
          </div>

          {#if actionLabel || dismissible}
            <div class="wire-announcement__actions">
              {#if actionLabel}
                <a
                  href='{actionHref || "#"}'
                  class="wire-announcement__action"
                >
                  <span>{actionLabel}</span>

                  {#if actionIcon}
                    <span
                      class='{actionIcon}'
                      aria-hidden="true"
                    ></span>
                  {:else}
                    <span
                      class="icon-[lucide--arrow-right]"
                      aria-hidden="true"
                    ></span>
                  {/if}
                </a>
              {/if}

              {#if dismissible}
                <button
                  type="button"
                  aria-label='{dismissLabel}'
                  title='{dismissLabel}'
                  class="wire-announcement__dismiss"
                  @click='dismissed = true; output.dismiss({ message: message })'
                >
                  <span
                    class="icon-[lucide--x]"
                    aria-hidden="true"
                  ></span>
                </button>
              {/if}
            </div>
          {/if}
        </div>
      </div>
    </aside>
  }

  style {
    .wire-announcement {
      --announcement-accent: var(--wire-color-primary);
      --announcement-contrast: var(--wire-color-primary-contrast, #ffffff);
      --announcement-soft: var(--wire-color-primary-soft);
      --announcement-muted: var(--wire-color-primary-muted);

      position: relative;
      z-index: 40;
      display: block;
      width: 100vw;
      max-width: 100vw;
      margin-inline: calc(50% - 50vw);
      color: var(--wire-color-text);
    }

    .wire-announcement__surface {
      width: min(calc(100% - 2rem), 72rem);
      margin-inline: auto;
    }

    .wire-announcement[data-width="wide"] .wire-announcement__surface {
      width: min(calc(100% - 2rem), 90rem);
    }

    .wire-announcement[data-width="full"] .wire-announcement__surface {
      width: 100%;
      max-width: none;
      margin-inline: 0;
      border-inline: 0;
      border-radius: 0;
    }

    .wire-announcement[data-sticky="true"] {
      position: sticky;
      top: 0;
    }

    .wire-announcement[data-color="secondary"] {
      --announcement-accent: var(--wire-color-secondary);
      --announcement-contrast: var(--wire-color-secondary-contrast, #ffffff);
      --announcement-soft: var(--wire-color-secondary-soft);
      --announcement-muted: var(--wire-color-secondary-muted);
    }

    .wire-announcement[data-color="info"] {
      --announcement-accent: var(--wire-color-info);
      --announcement-contrast: var(--wire-color-info-contrast, #ffffff);
      --announcement-soft: var(--wire-color-info-soft);
      --announcement-muted: var(--wire-color-info-muted);
    }

    .wire-announcement[data-color="success"] {
      --announcement-accent: var(--wire-color-success);
      --announcement-contrast: var(--wire-color-success-contrast, #ffffff);
      --announcement-soft: var(--wire-color-success-soft);
      --announcement-muted: var(--wire-color-success-muted);
    }

    .wire-announcement[data-color="warning"] {
      --announcement-accent: var(--wire-color-warning);
      --announcement-contrast: var(--wire-color-warning-contrast, #111827);
      --announcement-soft: var(--wire-color-warning-soft);
      --announcement-muted: var(--wire-color-warning-muted);
    }

    .wire-announcement[data-color="danger"] {
      --announcement-accent: var(--wire-color-danger);
      --announcement-contrast: var(--wire-color-danger-contrast, #ffffff);
      --announcement-soft: var(--wire-color-danger-soft);
      --announcement-muted: var(--wire-color-danger-muted);
    }

    .wire-announcement__surface {
      position: relative;
      isolation: isolate;
      overflow: hidden;
      background: var(--wire-color-surface-raised);
      border: 1px solid var(--wire-color-border);
      border-radius: 1.25rem;
      box-shadow:
        0 1px 2px rgb(0 0 0 / 0.04),
        0 12px 32px rgb(0 0 0 / 0.08);
    }

    .wire-announcement[data-variant="soft"] .wire-announcement__surface {
      background:
        linear-gradient(
          135deg,
          color-mix(in srgb, var(--announcement-soft) 86%, var(--wire-color-surface-raised)),
          var(--wire-color-surface-raised)
        );
      border-color: color-mix(in srgb, var(--announcement-accent) 28%, var(--wire-color-border));
    }

    .wire-announcement[data-variant="outline"] .wire-announcement__surface {
      background: transparent;
      border-color: color-mix(in srgb, var(--announcement-accent) 46%, var(--wire-color-border));
      box-shadow: none;
    }

    .wire-announcement[data-variant="minimal"] .wire-announcement__surface {
      background: transparent;
      border-color: transparent;
      border-radius: 0;
      box-shadow: none;
    }

    .wire-announcement[data-variant="solid"] {
      color: var(--announcement-contrast);
    }

    .wire-announcement[data-variant="solid"] .wire-announcement__surface {
      background:
        radial-gradient(
          circle at 8% 15%,
          color-mix(in srgb, var(--announcement-contrast) 14%, transparent),
          transparent 30%
        ),
        linear-gradient(
          135deg,
          color-mix(in srgb, var(--announcement-accent) 96%, white 4%),
          color-mix(in srgb, var(--announcement-accent) 76%, black 24%)
        );
      border-color: color-mix(in srgb, var(--announcement-contrast) 28%, transparent);
      box-shadow:
        0 1px 0 color-mix(in srgb, var(--announcement-contrast) 20%, transparent) inset,
        0 18px 46px color-mix(in srgb, var(--announcement-accent) 30%, transparent);
    }

    .wire-announcement__accent {
      position: absolute;
      z-index: -1;
      inset-block: 0;
      inset-inline-start: 0;
      width: 0.25rem;
      background: var(--announcement-accent);
    }

    .wire-announcement[data-variant="solid"] .wire-announcement__accent,
    .wire-announcement[data-variant="minimal"] .wire-announcement__accent {
      display: none;
    }

    .wire-announcement__glow {
      position: absolute;
      z-index: -1;
      display: block;
      border-radius: 999px;
      pointer-events: none;
      filter: blur(3rem);
      opacity: 0.28;
    }

    .wire-announcement__glow--one {
      width: 16rem;
      height: 16rem;
      inset-block-start: -10rem;
      inset-inline-end: -4rem;
      background: var(--announcement-muted);
    }

    .wire-announcement__glow--two {
      width: 10rem;
      height: 10rem;
      inset-block-end: -7rem;
      inset-inline-start: 28%;
      background: var(--announcement-soft);
      opacity: 0.18;
    }

    .wire-announcement[data-variant="solid"] .wire-announcement__glow {
      background: var(--announcement-contrast);
      opacity: 0.1;
    }

    .wire-announcement[data-variant="minimal"] .wire-announcement__glow {
      display: none;
    }

    .wire-announcement__inner {
      position: relative;
      display: flex;
      flex-direction: column;
      gap: 1rem;
      width: 100%;
      padding: 1rem 1.125rem;
    }

    .wire-announcement[data-size="sm"] .wire-announcement__inner,
    .wire-announcement[data-compact="true"] .wire-announcement__inner {
      padding-block: 0.75rem;
    }

    .wire-announcement[data-size="lg"] .wire-announcement__inner {
      padding: 1.25rem 1.5rem;
    }

    .wire-announcement__content {
      display: flex;
      align-items: flex-start;
      gap: 0.875rem;
      min-width: 0;
    }

    .wire-announcement__icon {
      display: inline-flex;
      flex: 0 0 auto;
      align-items: center;
      justify-content: center;
      width: 2.75rem;
      height: 2.75rem;
      color: var(--announcement-accent);
      background: var(--announcement-soft);
      border: 1px solid color-mix(in srgb, var(--announcement-accent) 20%, transparent);
      border-radius: 0.875rem;
      box-shadow: 0 8px 20px color-mix(in srgb, var(--announcement-accent) 12%, transparent);
    }

    .wire-announcement__icon > span {
      width: 1.2rem;
      height: 1.2rem;
    }

    .wire-announcement[data-size="sm"] .wire-announcement__icon,
    .wire-announcement[data-compact="true"] .wire-announcement__icon {
      width: 2.35rem;
      height: 2.35rem;
      border-radius: 0.75rem;
    }

    .wire-announcement[data-size="lg"] .wire-announcement__icon {
      width: 3.15rem;
      height: 3.15rem;
    }

    .wire-announcement[data-variant="solid"] .wire-announcement__icon {
      color: var(--announcement-contrast);
      background: color-mix(in srgb, var(--announcement-contrast) 12%, transparent);
      border-color: color-mix(in srgb, var(--announcement-contrast) 22%, transparent);
      box-shadow: none;
    }

    .wire-announcement__copy {
      flex: 1 1 auto;
      min-width: 0;
      padding-top: 0.1rem;
    }

    .wire-announcement__headline {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 0.55rem 0.8rem;
    }

    .wire-announcement__badge {
      display: inline-flex;
      flex: 0 0 auto;
      align-items: center;
      gap: 0.35rem;
      min-height: 1.6rem;
      padding: 0.25rem 0.65rem;
      color: var(--announcement-accent);
      font-size: 0.6875rem;
      font-weight: 700;
      line-height: 1;
      white-space: nowrap;
      background: var(--announcement-soft);
      border: 1px solid color-mix(in srgb, var(--announcement-accent) 22%, transparent);
      border-radius: 999px;
    }

    .wire-announcement__badge > span:first-child:not(:last-child) {
      width: 0.8rem;
      height: 0.8rem;
    }

    .wire-announcement[data-variant="solid"] .wire-announcement__badge {
      color: var(--announcement-contrast);
      background: color-mix(in srgb, var(--announcement-contrast) 13%, transparent);
      border-color: color-mix(in srgb, var(--announcement-contrast) 24%, transparent);
    }

    .wire-announcement__message {
      margin: 0;
      color: var(--wire-color-text);
      font-size: 0.9375rem;
      font-weight: 650;
      line-height: 1.5;
    }

    .wire-announcement[data-size="sm"] .wire-announcement__message,
    .wire-announcement[data-compact="true"] .wire-announcement__message {
      font-size: 0.875rem;
    }

    .wire-announcement[data-size="lg"] .wire-announcement__message {
      font-size: 1rem;
    }

    .wire-announcement[data-variant="solid"] .wire-announcement__message {
      color: var(--announcement-contrast);
    }

    .wire-announcement__description {
      max-width: 52rem;
      margin: 0.28rem 0 0;
      color: var(--wire-color-text-muted);
      font-size: 0.8125rem;
      line-height: 1.55;
    }

    .wire-announcement[data-size="lg"] .wire-announcement__description {
      font-size: 0.875rem;
    }

    .wire-announcement[data-variant="solid"] .wire-announcement__description {
      color: color-mix(in srgb, var(--announcement-contrast) 76%, transparent);
    }

    .wire-announcement__actions {
      display: flex;
      flex: 0 0 auto;
      align-items: center;
      gap: 0.55rem;
      padding-inline-start: 3.625rem;
    }

    .wire-announcement__action,
    .wire-announcement__dismiss {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-height: 2.5rem;
      color: var(--wire-color-text);
      font-size: 0.8125rem;
      font-weight: 650;
      text-decoration: none;
      background: var(--wire-color-surface-raised);
      border: 1px solid var(--wire-color-border);
      border-radius: 0.75rem;
      box-shadow: 0 5px 14px rgb(0 0 0 / 0.06);
      transition:
        transform 160ms ease,
        border-color 160ms ease,
        background-color 160ms ease,
        box-shadow 160ms ease;
    }

    .wire-announcement__action {
      gap: 0.45rem;
      padding: 0.55rem 0.9rem;
    }

    .wire-announcement__action > span:last-child {
      width: 0.95rem;
      height: 0.95rem;
      transition: transform 160ms ease;
    }

    .wire-announcement__dismiss {
      width: 2.5rem;
      padding: 0;
      cursor: pointer;
    }

    .wire-announcement__dismiss > span {
      width: 1rem;
      height: 1rem;
    }

    .wire-announcement__action:hover,
    .wire-announcement__dismiss:hover {
      transform: translateY(-1px);
      border-color: color-mix(in srgb, var(--announcement-accent) 42%, var(--wire-color-border));
      box-shadow: 0 8px 20px rgb(0 0 0 / 0.1);
    }

    .wire-announcement__action:hover > span:last-child {
      transform: translateX(0.16rem);
    }

    .wire-announcement__action:focus-visible,
    .wire-announcement__dismiss:focus-visible {
      outline: 2px solid var(--wire-color-focus, var(--announcement-accent));
      outline-offset: 3px;
    }

    .wire-announcement[data-variant="solid"] .wire-announcement__action {
      color: var(--announcement-accent);
      background: var(--announcement-contrast);
      border-color: transparent;
      box-shadow: 0 8px 22px color-mix(in srgb, black 18%, transparent);
    }

    .wire-announcement[data-variant="solid"] .wire-announcement__dismiss {
      color: var(--announcement-contrast);
      background: color-mix(in srgb, var(--announcement-contrast) 13%, transparent);
      border-color: color-mix(in srgb, var(--announcement-contrast) 24%, transparent);
      box-shadow: none;
    }

    .wire-announcement[data-variant="solid"] .wire-announcement__action:hover {
      background: color-mix(in srgb, var(--announcement-contrast) 92%, transparent);
    }

    .wire-announcement[data-variant="solid"] .wire-announcement__dismiss:hover {
      background: color-mix(in srgb, var(--announcement-contrast) 20%, transparent);
      border-color: color-mix(in srgb, var(--announcement-contrast) 34%, transparent);
    }

    @media (min-width: 768px) {
      .wire-announcement__inner {
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
        gap: 1.5rem;
      }

      .wire-announcement__content {
        flex: 1 1 auto;
      }

      .wire-announcement__actions {
        justify-content: flex-end;
        padding-inline-start: 0;
      }
    }

    @media (max-width: 639px) {
      .wire-announcement__surface,
      .wire-announcement[data-width="wide"] .wire-announcement__surface {
        width: calc(100% - 1rem);
      }

      .wire-announcement[data-width="full"] .wire-announcement__surface {
        width: 100%;
        border-radius: 0;
      }

      .wire-announcement__surface {
        border-radius: 1rem;
      }

      .wire-announcement__inner {
        padding-inline: 0.9rem;
      }

      .wire-announcement__actions {
        width: 100%;
        padding-inline-start: 3.625rem;
      }

      .wire-announcement__action {
        flex: 1 1 auto;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .wire-announcement__action,
      .wire-announcement__dismiss,
      .wire-announcement__action > span:last-child {
        transition: none;
      }

      .wire-announcement__action:hover,
      .wire-announcement__dismiss:hover,
      .wire-announcement__action:hover > span:last-child {
        transform: none;
      }
    }
  }
}
```

---

## AuthForm

Showcase: https://component.wrnexusjs.dev/
Mount: <AuthForm /> (legacy: data-component="AuthForm")
Category: core
Purpose: Reusable auth form component.
Props: size: string = "default", color: string = "primary", mode: string = "sign-in", action: string = "/api/auth/login", method: string = "post", title: string = "Sign in", description: string = "", returnTo: string = "", schema: string = "", showRemember: boolean = true, showName: boolean = true, submitLabel: string = "Continue", class: string = ""
Slots: default
Events: submit, change, input, focus, blur

### Complete .wrn source contract

```wrn
import Button from "./button.wrn"
import Checkbox from "./Checkbox.wrn"
import Input from "./Input.wrn"
import PinInput from "./PinInput.wrn"

component AuthForm {
  outputs {
    submit(payload: { event: Event; mode: string; action: string })
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
    size: string = "default"
    color: string = "primary"
    mode: string = "sign-in"
    action: string = "/api/auth/login"
    method: string = "post"
    title: string = "Sign in"
    description: string = ""
    returnTo: string = ""
    schema: string = ""
    showRemember: boolean = true
    showName: boolean = true
    submitLabel: string = "Continue"
    class: string = ""
  }

  functions {
    shared function schemaName() {
      if (schema) return schema
      if (mode === "sign-in") return "auth-login"
      if (mode === "register") return "auth-register"
      if (mode === "recover") return "auth-password-request"
      if (mode === "reset") return "auth-password-reset"
      if (mode === "mfa") return "auth-mfa"
      return "auth-empty"
    }

    client function submitForm(event) {
      output.submit({ event: event, mode: mode, action: action })
    }
    client function fieldEvent(type, payload) {
      const detail = payload || {}
      output[type]({ sourceEvent: detail.sourceEvent, name: detail.name || "", value: detail.value || "" })
    }
  }

  view {
    <section class="wire-auth-form wire-component--color-{color} wire-component--size-{size} {class}">
      <header>
        <span class="wire-auth-form__icon icon-[lucide--fingerprint]" aria-hidden="true"></span>
        <div><h2>{title}</h2>{#if description}<p>{description}</p>{/if}</div>
      </header>
      <form
        action="{action}"
        method="{method}"
        data-schema="{schemaName()}"
        data-wrnexus-runtime="auth"
        novalidate="true"
        @submit="submitForm(event)"
      >
        {#if returnTo}<input type="hidden" name="returnTo" value="{returnTo}" />{/if}
        {#if mode === "register" && showName}
          <Input label="Full name" name="displayName" autocomplete="name" placeholder="Enter your full name" required="true" icon="icon-[lucide--user-round]" iconPosition="start" @change="fieldEvent('change', payload)" @input="fieldEvent('input', payload)" @focus="fieldEvent('focus', payload)" @blur="fieldEvent('blur', payload)" />
        {/if}
        {#if mode === "sign-in" || mode === "register" || mode === "recover"}
          <Input label="{mode === 'recover' ? 'Registered email or mobile' : 'Email, mobile, or username'}" name="{mode === 'recover' ? 'identifier' : mode === 'register' ? 'email' : 'identifier'}" type="{mode === 'register' ? 'email' : 'text'}" autocomplete="{mode === 'register' ? 'email' : 'username'}" placeholder="{mode === 'recover' ? 'Enter your registered identity' : 'Enter your identity'}" required="true" icon="icon-[lucide--at-sign]" iconPosition="start" @change="fieldEvent('change', payload)" @input="fieldEvent('input', payload)" @focus="fieldEvent('focus', payload)" @blur="fieldEvent('blur', payload)" />
        {/if}
        {#if mode === "sign-in" || mode === "register" || mode === "reset"}
          <Input label="{mode === 'reset' ? 'New password' : 'Password'}" name="password" type="password" autocomplete="{mode === 'sign-in' ? 'current-password' : 'new-password'}" placeholder="Enter your password" required="true" icon="icon-[lucide--lock-keyhole]" iconPosition="start" @change="fieldEvent('change', payload)" @input="fieldEvent('input', payload)" @focus="fieldEvent('focus', payload)" @blur="fieldEvent('blur', payload)" />
        {/if}
        {#if mode === "register" || mode === "reset"}
          <Input label="Confirm password" name="confirmPassword" type="password" autocomplete="new-password" placeholder="Enter the password again" required="true" icon="icon-[lucide--shield-check]" iconPosition="start" @change="fieldEvent('change', payload)" @input="fieldEvent('input', payload)" @focus="fieldEvent('focus', payload)" @blur="fieldEvent('blur', payload)" />
        {/if}
        {#if mode === "mfa"}
          <PinInput label="Verification code" name="code" length={6} inputMode="numeric" />
        {/if}
        {#if mode === "sign-in" && showRemember}
          <div class="wire-auth-form__options">
            <Checkbox label="Remember this device" name="rememberDevice" value="on" />
            <a href="/forgot-password">Forgot password?</a>
          </div>
        {/if}
        <Button type="submit" label="{submitLabel}" variant="default" color="{color}" size="lg" fullWidth="true" icon="icon-[lucide--arrow-right]" iconPosition="end" class="wire-auth-form__submit" />
      </form>
      <slot />
    </section>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-auth-form {
      width: 100%;
    }

    .wire-auth-form > header {
      display: flex;
      margin-bottom: 1.5rem;
      align-items: flex-start;
      gap: 0.85rem;
    }

    .wire-auth-form__icon {
      width: 2rem;
      height: 2rem;
      flex: none;
      color: var(--wire-color-primary);
    }

    .wire-auth-form h2 {
      margin: 0;
      font-size: 1.5rem;
      font-weight: 650;
      letter-spacing: -0.025em;
    }

    .wire-auth-form header p {
      margin: 0.35rem 0 0;
      color: var(--wire-color-muted);
      font-size: 0.75rem;
      line-height: 1.5;
    }

    .wire-auth-form form {
      display: grid;
      gap: 1rem;
    }

    .wire-auth-form__options {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
      font-size: 0.7rem;
    }

    .wire-auth-form__options a {
      color: var(--wire-color-primary);
      text-decoration: none;
    }

    .wire-auth-form__submit {
      width: 100%;
      justify-content: center;
    }
  }
}
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"
 import Button from "./button.wrn"
```

---

## AuthSplitLayout

Showcase: https://component.wrnexusjs.dev/
Mount: <AuthSplitLayout /> (legacy: data-component="AuthSplitLayout")
Category: core
Purpose: Reusable auth split layout component.
Props: size: string = "default", color: string = "primary", eyebrow: string = "Secure identity", title: string = "Welcome back", description: string = "", brand: string = "Police Management System", features: unknown[] = [], class: string = ""
Slots: aside-extra, form
Events: none

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component AuthSplitLayout {
  props {
    size: string = "default"
    color: string = "primary"
    eyebrow: string = "Secure identity"
    title: string = "Welcome back"
    description: string = ""
    brand: string = "Police Management System"
    features: unknown[] = []
    class: string = ""
  }

  view {
    <main
      class="wire-auth-split wire-component--color-{color} wire-component--size-{size} {class}"
    >
      <section
        class="wire-auth-split__content"
      >
        <div
          class="wire-auth-split__brand"
        >
          <span
            class="icon-[lucide--shield-check]"
            aria-hidden="true"
          >
          </span>
          <span><strong>{brand}</strong><small>Unified identity and access</small></span>
        </div>
        <div
          class="wire-auth-split__message"
        >
          <span
            class="wire-auth-split__eyebrow"
          >
            {eyebrow}
          </span>
          <h1>{title}</h1>
          {#if description}
            <p>{description}</p>
          {/if}
          <div
            class="wire-auth-split__features"
          >
            {#each features as item}
              <article>
                <span
                  class="{item.icon || 'icon-[lucide--check-circle-2]'}"
                  aria-hidden="true"
                >
                </span>
                <span><strong>{item.label}</strong>
                  {#if item.description}
                    <small>{item.description}</small>
                  {/if}
                </span>
              </article>
            {/each}
          </div>
          <slot
            name="aside-extra"
          />
        </div>
        <small
          class="wire-auth-split__legal"
        >
          Authorised access only · Activity may be monitored and audited.
        </small>
      </section>
      <section
        class="wire-auth-split__form"
      >
        <div
          class="wire-auth-split__form-inner"
        >
          <slot
            name="form"
          />
        </div>
      </section>
    </main>
    <ComponentBaseStyles hidden />
  }

  style {
    /* Shared authentication shell and forms */
    .wire-auth-split {
      display: grid;
      width: 100%;
      height: 100dvh;
      min-height: 0;
      overflow: hidden;
      grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
      color: var(--wire-color-text);
      background: var(--wire-color-bg);
    }

    .wire-auth-split__content,
    .wire-auth-split__form {
      min-width: 0;
      padding: clamp(1.5rem, 5vw, 5rem);
    }

    .wire-auth-split__content {
      display: flex;
      height: 100dvh;
      min-height: 0;
      overflow: hidden;
      justify-content: space-between;
      flex-direction: column;
      background:
        radial-gradient(
          circle at 15% 15%,
          color-mix(in srgb, var(--wire-color-primary) 22%, transparent),
          transparent 34%
        ),
        linear-gradient(145deg, var(--wire-color-surface-2), var(--wire-color-bg));
      border-right: 1px solid var(--wire-color-border);
    }

    .wire-auth-split__brand {
      display: flex;
      align-items: center;
      gap: 0.75rem;
    }

    .wire-auth-split__brand > span:first-child {
      width: 2.5rem;
      height: 2.5rem;
      color: var(--wire-color-primary);
    }

    .wire-auth-split__brand > span:last-child {
      display: grid;
    }

    .wire-auth-split__brand strong {
      font-size: 0.875rem;
      font-weight: 650;
    }

    .wire-auth-split__brand small,
    .wire-auth-split__legal {
      color: var(--wire-color-muted);
      font-size: 0.65rem;
    }

    .wire-auth-split__message {
      width: min(100%, 36rem);
      margin-block: 3rem;
    }

    .wire-auth-split__eyebrow {
      color: var(--wire-color-primary);
      font-size: 0.7rem;
      font-weight: 650;
      letter-spacing: 0.09em;
      text-transform: uppercase;
    }

    .wire-auth-split__message h1 {
      max-width: 14ch;
      margin: 0.65rem 0 0;
      font-size: clamp(2rem, 4vw, 3.75rem);
      font-weight: 650;
      letter-spacing: -0.045em;
      line-height: 1.08;
    }

    .wire-auth-split__message > p {
      max-width: 34rem;
      margin: 1rem 0 0;
      color: var(--wire-color-muted);
      font-size: 0.875rem;
      line-height: 1.7;
    }

    .wire-auth-split__features {
      display: grid;
      margin-top: 2rem;
      gap: 1rem;
    }

    .wire-auth-split__features article {
      display: flex;
      align-items: flex-start;
      gap: 0.75rem;
    }

    .wire-auth-split__features article > span:first-child {
      width: 1.1rem;
      height: 1.1rem;
      margin-top: 0.1rem;
      flex: none;
      color: var(--wire-color-primary);
    }

    .wire-auth-split__features article > span:last-child {
      display: grid;
    }

    .wire-auth-split__features strong {
      font-size: 0.75rem;
      font-weight: 600;
    }

    .wire-auth-split__features small {
      margin-top: 0.15rem;
      color: var(--wire-color-muted);
      font-size: 0.68rem;
    }

    .wire-auth-split__form {
      display: grid;
      height: 100dvh;
      min-height: 0;
      overflow-x: hidden;
      overflow-y: auto;
      overscroll-behavior: contain;
      place-items: center;
      background: var(--wire-color-surface);
    }

    .wire-auth-split__form-inner {
      width: min(100%, 30rem);
    }

    @media (max-width: 800px) {
    .wire-auth-split {
        height: auto;
        min-height: 100dvh;
        overflow: visible;
        grid-template-columns: 1fr;
      }
    .wire-auth-split__content {
        height: auto;
        min-height: auto;
        overflow: visible;
        padding-bottom: 2rem;
        border-right: 0;
        border-bottom: 1px solid var(--wire-color-border);
      }
    .wire-auth-split__message {
        margin-block: 2rem 0;
      }
    .wire-auth-split__message h1 {
        font-size: clamp(1.75rem, 8vw, 2.5rem);
      }
    .wire-auth-split__features,
      .wire-auth-split__legal {
        display: none;
      }
    .wire-auth-split__form {
        height: auto;
        min-height: auto;
        overflow: visible;
        padding-block: 2.5rem;
        place-items: start center;
      }
    }
  }
}
```

---

## Avatar

Showcase: https://component.wrnexusjs.dev/
Mount: <Avatar /> (legacy: data-component="Avatar")
Category: base
Purpose: Theme-aware, responsive avatar component.
Props: src: string = "", alt: string = "", initials: string = "", size: string = "md", color: string = "primary", variant: string = "solid", shape: string = "circle", status: string = "", statusLabel: string = "", statusPosition: string = "bottom", badge: string = "", badgeIcon: string = "", badgeLabel: string = "", tooltip: string = "", name: string = "", description: string = "", loading: string = "lazy", class: string = ""
Slots: none
Events: load, error, click

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component Avatar {
  outputs {
    load(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
    click(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
src: string = ""
    alt: string = ""
    initials: string = ""
    size: string = "md"
    color: string = "primary"
    variant: string = "solid"
    shape: string = "circle"

    status: string = ""
    statusLabel: string = ""
    statusPosition: string = "bottom"

    badge: string = ""
    badgeIcon: string = ""
    badgeLabel: string = ""

    tooltip: string = ""
    name: string = ""
    description: string = ""
    loading: string = "lazy"
    class: string = ""
  }

  view {
    <span
      {...attrs}
      class="wire-component wire-next--avatar {description ? 'wire-next--avatar-media' : ''} {class}"
      data-wrn-avatar
    >
      <span
        class="wire-next__avatar-wrap"
        data-shape="{shape}"
        data-size="{size}"
        data-color="{color}"
        data-variant="{variant}"
        data-status-position="{statusPosition}"
        tabindex="{tooltip ? '0' : '-1'}"
        aria-label="{tooltip || name || alt || initials || 'Avatar'}"
      >
        <span class="wire-next__avatar">
          {#if src}
            <img src="{src}" alt="{alt || name}" loading="{loading}" />
          {:else if initials}
            <span class="wire-next__avatar-initials" aria-hidden="true">{initials}</span>
          {:else}
            <span class="wire-next__avatar-placeholder icon-[lucide--user-round]" aria-hidden="true"></span>
          {/if}
        </span>

        {#if status}
          <span
            class="wire-next__avatar-status"
            data-status="{status}"
            title="{statusLabel || status}"
            aria-label="{statusLabel || status}"
          ></span>
        {/if}

        {#if badgeIcon}
          <span class="wire-next__avatar-badge" aria-label="{badgeLabel || 'Linked account'}">
            <span class="{badgeIcon}" aria-hidden="true"></span>
          </span>
        {:else if badge}
          <span class="wire-next__avatar-badge wire-next__avatar-badge--text" aria-label="{badgeLabel || badge}">
            {badge}
          </span>
        {/if}

        {#if tooltip}
          <span class="wire-next__avatar-tooltip" role="tooltip">{tooltip}</span>
        {/if}
      </span>

      {#if description}
        <span class="wire-next__avatar-copy">
          <strong>{name || alt || initials}</strong>
          <span>{description}</span>
        </span>
      {/if}
    </span>
    <ComponentBaseStyles hidden />
  }

  style {
    /* --- Avatar -------------------------------------------------------------- */
    .wire-next--avatar {
      display: inline-flex;
      width: fit-content;
      max-width: 100%;
      align-items: center;
    }

    .wire-next--avatar-media {
      gap: 0.8rem;
    }

    .wire-next__avatar-wrap {
      --wire-avatar-size: 3rem;
      --wire-avatar-color: var(--wire-color-primary);
      position: relative;
      display: inline-grid;
      width: var(--wire-avatar-size);
      height: var(--wire-avatar-size);
      flex: none;
      place-items: center;
      border-radius: 999px;
      outline: none;
    }

    .wire-next__avatar-wrap[data-size="xs"] {
      --wire-avatar-size: 1.75rem;
    }

    .wire-next__avatar-wrap[data-size="sm"] {
      --wire-avatar-size: 2.25rem;
    }

    .wire-next__avatar-wrap[data-size="md"],
    .wire-next__avatar-wrap[data-size="default"] {
      --wire-avatar-size: 3rem;
    }

    .wire-next__avatar-wrap[data-size="lg"] {
      --wire-avatar-size: 4rem;
    }

    .wire-next__avatar-wrap[data-size="xl"] {
      --wire-avatar-size: 5rem;
    }

    .wire-next__avatar-wrap[data-color="secondary"] {
      --wire-avatar-color: #737373;
    }

    .wire-next__avatar-wrap[data-color="success"] {
      --wire-avatar-color: var(--wire-color-success);
    }

    .wire-next__avatar-wrap[data-color="info"] {
      --wire-avatar-color: var(--wire-color-info);
    }

    .wire-next__avatar-wrap[data-color="danger"] {
      --wire-avatar-color: var(--wire-color-danger);
    }

    .wire-next__avatar-wrap[data-color="warning"] {
      --wire-avatar-color: var(--wire-color-warning);
    }

    .wire-next__avatar-wrap[data-color="light"] {
      --wire-avatar-color: #f4f4f5;
    }

    .wire-next__avatar-wrap[data-color="dark"] {
      --wire-avatar-color: #27272a;
    }

    .wire-next__avatar {
      display: grid;
      width: 100%;
      height: 100%;
      overflow: hidden;
      place-items: center;
      color: white;
      border: 1px solid color-mix(in srgb, var(--wire-avatar-color) 58%, var(--wire-color-border));
      border-radius: inherit;
      background: var(--wire-avatar-color);
      box-shadow:
        0 0 0 2px var(--wire-color-bg),
        0 5px 14px rgb(0 0 0 / 0.16);
    }

    .wire-next__avatar-wrap[data-shape="rounded"] {
      border-radius: calc(var(--wire-radius-sm, 0.5rem) * 0.72);
    }

    .wire-next__avatar-wrap[data-shape="square"] {
      border-radius: 0;
    }

    .wire-next__avatar img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }

    .wire-next__avatar-initials {
      font-size: calc(var(--wire-avatar-size) * 0.32);
      font-weight: 750;
      line-height: 1;
      letter-spacing: -0.02em;
    }

    .wire-next__avatar-placeholder {
      width: 54%;
      height: 54%;
      color: currentColor;
      opacity: 0.72;
    }

    .wire-next__avatar-wrap[data-variant="soft"] .wire-next__avatar {
      color: var(--wire-avatar-color);
      border-color: transparent;
      background: color-mix(in srgb, var(--wire-avatar-color) 15%, var(--wire-color-surface));
      box-shadow: none;
    }

    .wire-next__avatar-wrap[data-variant="outline"] .wire-next__avatar {
      color: var(--wire-avatar-color);
      border-color: var(--wire-avatar-color);
      background: transparent;
      box-shadow: none;
    }

    .wire-next__avatar-wrap[data-variant="white"] .wire-next__avatar {
      color: var(--wire-color-text);
      border-color: var(--wire-color-border);
      background: var(--wire-color-surface);
      box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
    }

    .wire-next__avatar-status {
      position: absolute;
      right: -0.04rem;
      bottom: -0.04rem;
      z-index: 2;
      width: max(0.58rem, calc(var(--wire-avatar-size) * 0.24));
      height: max(0.58rem, calc(var(--wire-avatar-size) * 0.24));
      border: 2px solid var(--wire-color-bg);
      border-radius: 999px;
      background: var(--wire-color-muted);
    }

    .wire-next__avatar-wrap[data-status-position="top"] .wire-next__avatar-status {
      top: -0.04rem;
      bottom: auto;
    }

    .wire-next__avatar-status[data-status="online"],
    .wire-next__avatar-status[data-status="success"] {
      background: #14cba8;
    }

    .wire-next__avatar-status[data-status="busy"],
    .wire-next__avatar-status[data-status="danger"] {
      background: #fb5b68;
    }

    .wire-next__avatar-status[data-status="away"],
    .wire-next__avatar-status[data-status="warning"] {
      background: #ffc400;
    }

    .wire-next__avatar-badge {
      position: absolute;
      right: -0.25rem;
      bottom: -0.25rem;
      z-index: 3;
      display: grid;
      width: max(1rem, calc(var(--wire-avatar-size) * 0.42));
      height: max(1rem, calc(var(--wire-avatar-size) * 0.42));
      place-items: center;
      color: white;
      border: 2px solid var(--wire-color-bg);
      border-radius: 999px;
      background: #27272a;
      box-shadow: 0 4px 10px rgb(0 0 0 / 0.22);
    }

    .wire-next__avatar-badge > span {
      width: 62%;
      height: 62%;
    }

    .wire-next__avatar-badge--text {
      font-size: calc(var(--wire-avatar-size) * 0.18);
      font-weight: 800;
    }

    .wire-next__avatar-tooltip {
      position: absolute;
      left: 50%;
      bottom: calc(100% + 0.65rem);
      z-index: 20;
      width: max-content;
      max-width: 15rem;
      padding: 0.4rem 0.6rem;
      color: var(--wire-color-bg);
      border-radius: 0.4rem;
      background: var(--wire-color-text);
      box-shadow: 0 8px 22px rgb(0 0 0 / 0.2);
      font-size: 0.75rem;
      font-weight: 650;
      line-height: 1.25;
      pointer-events: none;
      opacity: 0;
      transform: translate(-50%, 0.25rem);
      transition:
        opacity var(--wire-motion-fast) var(--wire-ease-standard),
        transform var(--wire-motion-fast) var(--wire-ease-standard);
    }

    .wire-next__avatar-tooltip::after {
      content: "";
      position: absolute;
      top: 100%;
      left: 50%;
      border: 0.3rem solid transparent;
      border-top-color: var(--wire-color-text);
      transform: translateX(-50%);
    }

    .wire-next__avatar-wrap:hover .wire-next__avatar-tooltip,
    .wire-next__avatar-wrap:focus-visible .wire-next__avatar-tooltip {
      opacity: 1;
      transform: translate(-50%, 0);
    }

    .wire-next__avatar-wrap:focus-visible {
      outline: 2px solid var(--wire-avatar-color);
      outline-offset: 4px;
    }

    .wire-next__avatar-copy {
      display: grid;
      min-width: 0;
      gap: 0.15rem;
    }

    .wire-next__avatar-copy strong {
      color: var(--wire-color-text);
      font-size: 0.9rem;
    }

    .wire-next__avatar-copy > span {
      overflow: hidden;
      color: var(--wire-color-muted);
      font-size: 0.78rem;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
  }
}
```

---

## AvatarGroup

Showcase: https://component.wrnexusjs.dev/
Mount: <AvatarGroup /> (legacy: data-component="AvatarGroup")
Category: base
Purpose: Theme-aware, responsive avatar group component.
Props: items: unknown[] = [], size: string = "md", color: string = "primary", variant: string = "solid", shape: string = "circle", layout: string = "stack", maxVisible: number = 4, columns: number = 3, borderColor: string = "", showTooltips: boolean = true, overflowLabel: string = "Show remaining members", class: string = ""
Slots: none
Events: overflow

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component AvatarGroup {
  outputs {
    overflow(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
  }

  props {
    items: unknown[] = []
    size: string = "md"
    color: string = "primary"
    variant: string = "solid"
    shape: string = "circle"
    layout: string = "stack"
    maxVisible: number = 4
    columns: number = 3
    borderColor: string = ""
    showTooltips: boolean = true
    overflowLabel: string = "Show remaining members"
    class: string = ""

}

  state overflowOpen: boolean = false

  functions {
    shared function visibleMembers() {
      return items.slice(0, Number(maxVisible))
    }

    shared function hiddenMembers() {
      return items.slice(Number(maxVisible))
    }

    client function toggleOverflow() {
      overflowOpen = !overflowOpen
      output.overflow({
        component: "AvatarGroup",
        open: overflowOpen,
        hiddenCount: hiddenMembers().length
      })
    }
  }

  view {
    <div
      {...attrs}
      class="wire-component wire-next--avatar-group {class}"
      data-wrn-avatar-group
      data-layout="{layout}"
      data-shape="{shape}"
      data-size="{size}"
      style="--wire-avatar-group-columns:{columns}; --wire-avatar-group-ring:{borderColor || 'var(--wire-color-bg)'}"
      role="group"
      aria-label="Avatar group"
    >
      <div
        class="wire-next__avatar-group-members"
      >
        {#each visibleMembers() as item}
          <span
            class="wire-next__avatar-group-member"
            data-size="{item.size || size}"
            data-shape="{item.shape || shape}"
            data-color="{item.color || color}"
            data-variant="{item.variant || variant}"
            tabindex="{showTooltips && (item.tooltip || item.name) ? '0' : '-1'}"
            aria-label="{item.name || item.alt || item.initials || 'Group member'}"
          >
            <span
              class="wire-next__avatar-group-avatar"
            >
              {#if item.src}
                <img
                  src="{item.src}"
                  alt="{item.alt || item.name || ''}"
                  loading="lazy"
                />
              {:else if item.initials}
                <span
                  aria-hidden="true"
                >
                  {item.initials}
                </span>
              {:else}
                <span
                  class="icon-[lucide--user-round]"
                  aria-hidden="true"
                >
                </span>
              {/if}
            </span>

            {#if showTooltips && (item.tooltip || item.name)}
              <span
                class="wire-next__avatar-group-tooltip"
                role="tooltip"
              >
                {item.tooltip || item.name}
              </span>
            {/if}
          </span>
        {/each}

        {#if hiddenMembers().length > 0}
          <span
            class="wire-next__avatar-group-overflow"
          >
            <button
              type="button"
              class="wire-next__avatar-group-overflow-button"
              aria-label="{overflowLabel}"
              aria-expanded="{overflowOpen ? 'true' : 'false'}"
              @click="toggleOverflow(event)"
            >
              +{hiddenMembers().length}
            </button>

            <span
              class="wire-next__avatar-group-menu"
              role="menu"
              data-show="overflowOpen"
              aria-hidden="{overflowOpen ? 'false' : 'true'}"
            >
              {#each hiddenMembers() as item}
                <span
                  class="wire-next__avatar-group-menu-item"
                  role="menuitem"
                >
                  <span
                    class="wire-next__avatar-group-menu-avatar"
                  >
                    {#if item.src}
                      <img
                        src="{item.src}"
                        alt=""
                        loading="lazy"
                      />
                    {:else}
                      <span>{item.initials || '?'}</span>
                    {/if}
                  </span>
                  <span>{item.name || item.alt || item.initials || 'Team member'}</span>
                </span>
              {/each}
            </span>
          </span>
        {/if}
      </div>
    </div>
    <ComponentBaseStyles hidden />
  }

  style {
    /* --- Avatar group -------------------------------------------------------- */
    .wire-next--avatar-group {
      --wire-avatar-group-size: 3rem;
      position: relative;
      display: inline-flex;
      width: fit-content;
      max-width: 100%;
    }

    .wire-next--avatar-group[data-size="xs"] {
      --wire-avatar-group-size: 1.75rem;
    }

    .wire-next--avatar-group[data-size="sm"] {
      --wire-avatar-group-size: 2.25rem;
    }

    .wire-next--avatar-group[data-size="md"],
    .wire-next--avatar-group[data-size="default"] {
      --wire-avatar-group-size: 3rem;
    }

    .wire-next--avatar-group[data-size="lg"] {
      --wire-avatar-group-size: 4rem;
    }

    .wire-next--avatar-group[data-size="xl"] {
      --wire-avatar-group-size: 5rem;
    }

    .wire-next__avatar-group-members {
      display: flex;
      align-items: center;
    }

    .wire-next--avatar-group[data-layout="stack"] .wire-next__avatar-group-member,
    .wire-next--avatar-group[data-layout="stack"] .wire-next__avatar-group-overflow {
      margin-left: calc(var(--wire-avatar-group-size) * -0.22);
    }

    .wire-next--avatar-group[data-layout="stack"] :first-child {
      margin-left: 0;
    }

    .wire-next--avatar-group[data-layout="grid"] .wire-next__avatar-group-members {
      display: grid;
      grid-template-columns: repeat(var(--wire-avatar-group-columns), var(--wire-avatar-group-size));
      gap: 0.65rem;
    }

    .wire-next__avatar-group-member,
    .wire-next__avatar-group-overflow {
      --wire-avatar-group-color: var(--wire-color-primary);
      position: relative;
      display: inline-grid;
      width: var(--wire-avatar-group-size);
      height: var(--wire-avatar-group-size);
      flex: none;
      place-items: center;
      border-radius: 999px;
      outline: none;
      transition:
        z-index var(--wire-motion-fast),
        transform var(--wire-motion-base) var(--wire-ease-emphasized);
    }

    .wire-next__avatar-group-member[data-color="secondary"] {
      --wire-avatar-group-color: #737373;
    }

    .wire-next__avatar-group-member[data-color="success"] {
      --wire-avatar-group-color: var(--wire-color-success);
    }

    .wire-next__avatar-group-member[data-color="info"] {
      --wire-avatar-group-color: var(--wire-color-info);
    }

    .wire-next__avatar-group-member[data-color="warning"] {
      --wire-avatar-group-color: var(--wire-color-warning);
    }

    .wire-next__avatar-group-member[data-color="danger"] {
      --wire-avatar-group-color: var(--wire-color-danger);
    }

    .wire-next__avatar-group-member[data-shape="rounded"] {
      border-radius: calc(var(--wire-radius-sm, 0.5rem) * 0.72);
    }

    .wire-next__avatar-group-avatar,
    .wire-next__avatar-group-overflow-button {
      display: grid;
      width: 100%;
      height: 100%;
      overflow: hidden;
      place-items: center;
      color: white;
      border: 2px solid var(--wire-avatar-group-ring);
      border-radius: inherit;
      background: var(--wire-avatar-group-color, var(--wire-color-primary));
      box-shadow: 0 5px 14px rgb(0 0 0 / 0.16);
      font: inherit;
      font-size: calc(var(--wire-avatar-group-size) * 0.3);
      font-weight: 800;
    }

    .wire-next__avatar-group-avatar img,
    .wire-next__avatar-group-menu-avatar img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }

    .wire-next__avatar-group-overflow-button {
      cursor: pointer;
      color: var(--wire-color-text);
      background: var(--wire-color-surface-2);
    }

    .wire-next__avatar-group-member:hover,
    .wire-next__avatar-group-member:focus-visible,
    .wire-next__avatar-group-overflow:focus-within {
      z-index: 5;
      transform: translateY(-3px);
    }

    .wire-next__avatar-group-member:focus-visible,
    .wire-next__avatar-group-overflow-button:focus-visible {
      outline: 2px solid var(--wire-color-primary);
      outline-offset: 3px;
    }

    .wire-next__avatar-group-tooltip {
      position: absolute;
      left: 50%;
      bottom: calc(100% + 0.65rem);
      z-index: 20;
      width: max-content;
      max-width: 14rem;
      padding: 0.4rem 0.6rem;
      color: var(--wire-color-bg);
      border-radius: 0.4rem;
      background: var(--wire-color-text);
      box-shadow: 0 8px 22px rgb(0 0 0 / 0.22);
      font-size: 0.75rem;
      font-weight: 650;
      pointer-events: none;
      opacity: 0;
      transform: translate(-50%, 0.25rem);
      transition:
        opacity var(--wire-motion-fast),
        transform var(--wire-motion-fast);
    }

    .wire-next__avatar-group-member:hover .wire-next__avatar-group-tooltip,
    .wire-next__avatar-group-member:focus-visible .wire-next__avatar-group-tooltip {
      opacity: 1;
      transform: translate(-50%, 0);
    }

    .wire-next__avatar-group-menu {
      position: absolute;
      top: calc(100% + 0.65rem);
      right: 0;
      z-index: 30;
      display: grid;
      width: max-content;
      min-width: 12rem;
      gap: 0.25rem;
      padding: 0.45rem;
      color: var(--wire-color-text);
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-sm, 0.5rem);
      background: var(--wire-color-surface);
      box-shadow: 0 16px 38px rgb(0 0 0 / 0.22);
    }

    .wire-next__avatar-group-menu-item {
      display: flex;
      align-items: center;
      gap: 0.6rem;
      padding: 0.45rem 0.55rem;
      border-radius: 0.4rem;
      font-size: 0.82rem;
    }

    .wire-next__avatar-group-menu-item:hover {
      background: var(--wire-color-surface-2);
    }

    .wire-next__avatar-group-menu-avatar {
      display: grid;
      width: 1.75rem;
      height: 1.75rem;
      overflow: hidden;
      flex: none;
      place-items: center;
      border-radius: 999px;
      background: var(--wire-color-surface-2);
      font-size: 0.65rem;
      font-weight: 800;
    }
  }
}
```

---

## BackToTop

Showcase: https://component.wrnexusjs.dev/
Mount: <BackToTop /> (legacy: data-component="BackToTop")
Category: navigation
Purpose: Provide a responsive floating control that returns long pages to the top and can show scroll progress.
Props: threshold: number = 500, label: string = "Back to top", ariaLabel: string = "Scroll back to top", icon: string = "icon-[lucide--arrow-up]", position: string = "right", offset: string = "md", behavior: string = "smooth", showProgress: boolean = false, showLabel: boolean = false, alwaysVisible: boolean = false, size: string = "default", color: string = "primary", variant: string = "solid", shape: string = "round", class: string = ""
Slots: none
Events: none

### Complete .wrn source contract

```wrn
component BackToTop {
  props {
    threshold: number = 500
    label: string = "Back to top"
    ariaLabel: string = "Scroll back to top"
    icon: string = "icon-[lucide--arrow-up]"
    position: string = "right"
    offset: string = "md"
    behavior: string = "smooth"
    showProgress: boolean = false
    showLabel: boolean = false
    alwaysVisible: boolean = false
    size: string = "default"
    color: string = "primary"
    variant: string = "solid"
    shape: string = "round"
    class: string = ""
  }

  state visible: boolean = false
  state progress: number = 0

  view {
    <button
      {...attrs}
      data-ui-component="BackToTop"
      type="button"
      aria-label='{ariaLabel}'
      title='{label}'
      data-position='{position}'
      data-offset='{offset}'
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      data-shape='{shape}'
      data-show-label='{showLabel ? "true" : "false"}'
      data-progress='{showProgress ? "true" : "false"}'
      data-show='{alwaysVisible || visible}'
      class='wire-back-to-top {class}'
      style='--wire-back-to-top-progress: {progress}%;'
      @window:scroll='visible = window.scrollY >= threshold; progress = Math.min(100, Math.max(0, Math.round((window.scrollY / Math.max(1, document.documentElement.scrollHeight - window.innerHeight)) * 100)))'
      @click='window.scrollTo({ top: 0, behavior: behavior })'
    >
      {#if showProgress}
        <span class="wire-back-to-top__progress" aria-hidden="true"></span>
      {/if}

      <span class="wire-back-to-top__surface">
        <span class='wire-back-to-top__icon {icon}' aria-hidden="true"></span>

        {#if showLabel}
          <span class="wire-back-to-top__label">{label}</span>
        {/if}
      </span>

      <span class="wire-back-to-top__sr">{label}</span>
    </button>
  }

  style {
    .wire-back-to-top {
      --back-accent: var(--wire-color-primary);
      --back-accent-hover: var(--wire-color-primary-hover);
      --back-accent-soft: var(--wire-color-primary-soft);
      --back-contrast: var(--wire-color-primary-contrast);

      position: fixed;
      z-index: 70;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-width: 3rem;
      min-height: 3rem;
      padding: 0;
      color: var(--back-contrast);
      background: transparent;
      border: 0;
      border-radius: 999px;
      cursor: pointer;
      filter: drop-shadow(0 14px 30px color-mix(in srgb, black 22%, transparent));
      transition:
        transform 180ms ease,
        opacity 180ms ease,
        filter 180ms ease;
    }

    .wire-back-to-top[data-color="secondary"] {
      --back-accent: var(--wire-color-secondary);
      --back-accent-hover: var(--wire-color-secondary-hover);
      --back-accent-soft: var(--wire-color-secondary-soft);
      --back-contrast: var(--wire-color-secondary-contrast);
    }

    .wire-back-to-top[data-color="info"] {
      --back-accent: var(--wire-color-info);
      --back-accent-hover: var(--wire-color-info-hover);
      --back-accent-soft: var(--wire-color-info-soft);
      --back-contrast: var(--wire-color-info-contrast);
    }

    .wire-back-to-top[data-color="success"] {
      --back-accent: var(--wire-color-success);
      --back-accent-hover: var(--wire-color-success-hover);
      --back-accent-soft: var(--wire-color-success-soft);
      --back-contrast: var(--wire-color-success-contrast);
    }

    .wire-back-to-top[data-color="warning"] {
      --back-accent: var(--wire-color-warning);
      --back-accent-hover: var(--wire-color-warning-hover);
      --back-accent-soft: var(--wire-color-warning-soft);
      --back-contrast: var(--wire-color-warning-contrast);
    }

    .wire-back-to-top[data-color="danger"] {
      --back-accent: var(--wire-color-danger);
      --back-accent-hover: var(--wire-color-danger-hover);
      --back-accent-soft: var(--wire-color-danger-soft);
      --back-contrast: var(--wire-color-danger-contrast);
    }

    .wire-back-to-top[data-position="right"] {
      right: 1.5rem;
    }

    .wire-back-to-top[data-position="left"] {
      left: 1.5rem;
    }

    .wire-back-to-top[data-position="center"] {
      left: 50%;
      transform: translateX(-50%);
    }

    .wire-back-to-top[data-position="center"]:hover {
      transform: translateX(-50%) translateY(-3px);
    }

    .wire-back-to-top[data-offset="sm"] {
      bottom: 1rem;
    }

    .wire-back-to-top[data-offset="md"] {
      bottom: 1.5rem;
    }

    .wire-back-to-top[data-offset="lg"] {
      bottom: 2rem;
    }

    .wire-back-to-top[data-offset="xl"] {
      bottom: 3rem;
    }

    .wire-back-to-top[data-size="sm"] {
      min-width: 2.5rem;
      min-height: 2.5rem;
    }

    .wire-back-to-top[data-size="lg"] {
      min-width: 3.5rem;
      min-height: 3.5rem;
    }

    .wire-back-to-top[data-show-label="true"] {
      min-width: 0;
    }

    .wire-back-to-top:hover {
      transform: translateY(-3px);
      filter: drop-shadow(0 18px 36px color-mix(in srgb, var(--back-accent) 24%, transparent));
    }

    .wire-back-to-top:active {
      transform: translateY(-1px) scale(0.98);
    }

    .wire-back-to-top__surface {
      position: relative;
      z-index: 2;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 0.55rem;
      min-width: 3rem;
      min-height: 3rem;
      padding: 0.75rem;
      color: var(--back-contrast);
      background:
        linear-gradient(145deg, color-mix(in srgb, white 10%, transparent), transparent 56%),
        var(--back-accent);
      border: 1px solid color-mix(in srgb, white 18%, transparent);
      border-radius: inherit;
      box-shadow:
        0 1px 0 color-mix(in srgb, white 20%, transparent) inset,
        0 10px 24px color-mix(in srgb, black 18%, transparent);
      backdrop-filter: blur(12px);
      transition:
        background-color 160ms ease,
        border-color 160ms ease,
        color 160ms ease;
    }

    .wire-back-to-top[data-size="sm"] .wire-back-to-top__surface {
      min-width: 2.5rem;
      min-height: 2.5rem;
      padding: 0.58rem;
    }

    .wire-back-to-top[data-size="lg"] .wire-back-to-top__surface {
      min-width: 3.5rem;
      min-height: 3.5rem;
      padding: 0.9rem;
    }

    .wire-back-to-top[data-show-label="true"] .wire-back-to-top__surface {
      min-width: 0;
      padding-inline: 1rem;
    }

    .wire-back-to-top[data-variant="outline"] .wire-back-to-top__surface {
      color: var(--back-accent);
      background: color-mix(in srgb, var(--wire-color-surface-raised) 88%, transparent);
      border-color: color-mix(in srgb, var(--back-accent) 46%, var(--wire-color-border));
    }

    .wire-back-to-top[data-variant="soft"] .wire-back-to-top__surface {
      color: var(--back-accent);
      background: color-mix(in srgb, var(--back-accent-soft) 88%, var(--wire-color-surface-raised));
      border-color: color-mix(in srgb, var(--back-accent) 22%, var(--wire-color-border));
    }

    .wire-back-to-top[data-variant="ghost"] .wire-back-to-top__surface {
      color: var(--back-accent);
      background: color-mix(in srgb, var(--wire-color-surface-raised) 68%, transparent);
      border-color: transparent;
      box-shadow: none;
    }

    .wire-back-to-top[data-variant="minimal"] .wire-back-to-top__surface {
      color: var(--back-accent);
      background: transparent;
      border-color: transparent;
      box-shadow: none;
      backdrop-filter: none;
    }

    .wire-back-to-top[data-shape="rounded"] {
      border-radius: 0.9rem;
    }

    .wire-back-to-top[data-shape="pill"] {
      border-radius: 999px;
    }

    .wire-back-to-top__progress {
      position: absolute;
      inset: -0.22rem;
      z-index: 1;
      border-radius: inherit;
      background: conic-gradient(
        var(--back-accent) var(--wire-back-to-top-progress),
        color-mix(in srgb, var(--wire-color-border) 72%, transparent) var(--wire-back-to-top-progress)
      );
      mask: radial-gradient(farthest-side, transparent calc(100% - 0.18rem), #000 calc(100% - 0.17rem));
      -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 0.18rem), #000 calc(100% - 0.17rem));
      pointer-events: none;
    }

    .wire-back-to-top__icon {
      width: 1.1rem;
      height: 1.1rem;
      flex: 0 0 auto;
    }

    .wire-back-to-top[data-size="sm"] .wire-back-to-top__icon {
      width: 0.95rem;
      height: 0.95rem;
    }

    .wire-back-to-top[data-size="lg"] .wire-back-to-top__icon {
      width: 1.25rem;
      height: 1.25rem;
    }

    .wire-back-to-top__label {
      white-space: nowrap;
      font-size: 0.78rem;
      font-weight: 700;
      line-height: 1;
    }

    .wire-back-to-top__sr {
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
      white-space: nowrap;
      border: 0;
    }

    .wire-back-to-top:focus-visible {
      outline: 2px solid var(--wire-color-focus);
      outline-offset: 4px;
    }

    @media (max-width: 639px) {
      .wire-back-to-top[data-position="right"] {
        right: 1rem;
      }

      .wire-back-to-top[data-position="left"] {
        left: 1rem;
      }

      .wire-back-to-top[data-offset="lg"],
      .wire-back-to-top[data-offset="xl"] {
        bottom: 1.25rem;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .wire-back-to-top,
      .wire-back-to-top__surface {
        transition: none;
      }

      .wire-back-to-top:hover,
      .wire-back-to-top[data-position="center"]:hover {
        transform: none;
      }
    }
  }
}
```

---

## Badge

Showcase: https://component.wrnexusjs.dev/
Mount: <Badge /> (legacy: data-component="Badge")
Category: base
Purpose: Theme-aware, responsive badge component.
Props: label: string = "Badge", size: string = "md", color: string = "primary", variant: string = "solid", shape: string = "pill", class: string = "", icon: string = "", iconPosition: string = "start", dot: boolean = false, dotOnly: boolean = false, dotLabel: string = "Status", animated: boolean = false, avatarSrc: string = "", avatarAlt: string = "", dismissible: boolean = false, dismissLabel: string = "Remove badge", truncate: boolean = false, maxWidth: string = "12rem", anchorLabel: string = "", anchorIcon: string = "", placement: string = "inline", anchorLabelText: string = "Badge anchor"
Slots: none
Events: dismiss

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component Badge {
  outputs {
    dismiss(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
    label: string = "Badge"
    size: string = "md"
    color: string = "primary"
    variant: string = "solid"
    shape: string = "pill"
    class: string = ""

    icon: string = ""
    iconPosition: string = "start"
    dot: boolean = false
    dotOnly: boolean = false
    dotLabel: string = "Status"
    animated: boolean = false

    avatarSrc: string = ""
    avatarAlt: string = ""
    dismissible: boolean = false
    dismissLabel: string = "Remove badge"
    truncate: boolean = false
    maxWidth: string = "12rem"

    anchorLabel: string = ""
    anchorIcon: string = ""
    placement: string = "inline"
    anchorLabelText: string = "Badge anchor"

}

  state visible: boolean = true

  functions {
    // output.dismiss reaches a parent @dismiss binding; a raw dispatchEvent on
    // the root does not. The runtime registers parent handlers in a registry
    // that only the output proxy consults, so the CustomEvent this used to
    // build bubbled past every binding and was never seen by anyone.
    client function dismissBadge() {
      visible = false
      output.dismiss({ component: "Badge", label: label })
    }
  }

  view {
    <span
      {...attrs}
      class="wire-component wire-next--badge-root {anchorLabel || anchorIcon ? 'wire-next--badge-anchored' : ''} {class}"
      data-wrn-badge
      data-placement="{placement}"
      data-show="visible"
      aria-hidden="{visible ? 'false' : 'true'}"
    >
      {#if anchorLabel || anchorIcon}
        <button type="button" class="wire-next__badge-anchor" aria-label="{anchorLabelText}">
          {#if anchorIcon}<span class="{anchorIcon}" aria-hidden="true"></span>{/if}
          {#if anchorLabel}<span>{anchorLabel}</span>{/if}
        </button>
      {/if}

      <span
        class="wire-next__badge"
        data-size="{size}"
        data-color="{color}"
        data-variant="{variant}"
        data-shape="{shape}"
        data-animated="{animated ? 'true' : 'false'}"
        style="--wire-badge-max-width:{maxWidth}"
        role="{dotOnly ? 'status' : ''}"
        aria-label="{dotOnly ? dotLabel : ''}"
      >
        {#if animated}
          <span class="wire-next__badge-ping" aria-hidden="true"></span>
        {/if}
        {#if avatarSrc}
          <img class="wire-next__badge-avatar" src="{avatarSrc}" alt="{avatarAlt}" loading="lazy" />
        {/if}
        {#if dot || dotOnly}
          <span class="wire-next__badge-dot" aria-hidden="true"></span>
        {/if}
        {#if icon && iconPosition === "start"}
          <span class="wire-next__badge-icon {icon}" aria-hidden="true"></span>
        {/if}
        {#if !dotOnly}
          <span class="wire-next__badge-label {truncate ? 'wire-next__badge-label--truncate' : ''}">
            {label}
          </span>
        {/if}
        {#if icon && iconPosition === "end"}
          <span class="wire-next__badge-icon {icon}" aria-hidden="true"></span>
        {/if}
        {#if dismissible}
          <button type="button" class="wire-next__badge-dismiss" aria-label="{dismissLabel}" @click="dismissBadge(event)">
            <span class="icon-[lucide--x]" aria-hidden="true"></span>
          </button>
        {/if}
      </span>
    </span>
    <ComponentBaseStyles hidden />
  }

  style {
    @keyframes wire-badge-ping {
      75%,
      100% {
        opacity: 0;
        transform: scale(1.45);
      }
    }

    /* --- Badge --------------------------------------------------------------- */
    .wire-next--badge-root {
      position: relative;
      display: inline-flex;
      width: fit-content;
      max-width: 100%;
      vertical-align: middle;
    }

    .wire-next__badge {
      --wire-badge-color: var(--wire-color-primary);
      position: relative;
      isolation: isolate;
      display: inline-flex;
      min-width: 0;
      min-height: 1.65rem;
      align-items: center;
      justify-content: center;
      gap: 0.35rem;
      max-width: var(--wire-badge-max-width);
      padding: 0.25rem 0.6rem;
      color: white;
      border: 1px solid var(--wire-badge-color);
      border-radius: 999px;
      background: var(--wire-badge-color);
      box-shadow: 0 4px 12px color-mix(in srgb, var(--wire-badge-color) 18%, transparent);
      font-size: 0.75rem;
      font-weight: 750;
      line-height: 1;
      white-space: nowrap;
    }

    .wire-next__badge[data-size="xs"] {
      min-height: 1.25rem;
      padding: 0.15rem 0.4rem;
      font-size: 0.625rem;
    }

    .wire-next__badge[data-size="sm"] {
      min-height: 1.45rem;
      padding: 0.2rem 0.5rem;
      font-size: 0.6875rem;
    }

    .wire-next__badge[data-size="lg"] {
      min-height: 2rem;
      padding: 0.35rem 0.75rem;
      font-size: 0.825rem;
    }

    .wire-next__badge[data-color="secondary"] {
      --wire-badge-color: #737373;
    }

    .wire-next__badge[data-color="success"] {
      --wire-badge-color: #0f9f8f;
    }

    .wire-next__badge[data-color="info"] {
      --wire-badge-color: #2563eb;
    }

    .wire-next__badge[data-color="danger"] {
      --wire-badge-color: #ef3340;
    }

    .wire-next__badge[data-color="warning"] {
      --wire-badge-color: #eab308;
    }

    .wire-next__badge[data-color="dark"] {
      --wire-badge-color: #27272a;
    }

    .wire-next__badge[data-color="light"] {
      --wire-badge-color: #f4f4f5;
    }

    .wire-next__badge[data-color="warning"][data-variant="solid"],
    .wire-next__badge[data-color="light"][data-variant="solid"] {
      color: #18181b;
    }

    .wire-next__badge[data-variant="soft"] {
      color: var(--wire-badge-color);
      border-color: transparent;
      background: color-mix(in srgb, var(--wire-badge-color) 15%, var(--wire-color-surface));
      box-shadow: none;
    }

    .wire-next__badge[data-variant="outline"] {
      color: var(--wire-badge-color);
      background: transparent;
      box-shadow: none;
    }

    .wire-next__badge[data-variant="white"] {
      color: var(--wire-color-text);
      border-color: var(--wire-color-border);
      background: var(--wire-color-surface);
      box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
    }

    .wire-next__badge[data-shape="rounded"] {
      border-radius: 0.4rem;
    }

    .wire-next__badge[data-shape="square"] {
      border-radius: 0;
    }

    .wire-next__badge-label {
      min-width: 0;
    }

    .wire-next__badge-label--truncate {
      overflow: hidden;
      text-overflow: ellipsis;
    }

    .wire-next__badge-dot {
      width: 0.42rem;
      height: 0.42rem;
      flex: none;
      border-radius: 999px;
      background: currentColor;
    }

    .wire-next__badge:has(.wire-next__badge-dot):not(:has(.wire-next__badge-label)) {
      width: 0.7rem;
      min-width: 0.7rem;
      height: 0.7rem;
      min-height: 0.7rem;
      padding: 0;
      border: 2px solid var(--wire-color-bg);
    }

    .wire-next__badge:has(.wire-next__badge-dot):not(:has(.wire-next__badge-label))
      .wire-next__badge-dot {
      width: 100%;
      height: 100%;
    }

    .wire-next__badge-icon {
      width: 0.9rem;
      height: 0.9rem;
      flex: none;
    }

    .wire-next__badge-avatar {
      width: 1.25rem;
      height: 1.25rem;
      margin-left: -0.35rem;
      object-fit: cover;
      border: 1px solid color-mix(in srgb, currentColor 35%, transparent);
      border-radius: 999px;
    }

    .wire-next__badge-dismiss {
      display: grid;
      width: 1.15rem;
      height: 1.15rem;
      margin-right: -0.3rem;
      padding: 0;
      place-items: center;
      color: currentColor;
      border: 0;
      border-radius: 999px;
      background: color-mix(in srgb, currentColor 12%, transparent);
      cursor: pointer;
    }

    .wire-next__badge-dismiss:hover {
      background: color-mix(in srgb, currentColor 22%, transparent);
    }

    .wire-next__badge-dismiss:focus-visible {
      outline: 2px solid currentColor;
      outline-offset: 2px;
    }

    .wire-next__badge-dismiss span {
      width: 0.75rem;
      height: 0.75rem;
    }

    .wire-next__badge-anchor {
      display: inline-flex;
      min-height: 2.75rem;
      align-items: center;
      justify-content: center;
      gap: 0.5rem;
      padding: 0.65rem 0.9rem;
      color: var(--wire-color-text);
      border: 1px solid var(--wire-color-border);
      border-radius: 0.5rem;
      background: var(--wire-color-surface);
      font: inherit;
      font-size: 0.875rem;
      font-weight: 700;
    }

    .wire-next__badge-anchor > [class*="icon-"] {
      width: 1rem;
      height: 1rem;
    }

    .wire-next--badge-anchored[data-placement="inline"] {
      align-items: center;
    }

    .wire-next--badge-anchored[data-placement="inline"] .wire-next__badge {
      margin-left: -0.55rem;
    }

    .wire-next--badge-anchored[data-placement="top-right"] .wire-next__badge {
      position: absolute;
      top: -0.55rem;
      right: -0.75rem;
      z-index: 2;
    }

    .wire-next__badge-ping {
      position: absolute;
      z-index: -1;
      inset: -0.25rem;
      border-radius: inherit;
      background: var(--wire-badge-color);
      opacity: 0.5;
      animation: wire-badge-ping 1.4s cubic-bezier(0, 0, 0.2, 1) infinite;
    }

    @media (prefers-reduced-motion: reduce) {
    .wire-next__badge-ping {
        animation: none;
      }
    }
  }
}
```

---

## Blockquote

Showcase: https://component.wrnexusjs.dev/
Mount: <Blockquote /> (legacy: data-component="Blockquote")
Category: base
Purpose: Theme-aware, responsive blockquote component.
Props: quote: string = "I just wanted to say that I'm very happy with my purchase so far. The documentation is outstanding - clear and detailed.", citation: string = "", citationTitle: string = "", citationUrl: string = "", avatarSrc: string = "", avatarAlt: string = "", size: string = "md", color: string = "primary", align: string = "left", variant: string = "default", quoteMark: boolean = true, italic: boolean = true, class: string = ""
Slots: default
Events: none

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component Blockquote {
  props {
    quote: string = "I just wanted to say that I'm very happy with my purchase so far. The documentation is outstanding - clear and detailed."
    citation: string = ""
    citationTitle: string = ""
    citationUrl: string = ""
    avatarSrc: string = ""
    avatarAlt: string = ""
    size: string = "md"
    color: string = "primary"
    align: string = "left"
    variant: string = "default"
    quoteMark: boolean = true
    italic: boolean = true
    class: string = ""
  }

  view {
    <figure
      {...attrs}
      class="wire-component wire-next--blockquote wire-component--color-{color} wire-component--size-{size} {class}"
      data-size="{size}"
      data-color="{color}"
      data-align="{align}"
      data-variant="{variant}"
      data-italic="{italic}"
    >
      <blockquote cite="{citationUrl}">
        {#if quoteMark}
          <span class="wire-next__blockquote-mark" aria-hidden="true">“</span>
        {/if}

        <div class="wire-next__blockquote-copy">
          {#if quote}
            <p>{quote}</p>
          {:else}
            <slot />
          {/if}
        </div>
      </blockquote>

      {#if citation || citationTitle || avatarSrc}
        <figcaption class="wire-next__blockquote-citation">
          {#if avatarSrc}
            <img
              class="wire-next__blockquote-avatar"
              src="{avatarSrc}"
              alt="{avatarAlt}"
              loading="lazy"
            />
          {/if}

          <span class="wire-next__blockquote-attribution">
            {#if citation}
              {#if citationUrl}
                <cite><a href="{citationUrl}">{citation}</a></cite>
              {:else}
                <cite>{citation}</cite>
              {/if}
            {/if}
            {#if citationTitle}<span>{citationTitle}</span>{/if}
          </span>
        </figcaption>
      {/if}
    </figure>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--blockquote {
      --wire-blockquote-color: var(--wire-component-color, var(--wire-color-primary));
      --wire-blockquote-font-size: 1.125rem;
      --wire-blockquote-mark-size: 4.25rem;
      display: grid;
      gap: 1.25rem;
      width: 100%;
      color: var(--wire-color-text);
      text-align: left;
    }

    .wire-next--blockquote[data-size="xs"] {
      --wire-blockquote-font-size: 0.875rem;
      --wire-blockquote-mark-size: 3rem;
    }

    .wire-next--blockquote[data-size="sm"] {
      --wire-blockquote-font-size: 1rem;
      --wire-blockquote-mark-size: 3.5rem;
    }

    .wire-next--blockquote[data-size="md"],
    .wire-next--blockquote[data-size="default"] {
      --wire-blockquote-font-size: 1.125rem;
    }

    .wire-next--blockquote[data-size="lg"] {
      --wire-blockquote-font-size: clamp(1.35rem, 2.4vw, 1.75rem);
      --wire-blockquote-mark-size: 5rem;
    }

    .wire-next--blockquote[data-size="xl"] {
      --wire-blockquote-font-size: clamp(1.65rem, 3vw, 2.25rem);
      --wire-blockquote-mark-size: 5.75rem;
    }

    .wire-next--blockquote > blockquote {
      position: relative;
      display: block;
      min-width: 0;
      margin: 0;
      padding-top: calc(var(--wire-blockquote-mark-size) * 0.78);
    }

    .wire-next__blockquote-mark {
      position: absolute;
      top: 0;
      inset-inline: 0;
      display: block;
      width: 100%;
      color: color-mix(in srgb, var(--wire-blockquote-color) 28%, var(--wire-color-text));
      font-family: Georgia, "Times New Roman", serif;
      font-size: var(--wire-blockquote-mark-size);
      font-style: normal;
      font-weight: 700;
      line-height: 0.8;
      text-align: start;
      user-select: none;
    }

    .wire-next__blockquote-copy {
      min-width: 0;
      font-family: Georgia, "Times New Roman", serif;
      font-size: var(--wire-blockquote-font-size);
      line-height: 1.55;
      text-wrap: pretty;
    }

    .wire-next--blockquote[data-italic="true"] .wire-next__blockquote-copy {
      font-style: italic;
    }

    .wire-next__blockquote-copy > :first-child {
      margin-top: 0;
    }

    .wire-next__blockquote-copy > :last-child {
      margin-bottom: 0;
    }

    .wire-next__blockquote-citation {
      display: inline-flex;
      gap: 0.75rem;
      align-items: center;
      width: fit-content;
      margin-inline-start: 0;
      color: var(--wire-color-muted);
      font-size: 0.875rem;
      line-height: 1.35;
    }

    .wire-next__blockquote-avatar {
      width: 2.5rem;
      height: 2.5rem;
      flex: 0 0 auto;
      border: 2px solid color-mix(in srgb, var(--wire-blockquote-color) 35%, transparent);
      border-radius: 999px;
      object-fit: cover;
    }

    .wire-next__blockquote-attribution {
      display: grid;
      gap: 0.15rem;
    }

    .wire-next__blockquote-attribution cite {
      color: var(--wire-color-text);
      font-style: normal;
      font-weight: 700;
    }

    .wire-next__blockquote-attribution a {
      color: inherit;
      text-decoration-color: color-mix(in srgb, var(--wire-blockquote-color) 55%, transparent);
      text-underline-offset: 0.2em;
    }

    .wire-next__blockquote-attribution a:hover {
      color: var(--wire-blockquote-color);
    }

    .wire-next--blockquote[data-align="center"] {
      text-align: center;
    }

    .wire-next--blockquote[data-align="right"],
    .wire-next--blockquote[data-align="end"] {
      text-align: right;
    }

    .wire-next--blockquote[data-align="center"] > blockquote,
    .wire-next--blockquote[data-align="right"] > blockquote,
    .wire-next--blockquote[data-align="end"] > blockquote {
      text-align: inherit;
    }

    .wire-next--blockquote[data-align="center"] .wire-next__blockquote-mark {
      text-align: center;
    }

    .wire-next--blockquote[data-align="right"] .wire-next__blockquote-mark,
    .wire-next--blockquote[data-align="end"] .wire-next__blockquote-mark {
      text-align: end;
    }

    .wire-next--blockquote[data-align="center"] .wire-next__blockquote-citation {
      margin-inline: auto;
    }

    .wire-next--blockquote[data-align="right"] .wire-next__blockquote-citation,
    .wire-next--blockquote[data-align="end"] .wire-next__blockquote-citation {
      margin-inline-start: auto;
    }

    .wire-next--blockquote[data-variant="bordered"],
    .wire-next--blockquote[data-variant="border"],
    .wire-next--blockquote[data-variant="left-border"] {
      padding-inline-start: 1.5rem;
      border-inline-start: 4px solid var(--wire-blockquote-color);
    }

    .wire-next--blockquote[data-variant="bordered"] .wire-next__blockquote-mark,
    .wire-next--blockquote[data-variant="border"] .wire-next__blockquote-mark,
    .wire-next--blockquote[data-variant="left-border"] .wire-next__blockquote-mark {
      display: none;
    }

    .wire-next--blockquote[data-variant="bordered"] > blockquote,
    .wire-next--blockquote[data-variant="border"] > blockquote,
    .wire-next--blockquote[data-variant="left-border"] > blockquote {
      display: block;
      padding-top: 0;
    }

    .wire-next--blockquote[data-variant="bordered"] .wire-next__blockquote-copy,
    .wire-next--blockquote[data-variant="border"] .wire-next__blockquote-copy,
    .wire-next--blockquote[data-variant="left-border"] .wire-next__blockquote-copy {
      padding-top: 0;
    }

    .wire-next--blockquote[data-variant="bordered"] .wire-next__blockquote-citation,
    .wire-next--blockquote[data-variant="border"] .wire-next__blockquote-citation,
    .wire-next--blockquote[data-variant="left-border"] .wire-next__blockquote-citation {
      margin-inline-start: 0;
    }

    @media (max-width: 480px) {
    .wire-next--blockquote {
        --wire-blockquote-mark-size: 3.25rem;
        --wire-blockquote-font-size: 1rem;
        gap: 1rem;
      }
    .wire-next--blockquote[data-size="lg"],
      .wire-next--blockquote[data-size="xl"] {
        --wire-blockquote-font-size: 1.25rem;
      }
    .wire-next__blockquote-citation {
        margin-inline-start: 0;
      }
    }
  }
}
```

---

## Breadcrumb

Showcase: https://component.wrnexusjs.dev/
Mount: <Breadcrumb /> (legacy: data-component="Breadcrumb")
Category: navigation
Purpose: Show responsive hierarchical navigation with home support, separators, current-page state, sizes, and selection events.
Props: label: string = "Breadcrumb", items: unknown[] = [], active: string = "", separator: string = "chevron", showHome: boolean = false, homeLabel: string = "Home", homeHref: string = "/", homeIcon: string = "icon-[lucide--house]", size: string = "default", color: string = "primary", variant: string = "minimal", class: string = ""
Slots: none
Events: select

### Complete .wrn source contract

```wrn
component Breadcrumb {
  outputs {
    select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
  }

  props {
label: string = "Breadcrumb"
    items: unknown[] = []
    active: string = ""
    separator: string = "chevron"
    showHome: boolean = false
    homeLabel: string = "Home"
    homeHref: string = "/"
    homeIcon: string = "icon-[lucide--house]"
    size: string = "default"
    color: string = "primary"
    variant: string = "minimal"
    class: string = ""
  }

  view {
    <nav
      {...attrs}
      data-ui-component="Breadcrumb"
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      aria-label='{label}'
      class='wire-breadcrumb {class}'
    >
      <ol class="wire-breadcrumb__list">
        {#if showHome}
          <li class="wire-breadcrumb__item wire-breadcrumb__item--home">
            <a
              href='{homeHref || "/"}'
              class="wire-breadcrumb__link wire-breadcrumb__home"
              @click='output.select({ item: { label: homeLabel, href: homeHref || "/", value: "home" }, itemIndex: -1 })'
            >
              {#if homeIcon === "icon-[lucide--house]"}
                <span class="icon-[lucide--house] wire-breadcrumb__icon" aria-hidden="true"></span>
              {:else if homeIcon}
                <span class='{homeIcon} wire-breadcrumb__icon' aria-hidden="true"></span>
              {/if}

              <span class="wire-breadcrumb__home-label">{homeLabel}</span>
            </a>
          </li>
        {/if}

        {#each items as item, itemIndex}
          <li
            class="wire-breadcrumb__item"
            data-current='{item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1) ? "true" : "false"}'
          >
            {#if showHome || itemIndex > 0}
              <span class="wire-breadcrumb__separator" aria-hidden="true">
                {#if separator === "slash"}
                  <span>/</span>
                {:else if separator === "dot"}
                  <span>•</span>
                {:else if separator === "arrow"}
                  <span>→</span>
                {:else}
                  <span class="icon-[lucide--chevron-right] wire-breadcrumb__separator-icon"></span>
                {/if}
              </span>
            {/if}

            {#if item.href && !item.disabled && !(item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1))}
              <a
                href='{item.href}'
                target='{item.target || ""}'
                rel='{item.external ? "noopener noreferrer" : (item.rel || "")}'
                class="wire-breadcrumb__link"
                @click='output.select({ item: item, itemIndex: itemIndex })'
              >
                {#if item.icon}
                  <span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
                {/if}

                <span class="wire-breadcrumb__label">{item.label || item.title}</span>

                {#if item.external}
                  <span class="icon-[lucide--arrow-up-right] wire-breadcrumb__external" aria-hidden="true"></span>
                {/if}
              </a>
            {:else}
              <span
                class="wire-breadcrumb__current"
                aria-current='{item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1) ? "page" : "false"}'
                aria-disabled='{item.disabled ? "true" : "false"}'
              >
                {#if item.icon}
                  <span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
                {/if}

                <span class="wire-breadcrumb__label">{item.label || item.title}</span>
              </span>
            {/if}
          </li>
        {/each}
      </ol>
    </nav>
  }

  style {
    .wire-breadcrumb {
      --breadcrumb-accent: var(--wire-color-primary);
      --breadcrumb-soft: var(--wire-color-primary-soft);
      --breadcrumb-muted: var(--wire-color-primary-muted);
      --breadcrumb-text: var(--wire-color-primary-text);

      min-width: 0;
      color: var(--wire-color-text-muted);
    }

    .wire-breadcrumb[data-color="secondary"] {
      --breadcrumb-accent: var(--wire-color-secondary);
      --breadcrumb-soft: var(--wire-color-secondary-soft);
      --breadcrumb-muted: var(--wire-color-secondary-muted);
      --breadcrumb-text: var(--wire-color-secondary-text);
    }

    .wire-breadcrumb[data-color="info"] {
      --breadcrumb-accent: var(--wire-color-info);
      --breadcrumb-soft: var(--wire-color-info-soft);
      --breadcrumb-muted: var(--wire-color-info-muted);
      --breadcrumb-text: var(--wire-color-info-text);
    }

    .wire-breadcrumb[data-color="success"] {
      --breadcrumb-accent: var(--wire-color-success);
      --breadcrumb-soft: var(--wire-color-success-soft);
      --breadcrumb-muted: var(--wire-color-success-muted);
      --breadcrumb-text: var(--wire-color-success-text);
    }

    .wire-breadcrumb[data-color="warning"] {
      --breadcrumb-accent: var(--wire-color-warning);
      --breadcrumb-soft: var(--wire-color-warning-soft);
      --breadcrumb-muted: var(--wire-color-warning-muted);
      --breadcrumb-text: var(--wire-color-warning-text);
    }

    .wire-breadcrumb[data-color="danger"] {
      --breadcrumb-accent: var(--wire-color-danger);
      --breadcrumb-soft: var(--wire-color-danger-soft);
      --breadcrumb-muted: var(--wire-color-danger-muted);
      --breadcrumb-text: var(--wire-color-danger-text);
    }

    .wire-breadcrumb[data-variant="soft"] {
      width: fit-content;
      max-width: 100%;
      padding: 0.45rem 0.65rem;
      background: var(--breadcrumb-soft);
      border: 1px solid var(--breadcrumb-muted);
      border-radius: 0.75rem;
    }

    .wire-breadcrumb[data-variant="outline"] {
      width: fit-content;
      max-width: 100%;
      padding: 0.45rem 0.65rem;
      background: var(--wire-color-surface-raised);
      border: 1px solid var(--wire-color-border);
      border-radius: 0.75rem;
      box-shadow: var(--wire-shadow-sm);
    }

    .wire-breadcrumb[data-variant="contrast"] {
      color: rgba(255, 255, 255, 0.78);
    }

    .wire-breadcrumb__list {
      display: flex;
      align-items: center;
      gap: 0;
      min-width: 0;
      margin: 0;
      padding: 0;
      overflow-x: auto;
      list-style: none;
      scrollbar-width: none;
      white-space: nowrap;
    }

    .wire-breadcrumb__list::-webkit-scrollbar {
      display: none;
    }

    .wire-breadcrumb__item {
      display: inline-flex;
      align-items: center;
      min-width: 0;
      flex: 0 0 auto;
    }

    .wire-breadcrumb__separator {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      margin-inline: 0.45rem;
      color: var(--wire-color-text-subtle);
      font-size: 0.75rem;
      opacity: 0.72;
    }

    .wire-breadcrumb__separator-icon {
      width: 0.9rem;
      height: 0.9rem;
    }

    .wire-breadcrumb__link,
    .wire-breadcrumb__current {
      display: inline-flex;
      align-items: center;
      gap: 0.4rem;
      min-width: 0;
      min-height: 2rem;
      padding: 0.25rem 0.4rem;
      border-radius: 0.55rem;
      font-size: 0.8125rem;
      line-height: 1.25rem;
      text-decoration: none;
    }

    .wire-breadcrumb[data-size="sm"] .wire-breadcrumb__link,
    .wire-breadcrumb[data-size="sm"] .wire-breadcrumb__current {
      min-height: 1.75rem;
      font-size: 0.75rem;
    }

    .wire-breadcrumb[data-size="lg"] .wire-breadcrumb__link,
    .wire-breadcrumb[data-size="lg"] .wire-breadcrumb__current {
      min-height: 2.25rem;
      font-size: 0.875rem;
    }

    .wire-breadcrumb__link {
      color: var(--wire-color-text-muted);
      font-weight: 600;
      transition:
        color 160ms ease,
        background 160ms ease;
    }

    .wire-breadcrumb__link:hover {
      color: var(--breadcrumb-accent);
      background: var(--breadcrumb-soft);
    }

    .wire-breadcrumb__link:focus-visible {
      color: var(--breadcrumb-accent);
      outline: 2px solid var(--breadcrumb-accent);
      outline-offset: 2px;
    }

    .wire-breadcrumb__current {
      max-width: 22rem;
      color: var(--wire-color-text);
      font-weight: 750;
    }

    .wire-breadcrumb__item[data-current="true"] .wire-breadcrumb__current {
      color: var(--breadcrumb-text);
      background: var(--breadcrumb-soft);
    }

    .wire-breadcrumb__current[aria-disabled="true"] {
      opacity: 0.58;
    }

    .wire-breadcrumb__icon,
    .wire-breadcrumb__external {
      flex: 0 0 auto;
      width: 0.95rem;
      height: 0.95rem;
    }

    .wire-breadcrumb__home {
      color: var(--breadcrumb-accent);
    }

    .wire-breadcrumb__label {
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
    }

    .wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__link,
    .wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__current,
    .wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__home {
      color: inherit;
    }

    .wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__link:hover,
    .wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__item[data-current="true"] .wire-breadcrumb__current {
      color: #ffffff;
      background: rgba(255, 255, 255, 0.12);
    }

    .wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__separator {
      color: rgba(255, 255, 255, 0.62);
    }

    @media (max-width: 639px) {
      .wire-breadcrumb__home-label {
        display: none;
      }

      .wire-breadcrumb__current {
        max-width: 13rem;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .wire-breadcrumb__link {
        transition: none;
      }
    }
  }
}
```

---

## Button

Showcase: https://component.wrnexusjs.dev/
Mount: <Button /> (legacy: data-component="Button")
Category: base
Purpose: Theme-aware, responsive button component.
Props: label: string = "Button", loadingLabel: string = "Loading…", description: string = "", as: string = "", href: string = "", target: string = "", rel: string = "", type: string = "button", variant: string = "default", color: string = "primary", size: string = "default", disabled: boolean = false, loading: boolean = false, pill: boolean = false, fullWidth: boolean = false, icon: string = "", iconPosition: string = "start", ariaLabel: string = "", ariaPressed: string = "", ariaExpanded: string = "", ariaControls: string = "", title: string = "", autofocus: boolean = false, controlClass: string = "", class: string = ""
Slots: default
Events: click, focus, blur

### Complete .wrn source contract

```wrn
component Button {
  outputs {
    click(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
label: string = "Button"
    loadingLabel: string = "Loading…"
    description: string = ""
    as: string = ""
    href: string = ""
    target: string = ""
    rel: string = ""
    type: string = "button"
    variant: string = "default"
    color: string = "primary"
    size: string = "default"
    disabled: boolean = false
    loading: boolean = false
    pill: boolean = false
    fullWidth: boolean = false
    icon: string = ""
    iconPosition: string = "start"
    ariaLabel: string = ""
    ariaPressed: string = ""
    ariaExpanded: string = ""
    ariaControls: string = ""
    title: string = ""
    autofocus: boolean = false
    controlClass: string = ""
    class: string = ""
  }

  view {
    <span
      class="wire-action {fullWidth ? 'w-full' : ''} {class}"
    >
      {#if as === "a" || (as === "" && href)}
        <a
          {...attrs}
          href="{disabled || loading ? '' : href}"
          target="{target}"
          rel="{rel}"
          title="{title}"
          aria-label="{ariaLabel || ((size === 'icon' || size === 'icon-xs' || size === 'icon-sm' || size === 'icon-lg') ? label : '')}"
          aria-disabled="{disabled || loading}"
          aria-busy="{loading}"
          aria-pressed="{ariaPressed}"
          aria-expanded="{ariaExpanded}"
          aria-controls="{ariaControls}"
          tabindex="{disabled || loading ? '-1' : '0'}"
          class="wire-btn wire-btn--variant-{variant} wire-btn--color-{color} wire-btn--size-{size} {pill ? 'wire-btn--pill' : ''} {description ? 'wire-btn--with-description' : ''} {fullWidth ? 'w-full' : ''} {controlClass}"
        >
          {#if loading}
            <span
              class="wire-spinner wire-spinner--inline"
              aria-hidden="true"
            >
            </span>
          {/if}
          {#if icon && !loading && iconPosition === "start"}
            <span
              class="wire-btn__icon {icon}"
              aria-hidden="true"
            >
            </span>
          {/if}
          <span
            class="wire-btn__copy {(size === 'icon' || size === 'icon-xs' || size === 'icon-sm' || size === 'icon-lg') ? 'wire-visually-hidden' : ''}"
          >
            <span
              class="wire-btn__label"
            >
              <slot>{loading ? loadingLabel : label}</slot>
            </span>
            {#if description && !loading}
              <span
                class="wire-btn__description"
              >
                {description}
              </span>
            {/if}
          </span>
          {#if icon && !loading && iconPosition === "end"}
            <span
              class="wire-btn__icon {icon}"
              aria-hidden="true"
            >
            </span>
          {/if}
          {#if (size === "icon" || size === "icon-xs" || size === "icon-sm" || size === "icon-lg") && (ariaLabel || label)}
            <span
              class="wire-btn__tooltip"
              role="tooltip"
              aria-hidden="true"
            >
              {ariaLabel || label}
            </span>
          {/if}
        </a>
      {:else}
        <button
          {...attrs}
          type="{type}"
          title="{title}"
          autofocus="{autofocus}"
          disabled="{disabled || loading}"
          aria-label="{ariaLabel || ((size === 'icon' || size === 'icon-xs' || size === 'icon-sm' || size === 'icon-lg') ? label : '')}"
          aria-busy="{loading}"
          aria-pressed="{ariaPressed}"
          aria-expanded="{ariaExpanded}"
          aria-controls="{ariaControls}"
          class="wire-btn wire-btn--variant-{variant} wire-btn--color-{color} wire-btn--size-{size} {pill ? 'wire-btn--pill' : ''} {description ? 'wire-btn--with-description' : ''} {fullWidth ? 'w-full' : ''} {controlClass}"
        >
          {#if loading}
            <span
              class="wire-spinner wire-spinner--inline"
              aria-hidden="true"
            >
            </span>
          {/if}
          {#if icon && !loading && iconPosition === "start"}
            <span
              class="wire-btn__icon {icon}"
              aria-hidden="true"
            >
            </span>
          {/if}
          <span
            class="wire-btn__copy {(size === 'icon' || size === 'icon-xs' || size === 'icon-sm' || size === 'icon-lg') ? 'wire-visually-hidden' : ''}"
          >
            <span
              class="wire-btn__label"
            >
              <slot>{loading ? loadingLabel : label}</slot>
            </span>
            {#if description && !loading}
              <span
                class="wire-btn__description"
              >
                {description}
              </span>
            {/if}
          </span>
          {#if icon && !loading && iconPosition === "end"}
            <span
              class="wire-btn__icon {icon}"
              aria-hidden="true"
            >
            </span>
          {/if}
          {#if (size === "icon" || size === "icon-xs" || size === "icon-sm" || size === "icon-lg") && (ariaLabel || label)}
            <span
              class="wire-btn__tooltip"
              role="tooltip"
              aria-hidden="true"
            >
              {ariaLabel || label}
            </span>
          {/if}
        </button>
      {/if}
    </span>
  }

  style {
    .wire-spinner {
      display: inline-block;
      width: 1.15rem;
      height: 1.15rem;
      border: 2px solid var(--wire-color-border);
      border-top-color: var(--wire-color-primary);
      border-radius: 999px;
      animation: wire-spin 0.7s linear infinite;
    }

    .wire-spinner--inline {
      width: 1rem;
      height: 1rem;
    }

    @keyframes wire-spin {
      to {
        transform: rotate(360deg);
      }
    }

    /* --- Button --------------------------------------------------------------- */
    .wire-btn {
      position: relative;
      --wire-btn-color: var(--wire-color-primary);
      --wire-btn-contrast: var(--wire-color-primary-contrast);

      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 0.5rem;
      font: inherit;
      font-size: 0.875rem;
      font-weight: 600;
      line-height: 1;
      border: 1px solid transparent;
      border-radius: var(--wire-radius-sm);
      padding: 0 0.75rem;
      cursor: pointer;
      text-decoration: none;
      min-height: 2rem;
      transition:
        color var(--wire-motion-base) var(--wire-ease-standard),
        border-color var(--wire-motion-base) var(--wire-ease-standard),
        background var(--wire-motion-base) var(--wire-ease-standard),
        box-shadow var(--wire-motion-base) var(--wire-ease-standard),
        transform var(--wire-motion-base) var(--wire-ease-emphasized);
    }
    .wire-btn:active {
      transform: scale(0.98);
    }
    .wire-btn:focus-visible {
      outline: 2px solid var(--wire-color-primary);
      outline-offset: 2px;
    }
    .wire-btn--color-primary {
      --wire-btn-color: var(--wire-color-primary);
      --wire-btn-contrast: var(--wire-color-primary-contrast);
    }
    .wire-btn--color-secondary {
      --wire-btn-color: var(--wire-color-muted);
      --wire-btn-contrast: var(--wire-color-bg);
    }
    .wire-btn--color-success {
      --wire-btn-color: var(--wire-color-success);
      --wire-btn-contrast: var(--wire-color-primary-contrast);
    }
    .wire-btn--color-warning {
      --wire-btn-color: var(--wire-color-warning);
      --wire-btn-contrast: var(--wire-color-bg);
    }
    .wire-btn--color-danger {
      --wire-btn-color: var(--wire-color-danger);
      --wire-btn-contrast: var(--wire-color-primary-contrast);
    }
    .wire-btn--color-info {
      --wire-btn-color: var(--wire-color-info);
      --wire-btn-contrast: var(--wire-color-primary-contrast);
    }
    .wire-btn--variant-default,
    .wire-btn--variant-solid,
    .wire-btn--default,
    .wire-btn--solid,
    .wire-btn--primary {
      color: var(--wire-btn-contrast);
      background: var(--wire-btn-color);
      box-shadow:
        0 1px 2px color-mix(in srgb, black 20%, transparent),
        0 0 0 1px color-mix(in srgb, var(--wire-btn-color) 72%, transparent);
    }
    .wire-btn--variant-default:hover,
    .wire-btn--variant-solid:hover,
    .wire-btn--default:hover,
    .wire-btn--solid:hover,
    .wire-btn--primary:hover {
      background: color-mix(in srgb, var(--wire-btn-color) 84%, black);
      transform: translateY(-1px);
      box-shadow:
        0 0.35rem 0.9rem color-mix(in srgb, var(--wire-btn-color) 20%, transparent),
        0 0 0 1px color-mix(in srgb, var(--wire-btn-color) 82%, transparent);
    }
    .wire-btn--variant-outline,
    .wire-btn--outline {
      color: var(--wire-btn-color);
      background: transparent;
      border-color: color-mix(in srgb, var(--wire-btn-color) 55%, var(--wire-color-border));
    }
    .wire-btn--variant-outline:hover,
    .wire-btn--outline:hover {
      background: color-mix(in srgb, var(--wire-btn-color) 10%, transparent);
      border-color: var(--wire-btn-color);
    }
    .wire-btn--variant-destructive,
    .wire-btn--danger,
    .wire-btn--destructive {
      color: var(--wire-btn-color);
      background: color-mix(in srgb, var(--wire-btn-color) 12%, transparent);
      border-color: color-mix(in srgb, var(--wire-btn-color) 28%, transparent);
    }
    .wire-btn--danger,
    .wire-btn--destructive {
      --wire-btn-color: var(--wire-color-danger);
    }
    .wire-btn--variant-destructive:hover,
    .wire-btn--danger:hover,
    .wire-btn--destructive:hover {
      background: color-mix(in srgb, var(--wire-btn-color) 20%, transparent);
    }
    .wire-btn--variant-ghost,
    .wire-btn--ghost {
      background: transparent;
      color: var(--wire-btn-color);
    }
    .wire-btn--variant-ghost:hover,
    .wire-btn--ghost:hover {
      background: color-mix(in srgb, var(--wire-btn-color) 10%, transparent);
    }
    .wire-btn--variant-secondary,
    .wire-btn--secondary {
      color: var(--wire-color-text);
      background: color-mix(in srgb, var(--wire-btn-color) 10%, var(--wire-color-surface));
      border-color: color-mix(in srgb, var(--wire-btn-color) 24%, var(--wire-color-border));
      box-shadow: var(--wire-shadow-1);
    }
    .wire-btn--variant-secondary:hover,
    .wire-btn--secondary:hover {
      color: var(--wire-btn-color);
      border-color: color-mix(in srgb, var(--wire-btn-color) 55%, var(--wire-color-border));
      background: color-mix(in srgb, var(--wire-btn-color) 15%, var(--wire-color-surface));
      transform: translateY(-2px);
    }
    .wire-btn--variant-link,
    .wire-btn--link {
      min-height: auto;
      padding: 0;
      color: var(--wire-btn-color);
      background: transparent;
      border-color: transparent;
      border-radius: 0;
      text-underline-offset: 0.22em;
    }
    .wire-btn--variant-link:hover,
    .wire-btn--link:hover {
      text-decoration: underline;
    }
    .wire-btn:disabled,
    .wire-btn[aria-busy="true"] {
      cursor: not-allowed;
      opacity: 0.58;
      transform: none;
      box-shadow: none;
    }
    .wire-btn--size-xs,
    .wire-btn--xs {
      min-height: 1.5rem;
      padding-inline: 0.5rem;
      gap: 0.25rem;
      font-size: 0.75rem;
    }
    .wire-btn--size-sm,
    .wire-btn--sm {
      min-height: 2.125rem;
      padding-inline: 0.625rem;
      gap: 0.25rem;
      font-size: 0.8rem;
    }
    .wire-btn--size-default,
    .wire-btn--md {
      min-height: 2.5rem;
      padding-inline: 0.75rem;
      font-size: 0.875rem;
    }
    .wire-btn--size-lg,
    .wire-btn--lg {
      min-height: 2.75rem;
      padding-inline: 0.875rem;
      font-size: 0.925rem;
    }
    .wire-btn--size-icon,
    .wire-btn--size-icon-xs,
    .wire-btn--size-icon-sm,
    .wire-btn--size-icon-lg,
    .wire-btn--icon,
    .wire-btn--icon-xs,
    .wire-btn--icon-sm,
    .wire-btn--icon-lg {
      flex: none;
      padding: 0;
    }
    .wire-btn--size-icon,
    .wire-btn--icon {
      width: 2rem;
      height: 2rem;
    }
    .wire-btn--size-icon-xs,
    .wire-btn--icon-xs {
      width: 1.5rem;
      min-height: 1.5rem;
    }
    .wire-btn--size-icon-sm,
    .wire-btn--icon-sm {
      width: 1.75rem;
      min-height: 1.75rem;
    }
    .wire-btn--size-icon-lg,
    .wire-btn--icon-lg {
      width: 2.25rem;
      min-height: 2.25rem;
    }
    .wire-btn__icon {
      width: 1rem;
      height: 1rem;
      flex: none;
    }
    .wire-btn__label {
      font-weight: 650;
    }
    .wire-btn:hover > .wire-btn__tooltip,
    .wire-btn:focus-visible > .wire-btn__tooltip {
      opacity: 1;
      transform: translate(-50%, 0);
    }

    .wire-action {
      position: relative;
      display: inline-flex;
    }

    .wire-btn--pill {
      border-radius: 999px;
    }

    .wire-btn--with-description {
      min-height: 3.5rem;
      justify-content: flex-start;
      padding: 0.6rem 0.875rem;
      text-align: left;
    }

    .wire-btn__copy {
      display: grid;
      min-width: 0;
      gap: 0.15rem;
    }

    .wire-btn__description {
      color: currentColor;
      font-size: 0.75rem;
      font-weight: 450;
      line-height: 1.35;
      opacity: 0.74;
    }

    .wire-btn__tooltip {
      position: absolute;
      z-index: 80;
      inset-block-end: calc(100% + 0.5rem);
      inset-inline-start: 50%;
      width: max-content;
      max-width: min(16rem, calc(100vw - 2rem));
      padding: 0.4rem 0.6rem;
      color: var(--wire-color-bg);
      background: var(--wire-color-text);
      border: 1px solid color-mix(in srgb, var(--wire-color-text) 85%, var(--wire-color-border));
      border-radius: var(--wire-radius-sm);
      box-shadow: var(--wire-shadow-2);
      font-size: 0.75rem;
      font-weight: 600;
      line-height: 1.25;
      text-align: center;
      pointer-events: none;
      opacity: 0;
      transform: translate(-50%, 0.25rem);
      transition:
        opacity var(--wire-motion-fast) var(--wire-ease-standard),
        transform var(--wire-motion-fast) var(--wire-ease-standard);
    }
  }
}
```

---

## ButtonGroup

Showcase: https://component.wrnexusjs.dev/
Mount: <ButtonGroup /> (legacy: data-component="ButtonGroup")
Category: base
Purpose: Theme-aware, responsive button group component.
Props: items: unknown[] = [], value: string = "", size: string = "md", color: string = "primary", variant: string = "default", orientation: string = "horizontal", responsive: boolean = false, attached: boolean = true, selectable: boolean = false, toolbar: boolean = false, disabled: boolean = false, ariaLabel: string = "Button group", class: string = ""
Slots: default
Events: click, select, change

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component ButtonGroup {
  outputs {
    click(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    select(payload: { value: string | number | boolean | null | object; previousValue: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })
    change(payload: { value: string | number | boolean | null | object; previousValue: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })
  }

  props {
items: unknown[] = []
    value: string = ""
    size: string = "md"
    color: string = "primary"
    variant: string = "default"
    orientation: string = "horizontal"
    responsive: boolean = false
    attached: boolean = true
    selectable: boolean = false
    toolbar: boolean = false
    disabled: boolean = false
    ariaLabel: string = "Button group"
    class: string = ""
  }

  state selectedValue = value

  functions {
    shared function itemValue(item, index) {
      return item.value || item.label || String(index)
    }

    client function selectItem(item, index) {
      previousValue = selectedValue
      nextValue = itemValue(item, index)

      output.select({
        value: nextValue,
        previousValue: previousValue,
        item: item,
        index: index
      })

      if (selectable && previousValue !== nextValue) {
        selectedValue = nextValue
        output.change({
          value: nextValue,
          previousValue: previousValue,
          item: item,
          index: index
        })
      }
    }
  }

  view {
    <div
      {...attrs}
      class="wire-component wire-next--button-group {class}"
      role="{toolbar ? 'toolbar' : 'group'}"
      aria-label="{ariaLabel}"
      aria-orientation="{orientation}"
      data-size="{size}"
      data-color="{color}"
      data-variant="{variant}"
      data-orientation="{orientation}"
      data-responsive="{responsive}"
      data-attached="{attached}"
      data-selectable="{selectable}"
      data-disabled="{disabled}"
    >
      {#if items}
        {#each items as item, index}
          <button
            type="{item.type || 'button'}"
            class="wire-btn wire-btn--variant-{item.variant || variant} wire-btn--color-{item.color || color} wire-btn--size-{item.size || size}"
            disabled="{disabled || item.disabled}"
            aria-label="{item.ariaLabel || item.label}"
            aria-pressed="{selectable ? (selectedValue === (item.value || item.label || String(index)) ? 'true' : 'false') : ''}"
            data-value="{item.value || item.label || String(index)}"
            data-selected="{selectable && selectedValue === (item.value || item.label || String(index)) ? 'true' : 'false'}"
            @click="selectItem(item, index)"
          >
            {#if item.icon}
              <span class="wire-btn__icon {item.icon}" aria-hidden="true"></span>
            {/if}
            <span class="wire-btn__label">{item.label}</span>
          </button>
        {/each}
      {/if}
      <slot />
    </div>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--button-group {
      display: inline-flex;
      align-items: stretch;
      width: fit-content;
      max-width: 100%;
      isolation: isolate;
    }

    .wire-next--button-group[data-orientation="vertical"] {
      flex-direction: column;
    }

    .wire-next--button-group[data-attached="false"] {
      gap: 0.5rem;
    }

    .wire-next--button-group[data-disabled="true"] {
      cursor: not-allowed;
      opacity: 0.6;
    }

    .wire-next--button-group[data-attached="true"] > :is(.wire-btn, .wire-action) {
      position: relative;
      margin: 0;
    }
    .wire-next--button-group[data-attached="true"] > .wire-action {
      display: flex;
    }
    .wire-next--button-group[data-attached="true"] > :is(.wire-btn, .wire-action) :is(.wire-btn) {
      height: 100%;
    }
    .wire-next--button-group[data-attached="true"][data-orientation="horizontal"]
      > :is(.wire-btn, .wire-action):not(:first-child) {
      margin-inline-start: -1px;
    }
    .wire-next--button-group[data-attached="true"][data-orientation="horizontal"]
      > .wire-btn:not(:first-child):not(:last-child),
    .wire-next--button-group[data-attached="true"][data-orientation="horizontal"]
      > .wire-action:not(:first-child):not(:last-child)
      > .wire-btn {
      border-radius: 0;
    }
    .wire-next--button-group[data-attached="true"][data-orientation="horizontal"]
      > .wire-btn:first-child:not(:last-child),
    .wire-next--button-group[data-attached="true"][data-orientation="horizontal"]
      > .wire-action:first-child:not(:last-child)
      > .wire-btn {
      border-start-end-radius: 0;
      border-end-end-radius: 0;
    }
    .wire-next--button-group[data-attached="true"][data-orientation="horizontal"]
      > .wire-btn:last-child:not(:first-child),
    .wire-next--button-group[data-attached="true"][data-orientation="horizontal"]
      > .wire-action:last-child:not(:first-child)
      > .wire-btn {
      border-start-start-radius: 0;
      border-end-start-radius: 0;
    }
    .wire-next--button-group[data-attached="true"][data-orientation="vertical"]
      > :is(.wire-btn, .wire-action):not(:first-child) {
      margin-top: -1px;
    }
    .wire-next--button-group[data-attached="true"][data-orientation="vertical"]
      > .wire-btn:not(:first-child):not(:last-child),
    .wire-next--button-group[data-attached="true"][data-orientation="vertical"]
      > .wire-action:not(:first-child):not(:last-child)
      > .wire-btn {
      border-radius: 0;
    }
    .wire-next--button-group[data-attached="true"][data-orientation="vertical"]
      > .wire-btn:first-child:not(:last-child),
    .wire-next--button-group[data-attached="true"][data-orientation="vertical"]
      > .wire-action:first-child:not(:last-child)
      > .wire-btn {
      border-end-start-radius: 0;
      border-end-end-radius: 0;
    }
    .wire-next--button-group[data-attached="true"][data-orientation="vertical"]
      > .wire-btn:last-child:not(:first-child),
    .wire-next--button-group[data-attached="true"][data-orientation="vertical"]
      > .wire-action:last-child:not(:first-child)
      > .wire-btn {
      border-start-start-radius: 0;
      border-start-end-radius: 0;
    }
    .wire-next--button-group > :is(.wire-btn, .wire-action):focus-within,
    .wire-next--button-group > .wire-btn:focus-visible {
      z-index: 2;
    }
    .wire-next--button-group[data-selectable="true"] > .wire-btn[data-selected="true"] {
      z-index: 1;
      border-color: var(--wire-component-color);
      background: color-mix(in srgb, var(--wire-component-color) 16%, var(--wire-color-surface));
      color: var(--wire-component-color);
    }
    @media (max-width: 480px) {
      .wire-next--button-group[data-responsive="true"] > :is(.wire-btn, .wire-action) {
        display: flex;
        width: 100%;
        flex-direction: column;
      }

      .wire-next--button-group[data-responsive="true"] > .wire-action > .wire-btn {
        width: 100%;
      }

      .wire-next--button-group[data-responsive="true"][data-attached="true"]
        > :is(.wire-btn, .wire-action) {
        margin-top: -1px;
        margin-inline-start: 0;
      }

      .wire-next--button-group[data-responsive="true"][data-attached="true"]
        > :is(.wire-btn, .wire-action):first-child {
        margin-top: 0;
      }

      .wire-next--button-group[data-responsive="true"][data-attached="true"] > .wire-btn,
      .wire-next--button-group[data-responsive="true"][data-attached="true"]
        > .wire-action
        > .wire-btn {
        border-radius: 0;
      }

      .wire-next--button-group[data-responsive="true"][data-attached="true"] > .wire-btn:first-child,
      .wire-next--button-group[data-responsive="true"][data-attached="true"]
        > .wire-action:first-child
        > .wire-btn {
        border-start-start-radius: var(--wire-radius-sm);
        border-start-end-radius: var(--wire-radius-sm);
      }

      .wire-next--button-group[data-responsive="true"][data-attached="true"] > .wire-btn:last-child,
      .wire-next--button-group[data-responsive="true"][data-attached="true"]
        > .wire-action:last-child
        > .wire-btn {
        border-end-start-radius: var(--wire-radius-sm);
        border-end-end-radius: var(--wire-radius-sm);
      }
    }

  }
}
```

---

## CTASection

Showcase: https://component.wrnexusjs.dev/
Mount: <CTASection /> (legacy: data-component="CTASection")
Category: marketing
Purpose: Close a page or major section with conversion-focused copy, actions, and optional supporting visual content.
Props: eyebrow: string = "", title: string = "Ready to get started?", description: string = "", icon: string = "", align: string = "center", size: string = "default", color: string = "primary", variant: string = "solid", primaryLabel: string = "Get started", primaryHref: string = "#", primaryIcon: string = "", secondaryLabel: string = "", secondaryHref: string = "", secondaryIcon: string = "", backgroundImage: string = "", visualImage: string = "", visualAlt: string = "", visualIcon: string = "", visualTitle: string = "", visualDescription: string = "", visualItems: unknown[] = [], visualPosition: string = "right", maxWidth: string = "xl", fullBleed: boolean = false, class: string = ""
Slots: default, actions, visual, footer
Events: none

### Complete .wrn source contract

```wrn
component CTASection {
  props {
    eyebrow: string = ""
    title: string = "Ready to get started?"
    description: string = ""
    icon: string = ""
    align: string = "center"
    size: string = "default"
    color: string = "primary"
    variant: string = "solid"
    primaryLabel: string = "Get started"
    primaryHref: string = "#"
    primaryIcon: string = ""
    secondaryLabel: string = ""
    secondaryHref: string = ""
    secondaryIcon: string = ""
    backgroundImage: string = ""
    visualImage: string = ""
    visualAlt: string = ""
    visualIcon: string = ""
    visualTitle: string = ""
    visualDescription: string = ""
    visualItems: unknown[] = []
    visualPosition: string = "right"
    maxWidth: string = "xl"
    fullBleed: boolean = false
    class: string = ""
  }

  view {
    <section
      {...attrs}
      data-ui-component="CTASection"
      data-align='{align}'
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      data-max-width='{maxWidth}'
      data-full-bleed='{fullBleed ? "true" : "false"}'
      data-visual-position='{visualPosition}'
      class='wire-cta-section {class}'
    >
      <div class="wire-cta-section__outer">
        <div class="wire-cta-section__surface">
          {#if backgroundImage}
            <img
              src='{backgroundImage}'
              alt=""
              aria-hidden="true"
              class="wire-cta-section__background-image"
            />
          {/if}

          <div class="wire-cta-section__glow wire-cta-section__glow--one" aria-hidden="true"></div>
          <div class="wire-cta-section__glow wire-cta-section__glow--two" aria-hidden="true"></div>

          <div class="wire-cta-section__layout">
            <div class="wire-cta-section__content">
              {#if icon}
                <div class="wire-cta-section__icon" aria-hidden="true">
                  <span class='{icon}'></span>
                </div>
              {/if}

              {#if eyebrow}
                <p class="wire-cta-section__eyebrow">{eyebrow}</p>
              {/if}

              <h2 class="wire-cta-section__title">{title}</h2>

              {#if description}
                <p class="wire-cta-section__description">{description}</p>
              {/if}

              <slot></slot>

              {#if primaryLabel || secondaryLabel}
                <div class="wire-cta-section__actions">
                  {#if primaryLabel}
                    <a href='{primaryHref || "#"}' class="wire-cta-section__action wire-cta-section__action--primary">
                      {#if primaryIcon}
                        <span class='wire-cta-section__action-icon {primaryIcon}' aria-hidden="true"></span>
                      {/if}
                      <span>{primaryLabel}</span>
                    </a>
                  {/if}

                  {#if secondaryLabel}
                    <a href='{secondaryHref || "#"}' class="wire-cta-section__action wire-cta-section__action--secondary">
                      {#if secondaryIcon}
                        <span class='wire-cta-section__action-icon {secondaryIcon}' aria-hidden="true"></span>
                      {/if}
                      <span>{secondaryLabel}</span>
                    </a>
                  {/if}

                  <slot name="actions"></slot>
                </div>
              {:else}
                <div class="wire-cta-section__actions wire-cta-section__actions--slot">
                  <slot name="actions"></slot>
                </div>
              {/if}
            </div>

            <div class="wire-cta-section__visual-column">
              <slot name="visual"></slot>

              {#if visualImage || visualIcon || visualTitle || visualDescription || visualItems.length > 0}
                <div class="wire-cta-section__visual">
                  {#if visualImage}
                    <div class="wire-cta-section__visual-media">
                      <img src='{visualImage}' alt='{visualAlt}' class="wire-cta-section__visual-image" />
                    </div>
                  {/if}

                  {#if visualIcon || visualTitle || visualDescription}
                    <div class="wire-cta-section__visual-header">
                      {#if visualIcon}
                        <div class="wire-cta-section__visual-icon" aria-hidden="true">
                          <span class='{visualIcon}'></span>
                        </div>
                      {/if}

                      <div class="wire-cta-section__visual-copy">
                        {#if visualTitle}
                          <h3 class="wire-cta-section__visual-title">{visualTitle}</h3>
                        {/if}

                        {#if visualDescription}
                          <p class="wire-cta-section__visual-description">{visualDescription}</p>
                        {/if}
                      </div>
                    </div>
                  {/if}

                  {#if visualItems.length > 0}
                    <div class="wire-cta-section__visual-list">
                      {#each visualItems as item}
                        <div class="wire-cta-section__visual-item">
                          {#if item.icon}
                            <span class='wire-cta-section__visual-item-icon {item.icon}' aria-hidden="true"></span>
                          {/if}
                          <div>
                            <strong>{item.label || item.title}</strong>
                            {#if item.description}
                              <p>{item.description}</p>
                            {/if}
                          </div>
                        </div>
                      {/each}
                    </div>
                  {/if}
                </div>
              {/if}
            </div>
          </div>

          <div class="wire-cta-section__footer">
            <slot name="footer"></slot>
          </div>
        </div>
      </div>
    </section>
  }

  style {
    .wire-cta-section {
      --cta-accent: var(--wire-color-primary);
      --cta-accent-hover: var(--wire-color-primary-hover);
      --cta-accent-soft: var(--wire-color-primary-soft);
      --cta-accent-muted: var(--wire-color-primary-muted);
      --cta-contrast: var(--wire-color-primary-contrast);
      --cta-border: color-mix(in srgb, var(--cta-accent) 15%, var(--wire-color-border));

      position: relative;
      width: 100%;
      color: var(--wire-color-text);
    }

    .wire-cta-section[data-color="secondary"] {
      --cta-accent: var(--wire-color-secondary);
      --cta-accent-hover: var(--wire-color-secondary-hover);
      --cta-accent-soft: var(--wire-color-secondary-soft);
      --cta-accent-muted: var(--wire-color-secondary-muted);
      --cta-contrast: var(--wire-color-secondary-contrast);
    }

    .wire-cta-section[data-color="info"] {
      --cta-accent: var(--wire-color-info);
      --cta-accent-hover: var(--wire-color-info-hover);
      --cta-accent-soft: var(--wire-color-info-soft);
      --cta-accent-muted: var(--wire-color-info-muted);
      --cta-contrast: var(--wire-color-info-contrast);
    }

    .wire-cta-section[data-color="success"] {
      --cta-accent: var(--wire-color-success);
      --cta-accent-hover: var(--wire-color-success-hover);
      --cta-accent-soft: var(--wire-color-success-soft);
      --cta-accent-muted: var(--wire-color-success-muted);
      --cta-contrast: var(--wire-color-success-contrast);
    }

    .wire-cta-section[data-color="warning"] {
      --cta-accent: var(--wire-color-warning);
      --cta-accent-hover: var(--wire-color-warning-hover);
      --cta-accent-soft: var(--wire-color-warning-soft);
      --cta-accent-muted: var(--wire-color-warning-muted);
      --cta-contrast: var(--wire-color-warning-contrast);
    }

    .wire-cta-section[data-color="danger"] {
      --cta-accent: var(--wire-color-danger);
      --cta-accent-hover: var(--wire-color-danger-hover);
      --cta-accent-soft: var(--wire-color-danger-soft);
      --cta-accent-muted: var(--wire-color-danger-muted);
      --cta-contrast: var(--wire-color-danger-contrast);
    }

    .wire-cta-section__outer {
      width: min(calc(100% - 2rem), 80rem);
      margin-inline: auto;
    }

    .wire-cta-section[data-max-width="compact"] .wire-cta-section__outer {
      width: min(calc(100% - 2rem), 64rem);
    }

    .wire-cta-section[data-max-width="lg"] .wire-cta-section__outer {
      width: min(calc(100% - 2rem), 72rem);
    }

    .wire-cta-section[data-max-width="wide"] .wire-cta-section__outer,
    .wire-cta-section[data-max-width="2xl"] .wire-cta-section__outer {
      width: min(calc(100% - 2rem), 90rem);
    }

    .wire-cta-section[data-max-width="full"] .wire-cta-section__outer {
      width: 100%;
      max-width: none;
    }

    .wire-cta-section[data-full-bleed="true"] .wire-cta-section__outer {
      width: 100%;
      max-width: none;
    }

    .wire-cta-section__surface {
      position: relative;
      isolation: isolate;
      overflow: hidden;
      padding: 3rem;
      background:
        radial-gradient(circle at 92% 12%, color-mix(in srgb, var(--cta-accent) 10%, transparent), transparent 28%),
        var(--wire-color-surface-raised);
      border: 1px solid var(--cta-border);
      border-radius: 1.5rem;
      box-shadow:
        0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
        0 24px 70px color-mix(in srgb, black 15%, transparent);
    }

    .wire-cta-section[data-size="compact"] .wire-cta-section__surface,
    .wire-cta-section[data-size="sm"] .wire-cta-section__surface {
      padding: 2rem;
    }

    .wire-cta-section[data-size="lg"] .wire-cta-section__surface {
      padding: 4rem;
    }

    .wire-cta-section[data-full-bleed="true"] .wire-cta-section__surface {
      border-inline: 0;
      border-radius: 0;
    }

    .wire-cta-section[data-variant="soft"] .wire-cta-section__surface {
      background:
        radial-gradient(circle at 92% 12%, color-mix(in srgb, var(--cta-accent) 14%, transparent), transparent 30%),
        linear-gradient(135deg, var(--cta-accent-soft), transparent 64%),
        var(--wire-color-surface-raised);
      box-shadow: none;
    }

    .wire-cta-section[data-variant="outline"] .wire-cta-section__surface {
      background: transparent;
      box-shadow: none;
    }

    .wire-cta-section[data-variant="gradient"] .wire-cta-section__surface {
      color: var(--cta-contrast);
      background:
        radial-gradient(circle at 90% 8%, color-mix(in srgb, white 16%, transparent), transparent 30%),
        linear-gradient(135deg, var(--cta-accent), color-mix(in srgb, var(--cta-accent) 70%, var(--wire-color-secondary)));
      border-color: color-mix(in srgb, white 22%, transparent);
    }

    .wire-cta-section[data-variant="solid"] .wire-cta-section__surface {
      color: var(--cta-contrast);
      background:
        radial-gradient(circle at 90% 8%, color-mix(in srgb, white 14%, transparent), transparent 30%),
        var(--cta-accent);
      border-color: color-mix(in srgb, white 22%, transparent);
      box-shadow: 0 30px 80px color-mix(in srgb, var(--cta-accent) 28%, transparent);
    }

    .wire-cta-section__background-image {
      position: absolute;
      inset: 0;
      z-index: -3;
      width: 100%;
      height: 100%;
      object-fit: cover;
      opacity: 0.14;
    }

    .wire-cta-section__glow {
      position: absolute;
      z-index: -2;
      width: 22rem;
      height: 22rem;
      pointer-events: none;
      background: var(--cta-accent);
      border-radius: 999px;
      filter: blur(110px);
      opacity: 0.1;
    }

    .wire-cta-section__glow--one {
      top: -12rem;
      right: -7rem;
    }

    .wire-cta-section__glow--two {
      bottom: -14rem;
      left: -8rem;
      opacity: 0.06;
    }

    .wire-cta-section__layout {
      display: grid;
      grid-template-columns: minmax(0, 1fr);
      align-items: center;
      gap: 2.5rem;
    }

    .wire-cta-section__content,
    .wire-cta-section__visual-column {
      min-width: 0;
    }

    .wire-cta-section__content {
      max-width: 48rem;
    }

    .wire-cta-section[data-align="center"] .wire-cta-section__content {
      margin-inline: auto;
      text-align: center;
    }

    .wire-cta-section[data-align="right"] .wire-cta-section__content {
      margin-left: auto;
      text-align: right;
    }

    .wire-cta-section__icon {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 3rem;
      height: 3rem;
      margin-bottom: 1.1rem;
      color: var(--cta-accent);
      background: var(--cta-accent-soft);
      border: 1px solid color-mix(in srgb, var(--cta-accent) 20%, transparent);
      border-radius: 0.9rem;
      font-size: 1.35rem;
    }

    .wire-cta-section[data-variant="solid"] .wire-cta-section__icon,
    .wire-cta-section[data-variant="gradient"] .wire-cta-section__icon {
      color: currentColor;
      background: color-mix(in srgb, white 14%, transparent);
      border-color: color-mix(in srgb, white 22%, transparent);
    }

    .wire-cta-section__eyebrow {
      margin: 0;
      color: var(--cta-accent);
      font-size: 0.72rem;
      font-weight: 650;
      letter-spacing: 0.16em;
      text-transform: uppercase;
    }

    .wire-cta-section[data-variant="solid"] .wire-cta-section__eyebrow,
    .wire-cta-section[data-variant="gradient"] .wire-cta-section__eyebrow {
      color: currentColor;
      opacity: 0.78;
    }

    .wire-cta-section__title {
      max-width: 18ch;
      margin: 0.75rem 0 0;
      color: var(--wire-color-text);
      font-size: clamp(2rem, 5vw, 4rem);
      font-weight: 650;
      line-height: 1.05;
      letter-spacing: -0.045em;
    }

    .wire-cta-section[data-align="center"] .wire-cta-section__title,
    .wire-cta-section[data-align="right"] .wire-cta-section__title {
      margin-inline: auto;
    }

    .wire-cta-section[data-align="right"] .wire-cta-section__title {
      margin-right: 0;
    }

    .wire-cta-section[data-variant="solid"] .wire-cta-section__title,
    .wire-cta-section[data-variant="gradient"] .wire-cta-section__title {
      color: currentColor;
    }

    .wire-cta-section__description {
      max-width: 44rem;
      margin: 1rem 0 0;
      color: var(--wire-color-text-muted);
      font-size: 0.96rem;
      line-height: 1.75;
    }

    .wire-cta-section[data-align="center"] .wire-cta-section__description,
    .wire-cta-section[data-align="right"] .wire-cta-section__description {
      margin-inline: auto;
    }

    .wire-cta-section[data-align="right"] .wire-cta-section__description {
      margin-right: 0;
    }

    .wire-cta-section[data-variant="solid"] .wire-cta-section__description,
    .wire-cta-section[data-variant="gradient"] .wire-cta-section__description {
      color: color-mix(in srgb, currentColor 82%, transparent);
    }

    .wire-cta-section__actions {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 0.75rem;
      margin-top: 1.75rem;
    }

    .wire-cta-section__actions--slot:empty {
      display: none;
    }

    .wire-cta-section[data-align="center"] .wire-cta-section__actions {
      justify-content: center;
    }

    .wire-cta-section[data-align="right"] .wire-cta-section__actions {
      justify-content: flex-end;
    }

    .wire-cta-section__action {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 0.55rem;
      min-height: 2.85rem;
      padding: 0.78rem 1.1rem;
      border-radius: 0.75rem;
      font-size: 0.84rem;
      font-weight: 700;
      line-height: 1;
      text-decoration: none;
      transition:
        transform 160ms ease,
        background-color 160ms ease,
        color 160ms ease,
        border-color 160ms ease;
    }

    .wire-cta-section__action:hover {
      transform: translateY(-2px);
    }

    .wire-cta-section__action--primary {
      color: var(--cta-contrast);
      background: var(--cta-accent);
      border: 1px solid transparent;
    }

    .wire-cta-section__action--primary:hover {
      background: var(--cta-accent-hover);
    }

    .wire-cta-section__action--secondary {
      color: var(--cta-accent);
      background: transparent;
      border: 1px solid color-mix(in srgb, var(--cta-accent) 42%, var(--wire-color-border));
    }

    .wire-cta-section__action--secondary:hover {
      background: var(--cta-accent-soft);
      border-color: var(--cta-accent);
    }

    .wire-cta-section[data-variant="solid"] .wire-cta-section__action--primary,
    .wire-cta-section[data-variant="gradient"] .wire-cta-section__action--primary {
      color: var(--cta-accent);
      background: var(--cta-contrast);
    }

    .wire-cta-section[data-variant="solid"] .wire-cta-section__action--secondary,
    .wire-cta-section[data-variant="gradient"] .wire-cta-section__action--secondary {
      color: currentColor;
      border-color: color-mix(in srgb, white 46%, transparent);
    }

    .wire-cta-section[data-variant="solid"] .wire-cta-section__action--secondary:hover,
    .wire-cta-section[data-variant="gradient"] .wire-cta-section__action--secondary:hover {
      background: color-mix(in srgb, white 12%, transparent);
      border-color: color-mix(in srgb, white 68%, transparent);
    }

    .wire-cta-section__action-icon {
      width: 1rem;
      height: 1rem;
    }

    .wire-cta-section__action:focus-visible {
      outline: 2px solid var(--wire-color-focus);
      outline-offset: 3px;
    }

    .wire-cta-section__visual-column {
      position: relative;
      min-height: 0;
    }

    .wire-cta-section__visual {
      overflow: hidden;
      color: var(--wire-color-text);
      background: var(--wire-color-surface-raised);
      border: 1px solid var(--wire-color-border);
      border-radius: 1.2rem;
      box-shadow: 0 20px 54px color-mix(in srgb, black 18%, transparent);
    }

    .wire-cta-section__visual-media {
      aspect-ratio: 16 / 9;
      overflow: hidden;
      background: var(--wire-color-surface-soft);
    }

    .wire-cta-section__visual-image {
      display: block;
      width: 100%;
      height: 100%;
      object-fit: cover;
    }

    .wire-cta-section__visual-header {
      display: grid;
      grid-template-columns: auto minmax(0, 1fr);
      gap: 0.9rem;
      padding: 1.25rem;
    }

    .wire-cta-section__visual-icon {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 2.6rem;
      height: 2.6rem;
      color: var(--cta-accent);
      background: var(--cta-accent-soft);
      border-radius: 0.82rem;
      font-size: 1.15rem;
    }

    .wire-cta-section__visual-copy {
      min-width: 0;
    }

    .wire-cta-section__visual-title {
      margin: 0;
      color: var(--wire-color-text);
      font-size: 1rem;
      font-weight: 650;
    }

    .wire-cta-section__visual-description {
      margin: 0.42rem 0 0;
      color: var(--wire-color-text-muted);
      font-size: 0.78rem;
      line-height: 1.6;
    }

    .wire-cta-section__visual-list {
      display: grid;
      border-top: 1px solid var(--wire-color-border);
    }

    .wire-cta-section__visual-item {
      display: grid;
      grid-template-columns: auto minmax(0, 1fr);
      gap: 0.7rem;
      padding: 0.95rem 1.25rem;
    }

    .wire-cta-section__visual-item + .wire-cta-section__visual-item {
      border-top: 1px solid var(--wire-color-border);
    }

    .wire-cta-section__visual-item-icon {
      width: 1rem;
      height: 1rem;
      margin-top: 0.12rem;
      color: var(--cta-accent);
    }

    .wire-cta-section__visual-item strong {
      color: var(--wire-color-text);
      font-size: 0.78rem;
    }

    .wire-cta-section__visual-item p {
      margin: 0.3rem 0 0;
      color: var(--wire-color-text-muted);
      font-size: 0.73rem;
      line-height: 1.5;
    }

    .wire-cta-section__footer:empty {
      display: none;
    }

    .wire-cta-section__footer:not(:empty) {
      margin-top: 2rem;
      padding-top: 1.25rem;
      border-top: 1px solid color-mix(in srgb, currentColor 14%, transparent);
    }

    @media (min-width: 900px) {
      .wire-cta-section[data-align="split"] .wire-cta-section__layout {
        grid-template-columns: minmax(0, 1.25fr) minmax(18rem, 0.75fr);
        gap: 4rem;
      }

      .wire-cta-section[data-align="split"][data-visual-position="left"] .wire-cta-section__content {
        order: 2;
      }

      .wire-cta-section[data-align="split"][data-visual-position="left"] .wire-cta-section__visual-column {
        order: 1;
      }

      .wire-cta-section[data-align="split"] .wire-cta-section__visual-column:empty {
        display: none;
      }

      .wire-cta-section[data-align="split"] .wire-cta-section__visual-column:empty + * {
        grid-column: 1 / -1;
      }
    }

    @media (max-width: 639px) {
      .wire-cta-section__surface {
        padding: 1.6rem;
        border-radius: 1.2rem;
      }

      .wire-cta-section__actions {
        align-items: stretch;
      }

      .wire-cta-section__action {
        width: 100%;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .wire-cta-section__action {
        transition: none;
      }

      .wire-cta-section__action:hover {
        transform: none;
      }
    }
  }
}
```

---

## Card

Showcase: https://component.wrnexusjs.dev/
Mount: <Card /> (legacy: data-component="Card")
Category: base
Purpose: Group related content in a responsive themed surface with title, description, content, and supporting slots.
Props: title: string = "Card title", subtitle: string = "", description: string = "", header: string = "", footer: string = "", imageSrc: string = "", imageAlt: string = "", imagePosition: string = "top", actionLabel: string = "", actionHref: string = "", headerActions: unknown[] = [], navigation: unknown[] = [], activeNav: string = "", mobileNavigation: boolean = false, alertTitle: string = "", alertDescription: string = "", empty: boolean = false, emptyTitle: string = "No data to show", emptyIcon: string = "icon-[lucide--inbox]", items: unknown[] = [], size: string = "md", color: string = "primary", variant: string = "default", layout: string = "vertical", align: string = "left", hover: string = "none", scrollable: boolean = false, maxHeight: string = "18rem", dismissible: boolean = false, ariaLabel: string = "", class: string = ""
Slots: default
Events: click, action, navigate, dismiss, load, error

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component Card {
  outputs {
    click(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    action(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    navigate(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    dismiss(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    load(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
  }

  props {
    title: string = "Card title"
    subtitle: string = ""
    description: string = ""
    header: string = ""
    footer: string = ""
    imageSrc: string = ""
    imageAlt: string = ""
    imagePosition: string = "top"
    actionLabel: string = ""
    actionHref: string = ""
    headerActions: unknown[] = []
    navigation: unknown[] = []
    activeNav: string = ""
    mobileNavigation: boolean = false
    alertTitle: string = ""
    alertDescription: string = ""
    empty: boolean = false
    emptyTitle: string = "No data to show"
    emptyIcon: string = "icon-[lucide--inbox]"
    items: unknown[] = []
    size: string = "md"
    color: string = "primary"
    variant: string = "default"
    layout: string = "vertical"
    align: string = "left"
    hover: string = "none"
    scrollable: boolean = false
    maxHeight: string = "18rem"
    dismissible: boolean = false
    ariaLabel: string = ""
    class: string = ""
  }

  state dismissed: boolean = false

  functions {
    // Outputs must go through output.*; a CustomEvent dispatched on the root
    // bubbles past every parent @binding without being seen, because the
    // runtime keeps parent handlers in a registry only the output proxy reads.
    client function dispatchCardNavigation(sourceEvent, item, index) {
      output.navigate({ component: "Card", item: item, index: index })
    }

    client function dispatchCardNavigationValue(sourceEvent) {
      output.navigate({
        component: "Card",
        value: sourceEvent.currentTarget.value
      })
    }

    client function dispatchCardHeaderAction(sourceEvent, action, index) {
      output.action({ component: "Card", action: action, index: index })
    }

    client function dismissCard() {
      dismissed = true
      output.dismiss({ component: "Card", title: title })
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="Card"
      data-wrn-card
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      data-layout='{layout}'
      data-hover='{hover}'
      data-align='{align}'
      class='wire-component wire-next--card wire-component--color-{color} wire-component--size-{size} wire-next--variant-{variant} {class}'
    >
      {#if items.length > 0}
        <div class="wire-next__card-group">
          {#each items as item, itemIndex}
            <article
              class="wire-next__card-panel"
              aria-label='{item.ariaLabel || item.title || item.label || "Card item"}'
              data-index='{itemIndex}'
            >
              {#if item.imageSrc || item.image}
                <div class="wire-next__card-media">
                  <img
                    src='{item.imageSrc || item.image}'
                    alt='{item.imageAlt || item.alt || ""}'
                    loading='{item.loading || "lazy"}'
                    decoding="async"
                    @load='output.load({ item: item, index: itemIndex })'
                    @error='output.error({ item: item, index: itemIndex })'
                  />
                </div>
              {/if}

              <div class="wire-next__card-content">
                {#if item.title || item.label}
                  <h3>{item.title || item.label}</h3>
                {/if}
                {#if item.subtitle}
                  <p class="wire-next__card-subtitle">{item.subtitle}</p>
                {/if}
                {#if item.description}
                  <p class="wire-next__card-description">{item.description}</p>
                {/if}
                {#if item.actionLabel || item.href}
                  <a
                    href='{item.actionHref || item.href || "#"}'
                    class="wire-next__card-action"
                    @click='output.action({ item: item, index: itemIndex })'
                  >
                    <span>{item.actionLabel || "Learn more"}</span>
                    <span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
                  </a>
                {/if}
              </div>
            </article>
          {/each}
        </div>
      {:else}
        <article
          class="wire-next__card-panel"
          aria-label='{ariaLabel || title || "Card"}'
          hidden='{dismissed}'
        >
          {#if imageSrc && imagePosition === "overlay"}
            <div class="wire-next__card-overlay">
              <img
                src='{imageSrc}'
                alt='{imageAlt}'
                loading="lazy"
                decoding="async"
                @load='output.load({ src: imageSrc })'
                @error='output.error({ src: imageSrc })'
              />
              <div class="wire-next__card-overlay-shade" aria-hidden="true"></div>
            </div>
          {/if}

          {#if imageSrc && (imagePosition === "top" || imagePosition === "left")}
            <div class="wire-next__card-media wire-next__card-media--{imagePosition}">
              <img
                src='{imageSrc}'
                alt='{imageAlt}'
                loading="lazy"
                decoding="async"
                @load='output.load({ src: imageSrc })'
                @error='output.error({ src: imageSrc })'
              />
            </div>
          {/if}

          <div class="wire-next__card-content">
            {#if header || title || subtitle || headerActions.length > 0 || dismissible}
              <header class="wire-next__card-header">
                <div class="wire-next__card-heading">
                  {#if header}
                    <p class="wire-next__card-eyebrow">{header}</p>
                  {/if}
                  {#if title}
                    <h3>{title}</h3>
                  {/if}
                  {#if subtitle}
                    <p class="wire-next__card-subtitle">{subtitle}</p>
                  {/if}
                </div>

                {#if headerActions.length > 0 || dismissible}
                  <div class="wire-next__card-header-actions">
                    {#each headerActions as headerAction, actionIndex}
                      <button
                        type="button"
                        aria-label='{headerAction.ariaLabel || headerAction.label || "Card action"}'
                        @click='dispatchCardHeaderAction(event, headerAction, actionIndex)'
                      >
                        {#if headerAction.icon}
                          <span class='{headerAction.icon}' aria-hidden="true"></span>
                        {/if}
                        {#if headerAction.label && !headerAction.icon}
                          <span>{headerAction.label}</span>
                        {/if}
                      </button>
                    {/each}

                    {#if dismissible}
                      <button
                        type="button"
                        aria-label="Dismiss card"
                        @click='dismissCard(event)'
                      >
                        <span class="icon-[lucide--x]" aria-hidden="true"></span>
                      </button>
                    {/if}
                  </div>
                {/if}
              </header>
            {/if}

            {#if navigation.length > 0}
              <nav aria-label='{ariaLabel || title || "Card navigation"}' class="wire-next__card-navigation">
                {#if mobileNavigation}
                  <select
                    class="wire-next__card-navigation-select"
                    aria-label='{ariaLabel || title || "Card navigation"}'
                    @change='dispatchCardNavigationValue(event)'
                  >
                    {#each navigation as navItem}
                      <option
                        value='{navItem.value || navItem.href || navItem.label}'
                        selected='{activeNav === navItem.value || activeNav === navItem.href}'
                      >
                        {navItem.label}
                      </option>
                    {/each}
                  </select>
                {/if}

                <div class="wire-next__card-tabs" data-mobile-navigation='{mobileNavigation}'>
                  {#each navigation as navItem, navIndex}
                    <button
                      type="button"
                      aria-pressed='{activeNav === navItem.value || activeNav === navItem.href}'
                      @click='dispatchCardNavigation(event, navItem, navIndex)'
                    >
                      {#if navItem.icon}
                        <span class='{navItem.icon}' aria-hidden="true"></span>
                      {/if}
                      <span>{navItem.label}</span>
                      {#if navItem.badge}
                        <span class="wire-next__card-tab-badge">{navItem.badge}</span>
                      {/if}
                    </button>
                  {/each}
                </div>
              </nav>
            {/if}

            {#if alertTitle || alertDescription}
              <div class="wire-next__card-alert" role="status">
                {#if alertTitle}
                  <strong>{alertTitle}</strong>
                {/if}
                {#if alertDescription}
                  <p>{alertDescription}</p>
                {/if}
              </div>
            {/if}

            <div
              class="wire-next__card-body"
              data-scrollable='{scrollable}'
              style='max-height: {scrollable ? maxHeight : "none"};'
            >
              {#if empty}
                <div class="wire-next__card-empty">
                  <span class='{emptyIcon}' aria-hidden="true"></span>
                  <p>{emptyTitle}</p>
                </div>
              {:else}
                {#if description}
                  <p class="wire-next__card-description">{description}</p>
                {/if}
                <slot></slot>
              {/if}
            </div>

            {#if footer || actionLabel}
              <footer class="wire-next__card-footer">
                {#if footer}
                  <span>{footer}</span>
                {/if}
                {#if actionLabel}
                  <a
                    href='{actionHref || "#"}'
                    class="wire-next__card-action"
                    @click='output.action({ href: actionHref, label: actionLabel })'
                  >
                    <span>{actionLabel}</span>
                    <span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
                  </a>
                {/if}
              </footer>
            {/if}
          </div>

          {#if imageSrc && (imagePosition === "bottom" || imagePosition === "right")}
            <div class="wire-next__card-media wire-next__card-media--{imagePosition}">
              <img
                src='{imageSrc}'
                alt='{imageAlt}'
                loading="lazy"
                decoding="async"
                @load='output.load({ src: imageSrc })'
                @error='output.error({ src: imageSrc })'
              />
            </div>
          {/if}
        </article>
      {/if}
    </div>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next__card-media--overlay {
      aspect-ratio: 16 / 9;
      min-height: 18rem;
    }
    .wire-next__card-media--overlay > img {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
    }
    @media (max-width: 480px) {
      .wire-next__card-media--overlay {
        aspect-ratio: 4 / 3;
        min-height: 16rem;
      }
    }

    .wire-next--card {
      --wire-card-color: var(--wire-component-color, var(--wire-color-primary));
      --wire-card-padding: 1.25rem;
      display: block;
      width: 100%;
      color: var(--wire-color-text);
    }

    .wire-next--card[data-size="sm"] {
      --wire-card-padding: 0.85rem;
    }

    .wire-next--card[data-size="lg"] {
      --wire-card-padding: 1.75rem;
    }

    .wire-next__card-panel {
      position: relative;
      display: flex;
      overflow: hidden;
      flex-direction: column;
      width: 100%;
      min-width: 0;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-md);
      background: var(--wire-color-surface);
      box-shadow: var(--wire-shadow-1);
      transition:
        transform var(--wire-motion-base) var(--wire-ease-emphasized),
        border-color var(--wire-motion-base) ease,
        box-shadow var(--wire-motion-base) var(--wire-ease-emphasized);
    }

    .wire-next--card[data-variant="top-border"] > .wire-next__card-panel,
    .wire-next--card[data-variant="accent"] > .wire-next__card-panel {
      border-top: 4px solid var(--wire-card-color);
    }

    .wire-next--card[data-hover="raise"] > .wire-next__card-panel:hover {
      border-color: color-mix(in srgb, var(--wire-card-color) 38%, var(--wire-color-border));
      box-shadow: var(--wire-shadow-3);
      transform: translateY(-4px);
    }

    .wire-next__card-header,
    .wire-next__card-footer {
      display: flex;
      gap: 1rem;
      align-items: center;
      justify-content: space-between;
      padding: 0.85rem var(--wire-card-padding);
      background: var(--wire-color-surface-2);
      color: var(--wire-color-muted);
      font-size: 0.875rem;
    }

    .wire-next__card-header {
      border-bottom: 1px solid var(--wire-color-border);
    }

    .wire-next__card-footer {
      margin-top: auto;
      border-top: 1px solid var(--wire-color-border);
    }

    .wire-next__card-header-actions {
      display: inline-flex;
      gap: 0.2rem;
      align-items: center;
    }

    .wire-next__card-header-actions button {
      display: inline-grid;
      width: 2rem;
      height: 2rem;
      border: 0;
      border-radius: var(--wire-radius-sm);
      background: transparent;
      color: var(--wire-color-muted);
      cursor: pointer;
      place-items: center;
    }

    .wire-next__card-header-actions button:hover {
      background: color-mix(in srgb, var(--wire-card-color) 12%, transparent);
      color: var(--wire-card-color);
    }

    .wire-next__card-header-actions button:focus-visible {
      outline: 2px solid var(--wire-card-color);
      outline-offset: 1px;
    }

    .wire-next__card-media {
      position: relative;
      overflow: hidden;
      min-height: 10rem;
      background: var(--wire-color-surface-2);
    }

    .wire-next__card-media > img {
      display: block;
      width: 100%;
      height: 100%;
      min-height: inherit;
      object-fit: cover;
      transition: transform 420ms var(--wire-ease-emphasized);
    }

    .wire-next--card[data-hover="image"] .wire-next__card-panel:hover .wire-next__card-media > img {
      transform: scale(1.06);
    }

    .wire-next__card-overlay {
      position: absolute;
      inset: 0;
      display: flex;
      flex-direction: column;
      gap: 0.55rem;
      padding: clamp(1.25rem, 4vw, 2rem);
      background: linear-gradient(
        180deg,
        rgb(2 6 23 / 0.9) 0%,
        rgb(2 6 23 / 0.68) 38%,
        rgb(2 6 23 / 0.34) 58%,
        rgb(2 6 23 / 0.86) 100%
      );
      color: white;
      text-shadow: 0 1px 2px rgb(0 0 0 / 0.42);
    }

    .wire-next__card-overlay h3 {
      color: white;
      font-size: clamp(1.1rem, 2.2vw, 1.35rem);
    }

    .wire-next__card-overlay p {
      max-width: 48rem;
      color: #fff;
      font-weight: 500;
      line-height: 1.6;
      text-shadow:
        0 1px 2px rgb(0 0 0 / 0.9),
        0 2px 8px rgb(0 0 0 / 0.62);
    }

    .wire-next__card-overlay .wire-next__card-subtitle {
      color: rgb(255 255 255 / 0.78);
    }

    .wire-next__card-overlay small {
      margin-top: auto;
      color: rgb(255 255 255 / 0.88);
      font-weight: 600;
      text-shadow: 0 1px 2px rgb(0 0 0 / 0.55);
    }

    .wire-next__card-body {
      display: grid;
      gap: 0.65rem;
      min-width: 0;
      padding: var(--wire-card-padding);
    }

    .wire-next__card-body[data-scrollable="true"] {
      max-height: var(--wire-card-max-height);
      overflow: auto;
      overscroll-behavior: contain;
      scrollbar-color: var(--wire-card-color) transparent;
      scrollbar-width: thin;
    }

    .wire-next__card-body h3,
    .wire-next__card-body p,
    .wire-next__card-overlay h3,
    .wire-next__card-overlay p {
      margin: 0;
    }

    .wire-next__card-body p {
      color: var(--wire-color-muted);
      line-height: 1.65;
    }

    .wire-next__card-subtitle {
      color: var(--wire-color-muted);
      font-size: 0.72rem;
      font-weight: 700;
      letter-spacing: 0.06em;
      text-transform: uppercase;
    }

    .wire-next__card-action {
      display: inline-flex;
      gap: 0.25rem;
      align-items: center;
      width: fit-content;
      margin-top: 0.2rem;
      color: var(--wire-card-color);
      font-weight: 700;
      text-decoration: none;
    }

    .wire-next__card-action:hover {
      text-decoration: underline;
      text-underline-offset: 0.2em;
    }

    .wire-next__card-navigation {
      background: var(--wire-color-surface-2);
    }

    .wire-next__card-tabs {
      display: flex;
      overflow-x: auto;
    }

    .wire-next__card-tabs button {
      min-height: 3rem;
      flex: 1 0 auto;
      padding-inline: 1rem;
      border: 0;
      border-bottom: 2px solid transparent;
      background: transparent;
      color: var(--wire-color-muted);
      cursor: pointer;
      font-weight: 700;
    }

    .wire-next__card-tabs button[data-active="true"] {
      border-color: var(--wire-card-color);
      color: var(--wire-color-text);
    }

    .wire-next__card-navigation-select {
      display: none;
      width: calc(100% - 2rem);
      min-height: 2.75rem;
      margin: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-sm);
      background: var(--wire-color-surface);
      color: var(--wire-color-text);
    }

    .wire-next__card-alert {
      display: flex;
      gap: 0.35rem;
      padding: 0.8rem var(--wire-card-padding);
      background: color-mix(in srgb, var(--wire-card-color) 14%, var(--wire-color-surface-2));
      color: var(--wire-color-text);
      font-size: 0.875rem;
    }

    .wire-next__card-empty {
      display: grid;
      min-height: 12rem;
      color: var(--wire-color-muted);
      place-content: center;
      place-items: center;
    }

    .wire-next__card-empty > span {
      margin-bottom: 0.75rem;
      font-size: 2.25rem;
    }

    .wire-next--card[data-align="center"] .wire-next__card-body {
      text-align: center;
      place-items: center;
    }

    .wire-next--card[data-layout="horizontal"] > .wire-next__card-panel {
      display: grid;
      grid-template-columns: minmax(12rem, 42%) minmax(0, 1fr);
    }

    .wire-next--card[data-layout="horizontal"] .wire-next__card-media {
      grid-row: 1 / span 4;
      min-height: 100%;
    }

    .wire-next__card-group {
      display: grid;
      grid-template-columns: repeat(var(--wire-card-columns, 3), minmax(0, 1fr));
    }

    .wire-next__card-group > .wire-next__card-panel {
      border-radius: 0;
      box-shadow: none;
    }

    .wire-next__card-group > .wire-next__card-panel:not(:first-child) {
      margin-inline-start: -1px;
    }

    .wire-next__card-group > .wire-next__card-panel:first-child {
      border-start-start-radius: var(--wire-radius-md);
      border-end-start-radius: var(--wire-radius-md);
    }

    .wire-next__card-group > .wire-next__card-panel:last-child {
      border-start-end-radius: var(--wire-radius-md);
      border-end-end-radius: var(--wire-radius-md);
    }

    @media (max-width: 768px) {
    .wire-next--card[data-layout="horizontal"] > .wire-next__card-panel {
        display: flex;
      }
    .wire-next--card[data-layout="horizontal"] .wire-next__card-media {
        min-height: 12rem;
      }
    .wire-next__card-group {
        grid-template-columns: 1fr;
      }
    .wire-next__card-group > .wire-next__card-panel:not(:first-child) {
        margin-top: -1px;
        margin-inline-start: 0;
      }
    .wire-next__card-group > .wire-next__card-panel {
        border-radius: 0;
      }
    .wire-next__card-group > .wire-next__card-panel:first-child {
        border-start-start-radius: var(--wire-radius-md);
        border-start-end-radius: var(--wire-radius-md);
      }
    .wire-next__card-group > .wire-next__card-panel:last-child {
        border-end-start-radius: var(--wire-radius-md);
        border-end-end-radius: var(--wire-radius-md);
      }
    }

    @media (max-width: 480px) {
    .wire-next__card-tabs:has(+ .wire-next__card-navigation-select) {
        display: none;
      }
    .wire-next__card-navigation-select {
        display: block;
      }
    }
  }
}
```

---

## Carousel

Showcase: https://component.wrnexusjs.dev/
Mount: <Carousel /> (legacy: data-component="Carousel")
Category: base
Purpose: Theme-aware, responsive carousel component.
Props: size: string = "default", color: string = "primary", title: string = "", description: string = "", items: unknown[] = [], activeIndex: number = 0, slidesPerView: number = 1, gap: string = "0.75rem", showPagination: boolean = false, isAutoPlay: boolean = false, autoplayInterval: number = 4000, isInfiniteLoop: boolean = false, isRTL: boolean = false, isCentered: boolean = false, isDraggable: boolean = false, isAutoHeight: boolean = false, isSnap: boolean = false, showCounter: boolean = false, thumbnails: string = "none", ariaLabel: string = "Content carousel", variant: string = "default", class: string = ""
Slots: default
Events: initialize, change, previous, next, play, pause, reachStart, reachEnd, dragStart, dragEnd

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

// Interactive, hydration-safe content carousel.
component Carousel {
  outputs {
    initialize(payload: { index: number; count: number })
    change(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object; reason: string | boolean })
    previous(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object })
    next(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object })
    play(payload: { index: number; interval: number })
    pause(payload: { index: number })
    reachStart(payload: { index: number })
    reachEnd(payload: { index: number })
    dragStart(payload: { index: number; x: null })
    dragEnd(payload: { index: number; distance: number })
  }

  props {
    size: string = "default"
    color: string = "primary"
    title: string = ""
    description: string = ""
    items: unknown[] = []
    activeIndex: number = 0
    slidesPerView: number = 1
    gap: string = "0.75rem"
    showPagination: boolean = false
    isAutoPlay: boolean = false
    autoplayInterval: number = 4000
    isInfiniteLoop: boolean = false
    isRTL: boolean = false
    isCentered: boolean = false
    isDraggable: boolean = false
    isAutoHeight: boolean = false
    isSnap: boolean = false
    showCounter: boolean = false
    thumbnails: string = "none"
    ariaLabel: string = "Content carousel"
    variant: string = "default"
    class: string = ""
  }

  state currentIndex = activeIndex
  state playing: boolean = false
  state dragOrigin = null
  state dragOffset: number = 0
  state autoplayTimer = null
  state carouselRoot = null

  functions {
    shared function slideCount() {
      return items.length
    }

    shared function maximumIndex(count, visible) {
      count = slideCount()
      visible = Math.max(1, Number(slidesPerView) || 1)
      if (isCentered || isSnap) {
        return Math.max(0, count - 1)
      }
      return Math.max(0, count - visible)
    }

    shared function normalizedIndex(index, maximum) {
      maximum = maximumIndex()
      if (slideCount() === 0) {
        return 0
      }
      if (isInfiniteLoop) {
        if (index < 0) {
          return maximum
        }
        if (index > maximum) {
          return 0
        }
      }
      return Math.max(0, Math.min(index, maximum))
    }

    client function rememberRoot(sourceEvent, root) {
      if (!sourceEvent || !sourceEvent.currentTarget) {
        return
      }
      root = sourceEvent.currentTarget.closest(".wire-next--carousel")
      if (root) {
        carouselRoot = root
      }
    }

    client function syncNavigation(sourceEvent, index, root, viewport, slides, slide, thumbnails, thumbnail, rail) {
      rememberRoot(sourceEvent)
      root = carouselRoot
      if (!root) {
        return
      }
      if (isSnap) {
        viewport = root.querySelector(".wire-next__carousel-viewport")
        slides = root.querySelectorAll(".wire-next__carousel-slide")
        slide = slides[index]
        if (viewport && slide) {
          viewport.scrollTo({
            left: slide.offsetLeft - (viewport.clientWidth - slide.clientWidth) / 2,
            behavior: "smooth"
          })
        }
      }
      thumbnails = root.querySelectorAll(".wire-next__carousel-thumbnails button")
      if (thumbnails[index]) {
        thumbnail = thumbnails[index]
        rail = thumbnail.parentElement
        rail.scrollTo({
          left: thumbnail.offsetLeft - (rail.clientWidth - thumbnail.clientWidth) / 2,
          top: thumbnail.offsetTop - (rail.clientHeight - thumbnail.clientHeight) / 2,
          behavior: "smooth"
        })
      }
    }

    client function handleSnapScroll(sourceEvent, slides, viewport, viewportCenter, closestIndex, closestDistance, slide, slideCenter, distance, previousIndex) {
      if (!isSnap) {
        return
      }
      rememberRoot(sourceEvent)
      viewport = sourceEvent.currentTarget
      slides = viewport.querySelectorAll(".wire-next__carousel-slide")
      viewportCenter = viewport.scrollLeft + viewport.clientWidth / 2
      closestIndex = currentIndex
      closestDistance = 1000000000
      slides.forEach(function (candidate, index) {
        slideCenter = candidate.offsetLeft + candidate.clientWidth / 2
        distance = Math.abs(slideCenter - viewportCenter)
        if (distance < closestDistance) {
          closestDistance = distance
          closestIndex = index
        }
      })
      closestIndex = normalizedIndex(closestIndex)
      if (closestIndex !== currentIndex) {
        previousIndex = currentIndex
        currentIndex = closestIndex
        output.change({
          index: currentIndex,
          previousIndex: previousIndex,
          item: items[currentIndex],
          reason: "snap"
        })
        slides = carouselRoot.querySelectorAll(".wire-next__carousel-thumbnails button")
        slide = slides[currentIndex]
        if (slide) {
          viewport = slide.parentElement
          viewport.scrollTo({
            left: slide.offsetLeft - (viewport.clientWidth - slide.clientWidth) / 2,
            top: slide.offsetTop - (viewport.clientHeight - slide.clientHeight) / 2,
            behavior: "smooth"
          })
        }
      }
    }

    client function selectSlide(index, reason, sourceEvent, previousIndex) {
      if (slideCount() === 0) {
        return
      }
      previousIndex = currentIndex
      currentIndex = normalizedIndex(Number(index))
      syncNavigation(sourceEvent, currentIndex)
      if (previousIndex === currentIndex && reason !== "initialize") {
        return
      }
      output.change({
        index: currentIndex,
        previousIndex: previousIndex,
        item: items[currentIndex],
        reason: reason || "select"
      })
      if (currentIndex === 0) {
        output.reachStart({ index: currentIndex })
      }
      if (currentIndex === maximumIndex()) {
        output.reachEnd({ index: currentIndex })
      }
    }

    client function previousSlide(sourceEvent, previousIndex) {
      previousIndex = currentIndex
      selectSlide(currentIndex - 1, "previous", sourceEvent)
      output.previous({
        index: currentIndex,
        previousIndex: previousIndex,
        item: items[currentIndex]
      })
    }

    client function nextSlide(sourceEvent, previousIndex) {
      previousIndex = currentIndex
      selectSlide(currentIndex + 1, "next", sourceEvent)
      output.next({
        index: currentIndex,
        previousIndex: previousIndex,
        item: items[currentIndex]
      })
    }

    client function handleKeydown(sourceEvent) {
      if (sourceEvent.key === "ArrowLeft") {
        sourceEvent.preventDefault()
        if (isRTL) {
          nextSlide(sourceEvent)
        } else {
          previousSlide(sourceEvent)
        }
      }
      if (sourceEvent.key === "ArrowRight") {
        sourceEvent.preventDefault()
        if (isRTL) {
          previousSlide(sourceEvent)
        } else {
          nextSlide(sourceEvent)
        }
      }
      if (sourceEvent.key === "Home") {
        sourceEvent.preventDefault()
        selectSlide(0, "keyboard", sourceEvent)
      }
      if (sourceEvent.key === "End") {
        sourceEvent.preventDefault()
        selectSlide(maximumIndex(), "keyboard", sourceEvent)
      }
    }

    client function beginDrag(sourceEvent) {
      if (!isDraggable || isSnap) {
        return
      }
      sourceEvent.preventDefault()
      dragOrigin = sourceEvent.clientX
      dragOffset = 0
      sourceEvent.currentTarget.setPointerCapture(sourceEvent.pointerId)
      output.dragStart({ index: currentIndex, x: dragOrigin })
    }

    client function moveDrag(sourceEvent) {
      if (!isDraggable || isSnap || dragOrigin === null) {
        return
      }
      sourceEvent.preventDefault()
      dragOffset = sourceEvent.clientX - dragOrigin
    }

    shared function cancelDrag() {
      dragOrigin = null
      dragOffset = 0
    }

    client function endDrag(sourceEvent, distance) {
      if (!isDraggable || isSnap || dragOrigin === null) {
        return
      }
      sourceEvent.preventDefault()
      distance = dragOffset || sourceEvent.clientX - dragOrigin
      dragOrigin = null
      dragOffset = 0
      if (Math.abs(distance) > 20) {
        if ((distance < 0 && !isRTL) || (distance > 0 && isRTL)) {
          nextSlide(sourceEvent)
        } else {
          previousSlide(sourceEvent)
        }
      }
      output.dragEnd({
        index: currentIndex,
        distance: distance
      })
    }

    shared function advanceAutoplay() {
      if (currentIndex >= maximumIndex()) {
        selectSlide(0, "autoplay")
      } else {
        selectSlide(currentIndex + 1, "autoplay")
      }
    }

    client function startAutoplay() {
      if (!isAutoPlay || playing || slideCount() < 2) {
        return
      }
      playing = true
      autoplayTimer = setInterval(advanceAutoplay, Math.max(1000, Number(autoplayInterval)))
      output.play({ index: currentIndex, interval: autoplayInterval })
    }

    client function pauseAutoplay() {
      if (autoplayTimer) {
        clearInterval(autoplayTimer)
      }
      autoplayTimer = null
      if (playing) {
        output.pause({ index: currentIndex })
      }
      playing = false
    }

  }

  lifecycle {
    mount {
      output.initialize({ index: currentIndex, count: slideCount() })
      startAutoplay()
    }
    unmount {
      if (autoplayTimer) {
        clearInterval(autoplayTimer)
      }
    }
  }

  view {
    <section
      {...attrs}
      class="wire-component wire-next--carousel wire-component--color-{color} wire-component--size-{size} wire-next--variant-{variant} {class}"
      data-rtl="{isRTL}"
      data-centered="{isCentered}"
      data-draggable="{isDraggable && !isSnap}"
      data-dragging="{dragOrigin !== null}"
      data-auto-height="{isAutoHeight}"
      data-snap="{isSnap}"
      data-thumbnails="{thumbnails}"
      dir="{isRTL ? 'rtl' : 'ltr'}"
      role="region"
      aria-roledescription="carousel"
      aria-label="{ariaLabel}"
      @keydown="handleKeydown(event)"
      @mouseenter="rememberRoot(event); pauseAutoplay()"
      @mouseleave="startAutoplay()"
      @focusin="rememberRoot(event); pauseAutoplay()"
      @focusout="startAutoplay()"
    >
      {#if title || description}
        <header class="wire-next__carousel-header">
          {#if title}<h3>{title}</h3>{/if}
          {#if description}<p>{description}</p>{/if}
        </header>
      {/if}

        <div class="wire-next__carousel-layout">
          {#if thumbnails === "vertical"}
            <div class="wire-next__carousel-thumbnails" aria-label="Choose a slide">
              {#each items as item, index}
                <button
                  type="button"
                  data-active="{index === currentIndex}"
                  aria-label="Show slide {index + 1}: {item.label || item.title}"
                  aria-current="{index === currentIndex ? 'true' : 'false'}"
                  @click="selectSlide(index, 'thumbnail', event)"
                >
                  {#if item.thumbnail}<img src="{item.thumbnail}" alt="" />{/if}
                  <span>{item.label || item.title || "Slide " + (index + 1)}</span>
                </button>
              {/each}
            </div>
          {/if}

          <div class="wire-next__carousel-main">
            <div class="wire-next__carousel-stage">
              <div
                class="wire-next__carousel-viewport"
                tabindex="0"
                @scroll="handleSnapScroll(event)"
                @pointerdown="beginDrag(event)"
                @pointermove="moveDrag(event)"
                @pointerup="endDrag(event)"
                @pointercancel="cancelDrag()"
                @lostpointercapture="cancelDrag()"
              >
                <div
                  class="wire-next__carousel-track"
                  style="--wire-carousel-index: {currentIndex}; --wire-carousel-per-view: {slidesPerView}; --wire-carousel-gap: {gap}; --wire-carousel-drag-offset: {dragOffset}px"
                >
                  {#each items as item, index}
                    <article
                      class="wire-next__carousel-slide"
                      data-active="{index === currentIndex}"
                      role="group"
                      aria-roledescription="slide"
                      aria-label="{index + 1} of {items.length}"
                      aria-hidden="{index === currentIndex ? 'false' : 'true'}"
                    >
                      {#if item.imageSrc}
                        <img src="{item.imageSrc}" alt="{item.imageAlt || item.title || ''}" />
                      {/if}
                      <div class="wire-next__carousel-slide-content">
                        {#if item.eyebrow}<span>{item.eyebrow}</span>{/if}
                        {#if item.title || item.label}<h4>{item.title || item.label}</h4>{/if}
                        {#if item.description}<p>{item.description}</p>{/if}
                        {#if item.actionLabel}
                          <a href="{item.actionHref || '#'}">{item.actionLabel}</a>
                        {/if}
                      </div>
                    </article>
                  {/each}
                  <slot />
                </div>
              </div>

              <button
                class="wire-next__carousel-control wire-next__carousel-control--previous"
                type="button"
                aria-label="Previous slide"
                disabled="{!isInfiniteLoop && currentIndex === 0}"
                @click="previousSlide(event)"
              >
                <span class="icon-[lucide--chevron-left]" aria-hidden="true"></span>
              </button>
              <button
                class="wire-next__carousel-control wire-next__carousel-control--next"
                type="button"
                aria-label="Next slide"
                disabled="{!isInfiniteLoop && currentIndex === maximumIndex()}"
                @click="nextSlide(event)"
              >
                <span class="icon-[lucide--chevron-right]" aria-hidden="true"></span>
              </button>

              {#if showCounter}
                <output class="wire-next__carousel-counter" aria-live="polite">
                  {currentIndex + 1} / {items.length}
                </output>
              {/if}
            </div>

            {#if showPagination}
              <div class="wire-next__carousel-pagination" aria-label="Choose a slide">
                {#each items as item, index}
                  <button
                    type="button"
                    data-active="{index === currentIndex}"
                    aria-label="Show slide {index + 1}"
                    aria-current="{index === currentIndex ? 'true' : 'false'}"
                    @click="selectSlide(index, 'pagination', event)"
                  ></button>
                {/each}
              </div>
            {/if}

            {#if thumbnails === "horizontal"}
              <div class="wire-next__carousel-thumbnails" aria-label="Choose a slide">
                {#each items as item, index}
                  <button
                    type="button"
                    data-active="{index === currentIndex}"
                    aria-label="Show slide {index + 1}: {item.label || item.title}"
                    aria-current="{index === currentIndex ? 'true' : 'false'}"
                    @click="selectSlide(index, 'thumbnail', event)"
                  >
                    {#if item.thumbnail}<img src="{item.thumbnail}" alt="" />{/if}
                    <span>{item.label || item.title || "Slide " + (index + 1)}</span>
                  </button>
                {/each}
              </div>
            {/if}
          </div>
        </div>
    </section>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--carousel {
      --wire-carousel-color: var(--wire-component-color, var(--wire-color-primary));
      display: grid;
      width: 100%;
      min-width: 0;
      gap: 1rem;
      padding: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-md);
      color: var(--wire-color-text);
      background: var(--wire-color-surface);
      box-shadow: var(--wire-shadow-1);
    }

    .wire-next__carousel-header h3,
    .wire-next__carousel-header p {
      margin: 0;
    }

    .wire-next__carousel-header p {
      margin-top: 0.35rem;
      color: var(--wire-color-text-muted);
    }

    .wire-next__carousel-layout {
      display: flex;
      min-width: 0;
      gap: 0.75rem;
    }

    .wire-next__carousel-main {
      display: grid;
      flex: 1;
      min-width: 0;
      gap: 0.75rem;
    }

    .wire-next__carousel-stage {
      position: relative;
      min-width: 0;
    }

    .wire-next__carousel-viewport {
      position: relative;
      overflow: hidden;
      min-width: 0;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-md);
      background: var(--wire-color-surface-subtle, var(--wire-color-surface));
      outline: none;
    }

    .wire-next__carousel-viewport:focus-visible {
      box-shadow: 0 0 0 3px color-mix(in srgb, var(--wire-carousel-color) 30%, transparent);
    }

    .wire-next__carousel-track {
      display: flex;
      align-items: stretch;
      gap: var(--wire-carousel-gap);
      transform: translateX(
        calc(
          var(--wire-carousel-index) * -1 *
            (
              (100% - (var(--wire-carousel-per-view) - 1) * var(--wire-carousel-gap)) /
                var(--wire-carousel-per-view) + var(--wire-carousel-gap)
            )
        )
      );
      transition: transform var(--wire-motion-base) var(--wire-ease-emphasized);
      translate: var(--wire-carousel-drag-offset, 0) 0;
      will-change: transform;
    }

    .wire-next--carousel[data-dragging="true"] .wire-next__carousel-track {
      transition: none;
    }

    .wire-next--carousel[data-rtl="true"] .wire-next__carousel-track {
      transform: translateX(
        calc(
          var(--wire-carousel-index) *
            (
              (100% - (var(--wire-carousel-per-view) - 1) * var(--wire-carousel-gap)) /
                var(--wire-carousel-per-view) + var(--wire-carousel-gap)
            )
        )
      );
    }

    .wire-next__carousel-slide {
      position: relative;
      display: grid;
      flex: 0 0
        calc(
          (100% - (var(--wire-carousel-per-view) - 1) * var(--wire-carousel-gap)) /
            var(--wire-carousel-per-view)
        );
      overflow: hidden;
      min-width: 0;
      min-height: 20rem;
      place-items: center;
      background: var(--wire-color-surface-raised, var(--wire-color-surface));
    }

    .wire-next--carousel[data-auto-height="true"] .wire-next__carousel-slide {
      min-height: 0;
    }

    .wire-next--carousel[data-centered="true"] .wire-next__carousel-slide {
      flex-basis: min(78%, 32rem);
    }

    .wire-next--carousel[data-centered="true"] .wire-next__carousel-track {
      box-sizing: border-box;
      transform: translateX(
        calc(var(--wire-carousel-index) * -1 * (min(78%, 32rem) + var(--wire-carousel-gap)))
      );
    }

    .wire-next--carousel[data-centered="true"] .wire-next__carousel-slide:first-child {
      margin-inline-start: calc((100% - min(78%, 32rem)) / 2);
    }

    .wire-next--carousel[data-centered="true"] .wire-next__carousel-slide:last-child {
      margin-inline-end: calc((100% - min(78%, 32rem)) / 2);
    }

    .wire-next--carousel[data-centered="true"][data-rtl="true"] .wire-next__carousel-track {
      transform: translateX(
        calc(var(--wire-carousel-index) * (min(78%, 32rem) + var(--wire-carousel-gap)))
      );
    }

    .wire-next__carousel-slide > img {
      position: absolute;
      width: 100%;
      height: 100%;
      object-fit: cover;
      inset: 0;
    }

    .wire-next__carousel-slide-content {
      position: relative;
      z-index: 1;
      width: min(100%, 38rem);
      padding: clamp(2rem, 8vw, 5rem);
      text-align: center;
    }

    .wire-next__carousel-slide-content h4,
    .wire-next__carousel-slide-content p {
      margin: 0;
    }

    .wire-next__carousel-slide-content h4 {
      font-size: clamp(1.5rem, 4vw, 2.4rem);
    }

    .wire-next__carousel-slide-content p {
      margin-top: 0.65rem;
      color: var(--wire-color-text-muted);
    }

    .wire-next__carousel-control {
      position: absolute;
      z-index: 3;
      top: 50%;
      display: grid;
      width: 2.75rem;
      min-height: 2.75rem;
      padding: 0;
      border: 1px solid color-mix(in srgb, var(--wire-color-border) 70%, transparent);
      border-radius: 999px;
      background: color-mix(in srgb, var(--wire-color-text) 76%, transparent);
      color: var(--wire-color-surface);
      cursor: pointer;
      place-items: center;
      transform: translateY(-50%);
    }

    .wire-next__carousel-control:focus-visible,
    .wire-next__carousel-pagination button:focus-visible,
    .wire-next__carousel-thumbnails button:focus-visible {
      outline: 2px solid var(--wire-carousel-color);
      outline-offset: 2px;
    }

    .wire-next__carousel-control:disabled {
      cursor: not-allowed;
      opacity: 0.35;
    }

    .wire-next__carousel-control--previous {
      left: 1rem;
    }

    .wire-next__carousel-control--next {
      right: 1rem;
    }

    .wire-next--carousel[data-rtl="true"] .wire-next__carousel-control--previous {
      right: 1rem;
      left: auto;
    }

    .wire-next--carousel[data-rtl="true"] .wire-next__carousel-control--next {
      right: auto;
      left: 1rem;
    }

    .wire-next__carousel-pagination {
      display: flex;
      min-height: 1.5rem;
      align-items: center;
      justify-content: center;
      gap: 0.5rem;
    }

    .wire-next__carousel-pagination button {
      width: 0.65rem;
      min-height: 0.65rem;
      padding: 0;
      border: 1px solid var(--wire-color-text-muted);
      border-radius: 999px;
      background: transparent;
      cursor: pointer;
    }

    .wire-next__carousel-pagination button[data-active="true"] {
      border-color: var(--wire-carousel-color);
      background: var(--wire-carousel-color);
    }

    .wire-next__carousel-counter {
      position: absolute;
      z-index: 3;
      bottom: 0.8rem;
      left: 50%;
      padding: 0.25rem 0.65rem;
      border-radius: 999px;
      background: color-mix(in srgb, var(--wire-color-text) 82%, transparent);
      color: var(--wire-color-surface);
      font-weight: 700;
      transform: translateX(-50%);
    }

    .wire-next__carousel-thumbnails {
      display: flex;
      overflow: auto;
      gap: 0.55rem;
      scrollbar-width: thin;
    }

    .wire-next__carousel-thumbnails button {
      display: grid;
      flex: 0 0 8.5rem;
      min-height: 3.75rem;
      overflow: hidden;
      padding: 0;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-sm);
      background: var(--wire-color-surface);
      color: var(--wire-color-text);
      cursor: pointer;
      place-items: center;
    }

    .wire-next__carousel-thumbnails button[data-active="true"] {
      border-color: var(--wire-carousel-color);
      box-shadow: inset 0 0 0 1px var(--wire-carousel-color);
    }

    .wire-next__carousel-thumbnails img {
      width: 100%;
      height: 4rem;
      object-fit: cover;
    }

    .wire-next__carousel-thumbnails span {
      padding: 0.5rem;
    }

    .wire-next--carousel[data-thumbnails="vertical"] .wire-next__carousel-thumbnails {
      flex: 0 0 9rem;
      flex-direction: column;
      max-height: 28rem;
    }

    .wire-next--carousel[data-thumbnails="vertical"] .wire-next__carousel-thumbnails button {
      flex-basis: 7.5rem;
      width: 100%;
    }

    .wire-next--carousel[data-draggable="true"] .wire-next__carousel-viewport {
      cursor: grab;
      touch-action: pan-y;
      user-select: none;
      -webkit-user-select: none;
    }

    .wire-next--carousel[data-draggable="true"] .wire-next__carousel-viewport:active {
      cursor: grabbing;
    }

    .wire-next--carousel[data-draggable="true"] img,
    .wire-next--carousel[data-draggable="true"] a {
      -webkit-user-drag: none;
    }

    .wire-next--carousel[data-snap="true"] .wire-next__carousel-viewport {
      overflow-x: auto;
      scroll-behavior: smooth;
      scroll-snap-type: x mandatory;
      scrollbar-width: thin;
    }

    .wire-next--carousel[data-snap="true"] .wire-next__carousel-track {
      --wire-carousel-snap-slide: calc(
        (100% - (var(--wire-carousel-per-view) - 1) * var(--wire-carousel-gap)) /
          var(--wire-carousel-per-view)
      );
      display: grid;
      width: 100%;
      min-width: 100%;
      box-sizing: content-box;
      grid-auto-columns: var(--wire-carousel-snap-slide);
      grid-auto-flow: column;
      padding-inline: calc((100% - var(--wire-carousel-snap-slide)) / 2);
      transform: none;
    }

    .wire-next--carousel[data-snap="true"] .wire-next__carousel-slide {
      width: auto;
      min-width: 0;
      scroll-snap-align: center;
    }

    @media (max-width: 640px) {
    .wire-next__carousel-slide {
        flex-basis: 100%;
        min-height: 16rem;
      }
    .wire-next--carousel[data-thumbnails="vertical"] .wire-next__carousel-layout {
        flex-direction: column-reverse;
      }
    .wire-next--carousel[data-thumbnails="vertical"] .wire-next__carousel-thumbnails {
        flex: auto;
        flex-direction: row;
        max-height: none;
      }
    .wire-next--carousel[data-thumbnails="vertical"] .wire-next__carousel-thumbnails button {
        flex-basis: 8.5rem;
        width: auto;
      }
    }

    @media (prefers-reduced-motion: reduce) {
    .wire-next__carousel-track {
        transition-duration: 0.01ms;
      }
    }
  }
}
```

---

## Chart

Showcase: https://component.wrnexusjs.dev/
Mount: <Chart /> (legacy: data-component="Chart")
Category: integrations
Purpose: Theme-aware, responsive chart component.
Props: title: string = "Chart", description: string = "", items: unknown[] = [], valueKey: string = "value", labelKey: string = "label", height: number = 240, showLegend: boolean = true, showValues: boolean = true, size: string = "default", color: string = "primary", variant: string = "bar", class: string = ""
Slots: default
Events: select, dataPointClick, legendToggle

### Complete .wrn source contract

```wrn
component Chart {
  outputs {
    select(payload: { item: object; index: number; sourceEvent?: Event })
    dataPointClick(payload: { item: object; index: number; sourceEvent?: Event })
    legendToggle(payload: { item: object; index: number; hidden: boolean; sourceEvent?: Event })
  }

  props {
    title: string = "Chart"
    description: string = ""
    items: unknown[] = []
    valueKey: string = "value"
    labelKey: string = "label"
    height: number = 240
    showLegend: boolean = true
    showValues: boolean = true
    size: string = "default"
    color: string = "primary"
    variant: string = "bar"
    class: string = ""
  }

  state hiddenIndexes = []

  functions {
    shared function numericValue(item) { return Math.max(0, Number(item[valueKey]) || 0) }
    shared function maximumValue() {
      return Math.max(1, ...items.map(function (item) { return numericValue(item) }))
    }
    shared function isHidden(index) { return hiddenIndexes.includes(index) }
    shared function barWidth() { return items.length ? Math.max(4, 80 / items.length) : 80 }
    shared function barX(index) { return 10 + index * (80 / Math.max(1, items.length)) }
    shared function barHeight(item) { return numericValue(item) / maximumValue() * 72 }
    shared function barY(item) { return 84 - barHeight(item) }

    client function selectPoint(item, index, sourceEvent) {
      output.dataPointClick({ item: item, index: index, sourceEvent: sourceEvent })
      output.select({ item: item, index: index, sourceEvent: sourceEvent })
    }

    client function toggleLegend(item, index, sourceEvent, next) {
      next = hiddenIndexes.slice()
      if (next.includes(index)) {
        next = next.filter(function (value) { return value !== index })
      } else {
        next.push(index)
      }
      hiddenIndexes = next
      output.legendToggle({ item: item, index: index, hidden: next.includes(index), sourceEvent: sourceEvent })
    }
  }

  view {
    <figure {...attrs} data-ui-component="Chart" data-variant='{variant}' class='wire-chart {class}'>
      {#if title || description}
        <figcaption class="wire-chart__header">
          {#if title}<strong>{title}</strong>{/if}
          {#if description}<span>{description}</span>{/if}
        </figcaption>
      {/if}
      <svg class="wire-chart__plot" viewBox="0 0 100 100" role="img" aria-label='{title}' style='height:{height}px'>
        <line x1="8" y1="84" x2="96" y2="84" class="wire-chart__axis" />
        {#each items as item, index}
          <g data-hidden='{isHidden(index) ? "true" : "false"}'>
            <rect
              x='{barX(index)}' y='{barY(item)}' width='{barWidth()}' height='{barHeight(item)}'
              rx="1.5" class="wire-chart__bar" role="button" tabindex="0"
              aria-label='{item[labelKey] + ": " + numericValue(item)}'
              @click='selectPoint(item, index, event)'
              @keydown='if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectPoint(item, index, event) }'
            />
            {#if showValues}<text x='{barX(index) + barWidth() / 2}' y='{barY(item) - 3}' class="wire-chart__value">{numericValue(item)}</text>{/if}
            <text x='{barX(index) + barWidth() / 2}' y="94" class="wire-chart__label">{item[labelKey]}</text>
          </g>
        {/each}
      </svg>
      {#if showLegend}
        <div class="wire-chart__legend" aria-label="Chart legend">
          {#each items as item, index}
            <button type="button" aria-pressed='{isHidden(index) ? "false" : "true"}' @click='toggleLegend(item, index, event)'>
              <span aria-hidden="true"></span>{item[labelKey]}
            </button>
          {/each}
        </div>
      {/if}
      <slot />
    </figure>
  }

  style {
    .wire-chart { display: grid; gap: 1rem; min-width: 0; margin: 0; padding: 1rem; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius); color: var(--wire-color-text); background: var(--wire-color-surface); box-shadow: var(--wire-shadow-1); }
    .wire-chart__header { display: grid; gap: 0.2rem; }
    .wire-chart__header > span { color: var(--wire-color-muted); font-size: 0.8rem; }
    .wire-chart__plot { display: block; width: 100%; min-height: 12rem; overflow: visible; }
    .wire-chart__axis { stroke: var(--wire-color-border); stroke-width: 0.5; }
    .wire-chart__bar { fill: var(--wire-color-primary); cursor: pointer; transition: opacity var(--wire-motion-fast), transform var(--wire-motion-fast); transform-box: fill-box; transform-origin: bottom; }
    .wire-chart__bar:hover { opacity: 0.82; transform: scaleY(1.02); }
    .wire-chart__bar:focus-visible { outline: 1.5px solid var(--wire-color-focus); outline-offset: 1px; }
    .wire-chart__plot g[data-hidden="true"] { opacity: 0.18; pointer-events: none; }
    .wire-chart__value, .wire-chart__label { fill: var(--wire-color-text); font-size: 3.5px; text-anchor: middle; }
    .wire-chart__label { fill: var(--wire-color-muted); }
    .wire-chart__legend { display: flex; flex-wrap: wrap; gap: 0.5rem; }
    .wire-chart__legend button { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.3rem 0.5rem; border: 1px solid var(--wire-color-border); border-radius: 999px; color: var(--wire-color-text); background: transparent; font: inherit; font-size: 0.75rem; cursor: pointer; }
    .wire-chart__legend button > span { width: 0.65rem; height: 0.65rem; border-radius: 0.2rem; background: var(--wire-color-primary); }
    .wire-chart__legend button[aria-pressed="false"] { opacity: 0.5; }
  }
}
```

---

## ChatBubble

Showcase: https://component.wrnexusjs.dev/
Mount: <ChatBubble /> (legacy: data-component="ChatBubble")
Category: base
Purpose: Theme-aware, responsive chat bubble component.
Props: size: string = "default", color: string = "primary", title: string = "", description: string = "", items: unknown[] = [], oneSided: boolean = false, showAvatars: boolean = false, showMetadata: boolean = false, ariaLabel: string = "Conversation", variant: string = "default", class: string = ""
Slots: default
Events: action, messageClick, avatarClick, linkClick

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component ChatBubble {
  outputs {
    action(payload: { action: boolean; item: string | number | boolean | null | object; index: number })
    messageClick(payload: { item: string | number | boolean | null | object; index: number; direction: string })
    avatarClick(payload: { item: string | number | boolean | null | object; index: number; direction: string })
    linkClick(payload: { link: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })
  }

  props {
    size: string = "default"
    color: string = "primary"
    title: string = ""
    description: string = ""
    items: unknown[] = []
    oneSided: boolean = false
    showAvatars: boolean = false
    showMetadata: boolean = false
    ariaLabel: string = "Conversation"
    variant: string = "default"
    class: string = ""
  }

  functions {
    shared function messageDirection(item) {
      return item.direction === "outgoing" ? "outgoing" : "incoming"
    }

    client function selectMessage(item, index) {
      output.messageClick({
        item: item,
        index: index,
        direction: messageDirection(item)
      })
    }

    client function selectAvatar(sourceEvent, item, index) {
      sourceEvent.stopPropagation()
      output.avatarClick({
        item: item,
        index: index,
        direction: messageDirection(item)
      })
    }

    client function selectLink(sourceEvent, link, item, index) {
      sourceEvent.stopPropagation()
      output.linkClick({
        link: link,
        item: item,
        index: index
      })
    }

    client function selectAction(sourceEvent, item, index) {
      sourceEvent.stopPropagation()
      output.action({
        action: item.action || "retry",
        item: item,
        index: index
      })
    }
  }

  view {
    <section
      {...attrs}
      class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--chat-bubble wire-next--variant-{variant} {class}"
      data-one-sided="{oneSided}"
      role="log"
      aria-label="{ariaLabel}"
      aria-live="polite"
    >
      {#if title || description}
        <header class="wire-next__chat-header">
          {#if title}<h3>{title}</h3>{/if}
          {#if description}<p>{description}</p>{/if}
        </header>
      {/if}

      {#if items.length}
        <div class="wire-next__chat-thread">
          {#each items as item, index}
            <article
              class="wire-next__chat-message"
              data-direction="{messageDirection(item)}"
              aria-label="{messageDirection(item) === 'outgoing' ? 'Sent message' : 'Received message'}"
            >
              <div class="wire-next__chat-row">
                {#if showAvatars && (item.avatarSrc || item.avatarFallback)}
                  <button
                    class="wire-next__chat-avatar"
                    type="button"
                    aria-label="{item.avatarLabel || item.author || 'Message author'}"
                    @click="selectAvatar(event, item, index)"
                  >
                    {#if item.avatarSrc}
                      <img src="{item.avatarSrc}" alt="{item.avatarAlt || ''}" />
                    {:else}
                      <span>{item.avatarFallback}</span>
                    {/if}
                  </button>
                {/if}

                <div
                  class="wire-next__chat-content"
                  role="button"
                  tabindex="0"
                  @click="selectMessage(item, index)"
                  @keydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); selectMessage(item, index) }"
                >
                  {#if item.title}<h4>{item.title}</h4>{/if}
                  {#if item.text}<p>{item.text}</p>{/if}
                  {#if item.bullets && item.bullets.length}
                    <ul>
                      {#each item.bullets as bullet}<li>{bullet}</li>{/each}
                    </ul>
                  {/if}
                  {#if item.links && item.links.length}
                    <nav aria-label="Message links">
                      {#each item.links as link}
                        <a
                          href="{link.href || '#'}"
                          @click="selectLink(event, link, item, index)"
                        >{link.label}</a>
                      {/each}
                    </nav>
                  {/if}
                </div>
              </div>

              {#if showMetadata && (item.timestamp || item.status || item.actionLabel)}
                <footer class="wire-next__chat-meta" data-tone="{item.statusTone || 'muted'}">
                  {#if item.statusIcon}<span class="{item.statusIcon}" aria-hidden="true"></span>{/if}
                  {#if item.status}<span>{item.status}</span>{/if}
                  {#if item.timestamp}<time datetime="{item.datetime || ''}">{item.timestamp}</time>{/if}
                  {#if item.actionLabel}
                    <button type="button" @click="selectAction(event, item, index)">
                      {item.actionLabel}
                    </button>
                  {/if}
                </footer>
              {/if}
            </article>
          {/each}
        </div>
      {/if}
      <slot />
    </section>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--chat-bubble.wire-component--size-sm {
      --wire-chat-padding: 0.75rem;
      font-size: 0.875rem;
    }
    .wire-next--chat-bubble.wire-component--size-lg {
      --wire-chat-padding: 1.25rem;
      font-size: 1.0625rem;
    }

    .wire-next--chat-bubble {
      --wire-chat-color: var(--wire-component-color, var(--wire-color-primary));
      --wire-chat-padding: 1rem;
      display: grid;
      width: 100%;
      min-width: 0;
      gap: 1rem;
      color: var(--wire-color-text);
    }

    .wire-next__chat-header h3,
    .wire-next__chat-header p,
    .wire-next__chat-content h4,
    .wire-next__chat-content p {
      margin: 0;
    }

    .wire-next__chat-header p {
      margin-top: 0.35rem;
      color: var(--wire-color-text-muted);
    }

    .wire-next__chat-thread {
      display: grid;
      min-width: 0;
      gap: 1.25rem;
    }

    .wire-next__chat-message {
      display: grid;
      width: min(78%, 34rem);
      min-width: 0;
      justify-self: start;
      gap: 0.4rem;
    }

    .wire-next__chat-message[data-direction="outgoing"] {
      justify-self: end;
    }

    .wire-next--chat-bubble[data-one-sided="true"] .wire-next__chat-message {
      justify-self: start;
    }

    .wire-next__chat-row {
      display: flex;
      min-width: 0;
      align-items: flex-start;
      gap: 0.75rem;
    }

    .wire-next__chat-message[data-direction="outgoing"] .wire-next__chat-row {
      flex-direction: row-reverse;
    }

    .wire-next--chat-bubble[data-one-sided="true"]
      .wire-next__chat-message[data-direction="outgoing"]
      .wire-next__chat-row {
      flex-direction: row;
    }

    .wire-next__chat-content {
      min-width: 0;
      padding: var(--wire-chat-padding);
      border: 1px solid var(--wire-color-border);
      border-radius: 1rem;
      background: var(--wire-color-surface-raised, var(--wire-color-surface));
      color: var(--wire-color-text);
      cursor: pointer;
    }

    .wire-next__chat-content:focus-visible {
      outline: 2px solid var(--wire-chat-color);
      outline-offset: 2px;
    }

    .wire-next__chat-message[data-direction="outgoing"] .wire-next__chat-content {
      border-color: var(--wire-chat-color);
      background: var(--wire-chat-color);
      color: var(--wire-color-primary-contrast, #fff);
    }

    .wire-next__chat-message[data-direction="outgoing"] .wire-next__chat-content :is(h4, p, li, span) {
      color: inherit;
    }

    .wire-next__chat-content h4 {
      margin-bottom: 0.65rem;
      font-size: 1em;
    }

    .wire-next__chat-content p + p,
    .wire-next__chat-content p + nav {
      margin-top: 0.75rem;
    }

    .wire-next__chat-content ul {
      display: grid;
      margin: 0.55rem 0 0;
      padding-inline-start: 1.25rem;
      gap: 0.35rem;
    }

    .wire-next__chat-content nav {
      display: grid;
      margin-top: 0.75rem;
      gap: 0.3rem;
    }

    .wire-next__chat-content a {
      color: var(--wire-chat-color);
      font-weight: 650;
      text-decoration: none;
    }

    .wire-next__chat-message[data-direction="outgoing"] .wire-next__chat-content a {
      color: inherit;
      text-decoration: underline;
    }

    .wire-next__chat-avatar {
      display: grid;
      flex: 0 0 2.5rem;
      width: 2.5rem;
      min-height: 2.5rem;
      overflow: hidden;
      padding: 0;
      border: 1px solid var(--wire-color-border);
      border-radius: 999px;
      background: var(--wire-color-surface-raised, var(--wire-color-surface));
      color: var(--wire-color-text);
      cursor: pointer;
      place-items: center;
    }

    .wire-next__chat-avatar img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }

    .wire-next__chat-avatar:focus-visible {
      outline: 2px solid var(--wire-chat-color);
      outline-offset: 2px;
    }

    .wire-next__chat-meta {
      display: flex;
      align-items: center;
      gap: 0.3rem;
      padding-inline: calc(2.5rem + 0.75rem);
      color: var(--wire-color-text-muted);
      font-size: 0.78em;
    }

    .wire-next__chat-message[data-direction="outgoing"] .wire-next__chat-meta {
      justify-content: flex-end;
    }

    .wire-next__chat-meta[data-tone="danger"] {
      color: var(--wire-color-danger);
    }

    .wire-next__chat-meta button {
      padding: 0;
      border: 0;
      background: transparent;
      color: inherit;
      font: inherit;
      font-weight: 700;
      cursor: pointer;
      text-decoration: underline;
    }

    @media (max-width: 640px) {
    .wire-next__chat-message {
        width: min(92%, 34rem);
      }
    .wire-next__chat-avatar {
        flex-basis: 2rem;
        width: 2rem;
        min-height: 2rem;
      }
    .wire-next__chat-meta {
        padding-inline: 0;
      }
    }
  }
}
```

---

## Checkbox

Showcase: https://component.wrnexusjs.dev/
Mount: <Checkbox /> (legacy: data-component="Checkbox")
Category: forms
Purpose: Theme-aware, responsive checkbox component.
Props: size: string = "default", color: string = "primary", id: string = "", name: string = "", label: string = "Checkbox", hiddenLabel: boolean = false, placeholder: string = "", variant: string = "normal", icon: string = "", iconPosition: string = "start", value: string = "on", values: unknown[] = [], options: unknown[] = [], checked: boolean = false, indeterminate: boolean = false, orientation: string = "vertical", card: boolean = false, rightAligned: boolean = false, list: boolean = false, helperText: string = "", cornerHint: string = "", error: string = "", inline: boolean = false, readonly: boolean = false, disabled: boolean = false, required: boolean = false, class: string = ""
Slots: default
Events: input, change, focus, blur, invalid

### Complete .wrn source contract

```wrn
import FieldStyles from "../styles/FieldStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component Checkbox {
  outputs {
    input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    invalid(payload: { message?: string; value: string | number | boolean | null | object; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
size: string = "default"
    color: string = "primary"
    id: string = ""
    name: string = ""
    label: string = "Checkbox"
    hiddenLabel: boolean = false
    placeholder: string = ""
    variant: string = "normal"
    icon: string = ""
    iconPosition: string = "start"
    value: string = "on"
    values: unknown[] = []
    options: unknown[] = []
    checked: boolean = false
    indeterminate: boolean = false
    orientation: string = "vertical"
    card: boolean = false
    rightAligned: boolean = false
    list: boolean = false
    helperText: string = ""
    cornerHint: string = ""
    error: string = ""
    inline: boolean = false
    readonly: boolean = false
    disabled: boolean = false
    required: boolean = false
    class: string = ""
  }
  functions {
    client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ checked: sourceEvent.currentTarget.checked, value: sourceEvent.currentTarget.value, values: values, name: name, sourceEvent: sourceEvent }) }
    client function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
  }
  view {
    <div
      {...attrs}
      class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--choice-field wire-next--checkbox-field {class}"
      data-inline="{inline}"
      data-invalid="{error ? 'true' : 'false'}"
      data-orientation="{orientation}"
      data-card="{card}"
      data-list="{list}"
      data-right-aligned="{rightAligned}"
    >
      <div
        class="wire-next__field-heading"
      >
        {#if options.length || cornerHint}
          <span
            class="{hiddenLabel ? 'wire-next__sr-only' : ''}"
          >
            {label}
          </span>
          {#if cornerHint}
            <span
              class="wire-next__field-hint"
            >
              {cornerHint}
            </span>
          {/if}
        {/if}
      </div>
      {#if options.length}
        <div
          class="wire-next__checkbox-group"
          role="group"
          aria-label="{hiddenLabel ? label : ''}"
          aria-invalid="{error ? 'true' : 'false'}"
        >
          {#each options as option}
            <label
              class="wire-next__choice wire-next--checkbox"
              for="{id || name}-{option.value}"
              data-variant="{variant}"
              data-disabled="{disabled || option.disabled}"
            >
              <input
                id="{id || name}-{option.value}"
                type="checkbox"
                name="{name}"
                value="{option.value}"
                checked="{values.includes(option.value) || option.checked}"
                disabled="{disabled || option.disabled}"
                required="{required}"
                readonly="{readonly}"
                @click="preventReadonly(event)"
                @input="emitField('input', event)"
                @change="emitField('change', event)"
                @focus="emitField('focus', event)"
                @blur="emitField('blur', event)"
                @invalid="emitField('invalid', event)"
              />
              <span
                class="wire-next__choice-copy"
              >
                <strong>{option.label}</strong>
                {#if option.description}
                  <small>{option.description}</small>
                {/if}
              </span>
            </label>
          {/each}
        </div>
      {:else}
        <div
          class="wire-next__checkbox-content"
        >
          <label
            class="wire-next__choice wire-next--checkbox"
            for="{id || name}"
            data-variant="{variant}"
            data-icon-position="{iconPosition}"
          >
            <input
              id="{id || name}"
              type="checkbox"
              name="{name}"
              value="{value}"
              checked="{checked}"
              disabled="{disabled}"
              required="{required}"
              readonly="{readonly}"
              aria-checked="{indeterminate ? 'mixed' : (checked ? 'true' : 'false')}"
              aria-invalid="{error ? 'true' : 'false'}"
              @click="preventReadonly(event)"
              @input="emitField('input', event)"
              @change="emitField('change', event)"
              @focus="emitField('focus', event)"
              @blur="emitField('blur', event)"
              @invalid="emitField('invalid', event)"
            />
            {#if icon}
              <span
                class="{icon}"
                aria-hidden="true"
              >
              </span>
            {/if}
            <span
              class="{hiddenLabel || cornerHint ? 'wire-next__sr-only' : ''}"
            >
              {label}
            </span>
            <div
              class="wire-next__checkbox-slot"
            >
              <slot></slot>
            </div>
          </label>
        </div>
      {/if}
      {#if helperText}
        <small
          class="wire-next__field-help"
        >
          {helperText}
        </small>
      {/if}
      <small
        class="wire-next__field-error"
        data-error="{name}"
      >
        {error}
      </small>
    </div>
    <FieldStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next__checkbox-content {
      display: grid;
      width: 100%;
      min-width: 0;
      align-items: start;
      grid-template-columns: minmax(0, 1fr);
    }

    .wire-next__checkbox-slot {
      display: block;
      min-width: 0;
      flex: 1 1 auto;
      color: var(--wire-color-muted);
      font-size: 0.75rem;
      line-height: 1.45;
    }

    .wire-next__checkbox-slot:empty {
      display: none;
    }

    .wire-next__checkbox-slot :is(a, button) {
      color: var(--wire-field-color);
    }
  }
}
```

---

## Clipboard

Showcase: https://component.wrnexusjs.dev/
Mount: <Clipboard /> (legacy: data-component="Clipboard")
Category: integrations
Purpose: Theme-aware, responsive clipboard component.
Props: value: string = "", label: string = "Copy", copiedLabel: string = "Copied", errorLabel: string = "Copy failed", title: string = "Clipboard", description: string = "", code: boolean = true, size: string = "default", color: string = "primary", disabled: boolean = false, class: string = ""
Slots: default
Events: copy, success, error

### Complete .wrn source contract

```wrn
component Clipboard {
  outputs {
    copy(payload: { value: string; sourceEvent?: Event })
    success(payload: { value: string; sourceEvent?: Event })
    error(payload: { error: Error | string; value: string; sourceEvent?: Event })
  }

  props {
    value: string = ""
    label: string = "Copy"
    copiedLabel: string = "Copied"
    errorLabel: string = "Copy failed"
    title: string = "Clipboard"
    description: string = ""
    code: boolean = true
    size: string = "default"
    color: string = "primary"
    disabled: boolean = false
    class: string = ""
  }

  state status = "idle"

  functions {
    client function resetStatus() { status = "idle" }
    client function copySucceeded() {
      status = "success"
      output.success({ value: value })
      setTimeout(resetStatus, 1600)
    }
    client function copyFailed(error) {
      status = "error"
      output.error({ error: error, value: value })
      setTimeout(resetStatus, 1600)
    }
    client function copyValue(sourceEvent) {
      if (disabled || status === "copying") {
        return
      }
      status = "copying"
      output.copy({ value: value, sourceEvent: sourceEvent })
      if (!window.navigator.clipboard || !window.navigator.clipboard.writeText) {
        copyFailed(new Error("Clipboard API is unavailable"))
        return
      }
      return window.navigator.clipboard.writeText(value).then(copySucceeded).catch(copyFailed)
    }
  }

  view {
    <section
      {...attrs}
      data-ui-component="Clipboard"
      data-status='{status}'
      data-size='{size}'
      data-color='{color}'
      class='wire-clipboard {class}'
    >
      {#if title}<strong class="wire-clipboard__title">{title}</strong>{/if}
      {#if description}<p class="wire-clipboard__description">{description}</p>{/if}
      <div class="wire-clipboard__control">
        {#if code}
          <code class="wire-clipboard__value">{value}</code>
        {:else}
          <span class="wire-clipboard__value">{value}</span>
        {/if}
        <button
          type="button"
          class="wire-clipboard__button"
          disabled='{disabled}'
          aria-label='{status === "success" ? copiedLabel : status === "error" ? errorLabel : label}'
          @click='copyValue(event)'
        >
          <span class="icon-[lucide--copy] wire-clipboard__icon" data-show='status !== "success"' aria-hidden="true"></span>
          <span class="icon-[lucide--check] wire-clipboard__icon" data-show='status === "success"' aria-hidden="true"></span>
          <span>{status === "success" ? copiedLabel : status === "error" ? errorLabel : label}</span>
        </button>
      </div>
      <slot />
    </section>
  }

  style {
    .wire-clipboard { display: grid; gap: 0.5rem; color: var(--wire-color-text); }
    .wire-clipboard__title { font-size: 0.875rem; }
    .wire-clipboard__description { margin: 0; color: var(--wire-color-muted); font-size: 0.8rem; }
    .wire-clipboard__control { display: flex; min-width: 0; align-items: stretch; overflow: hidden; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius); background: var(--wire-color-surface); box-shadow: var(--wire-shadow-1); }
    .wire-clipboard__value { min-width: 0; flex: 1; overflow: auto; padding: 0.7rem 0.85rem; color: var(--wire-color-text); background: var(--wire-color-surface-2); font: inherit; font-family: var(--wire-font-mono, ui-monospace, monospace); white-space: nowrap; }
    .wire-clipboard__button { display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.6rem 0.85rem; border: 0; border-left: 1px solid var(--wire-color-border); color: var(--wire-color-primary); background: transparent; font: inherit; font-size: 0.8rem; font-weight: 700; cursor: pointer; }
    .wire-clipboard__button:hover { background: var(--wire-color-primary-soft); }
    .wire-clipboard__button:focus-visible { outline: 2px solid var(--wire-color-focus); outline-offset: -3px; }
    .wire-clipboard__button:disabled { cursor: not-allowed; opacity: 0.55; }
    .wire-clipboard[data-status="success"] .wire-clipboard__button { color: var(--wire-color-success); }
    .wire-clipboard[data-status="error"] .wire-clipboard__button { color: var(--wire-color-danger); }
    .wire-clipboard__icon { width: 1rem; height: 1rem; flex: none; }
  }
}
```

---

## Collapse

Showcase: https://component.wrnexusjs.dev/
Mount: <Collapse /> (legacy: data-component="Collapse")
Category: base
Purpose: Theme-aware, responsive collapse component.
Props: size: string = "default", color: string = "primary", items: unknown[] = [], multiple: boolean = false, mode: string = "panel", initialOpenIndexes: unknown[] = [], ariaLabel: string = "Collapsible content", class: string = ""
Slots: default
Events: toggle, open, close

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component Collapse {
  outputs {
    toggle(payload: { index: number; item: string | number | boolean | null | object; open: boolean; openIndexes: number[] })
    open(payload: { index: number; item: string | number | boolean | null | object })
    close(payload: { index: number; item: string | number | boolean | null | object })
  }

  props {
size: string = "default"
    color: string = "primary"
    items: unknown[] = []
    multiple: boolean = false
    mode: string = "panel"
    initialOpenIndexes: unknown[] = []
    ariaLabel: string = "Collapsible content"
    class: string = ""
  }

  state openIndexes = initialOpenIndexes

  functions {
    shared function isOpen(index) {
      return openIndexes.includes(index)
    }

    client function toggleItem(index, item, opening) {
      if (item.disabled) {
        return
      }
      opening = !isOpen(index)
      if (opening) {
        if (multiple) {
          openIndexes = openIndexes.concat(index)
        } else {
          openIndexes = [index]
        }
        output.open({ index: index, item: item })
      } else {
        openIndexes = openIndexes.filter((openIndex) => openIndex !== index)
        output.close({ index: index, item: item })
      }
      output.toggle({
        index: index,
        item: item,
        open: opening,
        openIndexes: openIndexes
      })
    }
  }

  view {
    <section
      {...attrs}
      class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--collapse {class}"
      data-mode="{mode}"
      aria-label="{ariaLabel}"
    >
      {#each items as item, index}
        <article class="wire-next__collapse-item" data-open="{isOpen(index)}">
          {#if mode === "inline" && item.preview}
            <p class="wire-next__collapse-preview">{item.preview}</p>
          {/if}

          <button
            class="wire-next__collapse-trigger"
            type="button"
            aria-expanded="{isOpen(index) ? 'true' : 'false'}"
            aria-controls="{item.id || 'collapse-panel-' + index}"
            disabled="{item.disabled}"
            @click="toggleItem(index, item)"
          >
            <span>{isOpen(index) ? (item.closeLabel || "Read less") : item.label}</span>
            <span class="icon-[lucide--chevron-down] wire-next__collapse-chevron" aria-hidden="true"></span>
          </button>

          <div
            id="{item.id || 'collapse-panel-' + index}"
            class="wire-next__collapse-panel"
            data-open="{isOpen(index)}"
            aria-hidden="{isOpen(index) ? 'false' : 'true'}"
            inert="{!isOpen(index)}"
          >
            <div class="wire-next__collapse-content">
              {#if item.title}<h4>{item.title}</h4>{/if}
              {#if item.content}<p>{item.content}</p>{/if}
              {#if item.links && item.links.length}
                <nav aria-label="Related links">
                  {#each item.links as link}<a href="{link.href || '#'}">{link.label}</a>{/each}
                </nav>
              {/if}
            </div>
          </div>
        </article>
      {/each}
      <slot />
    </section>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--collapse.wire-component--size-sm {
      font-size: 0.875rem;
    }
    .wire-next--collapse.wire-component--size-lg {
      font-size: 1.0625rem;
    }

    .wire-next--collapse {
      --wire-collapse-color: var(--wire-component-color, var(--wire-color-primary));
      display: grid;
      width: 100%;
      min-width: 0;
      gap: 0.75rem;
      color: var(--wire-color-text);
    }

    .wire-next__collapse-item {
      display: grid;
      min-width: 0;
      justify-items: start;
      gap: 0;
    }

    .wire-next__collapse-trigger {
      display: inline-flex;
      min-height: 2.75rem;
      align-items: center;
      justify-content: center;
      gap: 0.55rem;
      padding: 0.7rem 1rem;
      border: 1px solid var(--wire-collapse-color);
      border-radius: var(--wire-radius-sm);
      background: var(--wire-collapse-color);
      color: var(--wire-color-primary-contrast, #fff);
      font: inherit;
      font-weight: 700;
      cursor: pointer;
    }

    .wire-next__collapse-trigger:focus-visible {
      outline: 2px solid var(--wire-collapse-color);
      outline-offset: 3px;
    }

    .wire-next__collapse-trigger:disabled {
      cursor: not-allowed;
      opacity: 0.5;
    }

    .wire-next__collapse-chevron {
      transition: transform var(--wire-motion-fast) var(--wire-ease-standard);
    }

    .wire-next__collapse-item[data-open="true"] .wire-next__collapse-chevron {
      transform: rotate(180deg);
    }

    .wire-next__collapse-panel {
      width: min(100%, 42rem);
      max-height: 0;
      overflow: hidden;
      margin-top: 0;
      opacity: 0;
      transition:
        max-height 350ms var(--wire-ease-emphasized),
        margin-top 300ms var(--wire-ease-emphasized),
        opacity 200ms var(--wire-ease-standard);
    }

    .wire-next__collapse-panel[data-open="true"] {
      max-height: 40rem;
      margin-top: 0.75rem;
      opacity: 1;
    }

    .wire-next__collapse-content {
      display: grid;
      overflow: hidden;
      min-height: 0;
      gap: 0.65rem;
      padding: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-md);
      background: var(--wire-color-surface-raised, var(--wire-color-surface));
    }

    .wire-next__collapse-content h4,
    .wire-next__collapse-content p,
    .wire-next__collapse-preview {
      margin: 0;
    }

    .wire-next__collapse-content nav {
      display: flex;
      flex-wrap: wrap;
      gap: 0.75rem;
    }

    .wire-next__collapse-content a {
      color: var(--wire-collapse-color);
      font-weight: 650;
      text-decoration: none;
    }

    .wire-next--collapse[data-mode="inline"] .wire-next__collapse-item {
      gap: 0;
    }

    .wire-next--collapse[data-mode="inline"] .wire-next__collapse-preview {
      order: 1;
      margin-bottom: 0.5rem;
    }

    .wire-next--collapse[data-mode="inline"] .wire-next__collapse-panel {
      order: 2;
      width: 100%;
    }

    .wire-next--collapse[data-mode="inline"] .wire-next__collapse-panel[data-open="true"] {
      margin-top: 0;
    }

    .wire-next--collapse[data-mode="inline"] .wire-next__collapse-trigger {
      order: 3;
      margin-top: 0.5rem;
      min-height: auto;
      padding: 0;
      border: 0;
      border-radius: 0;
      background: transparent;
      color: var(--wire-collapse-color);
    }

    .wire-next--collapse[data-mode="inline"] .wire-next__collapse-content {
      padding: 0;
      border: 0;
      background: transparent;
    }

    @media (prefers-reduced-motion: reduce) {
    .wire-next__collapse-chevron,
      .wire-next__collapse-panel {
        transition: none;
      }
    }
  }
}
```

---

## ColorPicker

Showcase: https://component.wrnexusjs.dev/
Mount: <ColorPicker /> (legacy: data-component="ColorPicker")
Category: forms
Purpose: Theme-aware, responsive color picker component.
Props: size: string = "default", color: string = "primary", id: string = "", name: string = "", label: string = "Color", hiddenLabel: boolean = false, placeholder: string = "", value: string = "#2563eb", icon: string = "", iconPosition: string = "start", helperText: string = "", cornerHint: string = "", error: string = "", inline: boolean = false, variant: string = "normal", readonly: boolean = false, disabled: boolean = false, required: boolean = false, class: string = ""
Slots: none
Events: input, change, focus, blur

### Complete .wrn source contract

```wrn
import FieldStyles from "../styles/FieldStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component ColorPicker {
  outputs {
    input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
    size: string = "default"
    color: string = "primary"
    id: string = ""
    name: string = ""
    label: string = "Color"
    hiddenLabel: boolean = false
    placeholder: string = ""
    value: string = "#2563eb"
    icon: string = ""
    iconPosition: string = "start"
    helperText: string = ""
    cornerHint: string = ""
    error: string = ""
    inline: boolean = false
    variant: string = "normal"
    readonly: boolean = false
    disabled: boolean = false
    required: boolean = false
    class: string = ""
  }
  functions {
    client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
    client function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
  }
  view {
    <div {...attrs} class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--field wire-component--color-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
      <div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
      <div class="wire-next__color-control" data-icon-position="{iconPosition}">{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}<input id="{id || name}" type="color" name="{name}" value="{value}" disabled="{disabled}" required="{required}" readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" @click="preventReadonly(event)" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" /><output>{value}</output></div>
      {#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
    </div>
    <FieldStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next__color-control input[type="color"] {
      width: 3rem;
      padding: 0.2rem;
    }
  }
}
```

---

## Columns

Showcase: https://component.wrnexusjs.dev/
Mount: <Columns /> (legacy: data-component="Columns")
Category: layout
Purpose: Create responsive balanced content columns with configurable count, gap, density, and maximum width.
Props: size: string = "default", color: string = "primary", columns: number = 2, gap: string = "md", maxWidth: string = "xl", class: string = ""
Slots: default
Events: none

### Complete .wrn source contract

```wrn
// Columns -- a simple multi-column split for page content.
//
//   <Columns columns={2} gap="lg">...</Columns>
//
// Columns and Grid overlap deliberately: Columns is the coarse two or three
// way split of a page, and its gaps are wider for that reason. Grid is the one
// to reach for when the items are cards and the count matters.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Columns {
  props {
    size: string = "default"
    color: string = "primary"
    columns: number = 2
    gap: string = "md"
    maxWidth: string = "xl"
    class: string = ""
  }

  functions {
    shared function columnCount() {
      var value = Number(columns)
      if (!value || value < 1) {
        return 1
      }
      return Math.min(4, value)
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="Columns"
      class='wire-columns {class}'
      data-size='{size}'
      data-color='{color}'
      data-gap='{gap}'
      data-max-width='{maxWidth}'
      data-columns='{columnCount()}'
    >
      <slot></slot>
    </div>
  }

  style {
    .wire-columns {
      --columns-gap: 1.5rem;
      display: grid;
      width: 100%;
      min-width: 0;
      gap: var(--columns-gap);
      grid-template-columns: 1fr;
      align-items: start;
    }

    .wire-columns[data-max-width="md"] {
      max-width: 48rem;
    }

    .wire-columns[data-max-width="lg"] {
      max-width: 64rem;
    }

    .wire-columns[data-max-width="xl"] {
      max-width: 80rem;
    }

    .wire-columns[data-max-width="2xl"] {
      max-width: 96rem;
    }

    .wire-columns[data-max-width="full"] {
      max-width: none;
    }

    .wire-columns[data-gap="sm"] {
      --columns-gap: 0.75rem;
    }

    .wire-columns[data-gap="lg"] {
      --columns-gap: 2.5rem;
    }

    .wire-columns[data-gap="xl"] {
      --columns-gap: 3.5rem;
    }

    /*
     * Splits at the tablet breakpoint rather than the phone one: a page split
     * is worth keeping single-column for longer than a card deck is.
     */
    @media (min-width: 768px) {
      .wire-columns:not([data-columns="1"]) {
        grid-template-columns: repeat(2, minmax(0, 1fr));
      }
    }

    @media (min-width: 1024px) {
      .wire-columns[data-columns="3"] {
        grid-template-columns: repeat(3, minmax(0, 1fr));
      }

      .wire-columns[data-columns="4"] {
        grid-template-columns: repeat(4, minmax(0, 1fr));
      }
    }
  }
}
```

---

## ComboBox

Showcase: https://component.wrnexusjs.dev/
Mount: <ComboBox /> (legacy: data-component="ComboBox")
Category: advanced-forms
Purpose: Editable autocomplete combobox with local and remote suggestions.
Props: size: string = "default", color: string = "primary", label: string = "ComboBox", name: string = "", value: string = "", options: unknown[] = [], groups: unknown[] = [], placeholder: string = "Search or select an option", searchPlaceholder: string = "Start typing…", clearable: boolean = true, allowCustomValue: boolean = false, disabled: boolean = false, required: boolean = false, invalid: boolean = false, validationMessage: string = "", helpText: string = "", loading: boolean = false, loadingLabel: string = "Loading suggestions…", emptyLabel: string = "No matching options", clearLabel: string = "Clear value", toggleLabel: string = "Toggle suggestions", searchMode: string = "contains", searchFields: string = "label,description", minSearchLength: number = 0, searchResultLimit: number = 0, optionTemplate: string = "default", defaultOpen: boolean = false, closeOnSelect: boolean = true, fixed: boolean = false, placement: string = "bottom", autocomplete: string = "off", remote: boolean = false, remoteUrl: string = "", remoteQueryParam: string = "q", remoteDebounce: number = 250, remoteAutoLoad: boolean = true, infinite: boolean = false, hasMore: boolean = false, page: number = 1, loadMoreLabel: string = "Load more", class: string = ""
Slots: none
Events: search, select, change, clear, open, close, load, error

### Complete .wrn source contract

```wrn
import SelectStyles from "../styles/SelectStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component ComboBox {
  outputs {
    search(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    load(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
  }

  props {
    size: string = "default"
    color: string = "primary"
    label: string = "ComboBox"
    name: string = ""
    value: string = ""
    options: unknown[] = []
    groups: unknown[] = []
    placeholder: string = "Search or select an option"
    searchPlaceholder: string = "Start typing…"
    clearable: boolean = true
    allowCustomValue: boolean = false
    disabled: boolean = false
    required: boolean = false
    invalid: boolean = false
    validationMessage: string = ""
    helpText: string = ""
    loading: boolean = false
    loadingLabel: string = "Loading suggestions…"
    emptyLabel: string = "No matching options"
    clearLabel: string = "Clear value"
    toggleLabel: string = "Toggle suggestions"
    searchMode: string = "contains"
    searchFields: string = "label,description"
    minSearchLength: number = 0
    searchResultLimit: number = 0
    optionTemplate: string = "default"
    defaultOpen: boolean = false
    closeOnSelect: boolean = true
    fixed: boolean = false
    placement: string = "bottom"
    autocomplete: string = "off"
    remote: boolean = false
    remoteUrl: string = ""
    remoteQueryParam: string = "q"
    remoteDebounce: number = 250
    remoteAutoLoad: boolean = true
    infinite: boolean = false
    hasMore: boolean = false
    page: number = 1
    loadMoreLabel: string = "Load more"
    class: string = ""
  }

  state open = defaultOpen
  state query: string = ""
  state selectedValue = value
  state activeIndex: number = -1

  functions {
    shared function allOptions() {
      return [...groups.flatMap((group) => group.options || []), ...options]
    }

    shared function searchableText(option) {
      return ((option.label || "") + " " + (option.description || "")).toLowerCase()
    }

    shared function matches(option) {
      if (!query || query.length < minSearchLength) {
        return true
      }
      if (searchMode === "startsWith") {
        return searchableText(option).startsWith(query.toLowerCase())
      }
      if (searchMode === "exact") {
        return searchableText(option) === query.toLowerCase()
      }
      return searchableText(option).includes(query.toLowerCase())
    }

    shared function matchingOptions(list) {
      return list.filter((option) => matches(option))
    }

    shared function visibleOptions(list) {
      if (searchResultLimit > 0) {
        return matchingOptions(list).slice(0, searchResultLimit)
      }
      return matchingOptions(list)
    }

    shared function flatVisibleOptions() {
      return visibleOptions(allOptions())
    }

    shared function isSelected(option) {
      return selectedValue === option.value
    }

    shared function selectedOptions() {
      return allOptions().filter((option) => isSelected(option))
    }

    shared function selectedText() {
      return selectedOptions().length ? selectedOptions()[0].label : selectedValue
    }

    shared function inputText() {
      return query || selectedText()
    }

    shared function shouldShowDropdown() {
      if (!open) {
        return false
      }
      if (loading) {
        return true
      }
      if (remote && query.length !== 0 && query.length < minSearchLength) {
        return true
      }
      if (flatVisibleOptions().length) {
        return true
      }
      return infinite && hasMore
    }

    shared function canSelect(option) {
      return !option.disabled
    }

    shared function chooseOption(option) {
      if (!canSelect(option)) {
        return
      }
      selectedValue = option.value
      query = option.label
      activeIndex = optionIndex(option)
      if (closeOnSelect) {
        open = false
      }
    }

    client function updateQuery(event) {
      query = event.target.value
      selectedValue = allowCustomValue ? event.target.value : ""
      open = true
      activeIndex = flatVisibleOptions().length ? 0 : -1
    }

    client function clearValue(event) {
      if (event) {
        event.stopPropagation()
      }
      query = ""
      selectedValue = ""
      activeIndex = -1
      open = false
    }

    shared function openDropdown() {
      if (!disabled) {
        open = true
        activeIndex = flatVisibleOptions().length ? 0 : -1
      }
    }

    shared function closeDropdown() {
      open = false
    }

    shared function toggle() {
      if (open) {
        closeDropdown()
      } else {
        openDropdown()
      }
    }

    shared function setValue(nextValue) {
      selectedValue = nextValue
      query = ""
    }

    shared function moveActive(direction) {
      if (!flatVisibleOptions().length) {
        return
      }
      activeIndex = (activeIndex + direction + flatVisibleOptions().length) % flatVisibleOptions().length
    }

    client function handleKeydown(event) {
      if (disabled) {
        return
      }
      if (event.key === "ArrowDown") {
        event.preventDefault()
        openDropdown()
        moveActive(1)
      } else if (event.key === "ArrowUp") {
        event.preventDefault()
        openDropdown()
        moveActive(-1)
      } else if (event.key === "Enter" && open && activeIndex >= 0) {
        event.preventDefault()
        chooseOption(flatVisibleOptions()[activeIndex])
      } else if (event.key === "Escape") {
        closeDropdown()
      } else if (event.key === "Home" && open) {
        event.preventDefault()
        activeIndex = 0
      } else if (event.key === "End" && open) {
        event.preventDefault()
        activeIndex = flatVisibleOptions().length - 1
      }
    }

    shared function optionIndex(option) {
      return flatVisibleOptions().findIndex((item) => item.value === option.value)
    }
  }

  view {
    <div
      class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--advanced-select wire-next--combobox {open ? 'wire-next--open' : ''} {fixed ? 'wire-next--advanced-select-fixed' : ''} {invalid ? 'wire-next--invalid' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
      data-placement="{placement}"
      data-search-mode="{searchMode}"
      data-remote="{remote ? 'true' : 'false'}"
      data-remote-url="{remoteUrl}"
      data-remote-query-param="{remoteQueryParam}"
      data-remote-debounce="{remoteDebounce}"
      data-remote-auto-load="{remoteAutoLoad ? 'true' : 'false'}"
      data-infinite="{infinite ? 'true' : 'false'}"
      data-page="{page}"
      data-wrn-select
      data-wrn-combobox
      @focusout="if (!event.currentTarget.contains(event.relatedTarget)) { closeDropdown() }"
    >
      <label id="{name}-label" for="{name}-input">{label}</label>

      <div class="wire-next__select-control wire-next__combobox-control">
        <span class="wire-next__search-icon icon-[lucide--search]" aria-hidden="true"></span>
        <input
          {...attrs}
          id="{name}-input"
          class="wire-next__select-trigger wire-next__combobox-input"
          type="text"
          role="combobox"
          name="{allowCustomValue ? name : ''}"
          value="{inputText()}"
          placeholder="{placeholder}"
          autocomplete="{autocomplete}"
          aria-autocomplete="list"
          aria-haspopup="listbox"
          aria-expanded="{open}"
          aria-controls="{name}-listbox"
          aria-labelledby="{name}-label"
          aria-invalid="{invalid}"
          disabled="{disabled}"
          required="{required && allowCustomValue}"
          @focus="openDropdown()"
          @click="openDropdown()"
          @input="updateQuery(event)"
          @keydown="handleKeydown(event)"
        />

        <button
          type="button"
          class="wire-next__clear-select"
          data-show="clearable && inputText() && !disabled"
          aria-label="{clearLabel}"
          title="{clearLabel}"
          @click="clearValue(event)"
        >
          <span class="icon-[lucide--x]" aria-hidden="true"></span>
        </button>
        <button
          type="button"
          class="wire-next__combobox-toggle"
          aria-label="{toggleLabel}"
          tabindex="-1"
          disabled="{disabled}"
          @click="toggle()"
        >
          <span class="wire-next__select-chevron icon-[lucide--chevrons-up-down]" aria-hidden="true"></span>
        </button>
      </div>

      <div
        class="wire-next__select-dropdown {fixed ? 'wire-next__select-dropdown--fixed' : ''}"
        data-show="shouldShowDropdown()"
        style="{shouldShowDropdown() ? '' : 'display: none'}"
      >
        <div
          class="wire-next__select-message"
          data-show="remote && query.length !== 0 && query.length < minSearchLength"
        >Enter at least {minSearchLength} characters.</div>
        <div class="wire-next__select-message" data-show="loading" role="status">
          <i class="wire-spinner wire-spinner--inline" aria-hidden="true"></i>
          {loadingLabel}
        </div>

        <div
          id="{name}-listbox"
          class="wire-next__select-list"
          data-show="(!remote || (query.length === 0 && remoteAutoLoad) || query.length >= minSearchLength) && !loading"
          role="listbox"
        >
          {#each groups as group}
            {#if visibleOptions(group.options || []).length}
              <div class="wire-next__option-group" role="group" aria-label="{group.label}">
                <div class="wire-next__group-label">{group.label}</div>
                {#each group.options || [] as option}
                  <button
                    type="button"
                    class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
                    data-option-value="{option.value}"
                    data-show="matches(option)"
                    role="option"
                    aria-selected="{isSelected(option)}"
                    disabled="{!canSelect(option)}"
                    @mouseenter="activeIndex = optionIndex(option)"
                    @mousedown="event.preventDefault()"
                    @click="chooseOption(option)"
                  >
                    {#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
                    {#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
                    {#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
                    <span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
                    <i class="wire-next__check icon-[lucide--check]" data-show="isSelected(option)" aria-hidden="true"></i>
                  </button>
                {/each}
              </div>
            {/if}
          {/each}

          {#each options as option}
            <button
              type="button"
              class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
              data-option-value="{option.value}"
              data-show="matches(option)"
              role="option"
              aria-selected="{isSelected(option)}"
              disabled="{!canSelect(option)}"
              @mouseenter="activeIndex = optionIndex(option)"
              @mousedown="event.preventDefault()"
              @click="chooseOption(option)"
            >
              {#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
              {#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
              {#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
              <span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
              <i class="wire-next__check icon-[lucide--check]" data-show="isSelected(option)" aria-hidden="true"></i>
            </button>
          {/each}
        </div>

        <button
          type="button"
          class="wire-next__load-more"
          data-wrn-select-load-more
          data-show="infinite && hasMore"
        >{loadMoreLabel}</button>
      </div>

      {#if !allowCustomValue}
        <input
          type="hidden"
          name="{name}"
          value="{selectedValue}"
          required="{required}"
          data-error="{name}"
          aria-describedby="{validationMessage ? `${name}-validation` : helpText ? `${name}-help` : ''}"
        />
      {/if}

      {#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
      <small
        id="{name}-validation"
        class="wire-next__validation"
        data-error="{name}"
      >{validationMessage}</small>
    </div>
    <SelectStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next__combobox-toggle {
      position: absolute;
      top: 50%;
      right: 0;
      z-index: 1;
      display: grid;
      width: 2.25em;
      height: 100%;
      padding: 0;
      place-items: center;
      border: 0;
      color: var(--wire-color-muted);
      background: transparent;
      cursor: pointer;
      transform: translateY(-50%);
    }

    .wire-next--combobox .wire-next__combobox-input {
      padding-right: 5.25em;
      padding-left: 2.5em;
      cursor: text;
    }
  }
}
```

---

## Confetti

Showcase: https://component.wrnexusjs.dev/
Mount: <Confetti /> (legacy: data-component="Confetti")
Category: integrations
Purpose: Theme-aware, responsive confetti component.
Props: label: string = "Celebrate", duration: number = 1200, count: number = 24, autoStart: boolean = false, disabled: boolean = false, size: string = "default", color: string = "primary", class: string = ""
Slots: default
Events: start, complete

### Complete .wrn source contract

```wrn
component Confetti {
  outputs {
    start(payload: { sourceEvent?: Event; duration: number })
    complete(payload: { sourceEvent?: Event; duration: number })
  }

  props {
    label: string = "Celebrate"
    duration: number = 1200
    count: number = 24
    autoStart: boolean = false
    disabled: boolean = false
    size: string = "default"
    color: string = "primary"
    class: string = ""
  }

  state running = false

  functions {
    shared function pieces() { return Array.from({ length: Math.max(1, Math.min(60, count)) }) }
    shared function pieceStyle(index) {
      return "--wire-confetti-index:" + index + ";--wire-confetti-x:" + ((index * 37) % 100) + ";--wire-confetti-delay:" + ((index * 29) % 240) + "ms;"
    }
    client function finish(sourceEvent) {
      running = false
      output.complete({ sourceEvent: sourceEvent, duration: duration })
    }
    client function start(sourceEvent) {
      if (disabled || running) {
        return
      }
      running = true
      output.start({ sourceEvent: sourceEvent, duration: duration })
      setTimeout(finish, duration, sourceEvent)
    }
  }

  lifecycle {
    mount { if (autoStart) { start(null) } }
  }

  view {
    <div {...attrs} data-ui-component="Confetti" data-running='{running ? "true" : "false"}' class='wire-confetti {class}' style='--wire-confetti-duration:{duration}ms'>
      <button type="button" class="wire-confetti__trigger" disabled='{disabled}' @click='start(event)'>
        <span class="icon-[lucide--party-popper]" aria-hidden="true"></span><span>{label}</span>
      </button>
      <div class="wire-confetti__burst" aria-hidden="true">
        {#each pieces() as piece, index}<i style='{pieceStyle(index)}'></i>{/each}
      </div>
      <slot />
    </div>
  }

  style {
    .wire-confetti { position: relative; display: inline-flex; }
    .wire-confetti__trigger { display: inline-flex; min-height: 2.5rem; align-items: center; gap: 0.5rem; padding: 0.55rem 0.85rem; border: 1px solid var(--wire-color-primary); border-radius: var(--wire-radius); color: var(--wire-color-on-primary); background: var(--wire-color-primary); font: inherit; font-weight: 700; cursor: pointer; }
    .wire-confetti__trigger > span:first-child { width: 1rem; height: 1rem; }
    .wire-confetti__trigger:focus-visible { outline: 2px solid var(--wire-color-focus); outline-offset: 2px; }
    .wire-confetti__trigger:disabled { cursor: not-allowed; opacity: 0.55; }
    .wire-confetti__burst { position: absolute; inset: 50% 0 auto; height: 0; pointer-events: none; }
    .wire-confetti__burst > i { position: absolute; left: calc(var(--wire-confetti-x) * 1%); width: 0.45rem; height: 0.7rem; border-radius: 0.1rem; opacity: 0; background: hsl(calc(var(--wire-confetti-index) * 47deg) 80% 55%); transform: translate(-50%, 0) rotate(calc(var(--wire-confetti-index) * 23deg)); }
    .wire-confetti[data-running="true"] .wire-confetti__burst > i { animation: wire-confetti-burst var(--wire-confetti-duration) cubic-bezier(0.2, 0.7, 0.2, 1) var(--wire-confetti-delay) both; }
    @keyframes wire-confetti-burst {
      0% { opacity: 1; transform: translate(-50%, 0) rotate(0); }
      100% { opacity: 0; transform: translate(calc(-50% + (var(--wire-confetti-x) - 50) * 0.9px), 9rem) rotate(540deg); }
    }
    @media (prefers-reduced-motion: reduce) {
      .wire-confetti[data-running="true"] .wire-confetti__burst > i { animation-duration: 1ms; animation-delay: 0ms; }
    }
  }
}
```

---

## Container

Showcase: https://component.wrnexusjs.dev/
Mount: <Container /> (legacy: data-component="Container")
Category: layout
Purpose: Constrain and align page content with responsive gutters and compact, wide, or full width options.
Props: size: string = "default", color: string = "primary", columns: number = 2, gap: string = "md", maxWidth: string = "xl", centered: boolean = true, class: string = ""
Slots: default
Events: none

### Complete .wrn source contract

```wrn
// Container -- a centred, width-limited page wrapper.
//
//   <Container maxWidth="lg">...</Container>
//
// It also accepts columns and gap, because it always has and applications
// depend on it. Reach for Grid when a grid is the point; reach for Container
// when the point is the reading width.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Container {
  props {
    size: string = "default"
    color: string = "primary"
    // Stays 2 to match what shipped. A published default cannot be changed
    // without silently reflowing every Container already in use.
    columns: number = 2
    gap: string = "md"
    maxWidth: string = "xl"
    centered: boolean = true
    class: string = ""
  }

  functions {
    shared function columnCount() {
      var value = Number(columns)
      if (!value || value < 1) {
        return 1
      }
      return Math.min(6, value)
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="Container"
      class='wire-container {class}'
      data-size='{size}'
      data-color='{color}'
      data-gap='{gap}'
      data-max-width='{maxWidth}'
      data-centered='{centered}'
      data-columns='{columnCount()}'
    >
      <slot></slot>
    </div>
  }

  style {
    .wire-container {
      --container-gap: 1.25rem;
      width: 100%;
      min-width: 0;
      padding-inline: 1rem;
    }

    .wire-container[data-centered="true"] {
      margin-inline: auto;
    }

    /* Reading widths, not breakpoints: the cap is about line length. */
    .wire-container[data-max-width="md"] {
      max-width: 48rem;
    }

    .wire-container[data-max-width="lg"] {
      max-width: 64rem;
    }

    .wire-container[data-max-width="xl"],
    .wire-container[data-max-width="wide"] {
      max-width: 80rem;
    }

    .wire-container[data-max-width="2xl"] {
      max-width: 96rem;
    }

    .wire-container[data-max-width="full"] {
      max-width: none;
    }

    .wire-container[data-size="compact"] {
      padding-inline: 1rem;
    }

    .wire-container[data-size="comfortable"] {
      padding-inline: 1.25rem;
    }

    .wire-container[data-size="spacious"] {
      padding-inline: 1.5rem;
    }

    .wire-container[data-gap="xs"] {
      --container-gap: 0.5rem;
    }

    .wire-container[data-gap="sm"] {
      --container-gap: 0.75rem;
    }

    .wire-container[data-gap="lg"] {
      --container-gap: 2rem;
    }

    .wire-container[data-gap="xl"] {
      --container-gap: 2.5rem;
    }

    /*
     * Columns are opt-in. One column stays plain flow so a container does not
     * quietly turn every page into a grid.
     */
    .wire-container:not([data-columns="1"]) {
      display: grid;
      gap: var(--container-gap);
      grid-template-columns: 1fr;
    }

    @media (min-width: 640px) {
      .wire-container[data-columns="2"],
      .wire-container[data-columns="3"],
      .wire-container[data-columns="4"],
      .wire-container[data-columns="5"],
      .wire-container[data-columns="6"] {
        grid-template-columns: repeat(2, minmax(0, 1fr));
      }

      .wire-container[data-size="default"],
      .wire-container[data-size="comfortable"],
      .wire-container[data-size="spacious"] {
        padding-inline: 1.5rem;
      }
    }

    @media (min-width: 1024px) {
      .wire-container[data-columns="3"] {
        grid-template-columns: repeat(3, minmax(0, 1fr));
      }

      .wire-container[data-columns="4"] {
        grid-template-columns: repeat(4, minmax(0, 1fr));
      }

      .wire-container[data-columns="5"] {
        grid-template-columns: repeat(5, minmax(0, 1fr));
      }

      .wire-container[data-columns="6"] {
        grid-template-columns: repeat(6, minmax(0, 1fr));
      }

      .wire-container[data-size="default"],
      .wire-container[data-size="comfortable"],
      .wire-container[data-size="spacious"] {
        padding-inline: 2rem;
      }
    }
  }
}
```

---

## ContextMenu

Showcase: https://component.wrnexusjs.dev/
Mount: <ContextMenu /> (legacy: data-component="ContextMenu")
Category: overlays
Purpose: Open an accessible keyboard-aware action menu from pointer or keyboard context interactions.
Props: items: unknown[] = [], open: boolean = false, defaultOpen: boolean = false, trigger: string = "contextmenu", placement: string = "pointer", align: string = "start", size: string = "default", color: string = "primary", variant: string = "raised", title: string = "", description: string = "", label: string = "Context menu", closeOnSelect: boolean = true, closeOnOutside: boolean = true, disabled: boolean = false, minWidth: string = "14rem", maxWidth: string = "20rem", class: string = ""
Slots: trigger, header, default, footer
Events: open, close, select, action

### Complete .wrn source contract

```wrn
component ContextMenu {
  outputs {
    open(payload: { x: number; y: number; trigger: string; sourceEvent: Event })
    close(payload: { reason: string; sourceEvent: Event })
    select(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
    action(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
  }

  props {

    items: unknown[] = []
    open: boolean = false
    defaultOpen: boolean = false
    trigger: string = "contextmenu"
    placement: string = "pointer"
    align: string = "start"
    size: string = "default"
    color: string = "primary"
    variant: string = "raised"
    title: string = ""
    description: string = ""
    label: string = "Context menu"
    closeOnSelect: boolean = true
    closeOnOutside: boolean = true
    disabled: boolean = false
    minWidth: string = "14rem"
    maxWidth: string = "20rem"
    class: string = ""
  }

  state visible = defaultOpen
  state positionX: number = 16
  state positionY: number = 16

  functions {
    shared function isOpen() {
      return open || visible
    }

    client function showMenu(sourceEvent) {
      if (disabled) {
        return
      }
      if (sourceEvent && sourceEvent.type === "contextmenu") {
        sourceEvent.preventDefault()
      }
      if (placement === "pointer" && sourceEvent) {
        // Place the menu at the pointer and let the anchored clamp in the
        // runtime pull it back on screen once it has been laid out and can
        // actually be measured. Subtracting a guessed 340x420 here instead
        // pushed every menu that was not that size away from the pointer.
        positionX = Math.max(12, sourceEvent.clientX || 12)
        positionY = Math.max(12, sourceEvent.clientY || 12)
      }
      visible = true
      output.open({
        x: positionX,
        y: positionY,
        trigger: trigger,
        sourceEvent: sourceEvent
      })
    }

    client function hideMenu(reason, sourceEvent) {
      visible = false
      output.close({
        reason: reason,
        sourceEvent: sourceEvent
      })
    }

    client function toggleMenu(sourceEvent) {
      if (isOpen()) {
        hideMenu("toggle", sourceEvent)
      } else {
        showMenu(sourceEvent)
      }
    }

    client function chooseItem(item, itemIndex, sourceEvent) {
      if (disabled || item.disabled || item.type === "header" || item.type === "divider") {
        sourceEvent.preventDefault()
        return
      }
      output.select({
        item: item,
        itemIndex: itemIndex,
        value: item.value || "",
        sourceEvent: sourceEvent
      })
      if (item.action) {
        output.action({
          item: item,
          itemIndex: itemIndex,
          value: item.value || "",
          sourceEvent: sourceEvent
        })
      }
      if (closeOnSelect) {
        hideMenu("select", sourceEvent)
      }
    }

    client function moveFocus(sourceEvent, direction) {
      const root = sourceEvent.currentTarget.closest(".wire-context-menu") || sourceEvent.currentTarget
      const options = [...root.querySelectorAll("[data-context-menu-item]:not([disabled])")]
      if (!options.length) {
        return
      }
      const activeIndex = options.indexOf(document.activeElement)
      const nextIndex = (activeIndex + direction + options.length) % options.length
      options[nextIndex].focus()
    }

    client function handleKeydown(sourceEvent) {
      if (sourceEvent.key === "Escape") {
        sourceEvent.preventDefault()
        hideMenu("escape", sourceEvent)
      } else if (sourceEvent.key === "ArrowDown") {
        sourceEvent.preventDefault()
        moveFocus(sourceEvent, 1)
      } else if (sourceEvent.key === "ArrowUp") {
        sourceEvent.preventDefault()
        moveFocus(sourceEvent, -1)
      }
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="ContextMenu"
      data-open='{open || visible ? "true" : "false"}'
      data-trigger='{trigger}'
      data-placement='{placement}'
      data-align='{align}'
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      class='wire-context-menu {class}'
      style='--wire-context-x:{positionX}px;--wire-context-y:{positionY}px;--wire-context-min-width:{minWidth};--wire-context-max-width:{maxWidth};'
      @keydown='handleKeydown(event)'
    >
      <div
        class="wire-context-menu__trigger"
        tabindex='{disabled ? "-1" : "0"}'
        aria-haspopup="menu"
        aria-expanded='{open || visible ? "true" : "false"}'
        @contextmenu='if (trigger === "contextmenu" || trigger === "both") { showMenu(event) }'
        @click='if (trigger === "click" || trigger === "both") { toggleMenu(event) }'
        @keydown='if (event.key === "Enter" || event.key === " ") { event.preventDefault(); toggleMenu(event) }'
      >
        <slot name="trigger"></slot>
      </div>

      {#if closeOnOutside}
        <button
          type="button"
          class="wire-context-menu__dismiss-layer"
          data-show='{open || visible}'
          aria-label="Close context menu"
          @click='hideMenu("outside", event)'
        ></button>
      {/if}

      <div
        class="wire-context-menu__panel"
        data-wrn-anchored="true"
        data-show='{open || visible}'
        role="menu"
        aria-label='{label}'
      >
        {#if title || description}
          <div class="wire-context-menu__header">
            {#if title}
              <strong>{title}</strong>
            {/if}
            {#if description}
              <span>{description}</span>
            {/if}
          </div>
        {/if}

        <slot name="header"></slot>

        <div class="wire-context-menu__items">
          {#each items as item, itemIndex}
            {#if item.type === "divider"}
              <div class="wire-context-menu__divider" role="separator"></div>
            {:else if item.type === "header"}
              <div class="wire-context-menu__section-label">{item.label || item.title}</div>
            {:else if item.href}
              <a
                href='{item.href}'
                target='{item.target || ""}'
                rel='{item.external || item.target === "_blank" ? "noopener noreferrer" : (item.rel || "")}'
                role="menuitem"
                data-context-menu-item
                data-danger='{item.danger ? "true" : "false"}'
                data-selected='{item.selected || item.checked ? "true" : "false"}'
                aria-disabled='{item.disabled ? "true" : "false"}'
                class="wire-context-menu__item"
                @click='chooseItem(item, itemIndex, event)'
              >
                {#if item.icon}
                  <span class='wire-context-menu__icon {item.icon}' aria-hidden="true"></span>
                {/if}
                <span class="wire-context-menu__copy">
                  <strong>{item.label || item.title}</strong>
                  {#if item.description}
                    <small>{item.description}</small>
                  {/if}
                </span>
                {#if item.shortcut}
                  <kbd>{item.shortcut}</kbd>
                {:else if item.checked || item.selected}
                  <span class="icon-[lucide--check] wire-context-menu__status" aria-hidden="true"></span>
                {:else if item.external}
                  <span class="icon-[lucide--arrow-up-right] wire-context-menu__status" aria-hidden="true"></span>
                {/if}
              </a>
            {:else}
              <button
                type="button"
                role="menuitem"
                data-context-menu-item
                data-danger='{item.danger ? "true" : "false"}'
                data-selected='{item.selected || item.checked ? "true" : "false"}'
                disabled='{item.disabled}'
                class="wire-context-menu__item"
                @click='chooseItem(item, itemIndex, event)'
              >
                {#if item.icon}
                  <span class='wire-context-menu__icon {item.icon}' aria-hidden="true"></span>
                {/if}
                <span class="wire-context-menu__copy">
                  <strong>{item.label || item.title}</strong>
                  {#if item.description}
                    <small>{item.description}</small>
                  {/if}
                </span>
                {#if item.shortcut}
                  <kbd>{item.shortcut}</kbd>
                {:else if item.checked || item.selected}
                  <span class="icon-[lucide--check] wire-context-menu__status" aria-hidden="true"></span>
                {/if}
              </button>
            {/if}
          {/each}
        </div>

        <slot></slot>
        <slot name="footer"></slot>
      </div>
    </div>
  }

  style {
    .wire-context-menu {
      --context-accent: var(--wire-color-primary);
      --context-soft: var(--wire-color-primary-soft);
      position: relative;
      display: block;
      min-width: 0;
    }

    .wire-context-menu[data-color="secondary"] {
      --context-accent: var(--wire-color-secondary);
      --context-soft: var(--wire-color-secondary-soft);
    }

    .wire-context-menu[data-color="info"] {
      --context-accent: var(--wire-color-info);
      --context-soft: var(--wire-color-info-soft);
    }

    .wire-context-menu[data-color="success"] {
      --context-accent: var(--wire-color-success);
      --context-soft: var(--wire-color-success-soft);
    }

    .wire-context-menu[data-color="warning"] {
      --context-accent: var(--wire-color-warning);
      --context-soft: var(--wire-color-warning-soft);
    }

    .wire-context-menu[data-color="danger"] {
      --context-accent: var(--wire-color-danger);
      --context-soft: var(--wire-color-danger-soft);
    }

    .wire-context-menu__trigger {
      display: block;
      min-width: 0;
      outline: none;
    }

    .wire-context-menu__trigger:focus-visible {
      border-radius: 0.6rem;
      outline: 2px solid var(--wire-color-focus);
      outline-offset: 3px;
    }

    .wire-context-menu__dismiss-layer {
      position: fixed;
      inset: 0;
      z-index: 1090;
      appearance: none;
      padding: 0;
      background: transparent;
      border: 0;
    }

    .wire-context-menu__panel {
      position: absolute;
      z-index: 1091;
      top: calc(100% + 0.5rem);
      left: 0;
      width: max-content;
      min-width: var(--wire-context-min-width);
      max-width: min(var(--wire-context-max-width), calc(100vw - 1.5rem));
      padding: 0.45rem;
      color: var(--wire-color-text);
      background:
        linear-gradient(145deg, color-mix(in srgb, var(--context-accent) 5%, transparent), transparent 58%),
        color-mix(in srgb, var(--wire-color-surface-raised) 96%, transparent);
      border: 1px solid color-mix(in srgb, var(--context-accent) 18%, var(--wire-color-border));
      border-radius: 1rem;
      box-shadow:
        0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
        0 24px 64px color-mix(in srgb, black 24%, transparent);
      backdrop-filter: blur(18px);
      transform-origin: top left;
    }

    .wire-context-menu[data-placement="pointer"] .wire-context-menu__panel {
      position: fixed;
      top: var(--wire-context-y);
      left: var(--wire-context-x);
    }

    .wire-context-menu[data-placement="bottom-end"] .wire-context-menu__panel {
      right: 0;
      left: auto;
      transform-origin: top right;
    }

    .wire-context-menu[data-placement="top-start"] .wire-context-menu__panel {
      top: auto;
      bottom: calc(100% + 0.5rem);
      transform-origin: bottom left;
    }

    .wire-context-menu[data-placement="top-end"] .wire-context-menu__panel {
      top: auto;
      right: 0;
      bottom: calc(100% + 0.5rem);
      left: auto;
      transform-origin: bottom right;
    }

    .wire-context-menu[data-variant="soft"] .wire-context-menu__panel {
      background:
        linear-gradient(145deg, var(--context-soft), transparent 72%),
        var(--wire-color-surface-raised);
      box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
    }

    .wire-context-menu[data-variant="outline"] .wire-context-menu__panel {
      background: var(--wire-color-surface-raised);
      box-shadow: none;
    }

    .wire-context-menu__header {
      display: grid;
      gap: 0.2rem;
      padding: 0.65rem 0.75rem 0.75rem;
      border-bottom: 1px solid var(--wire-color-border);
    }

    .wire-context-menu__header strong {
      font-size: 0.86rem;
      font-weight: 650;
    }

    .wire-context-menu__header span {
      color: var(--wire-color-text-muted);
      font-size: 0.75rem;
      line-height: 1.45;
    }

    .wire-context-menu__items {
      display: grid;
      gap: 0.15rem;
      padding-block: 0.25rem;
    }

    .wire-context-menu__section-label {
      padding: 0.55rem 0.75rem 0.3rem;
      color: var(--wire-color-text-muted);
      font-size: 0.68rem;
      font-weight: 700;
      letter-spacing: 0.12em;
      text-transform: uppercase;
    }

    .wire-context-menu__divider {
      height: 1px;
      margin: 0.3rem 0.45rem;
      background: var(--wire-color-border);
    }

    .wire-context-menu__item {
      appearance: none;
      display: grid;
      grid-template-columns: auto minmax(0, 1fr) auto;
      align-items: center;
      gap: 0.7rem;
      width: 100%;
      min-width: 0;
      padding: 0.65rem 0.7rem;
      color: var(--wire-color-text);
      background: transparent;
      border: 0;
      border-radius: 0.72rem;
      font: inherit;
      text-align: left;
      text-decoration: none;
      cursor: pointer;
      transition: background-color 150ms ease, color 150ms ease, transform 150ms ease;
    }

    .wire-context-menu__item:hover,
    .wire-context-menu__item:focus-visible,
    .wire-context-menu__item[data-selected="true"] {
      color: var(--context-accent);
      background: var(--context-soft);
      outline: none;
    }

    .wire-context-menu__item:active {
      transform: scale(0.985);
    }

    .wire-context-menu__item[data-danger="true"] {
      color: var(--wire-color-danger);
    }

    .wire-context-menu__item[data-danger="true"]:hover,
    .wire-context-menu__item[data-danger="true"]:focus-visible {
      background: var(--wire-color-danger-soft);
    }

    .wire-context-menu__item[disabled],
    .wire-context-menu__item[aria-disabled="true"] {
      opacity: 0.48;
      pointer-events: none;
    }

    .wire-context-menu__icon,
    .wire-context-menu__status {
      width: 1rem;
      height: 1rem;
      color: currentColor;
    }

    .wire-context-menu__copy {
      display: grid;
      gap: 0.12rem;
      min-width: 0;
    }

    .wire-context-menu__copy strong {
      overflow: hidden;
      font-size: 0.82rem;
      font-weight: 600;
      line-height: 1.3;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .wire-context-menu__copy small {
      overflow: hidden;
      color: var(--wire-color-text-muted);
      font-size: 0.7rem;
      line-height: 1.35;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .wire-context-menu kbd {
      padding: 0.16rem 0.38rem;
      color: var(--wire-color-text-muted);
      background: var(--wire-color-surface-soft);
      border: 1px solid var(--wire-color-border);
      border-radius: 0.36rem;
      font-size: 0.64rem;
      font-family: inherit;
    }

    .wire-context-menu[data-size="sm"] .wire-context-menu__item {
      padding: 0.52rem 0.6rem;
    }

    .wire-context-menu[data-size="lg"] .wire-context-menu__item {
      padding: 0.78rem 0.82rem;
    }

    @media (max-width: 639px) {
      .wire-context-menu[data-placement="pointer"] .wire-context-menu__panel {
        right: 0.75rem;
        bottom: 0.75rem;
        left: 0.75rem;
        top: auto;
        width: auto;
        max-width: none;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .wire-context-menu__item {
        transition: none;
      }
    }
  }
}
```

---

## CustomScrollbar

Showcase: https://component.wrnexusjs.dev/
Mount: <CustomScrollbar /> (legacy: data-component="CustomScrollbar")
Category: layout
Purpose: Theme-aware, responsive custom scrollbar component.
Props: color: string = "primary", size: string = "default", axis: string = "vertical", thickness: number = 8, maxHeight: string = "20rem", radius: string = "999px", class: string = ""
Slots: default
Events: none

### Complete .wrn source contract

```wrn
// CustomScrollbar -- a scrolling region with a themed scrollbar.
//
//   <CustomScrollbar maxHeight="20rem">...long content...</CustomScrollbar>
//
// Scrollbar appearance is CSS, not script: scrollbar-width and scrollbar-color
// are the standard properties, and the ::-webkit-scrollbar rules cover the
// browsers that still need them.
//
// This component used to declare a scroll output it never emitted, and its
// props were columns, gap and maxWidth copied from a grid scaffold. The output
// is removed rather than left unimplemented -- a caller can listen for a plain
// scroll event on the element, which is what it would have been anyway.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component CustomScrollbar {
  props {
    color: string = "primary"
    size: string = "default"
    axis: string = "vertical"
    // Track thickness in pixels. Clamped, because it arrives as an attribute
    // and a scrollbar wider than the content is not useful to anyone.
    thickness: number = 8
    maxHeight: string = "20rem"
    radius: string = "999px"
    class: string = ""
  }

  functions {
    shared function trackSize() {
      var value = Number(thickness)
      if (!value || value < 2) {
        return 8
      }
      return Math.min(24, value)
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="CustomScrollbar"
      class='wire-scrollbar {class}'
      data-color='{color}'
      data-size='{size}'
      data-axis='{axis}'
      style='--scrollbar-thickness:{trackSize()}px;--scrollbar-radius:{radius};max-height:{maxHeight};'
    >
      <slot />
    </div>
  }

  style {
    .wire-scrollbar {
      --scrollbar-accent: var(--wire-color-primary);
      min-width: 0;
      overflow: auto;
      /*
       * The standard properties. Firefox and current Chrome honour these; the
       * webkit rules below are only for the engines that still ignore them.
       */
      scrollbar-width: thin;
      scrollbar-color: color-mix(in srgb, var(--scrollbar-accent) 55%, transparent) transparent;
      overscroll-behavior: contain;
    }

    .wire-scrollbar[data-color="secondary"] {
      --scrollbar-accent: var(--wire-color-secondary);
    }

    .wire-scrollbar[data-color="success"] {
      --scrollbar-accent: var(--wire-color-success);
    }

    .wire-scrollbar[data-color="danger"] {
      --scrollbar-accent: var(--wire-color-danger);
    }

    .wire-scrollbar[data-color="info"] {
      --scrollbar-accent: var(--wire-color-info);
    }

    .wire-scrollbar[data-axis="vertical"] {
      overflow-x: hidden;
      overflow-y: auto;
    }

    .wire-scrollbar[data-axis="horizontal"] {
      overflow-x: auto;
      overflow-y: hidden;
    }

    .wire-scrollbar::-webkit-scrollbar {
      width: var(--scrollbar-thickness, 8px);
      height: var(--scrollbar-thickness, 8px);
    }

    .wire-scrollbar::-webkit-scrollbar-track {
      background: var(--wire-color-surface-soft);
      border-radius: var(--scrollbar-radius, 999px);
    }

    .wire-scrollbar::-webkit-scrollbar-thumb {
      background: color-mix(in srgb, var(--scrollbar-accent) 45%, transparent);
      border-radius: var(--scrollbar-radius, 999px);
    }

    .wire-scrollbar::-webkit-scrollbar-thumb:hover {
      background: var(--scrollbar-accent);
    }

    /*
     * A pointer-less device paints its own overlay scrollbar and ignores the
     * width above, so the region keeps its own padding instead.
     */
    @media (hover: none) {
      .wire-scrollbar {
        scrollbar-width: auto;
      }
    }
  }
}
```

---

## DataMap

Showcase: https://component.wrnexusjs.dev/
Mount: <DataMap /> (legacy: data-component="DataMap")
Category: integrations
Purpose: Theme-aware, responsive data map component.
Props: size: string = "default", color: string = "primary", title: string = "Data Map", description: string = "", items: unknown[] = [], variant: string = "default", class: string = ""
Slots: default
Events: select, change

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component DataMap {
  outputs {
    select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
  }

  props {
    size: string = "default"
    color: string = "primary"
    title: string = "Data Map"
    description: string = ""
    items: unknown[] = []
    variant: string = "default"
    class: string = ""
  }
  view {
    <section class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--data-map wire-next--variant-{variant} {class}">
      {#if title}<strong>{title}</strong>{/if}
      {#if description}<p>{description}</p>{/if}
      {#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
      <slot />
    </section>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--data-map {
      display: grid;
      gap: 0.75rem;
      padding: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius);
      color: var(--wire-color-text);
      background: var(--wire-color-surface);
    }
    .wire-next--data-map > p { margin: 0; color: var(--wire-color-muted); }
    .wire-next--data-map > .wire-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
    .wire-next--data-map > .wire-next__items > span { padding: 0.35rem 0.6rem; border-radius: var(--wire-radius-sm); background: var(--wire-color-surface-2); }
  }
}
```

---

## DataTable

Showcase: https://component.wrnexusjs.dev/
Mount: <DataTable /> (legacy: data-component="DataTable")
Category: tables
Purpose: Sortable, filterable, paginated data table with row selection.
Props: color: string = "primary", size: string = "default", columns: unknown[] = [], rows: unknown[] = [], rowKey: string = "id", remote: boolean = false, loadingLabel: string = "Loading", errorLabel: string = "Could not load this data", retryLabel: string = "Try again", caption: string = "", description: string = "", searchable: boolean = true, searchPlaceholder: string = "Search", paginated: boolean = true, pageSize: number = 10, paginationStyle: string = "compact", pageSizes: number[] = [10, 25, 50], selectable: boolean = false, actions: unknown[] = [], striped: boolean = true, bordered: boolean = true, gridlines: string = "rows", density: string = "default", emptyLabel: string = "No records to show", noResultsLabel: string = "No records match your search", clearSearchLabel: string = "Clear search", stickyFirstColumn: boolean = false, layout: string = "rows", class: string = ""
Slots: default
Events: sort, search, pageChange, select, change, rowClick, action, request

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

//
// DataTable -- columns, sorting, filtering, pagination and selection over an
// array of records.
//
//   <DataTable
//     columns='[{"key":"name","label":"Name","sortable":true},
//               {"key":"plan","label":"Plan"},
//               {"key":"seats","label":"Seats","align":"end","sortable":true}]'
//     rows='[{"id":1,"name":"Acme","plan":"Scale","seats":42}]'
//     pageSize={10}
//     selectable={true}
//   />
//
// Columns are plain objects so they survive being passed as an attribute:
//   key       which field of the row to show (required)
//   label     header text; defaults to the key
//   align     start | center | end
//   width     any CSS width for the column
//   sortable  show a sort control on the header
//
// Rows are matched by `rowKey` (default "id"). Sorting, filtering and paging
// are all derived through shared functions, so the first paint is server
// rendered and the same code keeps it live in the browser.
//
// Turn pagination off with paginated={false} to render every row.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
import Dropdown from "./Dropdown.wrn"

component DataTable {
  outputs {
    sort(payload: { key: string; direction: string })
    search(payload: { query: string })
    pageChange(payload: { page: number; pageSize: number })
    select(payload: { selected: Array<string | number>; all: boolean })
    change(payload: { page: number; pageSize: number; total: number; query: string; sortKey: string; sortDirection: string })
    rowClick(payload: { row: object; sourceEvent: Event })
    action(payload: { id: string; selected: Array<string | number>; rows: object[]; sourceEvent: Event })
    request(payload: { instanceId: number; page: number; pageSize: number; sortKey: string; sortDirection: string; query: string })
  }

  props {
    color: string = "primary"
    size: string = "default"
    columns: unknown[] = []
    rows: unknown[] = []
    rowKey: string = "id"
    // Async source.
    //
    // Set remote={true} and the table stops deriving anything locally: it
    // emits a `request` output carrying { instanceId, page, pageSize,
    // sortKey, sortDirection, query } whenever the view changes, and waits to
    // be handed rows back. Sorting, filtering and paging all become the
    // server's job, because the table only ever sees the page it was given.
    //
    // Answer it by dispatching the result back with the SAME instanceId:
    //
    //   function loadRows(payload) {
    //     fetch("/api/rows?page=" + payload.page)
    //       .then(function (r) { return r.json() })
    //       .then(function (data) {
    //         window.dispatchEvent(new CustomEvent("wrnexus:datatable:rows", {
    //           detail: { instanceId: payload.instanceId,
    //                     rows: data.rows, total: data.total }
    //         }))
    //       })
    //   }
    //
    // It is an event rather than a `load` function prop because props travel
    // as HTML attributes: a function passed that way arrives as its own source
    // text, not something callable.
    remote: boolean = false
    loadingLabel: string = "Loading"
    errorLabel: string = "Could not load this data"
    retryLabel: string = "Try again"
    caption: string = ""
    description: string = ""
    // Filtering
    searchable: boolean = true
    searchPlaceholder: string = "Search"
    // Pagination. paginated={false} renders every row and hides the footer.
    paginated: boolean = true
    pageSize: number = 10
    // compact = previous/next arrows only. numbered = clickable page numbers.
    paginationStyle: string = "compact"
    // Typed concretely, not unknown[]: the showcase generator fills an
    // unknown[] prop with rich demo objects, and every option in the picker
    // then rendered as [object Object].
    pageSizes: number[] = [10, 25, 50]
    // Selection
    selectable: boolean = false
    // Toolbar actions. Each entry is an object:
    //   id       returned with the action output so a host can tell them apart
    //   label    button text
    //   icon     optional icon class
    //   tone     "danger" styles it destructively
    //   when     "always" (default) or "selection" -- selection-only actions
    //            stay hidden until at least one row is ticked
    //   onClick  optional callback, receives the selected rows
    actions: unknown[] = []
    // Presentation
    striped: boolean = true
    bordered: boolean = true
    // rows = horizontal rules only. grid = full row and column gridlines.
    gridlines: string = "rows"
    density: string = "default"
    emptyLabel: string = "No records to show"
    // Shown when a filter matches nothing, as opposed to there being no data
    // at all -- the two need different wording and different remedies.
    noResultsLabel: string = "No records match your search"
    clearSearchLabel: string = "Clear search"
    // Keeps the first column in view while the rest scrolls sideways.
    stickyFirstColumn: boolean = false
    // rows = one record per row. comparison = transposed, fields down the
    // left and one column per record.
    layout: string = "rows"
    class: string = ""
  }

  state query = ""
  state sortKey = ""
  state sortDirection = "asc"
  state paginationPage = 1
  state perPage = pageSize
  state selectedKeys = []
  state remoteRows = []
  state remoteTotal = 0
  state loading = false
  state loadError = ""
  // Distinguishes this instance from any other table on the page, since the
  // async result comes back through a window event.
  state instanceId = 0
  state lastAnchor = ""

  functions {
    // --- derivation -------------------------------------------------------
    // Shared so the server renders the first page and the browser keeps it in
    // step from the same source. Each step is a pure function of state, which
    // is what lets the view call them straight from data-for.
    shared function columnList() {
      return Array.isArray(columns) ? columns : []
    }

    shared function isRemote() {
      return !!remote
    }

    shared function allRows() {
      if (isRemote()) {
        return Array.isArray(remoteRows) ? remoteRows : []
      }
      return Array.isArray(rows) ? rows : []
    }

    // Falls back to the row's position when it carries no key. Returning a
    // constant made every keyless row share one identity, so ticking one
    // ticked them all -- a silent, and very confusing, data bug.
    shared function keyOf(row, index) {
      if (row && row[rowKey] !== undefined && row[rowKey] !== null) {
        return row[rowKey]
      }
      return "__row:" + (index === undefined ? allRows().indexOf(row) : index)
    }

    shared function cellText(row, column) {
      var value = row ? row[column.key] : ""
      return value === undefined || value === null ? "" : String(value)
    }

    // A column may render markup instead of text, either by naming a field
    // that already holds markup (html: "statusBadge") or by supplying a
    // render(row) function. Returns "" when the column is plain text, which
    // is what keeps the escaping default in place for everything else.
    shared function cellMarkup(row, column) {
      if (!column || !row) {
        return ""
      }
      if (typeof column.render === "function") {
        try {
          var rendered = column.render(row)
          return rendered === undefined || rendered === null ? "" : String(rendered)
        } catch (error) {
          console.error("[wrnexus] data table column render failed", error)
          return ""
        }
      }
      if (column.html) {
        var field = column.html === true ? column.key : column.html
        var value = row[field]
        return value === undefined || value === null ? "" : String(value)
      }
      return ""
    }

    shared function matchesQuery(row) {
      if (!query) {
        return true
      }
      var needle = String(query).toLowerCase()
      return columnList().some(function (column) {
        return cellText(row, column).toLowerCase().indexOf(needle) !== -1
      })
    }

    shared function filteredRows() {
      // The server did the filtering; re-filtering here would hide rows it
      // deliberately returned.
      if (isRemote()) {
        return allRows()
      }
      return allRows().filter(function (row) { return matchesQuery(row) })
    }

    shared function sortedRows() {
      var list = filteredRows().slice()
      if (isRemote() || !sortKey) {
        return list
      }
      var factor = sortDirection === "desc" ? -1 : 1
      var kind = sortColumnType()
      return list.sort(function (left, right) {
        var a = left ? left[sortKey] : ""
        var b = right ? right[sortKey] : ""

        // Dates compared as text put "9 Feb" after "10 Jan"; parse them.
        if (kind === "date") {
          var at = Date.parse(a)
          var bt = Date.parse(b)
          if (!isNaN(at) && !isNaN(bt)) {
            return (at - bt) * factor
          }
        }

        if (kind === "number" || (typeof a === "number" && typeof b === "number")) {
          return ((Number(a) || 0) - (Number(b) || 0)) * factor
        }

        var as = a === undefined || a === null ? "" : String(a)
        var bs = b === undefined || b === null ? "" : String(b)
        return as.localeCompare(bs) * factor
      })
    }

    // A column may declare type: "date" | "number" | "text".
    shared function sortColumnType() {
      var found = ""
      columnList().forEach(function (column) {
        if (column.key === sortKey && column.type) {
          found = column.type
        }
      })
      return found
    }

    shared function totalCount() {
      // Only the source knows how many records exist beyond this page.
      if (isRemote()) {
        return Number(remoteTotal) || 0
      }
      return sortedRows().length
    }

    shared function pageCount() {
      if (!paginated) {
        return 1
      }
      var rowsPerPage = Number(perPage) > 0 ? Number(perPage) : 10
      return Math.max(1, Math.ceil(totalCount() / rowsPerPage))
    }

    // Clamped rather than stored: deleting or filtering rows can strand the
    // page number past the end, and a table showing nothing with rows
    // available is worse than one that quietly lands on the last page.
    shared function currentPage() {
      return Math.min(Math.max(1, Number(paginationPage) || 1), pageCount())
    }

    shared function visibleRows() {
      var list = sortedRows()
      if (isRemote()) {
        return list
      }
      if (!paginated) {
        return list
      }
      var rowsPerPage = Number(perPage) > 0 ? Number(perPage) : 10
      var start = (currentPage() - 1) * rowsPerPage
      return list.slice(start, start + rowsPerPage)
    }

    // Cells are built per row so the view can nest a loop inside its rows.
    shared function cellsFor(row) {
      return columnList().map(function (column) {
        return {
          key: column.key,
          label: columnLabel(column),
          value: cellText(row, column),
          html: cellMarkup(row, column),
          align: columnAlign(column),
          width: column.width || ""
        }
      })
    }

    /*
     * One view object per visible row, carrying everything the row renders.
     *
     * Same rule as headerColumns: a loop only re-runs when something its LIST
     * expression reads changes. The row loop read visibleRows(), which knows
     * nothing about selection, so ticking a checkbox left every row exactly as
     * it was built. Reading selectedKeys here ties the two together, and the
     * cells come along so the nested loop has nothing left to compute.
     */
    shared function rowViews() {
      var keys = selectedKeys
      return visibleRows().map(function (row, index) {
        var identity = keyOf(row, index)
        return {
          key: identity,
          raw: row,
          selected: keys.indexOf(identity) !== -1 ? "true" : "false",
          cells: cellsFor(row)
        }
      })
    }

    // Comparison layout: the table is transposed, so a rendered row is a
    // FIELD and each of its cells is one record. Built here rather than in the
    // view because a loop can only iterate one list.
    shared function comparisonRows() {
      var records = visibleRows()
      return columnList().map(function (column, columnIndex) {
        return {
          key: column.key || String(columnIndex),
          label: columnLabel(column),
          align: columnAlign(column),
          cells: records.map(function (record, recordIndex) {
            return {
              key: keyOf(record, recordIndex),
              value: cellText(record, column),
              html: cellMarkup(record, column),
              align: columnAlign(column)
            }
          })
        }
      })
    }

    // Column headings for the transposed table: the first record field makes
    // a readable caption for each record column.
    shared function comparisonHeadings() {
      var first = columnList()[0]
      return visibleRows().map(function (record, index) {
        return {
          key: keyOf(record, index),
          label: first ? cellText(record, first) : String(index + 1)
        }
      })
    }

    shared function rangeStart() {
      return totalCount() === 0 ? 0 : (currentPage() - 1) * (Number(perPage) || 10) + 1
    }

    shared function rangeEnd() {
      if (!paginated) {
        return totalCount()
      }
      return Math.min(currentPage() * (Number(perPage) || 10), totalCount())
    }

    // No data at all, versus a filter that happens to match nothing.
    shared function isFiltered() {
      return !!query
    }

    shared function emptyMessage() {
      return isFiltered() ? noResultsLabel : emptyLabel
    }

    shared function rangeLabel() {
      if (totalCount() === 0) {
        return emptyLabel
      }
      return "Showing " + rangeStart() + " to " + rangeEnd() + " of " + totalCount()
    }

    shared function columnAlign(column) {
      return column && column.align ? column.align : "start"
    }

    shared function columnLabel(column) {
      return column && column.label ? column.label : (column ? column.key : "")
    }

    /*
     * Headers carry their own sort state rather than each cell asking for it.
     * Attributes inside a data-for row are resolved when the row is built, and
     * this loop only depends on the column list -- so changing the sort left
     * aria-sort and the active highlight frozen at whatever they were on first
     * render. Reading sortKey/sortDirection here makes the header loop itself
     * depend on them, so the row is rebuilt with fresh values.
     */
    shared function headerColumns() {
      var activeKey = sortKey
      var direction = sortDirection
      return columnList().map(function (column) {
        var isActive = activeKey === column.key
        return {
          key: column.key,
          label: columnLabel(column),
          align: columnAlign(column),
          width: column.width || "",
          sortable: column.sortable ? true : false,
          active: isActive ? "true" : "false",
          ariaSort: isActive ? (direction === "desc" ? "descending" : "ascending") : "none"
        }
      })
    }

    // Windowed around the current page: a thousand-page table should not try
    // to render a thousand buttons.
    shared function actionList() {
      return Array.isArray(actions) ? actions : []
    }

    // The rows behind the current selection, in the order they appear in the
    // data, so a handler receives records rather than bare keys.
    shared function selectedRows() {
      var keys = selectedKeys
      return allRows().filter(function (row, index) {
        return keys.indexOf(keyOf(row, index)) !== -1
      })
    }

    shared function selectedCount() {
      return selectedRows().length
    }

    // Reads selectedKeys so the toolbar re-renders as the selection changes;
    // a loop only re-runs when its list expression depends on what changed.
    shared function visibleActions() {
      var count = selectedKeys.length
      return actionList()
        .filter(function (action) {
          return action.when === "selection" ? count > 0 : true
        })
        .map(function (action) {
          return {
            id: action.id || action.label,
            label: action.label || "",
            icon: action.icon || "",
            tone: action.tone || "",
            raw: action
          }
        })
    }

    shared function pageSizeItems() {
      var current = Number(perPage) || 10
      return (Array.isArray(pageSizes) ? pageSizes : [10, 25, 50]).map(function (option) {
        return {
          label: String(option),
          value: Number(option),
          selected: Number(option) === current
        }
      })
    }

    shared function pageNumbers() {
      var total = pageCount()
      var active = currentPage()
      var first = Math.max(1, active - 2)
      var last = Math.min(total, active + 2)
      var list = []
      var index = first
      while (index <= last) {
        list.push({ number: index, active: index === active ? "true" : "false" })
        index = index + 1
      }
      return list
    }

    shared function sortState(key) {
      if (sortKey !== key) {
        return "none"
      }
      return sortDirection === "desc" ? "descending" : "ascending"
    }

    shared function isSelectedKey(identity) {
      return selectedKeys.indexOf(identity) !== -1
    }

    shared function isSelected(row, index) {
      return isSelectedKey(keyOf(row, index))
    }

    shared function allVisibleSelected() {
      var visible = visibleRows()
      if (!visible.length) {
        return false
      }
      return visible.every(function (row, index) { return isSelected(row, index) })
    }

    // A snapshot of the view, sent with `change` after anything that alters
    // what the table is showing, so a host can mirror the state (deep links,
    // a server query) without wiring up five separate outputs.
    shared function viewState() {
      return {
        page: currentPage(),
        pageSize: Number(perPage) || 10,
        total: totalCount(),
        query: query,
        sortKey: sortKey,
        sortDirection: sortDirection
      }
    }

    // --- async source -----------------------------------------------------
    //
    // The result of load() cannot be written to state from its own .then():
    // a client function copies state in at entry and flushes it back when the
    // body returns, so anything a promise callback assigns lands in a dead
    // local. The callback therefore only DISPATCHES, carrying the instance id
    // so two tables on one page cannot answer each other, and the declarative
    // @window handlers below apply the result with live state.
    // Begins a load: flags the pending state and asks for rows. The answer
    // arrives later on a window event, because a promise callback cannot write
    // state -- see applyRows.
    client function reload() {
      if (!remote) {
        return
      }
      loading = true
      loadError = ""
      output.request({
        instanceId: instanceId,
        page: currentPage(),
        pageSize: Number(perPage) || 10,
        sortKey: sortKey,
        sortDirection: sortDirection,
        query: query
      })
    }

    client function applyRows(sourceEvent) {
      var detail = sourceEvent.detail || {}
      if (detail.instanceId !== instanceId) {
        return
      }
      remoteRows = detail.rows
      remoteTotal = detail.total
      loading = false
      loadError = ""
    }

    client function applyError(sourceEvent) {
      var detail = sourceEvent.detail || {}
      if (detail.instanceId !== instanceId) {
        return
      }
      loading = false
      loadError = detail.message || errorLabel
    }

    // --- interaction ------------------------------------------------------
    // Every write below is synchronous: a client function only flushes its
    // state when its body returns, so anything deferred would be lost.
    client function sortBy(key) {
      if (sortKey === key) {
        sortDirection = sortDirection === "asc" ? "desc" : "asc"
      } else {
        sortKey = key
        sortDirection = "asc"
      }
      paginationPage = 1
      output.sort({ key: sortKey, direction: sortDirection })
      reload()
      output.change(viewState())
    }

    client function updateQuery(sourceEvent) {
      query = sourceEvent.target.value
      paginationPage = 1
      output.search({ query: query })
      reload()
      output.change(viewState())
    }

    client function goToPage(next) {
      var target = Math.min(Math.max(1, next), pageCount())
      paginationPage = target
      output.pageChange({ page: target, pageSize: Number(perPage) || 10 })
      reload()
      output.change(viewState())
    }

    // Component outputs deliver a payload rather than a DOM event, so this
    // takes the Dropdown select detail directly.
    client function pickPageSize(detail) {
      perPage = Number(detail && detail.value) || 10
      paginationPage = 1
      output.pageChange({ page: 1, pageSize: perPage })
      reload()
      output.change(viewState())
    }

    // Shift-click extends from the previous click, the way every file list and
    // mail client behaves; without it selecting twenty rows means twenty
    // clicks.
    client function clearSearch() {
      query = ""
      paginationPage = 1
      output.search({ query: "" })
      reload()
    }

    client function toggleRowAt(identity, sourceEvent) {
      if (sourceEvent && sourceEvent.shiftKey && lastAnchor) {
        var order = rowViews().map(function (view) { return view.key })
        var from = order.indexOf(lastAnchor)
        var to = order.indexOf(identity)
        if (from !== -1 && to !== -1) {
          var start = Math.min(from, to)
          var end = Math.max(from, to)
          var next = selectedKeys.slice()
          var step = start
          while (step <= end) {
            if (next.indexOf(order[step]) === -1) {
              next.push(order[step])
            }
            step = step + 1
          }
          selectedKeys = next
          output.select({ selected: next, all: allVisibleSelected() })
          return
        }
      }
      lastAnchor = identity
      toggleKey(identity)
    }

    client function toggleKey(key) {
      var next = []
      var found = false
      selectedKeys.forEach(function (existing) {
        if (existing === key) {
          found = true
        } else {
          next.push(existing)
        }
      })
      if (!found) {
        next.push(key)
      }
      selectedKeys = next
      output.select({ selected: next, all: allVisibleSelected() })
    }

    client function toggleAll() {
      var visible = visibleRows()
      var everySelected = visible.every(function (row, index) { return isSelected(row, index) })
      var next = []
      if (!everySelected) {
        selectedKeys.forEach(function (existing) { next.push(existing) })
        visible.forEach(function (row, index) {
          var key = keyOf(row, index)
          if (next.indexOf(key) === -1) {
            next.push(key)
          }
        })
      } else {
        selectedKeys.forEach(function (existing) {
          var stillVisible = visible.some(function (row, index) { return keyOf(row, index) === existing })
          if (!stillVisible) {
            next.push(existing)
          }
        })
      }
      selectedKeys = next
      output.select({ selected: next, all: !everySelected })
    }

    // Runs a toolbar action with the selected records.
    //
    // NOTHING may call a peer function after the callback: it is application
    // code and may change state (clearing the selection, adding a row), and a
    // peer call would flush this function's pre-callback snapshot back over
    // whatever it did. This function assigns no state, so it flushes nothing.
    client function runAction(entry, sourceEvent) {
      var rows = selectedRows()
      output.action({
        id: entry.id,
        selected: selectedKeys,
        rows: rows,
        sourceEvent: sourceEvent
      })
      var handler = entry.raw && entry.raw.onClick
      if (typeof handler === "function") {
        try {
          handler(rows, selectedKeys)
        } catch (error) {
          console.error("[wrnexus] data table action handler failed", error)
        }
      }
    }

    client function emitRowClick(row, sourceEvent) {
      output.rowClick({ row: row, sourceEvent: sourceEvent })
    }
  }

  lifecycle {
    mount {
      // A per-instance id so the async result, which travels on a window
      // event, is only picked up by the table that asked for it.
      instanceId = Math.floor(Math.random() * 1000000) + 1
      reload()
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="DataTable"
      data-color='{color}'
      data-size='{size}'
      data-density='{density}'
      data-striped='{striped ? "true" : "false"}'
      data-bordered='{bordered ? "true" : "false"}'
      data-gridlines='{gridlines}'
      data-layout='{layout}'
      data-loading='{loading ? "true" : "false"}'
      data-sticky-first='{stickyFirstColumn ? "true" : "false"}'
      class='wire-table {class}'
      @window:wrnexus:datatable:rows='applyRows(event)'
      @window:wrnexus:datatable:error='applyError(event)'
    >
      {#if caption || description || searchable}
        <div class="wire-table__toolbar">
          <div class="wire-table__heading">
            {#if caption}
              <h3 class="wire-table__caption">{caption}</h3>
            {/if}
            {#if description}
              <p class="wire-table__description">{description}</p>
            {/if}
          </div>

          <div class="wire-table__actions" data-show="visibleActions().length">
            <span class="wire-table__selected-count" data-show="selectedCount()">
              {selectedCount()} selected
            </span>

            <button
              type="button"
              class="wire-table__action"
              data-for="entry in visibleActions()"
              data-key="entry.id"
              data-tone='{entry.tone}'
              @click='runAction(entry, event)'
            >
              <span class='{entry.icon}' data-show="entry.icon" aria-hidden="true"></span>
              <span>{entry.label}</span>
            </button>
          </div>

          {#if searchable}
            <div class="wire-table__search">
              <svg
                viewBox="0 0 24 24"
                width="15"
                height="15"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
                stroke-linecap="round"
                aria-hidden="true"
              >
                <circle cx="11" cy="11" r="7" />
                <path d="m20 20-3.2-3.2" />
              </svg>
              <input
                type="search"
                value='{query}'
                placeholder='{searchPlaceholder}'
                aria-label='{searchPlaceholder}'
                @input='updateQuery(event)'
              />
            </div>
          {/if}
        </div>
      {/if}

      <div class="wire-table__scroll">
        <table class="wire-table__table" data-show="layout !== 'comparison'">
          <thead>
            <tr>
              {#if selectable}
                <th class="wire-table__select-cell" scope="col">
                  <button
                    type="button"
                    class="wire-table__check"
                    role="checkbox"
                    aria-label="Select all rows on this page"
                    aria-checked='{allVisibleSelected() ? "true" : "false"}'
                    @click='toggleAll()'
                  >
                    <svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="3.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                      <path d="m5 12 5 5 9-9" />
                    </svg>
                  </button>
                </th>
              {/if}

              <th
                data-for="column in headerColumns()"
                data-key="column.key"
                scope="col"
                data-align='{column.align}'
                style='width: {column.width}'
                aria-sort='{column.ariaSort}'
              >
                <button
                  type="button"
                  class="wire-table__sort"
                  data-show="column.sortable"
                  data-active='{column.active}'
                  @click='sortBy(column.key)'
                >
                  <span>{column.label}</span>
                  <svg
                    class="wire-table__sort-icon"
                    viewBox="0 0 24 24"
                    width="12"
                    height="12"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="2.4"
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    aria-hidden="true"
                  >
                    <path d="m7 15 5 5 5-5" />
                    <path d="m7 9 5-5 5 5" />
                  </svg>
                </button>
                <span data-show="!column.sortable">{column.label}</span>
              </th>
            </tr>
          </thead>

          <tbody>
            <tr
              data-for="row in rowViews()"
              data-key="row.key"
              data-selected='{row.selected}'
              @click='emitRowClick(row.raw, event)'
            >
              {#if selectable}
                <td class="wire-table__select-cell">
                  <button
                    type="button"
                    class="wire-table__check"
                    role="checkbox"
                    aria-label="Select row"
                    aria-checked='{row.selected}'
                    @click='toggleRowAt(row.key, event)'
                  >
                    <svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="3.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                      <path d="m5 12 5 5 9-9" />
                    </svg>
                  </button>
                </td>
              {/if}

              <td
                data-for="cell in row.cells"
                data-key="cell.key"
                data-align='{cell.align}'
                data-label='{cell.label}'
              >
                <span data-show="cell.html" data-html="cell.html"></span>
                <span data-show="!cell.html">{cell.value}</span>
              </td>
            </tr>
          </tbody>
        </table>

        <!-- Comparison layout: fields down the left, one column per record. -->
        <table class="wire-table__table wire-table__table--comparison" data-show="layout === 'comparison'">
          <thead>
            <tr>
              <th scope="col">{caption}</th>
              <th
                data-for="heading in comparisonHeadings()"
                data-key="heading.key"
                scope="col"
              >
                {heading.label}
              </th>
            </tr>
          </thead>
          <tbody>
            <tr data-for="field in comparisonRows()" data-key="field.key">
              <th scope="row" class="wire-table__field-label">{field.label}</th>
              <td
                data-for="cell in field.cells"
                data-key="cell.key"
                data-align='{cell.align}'
              >
                <span data-show="cell.html" data-html="cell.html"></span>
                <span data-show="!cell.html">{cell.value}</span>
              </td>
            </tr>
          </tbody>
        </table>

        <div class="wire-table__status" data-show="loading">
          <span class="wire-table__spinner" aria-hidden="true"></span>
          <span>{loadingLabel}</span>
        </div>

        <div class="wire-table__status wire-table__status--error" data-show="loadError">
          <span>{loadError}</span>
          <button type="button" class="wire-table__action" @click='reload()'>
            {retryLabel}
          </button>
        </div>

        <div
          class="wire-table__status"
          data-show="!loading && !loadError && totalCount() === 0"
        >
          <span>{emptyMessage()}</span>
          <button
            type="button"
            class="wire-table__action"
            data-show="isFiltered()"
            @click='clearSearch()'
          >
            {clearSearchLabel}
          </button>
        </div>
      </div>

      <div class="wire-table__footer" data-show="paginated && totalCount() > 0">
        <p class="wire-table__range">{rangeLabel()}</p>

        <div class="wire-table__pager">
          <span class="wire-table__page-size">
            <span>Rows</span>
            <!-- The shared Dropdown rather than a native select: a select
                 renders its list with the operating system styling, which
                 ignores the theme entirely. The trigger label lives in the
                 slot so it stays reactive -- a prop would be fixed at the
                 value the dropdown was first rendered with. -->
            <Dropdown
              class="wire-table__page-dropdown"
              width="sm"
              size="sm"
              placement="top-end"
              label=""
              items='{pageSizeItems()}'
              @select='pickPageSize(payload)'
            >
              <span data-slot="trigger">{perPage}</span>
            </Dropdown>
          </span>

          <button
            type="button"
            class="wire-table__page-button"
            aria-label="Previous page"
            disabled='{currentPage() <= 1}'
            @click='goToPage(currentPage() - 1)'
          >
            <svg
              viewBox="0 0 24 24"
              width="14"
              height="14"
              fill="none"
              stroke="currentColor"
              stroke-width="2.4"
              stroke-linecap="round"
              stroke-linejoin="round"
              aria-hidden="true"
            >
              <path d="m15 18-6-6 6-6" />
            </svg>
          </button>

          <span
            class="wire-table__page-indicator"
            data-show="paginationStyle !== 'numbered'"
          >
            {currentPage()} / {pageCount()}
          </span>

          <span
            class="wire-table__page-numbers"
            data-show="paginationStyle === 'numbered'"
          >
            <button
              type="button"
              class="wire-table__page-number"
              data-for="entry in pageNumbers()"
              data-key="entry.number"
              data-active='{entry.active}'
              aria-label='{"Page " + entry.number}'
              @click='goToPage(entry.number)'
            >
              {entry.number}
            </button>
          </span>

          <button
            type="button"
            class="wire-table__page-button"
            aria-label="Next page"
            disabled='{currentPage() >= pageCount()}'
            @click='goToPage(currentPage() + 1)'
          >
            <svg
              viewBox="0 0 24 24"
              width="14"
              height="14"
              fill="none"
              stroke="currentColor"
              stroke-width="2.4"
              stroke-linecap="round"
              stroke-linejoin="round"
              aria-hidden="true"
            >
              <path d="m9 18 6-6-6-6" />
            </svg>
          </button>
        </div>
      </div>

      <slot></slot>
    </div>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--data-map {
      display: grid;
      gap: 0.75rem;
      padding: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-md);
      background: var(--wire-color-surface);
      box-shadow: var(--wire-shadow-1);
    }

    .wire-table {
      --table-accent: var(--wire-color-primary);
      display: flex;
      flex-direction: column;
      gap: 0.85rem;
      width: 100%;
      min-width: 0;
      max-width: 100%;
      color: var(--wire-color-text);
    }

    .wire-table[data-color="secondary"] {
      --table-accent: var(--wire-color-secondary);
    }

    .wire-table[data-color="success"] {
      --table-accent: var(--wire-color-success);
    }

    .wire-table[data-color="danger"] {
      --table-accent: var(--wire-color-danger);
    }

    .wire-table[data-color="info"] {
      --table-accent: var(--wire-color-info);
    }

    .wire-table__toolbar {
      display: flex;
      align-items: flex-end;
      justify-content: space-between;
      flex-wrap: wrap;
      gap: 0.75rem;
    }

    .wire-table__heading {
      display: grid;
      gap: 0.2rem;
      min-width: 0;
    }

    .wire-table__caption {
      margin: 0;
      color: var(--wire-color-text);
      font-size: 0.98rem;
      font-weight: 650;
    }

    .wire-table__description {
      margin: 0;
      color: var(--wire-color-text-muted);
      font-size: 0.82rem;
    }

    .wire-table__actions {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 0.4rem;
    }

    .wire-table__selected-count {
      color: var(--wire-color-text-muted);
      font-size: 0.78rem;
      font-variant-numeric: tabular-nums;
    }

    .wire-table__action {
      appearance: none;
      display: inline-flex;
      align-items: center;
      gap: 0.35rem;
      min-height: 2.1rem;
      padding: 0 0.7rem;
      color: var(--wire-color-text);
      background: var(--wire-color-surface-soft);
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-sm);
      font: inherit;
      font-size: 0.79rem;
      cursor: pointer;
      transition: border-color 140ms ease, color 140ms ease, background 140ms ease;
    }

    .wire-table__action:hover {
      color: var(--table-accent);
      border-color: color-mix(in srgb, var(--table-accent) 45%, var(--wire-color-border));
    }

    .wire-table__action[data-tone="danger"] {
      color: var(--wire-color-danger);
      border-color: color-mix(in srgb, var(--wire-color-danger) 35%, var(--wire-color-border));
    }

    .wire-table__action[data-tone="danger"]:hover {
      background: color-mix(in srgb, var(--wire-color-danger) 12%, transparent);
    }

    /* Full gridlines: vertical rules between columns as well as rows. */
    .wire-table[data-gridlines="grid"] .wire-table__table th,
    .wire-table[data-gridlines="grid"] .wire-table__table td {
      border-right: 1px solid color-mix(in srgb, var(--wire-color-border) 60%, transparent);
    }

    .wire-table[data-gridlines="grid"] .wire-table__table th:last-child,
    .wire-table[data-gridlines="grid"] .wire-table__table td:last-child {
      border-right: 0;
    }

    .wire-table__search {
      display: inline-flex;
      align-items: center;
      gap: 0.45rem;
      min-height: 2.35rem;
      padding: 0 0.7rem;
      color: var(--wire-color-text-muted);
      background: var(--wire-color-surface-soft);
      border: 1px solid var(--wire-color-border);
      border-radius: 0.7rem;
    }

    .wire-table__search input {
      width: 12rem;
      max-width: 100%;
      padding: 0;
      color: var(--wire-color-text);
      background: transparent;
      border: 0;
      font: inherit;
      font-size: 0.83rem;
      outline: none;
    }

    .wire-table__search:focus-within {
      border-color: color-mix(in srgb, var(--table-accent) 45%, var(--wire-color-border));
    }

    /*
     * The table scrolls sideways rather than squashing its columns.
     *
     * max-width matters as much as width here: a wide table -- a comparison
     * layout turns every record into a column -- makes the flex item grow to
     * fit its content unless it is told not to, and the whole table then
     * spilled out over the page beside it instead of scrolling inside its own
     * box. min-width: 0 alone is not enough once the parent is a grid.
     */
    .wire-table__scroll {
      position: relative;
      width: 100%;
      min-width: 0;
      max-width: 100%;
      overflow-x: auto;
      border: 1px solid var(--wire-color-border);
      border-radius: 0.9rem;
    }

    .wire-table[data-bordered="false"] .wire-table__scroll {
      border: 0;
      border-radius: 0;
    }

    .wire-table__table {
      width: 100%;
      border-collapse: collapse;
      font-size: 0.85rem;
    }

    .wire-table__table th,
    .wire-table__table td {
      padding: 0.7rem 0.85rem;
      text-align: left;
      vertical-align: middle;
      white-space: nowrap;
    }

    .wire-table[data-density="compact"] .wire-table__table th,
    .wire-table[data-density="compact"] .wire-table__table td {
      padding: 0.42rem 0.6rem;
    }

    .wire-table[data-density="comfortable"] .wire-table__table th,
    .wire-table[data-density="comfortable"] .wire-table__table td {
      padding: 0.95rem 1rem;
    }

    .wire-table__table th[data-align="center"],
    .wire-table__table td[data-align="center"] {
      text-align: center;
    }

    .wire-table__table th[data-align="end"],
    .wire-table__table td[data-align="end"] {
      text-align: right;
    }

    .wire-table__table thead th {
      position: sticky;
      top: 0;
      z-index: 1;
      color: var(--wire-color-text-muted);
      background: var(--wire-color-surface-soft);
      border-bottom: 1px solid var(--wire-color-border);
      font-size: 0.76rem;
      font-weight: 650;
      letter-spacing: 0.02em;
      text-transform: uppercase;
    }

    .wire-table__table tbody tr {
      border-bottom: 1px solid color-mix(in srgb, var(--wire-color-border) 60%, transparent);
    }

    .wire-table__table tbody tr:last-child {
      border-bottom: 0;
    }

    /*
     * Row background: stripe, then hover, then selection, each beating the one
     * before it.
     *
     * These were written at whatever specificity fell out naturally, and the
     * stripe selector was the strongest -- so a selected row on an even line
     * kept its stripe and looked different from a selected row on an odd line,
     * and hovering flipped rows between three shades. Selection is a state
     * about the row, not decoration, so it wins outright; every rule below
     * sits at the same specificity and is ordered deliberately.
     */
    .wire-table[data-striped="true"] .wire-table__table tbody tr:nth-child(even) {
      background: color-mix(in srgb, var(--wire-color-surface-soft) 55%, transparent);
    }

    .wire-table[data-striped] .wire-table__table tbody tr:hover:not([data-selected="true"]) {
      background: color-mix(in srgb, var(--table-accent) 7%, transparent);
    }

    /* Every selected row identical, striped or not. */
    .wire-table[data-striped] .wire-table__table tbody tr[data-selected="true"] {
      background: color-mix(in srgb, var(--table-accent) 15%, transparent);
    }

    .wire-table[data-striped] .wire-table__table tbody tr[data-selected="true"]:hover {
      background: color-mix(in srgb, var(--table-accent) 22%, transparent);
    }

    .wire-table__sort {
      appearance: none;
      display: inline-flex;
      align-items: center;
      gap: 0.32rem;
      padding: 0;
      color: inherit;
      background: transparent;
      border: 0;
      font: inherit;
      font-size: inherit;
      font-weight: inherit;
      letter-spacing: inherit;
      text-transform: inherit;
      cursor: pointer;
    }

    .wire-table__sort:hover {
      color: var(--table-accent);
    }

    .wire-table__sort-icon {
      flex: 0 0 auto;
      opacity: 0.45;
    }

    .wire-table__sort[data-active="true"] {
      color: var(--table-accent);
    }

    .wire-table__sort[data-active="true"] .wire-table__sort-icon {
      opacity: 1;
    }

    .wire-table__select-cell {
      width: 2.6rem;
      text-align: center;
    }

    .wire-table__check {
      appearance: none;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      width: 1.05rem;
      height: 1.05rem;
      color: transparent;
      background: var(--wire-color-surface-raised);
      border: 1px solid var(--wire-color-border);
      border-radius: 0.32rem;
      cursor: pointer;
      transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
    }

    .wire-table__check[aria-checked="true"] {
      color: var(--wire-color-primary-contrast);
      background: var(--table-accent);
      border-color: var(--table-accent);
    }

    .wire-table__check:focus-visible {
      outline: none;
      box-shadow: 0 0 0 3px color-mix(in srgb, var(--table-accent) 35%, transparent);
    }

    .wire-table__table--comparison {
      width: auto;
      min-width: 100%;
    }

    .wire-table__table--comparison th,
    .wire-table__table--comparison td {
      min-width: 7rem;
    }

    .wire-table__table--comparison th[scope="row"] {
      color: var(--wire-color-text-muted);
      background: var(--wire-color-surface-soft);
      font-size: 0.76rem;
      font-weight: 650;
      text-transform: uppercase;
      white-space: nowrap;
    }

    .wire-table[data-layout="comparison"] .wire-table__table th:first-child,
    .wire-table[data-layout="comparison"] .wire-table__table td:first-child {
      position: sticky;
      left: 0;
      z-index: 2;
    }

    .wire-table__status {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 0.6rem;
      padding: 1.75rem 1rem;
      color: var(--wire-color-text-muted);
      font-size: 0.85rem;
      text-align: center;
    }

    .wire-table__status--error {
      color: var(--wire-color-danger);
    }

    .wire-table__spinner {
      width: 0.95rem;
      height: 0.95rem;
      border: 2px solid color-mix(in srgb, currentColor 30%, transparent);
      border-top-color: currentColor;
      border-radius: 50%;
      animation: wire-table-spin 700ms linear infinite;
    }

    @keyframes wire-table-spin {
      to {
        transform: rotate(360deg);
      }
    }

    /* Dim the rows while a refresh is in flight rather than tearing them out:
       a table that empties on every keystroke is far harder to read. */
    .wire-table[data-loading="true"] .wire-table__table tbody {
      opacity: 0.45;
    }

    /* Keeps the first column readable while the rest scrolls sideways. */
    .wire-table[data-sticky-first="true"] .wire-table__table th:first-child,
    .wire-table[data-sticky-first="true"] .wire-table__table td:first-child {
      position: sticky;
      left: 0;
      z-index: 2;
      background: var(--wire-color-surface);
    }

    .wire-table[data-sticky-first="true"] .wire-table__table thead th:first-child {
      z-index: 3;
    }

    .wire-table__empty {
      margin: 0;
      padding: 1.75rem 1rem;
      color: var(--wire-color-text-muted);
      font-size: 0.85rem;
      text-align: center;
    }

    .wire-table__footer {
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-wrap: wrap;
      gap: 0.75rem;
    }

    .wire-table__range {
      margin: 0;
      color: var(--wire-color-text-muted);
      font-size: 0.8rem;
    }

    .wire-table__pager {
      display: flex;
      align-items: center;
      gap: 0.5rem;
    }

    .wire-table__page-size {
      display: inline-flex;
      align-items: center;
      gap: 0.4rem;
      color: var(--wire-color-text-muted);
      font-size: 0.78rem;
    }

    .wire-table__page-dropdown {
      display: inline-flex;
    }

    /*
     * padding is reset explicitly: an app-level button { padding: ... } rule
     * outranks the browser default and would crush the icon inside these
     * fixed-size controls.
     */
    .wire-table__page-button {
      appearance: none;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex: 0 0 auto;
      padding: 0;
      width: 2rem;
      height: 2rem;
      color: var(--wire-color-text);
      background: var(--wire-color-surface-soft);
      border: 1px solid var(--wire-color-border);
      border-radius: 0.55rem;
      cursor: pointer;
    }

    .wire-table__page-button:hover:not(:disabled) {
      color: var(--table-accent);
      border-color: color-mix(in srgb, var(--table-accent) 40%, var(--wire-color-border));
    }

    .wire-table__page-button:disabled {
      opacity: 0.45;
      cursor: not-allowed;
    }

    .wire-table__page-button svg {
      flex: 0 0 auto;
    }

    .wire-table__page-numbers {
      display: inline-flex;
      align-items: center;
      gap: 0.25rem;
    }

    .wire-table__page-number {
      appearance: none;
      min-width: 2rem;
      height: 2rem;
      padding: 0 0.4rem;
      color: var(--wire-color-text-muted);
      background: transparent;
      border: 1px solid transparent;
      border-radius: 0.55rem;
      font: inherit;
      font-size: 0.78rem;
      font-variant-numeric: tabular-nums;
      cursor: pointer;
    }

    .wire-table__page-number:hover {
      color: var(--wire-color-text);
      background: var(--wire-color-surface-soft);
    }

    .wire-table__page-number[data-active="true"] {
      color: var(--wire-color-primary-contrast);
      background: var(--table-accent);
      border-color: var(--table-accent);
    }

    .wire-table__page-indicator {
      color: var(--wire-color-text-muted);
      font-size: 0.78rem;
      font-variant-numeric: tabular-nums;
    }

    /*
     * Below the breakpoint each row becomes its own card and every cell grows
     * a label from its column, so the data stays readable without a horizontal
     * scrollbar on a phone.
     */
    @media (max-width: 639px) {
      .wire-table__scroll {
        overflow-x: visible;
        border: 0;
      }

      .wire-table__table,
      .wire-table__table tbody,
      .wire-table__table tr,
      .wire-table__table td {
        display: block;
        width: 100%;
      }

      .wire-table__table thead {
        display: none;
      }

      /*
       * Each row becomes a card. These are the exact tokens Card paints its
       * surface with (.wire-next__card-panel in ui.css) rather than a lookalike
       * of my own, so a stacked row and a real Card cannot drift apart.
       *
       * The Card component itself cannot be mounted per row: component tags
       * are resolved once, server side, while these rows are cloned in the
       * browser from a single template -- every row would share one mount.
       */
      .wire-table__table tbody tr {
        margin-bottom: 0.7rem;
        padding: 0.5rem 0.25rem;
        background: var(--wire-color-surface);
        border: 1px solid var(--wire-color-border);
        border-radius: var(--wire-radius-md);
        box-shadow: var(--wire-shadow-1);
      }

      .wire-table[data-striped="true"] .wire-table__table tbody tr:nth-child(even) {
        background: var(--wire-color-surface);
      }

      .wire-table[data-striped] .wire-table__table tbody tr[data-selected="true"] {
        background: color-mix(in srgb, var(--table-accent) 15%, var(--wire-color-surface));
        border-color: color-mix(in srgb, var(--table-accent) 45%, var(--wire-color-border));
      }

      .wire-table__table td {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 1rem;
        white-space: normal;
        text-align: right;
      }

      .wire-table__table td::before {
        content: attr(data-label);
        color: var(--wire-color-text-muted);
        font-size: 0.72rem;
        font-weight: 650;
        text-transform: uppercase;
      }

      .wire-table__select-cell {
        width: auto;
      }

      .wire-table__footer {
        justify-content: center;
      }

      .wire-table__actions {
        width: 100%;
      }

      /* The page-size menu is anchored to a control near the bottom of the
         screen; it must be able to render outside the footer box. */
      .wire-table__footer,
      .wire-table__pager {
        overflow: visible;
      }
    }
  }
}
```

---

## DatePicker

Showcase: https://component.wrnexusjs.dev/
Mount: <DatePicker /> (legacy: data-component="DatePicker")
Category: base
Purpose: Theme-aware, responsive date picker component.
Props: size: string = "default", color: string = "primary", label: string = "Date Picker", id: string = "", name: string = "", value: string = "", placeholder: string = "", type: string = "date", locale: string = "en-US", firstDayOfWeek: number = 0, months: string = [{ label: "Jan", value: "01" }, { label: "Feb", value: "02" }, { label: "Mar", value: "03" }, { label: "Apr", value: "04" }, { label: "May", value: "05" }, { label: "Jun", value: "06" }, { label: "Jul", value: "07" }, { label: "Aug", value: "08" }, { label: "Sep", value: "09" }, { label: "Oct", value: "10" }, { label: "Nov", value: "11" }, { label: "Dec", value: "12" }], days: string = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31], years: string = [2024, 2025, 2026, 2027, 2028, 2029, 2030], min: string = "", max: string = "", step: string = "", helperText: string = "", cornerHint: string = "", error: string = "", variant: string = "normal", inline: boolean = false, readonly: boolean = false, disabled: boolean = false, required: boolean = false, class: string = ""
Slots: none
Events: input, change, open, close, focus, blur, invalid

### Complete .wrn source contract

```wrn
import FieldStyles from "../styles/FieldStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component DatePicker {
  outputs {
    input(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
    change(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
    open(payload: { value: string; name: string; sourceEvent: Event })
    close(payload: { value: string; name: string; sourceEvent: Event })
    focus(payload: { value: string; name: string; sourceEvent: Event })
    blur(payload: { value: string; name: string; sourceEvent: Event })
    invalid(payload: { name: string; message: string; sourceEvent: Event })
  }

  props {
size: string = "default"
    color: string = "primary"
    label: string = "Date Picker"
    id: string = ""
    name: string = ""
    value: string = ""
    placeholder: string = ""
    type: string = "date"
    locale: string = "en-US"
    firstDayOfWeek: number = 0
    months = [{ label: "Jan", value: "01" }, { label: "Feb", value: "02" }, { label: "Mar", value: "03" }, { label: "Apr", value: "04" }, { label: "May", value: "05" }, { label: "Jun", value: "06" }, { label: "Jul", value: "07" }, { label: "Aug", value: "08" }, { label: "Sep", value: "09" }, { label: "Oct", value: "10" }, { label: "Nov", value: "11" }, { label: "Dec", value: "12" }]
    days = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
    years = [2024, 2025, 2026, 2027, 2028, 2029, 2030]
    min: string = ""
    max: string = ""
    step: string = ""
    helperText: string = ""
    cornerHint: string = ""
    error: string = ""
    variant: string = "normal"
    inline: boolean = false
    readonly: boolean = false
    disabled: boolean = false
    required: boolean = false
    class: string = ""
  }
  state currentValue = value
  state selectedYear = value ? value.split("-")[0] : ""
  state selectedMonth = value ? value.split("-")[1] : ""
  state selectedDay = value ? value.split("-")[2] : ""
  state expanded: boolean = false
  functions {
    client function openCalendar(sourceEvent) { expanded = true; output.open({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
    client function updateDate(part, nextValue, sourceEvent, nextDate, detail) { if (part === "year") { selectedYear = String(nextValue) } if (part === "month") { selectedMonth = String(nextValue).padStart(2, "0") } if (part === "day") { selectedDay = String(nextValue).padStart(2, "0") } if (!selectedYear || !selectedMonth || !selectedDay) { return } nextDate = selectedYear + "-" + selectedMonth + "-" + selectedDay; if ((min && nextDate < min) || (max && nextDate > max)) { return } currentValue = nextDate; detail = { value: currentValue, year: selectedYear, month: selectedMonth, day: selectedDay, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
    client function finishDate(sourceEvent) { if (!currentValue) { return } expanded = false; output.close({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
    client function selectDay(sourceEvent, root, input, nextDate, trigger, detail) { root = sourceEvent.currentTarget.closest(".wire-next--date-picker"); input = root.querySelector(".wire-next__picker-value"); nextDate = input.getAttribute("value").slice(0, 5) + root.querySelector("select").getAttribute("value") + "-" + sourceEvent.currentTarget.dataset.value; currentValue = nextDate; input.setAttribute("value", nextDate); trigger = root.querySelector(".wire-next__date-trigger span"); if (trigger) { trigger.replaceChildren(nextDate) } sourceEvent.currentTarget.setAttribute("aria-pressed", "true"); detail = { value: nextDate, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
    client function clearDate(sourceEvent, detail) { if (disabled || readonly) { return } currentValue = ""; detail = { value: currentValue, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
    client function handleFocus(sourceEvent) { output.focus({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
    client function handleBlur(sourceEvent) { output.blur({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
    client function handleInvalid(sourceEvent) { output.invalid({ name: name, message: sourceEvent.currentTarget.validationMessage, sourceEvent: sourceEvent }) }
  }
  view {
    <div {...attrs} class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--field wire-next--date-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}" data-expanded="{expanded}">
      <div class="wire-next__field-heading"><label for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
      <div class="wire-next__picker-control"><input id="{id || name}" class="wire-next__sr-only wire-next__picker-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" /><button type="button" class="wire-next__date-trigger" disabled="{disabled || readonly}" aria-haspopup="dialog" aria-expanded="{expanded}" @click="openCalendar(event)" @focus="handleFocus(event)" @blur="handleBlur(event)"><span>{currentValue || placeholder || "Select date"}</span><i class="icon-[lucide--calendar-days]" aria-hidden="true"></i></button>{#if currentValue && !readonly && !disabled}<button type="button" class="wire-next__picker-clear" aria-label="Clear date" @click="clearDate(event)">×</button>{/if}</div>
      <div class="wire-next__calendar" role="dialog" aria-label="{label}"><div class="wire-next__date-selectors"><label>Month<select value="{selectedMonth}" @change="updateDate('month', event.currentTarget.value, event)">{#each months as month}<option value="{month.value}" selected="{month.value === selectedMonth}">{month.label}</option>{/each}</select></label><label>Year<select value="{selectedYear}" @change="updateDate('year', event.currentTarget.value, event)">{#each years as year}<option value="{year}" selected="{String(year) === selectedYear}">{year}</option>{/each}</select></label></div><div class="wire-next__calendar-grid">{#each days as day}<button type="button" data-value="{String(day).padStart(2, '0')}" aria-pressed="{String(day).padStart(2, '0') === selectedDay}" @click="selectDay(event)">{day}</button>{/each}</div><button type="button" class="wire-next__date-done" disabled="{!currentValue}" @click="finishDate(event)">Done</button></div>
      {#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
    </div>
    <FieldStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next__picker-clear {
      position: absolute;
      right: 2.5rem;
      border: 0;
      background: transparent;
      color: var(--wire-color-muted);
    }

    .wire-next__date-selectors {
      display: grid;
      grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
      gap: 0.65rem;
    }

    .wire-next__date-selectors label {
      display: grid;
      gap: 0.3rem;
      color: var(--wire-color-muted);
      font-size: 0.78rem;
      font-weight: 600;
    }

    .wire-next__date-selectors select {
      width: 100%;
      min-height: 2.35rem;
      padding: 0.45rem 0.65rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-sm);
      background: var(--wire-color-surface);
      color: var(--wire-color-text);
      font: inherit;
    }

    .wire-next__date-done {
      background: var(--wire-field-color) !important;
      color: var(--wire-color-primary-contrast, #fff) !important;
      font-weight: 650;
    }

    .wire-next__calendar > header {
      display: grid;
      grid-template-columns: 2.25rem 1fr 2.25rem;
      align-items: center;
      text-align: center;
    }

    .wire-next__calendar-grid button[data-outside="true"] {
      color: var(--wire-color-muted);
      opacity: 0.55;
    }

    .wire-next__calendar-grid button[data-today="true"] {
      box-shadow: inset 0 0 0 1px var(--wire-field-color);
    }
  }
}
```

---

## DeviceFrame

Showcase: https://component.wrnexusjs.dev/
Mount: <DeviceFrame /> (legacy: data-component="DeviceFrame")
Category: base
Purpose: Theme-aware, responsive device frame component.
Props: size: string = "default", color: string = "primary", title: string = "Device Frame", description: string = "", items: unknown[] = [], variant: string = "default", device: string = "phone", orientation: string = "portrait", src: string = "", srcdoc: string = "", frameTitle: string = "Device preview", showToolbar: boolean = true, allow: string = "", class: string = ""
Slots: default
Events: change, rotate

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component DeviceFrame {
  outputs {
    change(payload: { device: string; orientation: string; sourceEvent: Event })
    rotate(payload: { device: string; orientation: string; sourceEvent: Event })
  }

  props {
    size: string = "default"
    color: string = "primary"
    title: string = "Device Frame"
    description: string = ""
    items: unknown[] = []
    variant: string = "default"
    device: string = "phone"
    orientation: string = "portrait"
    src: string = ""
    srcdoc: string = ""
    frameTitle: string = "Device preview"
    showToolbar: boolean = true
    allow: string = ""
    class: string = ""
  }
  state currentOrientation = orientation
  functions {
    client function setDevice(nextDevice, sourceEvent) { output.change({ device: nextDevice, orientation: currentOrientation, sourceEvent: sourceEvent }) }
    client function rotateFrame(sourceEvent) { currentOrientation = currentOrientation === "portrait" ? "landscape" : "portrait"; output.rotate({ device: device, orientation: currentOrientation, sourceEvent: sourceEvent }); output.change({ device: device, orientation: currentOrientation, sourceEvent: sourceEvent }) }
  }
  view {
    <section {...attrs} class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--device-frame wire-next--variant-{variant} {class}" data-device="{device}" data-orientation="{currentOrientation}">
      <header><div>{#if title}<strong>{title}</strong>{/if}{#if description}<p>{description}</p>{/if}</div>{#if showToolbar}<div class="wire-next__device-actions" role="toolbar" aria-label="Preview device">{#each items as item}<button type="button" aria-pressed="{item.value === device}" @click="setDevice(item.value, event)">{item.label}</button>{/each}<button type="button" aria-label="Rotate preview" @click="rotateFrame(event)">↻</button></div>{/if}</header>
      <div class="wire-next__device-shell">{#if src || srcdoc}<iframe title="{frameTitle}" src="{src}" srcdoc="{srcdoc}" allow="{allow}"></iframe>{:else}<div class="wire-next__device-content"><slot /></div>{/if}</div>
    </section>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--device-frame {
      display: grid;
      gap: 0.75rem;
      padding: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-md);
      background: var(--wire-color-surface);
      box-shadow: var(--wire-shadow-1);
    }

    .wire-next--device-frame > header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
    }

    .wire-next__device-actions {
      display: flex;
      flex-wrap: wrap;
      gap: 0.4rem;
    }

    .wire-next__device-actions button {
      min-height: 2.25rem;
      padding-inline: 0.7rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-sm);
      background: var(--wire-color-surface-2);
      color: inherit;
    }

    .wire-next__device-shell {
      overflow: hidden;
      width: min(100%, 24rem);
      min-height: 28rem;
      margin-inline: auto;
      border: 0.65rem solid var(--wire-color-text);
      border-radius: 2rem;
      background: var(--wire-color-background);
      transition: width var(--wire-motion-normal) var(--wire-ease-standard);
    }

    .wire-next--device-frame[data-device="tablet"] .wire-next__device-shell {
      width: min(100%, 42rem);
    }

    .wire-next--device-frame[data-device="desktop"] .wire-next__device-shell {
      width: 100%;
      border-width: 0.4rem;
      border-radius: var(--wire-radius-md);
    }

    .wire-next--device-frame[data-orientation="landscape"] .wire-next__device-shell {
      width: min(100%, 48rem);
      min-height: 20rem;
    }

    .wire-next__device-shell :is(iframe, .wire-next__device-content) {
      width: 100%;
      min-height: inherit;
      border: 0;
    }

    @media (max-width: 640px) {
    .wire-next--device-frame > header {
        align-items: stretch;
        flex-direction: column;
      }
    }
  }
}
```

---

## Divider

Showcase: https://component.wrnexusjs.dev/
Mount: <Divider /> (legacy: data-component="Divider")
Category: layout
Purpose: Separate related horizontal or vertical content with optional labels, sizes, and semantic colors.
Props: size: string = "default", color: string = "primary", label: string = "", orientation: string = "horizontal", variant: string = "solid", class: string = ""
Slots: none
Events: none

### Complete .wrn source contract

```wrn
// Divider -- a rule between sections, optionally labelled.
//
//   <Divider />
//   <Divider label="or" />
//   <Divider orientation="vertical" />
//
// The label sits between two rules rather than on top of one, so the text
// never overlaps the line at any width.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Divider {
  props {
    size: string = "default"
    color: string = "primary"
    label: string = ""
    orientation: string = "horizontal"
    variant: string = "solid"
    class: string = ""
  }

  functions {
    shared function isVertical() {
      return orientation === "vertical"
    }

    // The rule only takes the component colour when there is a label. A plain
    // divider is chrome and should stay the border token.
    shared function tone() {
      return label ? color : "border"
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="Divider"
      class='wire-divider {class}'
      data-size='{size}'
      data-color='{color}'
      data-tone='{tone()}'
      data-variant='{variant}'
      data-orientation='{orientation}'
      data-labelled='{label ? "true" : "false"}'
      role="separator"
      aria-orientation='{isVertical() ? "vertical" : "horizontal"}'
    >
      <span class="wire-divider__rule" aria-hidden="true"></span>
      <span class="wire-divider__label" data-show="label">{label}</span>
      <span class="wire-divider__rule" data-show="label" aria-hidden="true"></span>
    </div>
  }

  style {
    .wire-divider {
      --divider-tone: var(--wire-color-border);
      --divider-thickness: 1px;
      display: flex;
      align-items: center;
      gap: 1rem;
      width: 100%;
      min-width: 0;
    }

    .wire-divider[data-tone="primary"] {
      --divider-tone: var(--wire-color-primary);
    }

    .wire-divider[data-tone="secondary"] {
      --divider-tone: var(--wire-color-secondary);
    }

    .wire-divider[data-tone="success"] {
      --divider-tone: var(--wire-color-success);
    }

    .wire-divider[data-tone="warning"] {
      --divider-tone: var(--wire-color-warning);
    }

    .wire-divider[data-tone="danger"] {
      --divider-tone: var(--wire-color-danger);
    }

    .wire-divider[data-size="lg"] {
      --divider-thickness: 2px;
    }

    .wire-divider__rule {
      flex: 1 1 auto;
      height: var(--divider-thickness);
      background: var(--divider-tone);
    }

    .wire-divider[data-variant="dashed"] .wire-divider__rule {
      height: 0;
      background: transparent;
      border-top: var(--divider-thickness) dashed var(--divider-tone);
    }

    .wire-divider[data-variant="dotted"] .wire-divider__rule {
      height: 0;
      background: transparent;
      border-top: var(--divider-thickness) dotted var(--divider-tone);
    }

    .wire-divider__label {
      flex: 0 0 auto;
      color: var(--wire-color-text-muted);
      font-size: 0.72rem;
      font-weight: 700;
      letter-spacing: 0.16em;
      text-transform: uppercase;
    }

    .wire-divider[data-orientation="vertical"] {
      flex-direction: column;
      width: auto;
      height: 100%;
      min-height: 1.5rem;
      margin-inline: 0.75rem;
    }

    .wire-divider[data-orientation="vertical"] .wire-divider__rule {
      width: var(--divider-thickness);
      height: auto;
    }

    .wire-divider[data-orientation="vertical"][data-variant="dashed"] .wire-divider__rule {
      width: 0;
      border-top: 0;
      border-left: var(--divider-thickness) dashed var(--divider-tone);
    }

    .wire-divider[data-orientation="vertical"][data-variant="dotted"] .wire-divider__rule {
      width: 0;
      border-top: 0;
      border-left: var(--divider-thickness) dotted var(--divider-tone);
    }
  }
}
```

---

## DragAndDrop

Showcase: https://component.wrnexusjs.dev/
Mount: <DragAndDrop /> (legacy: data-component="DragAndDrop")
Category: integrations
Purpose: Theme-aware, responsive drag and drop component.
Props: size: string = "default", color: string = "primary", title: string = "Drag And Drop", description: string = "", items: unknown[] = [], variant: string = "default", class: string = ""
Slots: default
Events: dragStart, dragEnd, dragEnter, dragLeave, drop, change

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component DragAndDrop {
  outputs {
    dragStart(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    dragEnd(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    dragEnter(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    dragLeave(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    drop(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
  }

  props {
    size: string = "default"
    color: string = "primary"
    title: string = "Drag And Drop"
    description: string = ""
    items: unknown[] = []
    variant: string = "default"
    class: string = ""
  }
  view {
    <section class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--drag-and-drop wire-next--variant-{variant} {class}">
      {#if title}<strong>{title}</strong>{/if}
      {#if description}<p>{description}</p>{/if}
      {#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
      <slot />
    </section>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--drag-and-drop {
      display: grid;
      gap: 0.75rem;
      padding: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius);
      color: var(--wire-color-text);
      background: var(--wire-color-surface);
    }
    .wire-next--drag-and-drop > p { margin: 0; color: var(--wire-color-muted); }
    .wire-next--drag-and-drop > .wire-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
    .wire-next--drag-and-drop > .wire-next__items > span { padding: 0.35rem 0.6rem; border-radius: var(--wire-radius-sm); background: var(--wire-color-surface-2); }
  }
}
```

---

## Drawer

Showcase: https://component.wrnexusjs.dev/
Mount: <Drawer /> (legacy: data-component="Drawer")
Category: overlays
Purpose: Present responsive modal side or bottom content with focus management, backdrop behavior, slots, and close events.
Props: open: boolean = false, defaultOpen: boolean = false, placement: string = "right", size: string = "md", color: string = "primary", variant: string = "default", title: string = "Drawer", description: string = "", icon: string = "", label: string = "Drawer", closeLabel: string = "Close drawer", showClose: boolean = true, closeOnBackdrop: boolean = true, closeOnEscape: boolean = true, duration: number = 260, overlay: boolean = true, scrollable: boolean = true, triggerLabel: string = "", triggerIcon: string = "", class: string = ""
Slots: trigger, header, default, footer
Events: open, close, cancel

### Complete .wrn source contract

```wrn
component Drawer {
  outputs {
    open(payload: { placement: string; sourceEvent: Event })
    close(payload: { reason: string; placement: string; sourceEvent: Event })
    cancel(payload: { placement: string; sourceEvent: Event })
  }

  props {
open: boolean = false
    defaultOpen: boolean = false
    placement: string = "right"
    size: string = "md"
    color: string = "primary"
    variant: string = "default"
    title: string = "Drawer"
    description: string = ""
    icon: string = ""
    label: string = "Drawer"
    closeLabel: string = "Close drawer"
    showClose: boolean = true
    closeOnBackdrop: boolean = true
    closeOnEscape: boolean = true
    // Open/close animation length in ms. 0 disables the animation entirely.
    duration: number = 260
    overlay: boolean = true
    scrollable: boolean = true
    triggerLabel: string = ""
    triggerIcon: string = ""
    class: string = ""
  }

  state visible = defaultOpen

  functions {
    shared function isOpen() {
      return open || visible
    }

    client function showDrawer(sourceEvent) {
      visible = true
      output.open({
        placement: placement,
        sourceEvent: sourceEvent
      })
    }

    client function hideDrawer(reason, sourceEvent) {
      visible = false
      output.close({
        reason: reason,
        placement: placement,
        sourceEvent: sourceEvent
      })
    }

    client function cancelDrawer(sourceEvent) {
      output.cancel({
        placement: placement,
        sourceEvent: sourceEvent
      })
      hideDrawer("cancel", sourceEvent)
    }

    client function handleKeydown(sourceEvent) {
      if (closeOnEscape && sourceEvent.key === "Escape") {
        sourceEvent.preventDefault()
        cancelDrawer(sourceEvent)
      }
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="Drawer"
      data-open='{open || visible ? "true" : "false"}'
      data-placement='{placement}'
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      data-overlay='{overlay ? "true" : "false"}'
      data-scrollable='{scrollable ? "true" : "false"}'
      class='wire-drawer {class}'
      style='--drawer-duration: {duration}ms'
    >

      {#if triggerLabel}
        <button
          type="button"
          class="wire-drawer__trigger"
          aria-haspopup="dialog"
          aria-expanded='{open || visible ? "true" : "false"}'
          @click='showDrawer(event)'
        >
          {#if triggerIcon}
            <span class='{triggerIcon}' aria-hidden="true"></span>
          {/if}
          <span>{triggerLabel}</span>
        </button>
      {/if}

      <span class="wire-drawer__trigger-slot" @click='showDrawer(event)'>
        <slot name="trigger"></slot>
      </span>

      <div
        class="wire-drawer__layer"
        role="presentation"
        @keydown='handleKeydown(event)'
      >
        {#if overlay}
          <button
            type="button"
            class="wire-drawer__backdrop"
            aria-label='{closeLabel}'
            @click='if (closeOnBackdrop) { cancelDrawer(event) }'
          ></button>
        {/if}

        <section
          class="wire-drawer__panel"
          role="dialog"
          aria-modal="true"
          aria-label='{label || title}'
          tabindex="-1"
        >
          <div class="wire-drawer__handle" aria-hidden="true"></div>

          <header class="wire-drawer__header">
            <div class="wire-drawer__heading">
              {#if icon}
                <span class='wire-drawer__icon {icon}' aria-hidden="true"></span>
              {/if}

              <div class="wire-drawer__heading-copy">
                <slot name="header"></slot>
                {#if title}
                  <h2>{title}</h2>
                {/if}
                {#if description}
                  <p>{description}</p>
                {/if}
              </div>
            </div>

            {#if showClose}
              <button
                type="button"
                class="wire-drawer__close"
                aria-label='{closeLabel}'
                @click='hideDrawer("close-button", event)'
              >
                <span class="icon-[lucide--x]" aria-hidden="true"></span>
              </button>
            {/if}
          </header>

          <div class="wire-drawer__body">
            <slot></slot>
          </div>

          <footer class="wire-drawer__footer">
            <slot name="footer"></slot>
          </footer>
        </section>
      </div>
    </div>
  }

  style {
    .wire-drawer {
      --drawer-accent: var(--wire-color-primary);
      --drawer-soft: var(--wire-color-primary-soft);
      position: relative;
      display: inline-flex;
    }

    .wire-drawer[data-color="secondary"] {
      --drawer-accent: var(--wire-color-secondary);
      --drawer-soft: var(--wire-color-secondary-soft);
    }

    .wire-drawer[data-color="info"] {
      --drawer-accent: var(--wire-color-info);
      --drawer-soft: var(--wire-color-info-soft);
    }

    .wire-drawer[data-color="success"] {
      --drawer-accent: var(--wire-color-success);
      --drawer-soft: var(--wire-color-success-soft);
    }

    .wire-drawer[data-color="warning"] {
      --drawer-accent: var(--wire-color-warning);
      --drawer-soft: var(--wire-color-warning-soft);
    }

    .wire-drawer[data-color="danger"] {
      --drawer-accent: var(--wire-color-danger);
      --drawer-soft: var(--wire-color-danger-soft);
    }

    .wire-drawer__trigger,
    .wire-drawer__trigger-slot {
      display: inline-flex;
      align-items: center;
      gap: 0.55rem;
    }

    .wire-drawer__trigger {
      appearance: none;
      min-height: 2.55rem;
      padding: 0.65rem 1rem;
      color: var(--wire-color-primary-contrast);
      background: var(--drawer-accent);
      border: 0;
      border-radius: 0.8rem;
      font: inherit;
      font-size: 0.85rem;
      font-weight: 650;
      cursor: pointer;
    }

    /*
     * The layer stays in the layout and is revealed by [data-open]; it used to
     * be toggled with data-show, which sets display:none, and display cannot
     * be transitioned -- the drawer simply snapped in and out. visibility is
     * delayed by the duration on the way out so the panel can finish sliding
     * before the layer is taken out of the hit-testing tree.
     */
    .wire-drawer__layer {
      position: fixed;
      inset: 0;
      z-index: 1200;
      display: flex;
      pointer-events: none;
      visibility: hidden;
      opacity: 0;
      transition:
        opacity var(--drawer-duration, 260ms) ease,
        visibility 0s linear var(--drawer-duration, 260ms);
    }

    .wire-drawer[data-open="true"] .wire-drawer__layer {
      visibility: visible;
      opacity: 1;
      transition:
        opacity var(--drawer-duration, 260ms) ease,
        visibility 0s linear 0s;
    }

    .wire-drawer__backdrop {
      position: absolute;
      inset: 0;
      z-index: 0;
      appearance: none;
      padding: 0;
      background: color-mix(in srgb, black 56%, transparent);
      border: 0;
      backdrop-filter: blur(7px);
      pointer-events: auto;
    }

    .wire-drawer[data-overlay="false"] .wire-drawer__backdrop {
      display: none;
    }

    .wire-drawer__panel {
      position: relative;
      z-index: 1;
      display: flex;
      flex-direction: column;
      width: min(28rem, calc(100vw - 1rem));
      max-height: 100%;
      margin-left: auto;
      color: var(--wire-color-text);
      background:
        radial-gradient(
          circle at 100% 0%,
          color-mix(in srgb, var(--drawer-accent) 8%, transparent),
          transparent 34%
        ),
        var(--wire-color-surface-raised);
      border-left: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
      box-shadow: -28px 0 80px color-mix(in srgb, black 28%, transparent);
      pointer-events: auto;
      overflow: hidden;
      /* Slides in from whichever edge the placement puts it on. */
      transform: translateX(100%);
      transition: transform var(--drawer-duration, 260ms) cubic-bezier(0.32, 0.72, 0, 1);
    }

    .wire-drawer[data-open="true"] .wire-drawer__panel {
      transform: none;
    }

    .wire-drawer[data-placement="left"] .wire-drawer__panel {
      transform: translateX(-100%);
    }

    .wire-drawer[data-placement="top"] .wire-drawer__panel {
      transform: translateY(-100%);
    }

    .wire-drawer[data-placement="bottom"] .wire-drawer__panel {
      transform: translateY(100%);
    }

    .wire-drawer[data-open="true"][data-placement="left"] .wire-drawer__panel,
    .wire-drawer[data-open="true"][data-placement="top"] .wire-drawer__panel,
    .wire-drawer[data-open="true"][data-placement="bottom"] .wire-drawer__panel {
      transform: none;
    }

    @media (prefers-reduced-motion: reduce) {
      .wire-drawer__layer,
      .wire-drawer__panel {
        transition: none;
      }
    }

    .wire-drawer[data-size="sm"] .wire-drawer__panel {
      width: min(22rem, calc(100vw - 1rem));
    }

    .wire-drawer[data-size="lg"] .wire-drawer__panel {
      width: min(38rem, calc(100vw - 1rem));
    }

    .wire-drawer[data-size="xl"] .wire-drawer__panel {
      width: min(52rem, calc(100vw - 1rem));
    }

    .wire-drawer[data-size="full"] .wire-drawer__panel {
      width: 100vw;
    }

    .wire-drawer[data-placement="left"] .wire-drawer__panel {
      margin-right: auto;
      margin-left: 0;
      border-right: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
      border-left: 0;
      box-shadow: 28px 0 80px color-mix(in srgb, black 28%, transparent);
    }

    .wire-drawer[data-placement="top"] .wire-drawer__layer,
    .wire-drawer[data-placement="bottom"] .wire-drawer__layer {
      align-items: flex-start;
    }

    .wire-drawer[data-placement="top"] .wire-drawer__panel,
    .wire-drawer[data-placement="bottom"] .wire-drawer__panel {
      width: 100%;
      max-height: min(80vh, 44rem);
      margin: 0;
      border: 0;
      box-shadow: 0 24px 80px color-mix(in srgb, black 28%, transparent);
    }

    .wire-drawer[data-placement="top"] .wire-drawer__panel {
      border-bottom: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
    }

    .wire-drawer[data-placement="bottom"] .wire-drawer__layer {
      align-items: flex-end;
    }

    .wire-drawer[data-placement="bottom"] .wire-drawer__panel {
      border-top: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
      border-radius: 1.3rem 1.3rem 0 0;
    }

    .wire-drawer[data-variant="soft"] .wire-drawer__panel {
      background:
        linear-gradient(145deg, var(--drawer-soft), transparent 65%),
        var(--wire-color-surface-raised);
    }

    .wire-drawer[data-variant="solid"] .wire-drawer__panel {
      color: var(--wire-color-primary-contrast);
      background: var(--drawer-accent);
      border-color: color-mix(in srgb, white 18%, transparent);
    }

    .wire-drawer__handle {
      display: none;
      width: 2.75rem;
      height: 0.28rem;
      margin: 0.55rem auto 0;
      background: color-mix(in srgb, var(--wire-color-text-muted) 48%, transparent);
      border-radius: 999px;
    }

    .wire-drawer[data-placement="bottom"] .wire-drawer__handle {
      display: block;
    }

    .wire-drawer__header {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 1rem;
      padding: 1.35rem 1.35rem 1.1rem;
      border-bottom: 1px solid var(--wire-color-border);
    }

    .wire-drawer__heading {
      display: flex;
      align-items: flex-start;
      gap: 0.85rem;
      min-width: 0;
    }

    .wire-drawer__icon {
      flex: 0 0 auto;
      width: 1.25rem;
      height: 1.25rem;
      margin-top: 0.15rem;
      color: var(--drawer-accent);
    }

    .wire-drawer[data-variant="solid"] .wire-drawer__icon {
      color: currentColor;
    }

    .wire-drawer__heading-copy {
      display: grid;
      gap: 0.3rem;
      min-width: 0;
    }

    .wire-drawer__heading-copy h2,
    .wire-drawer__heading-copy p {
      margin: 0;
    }

    .wire-drawer__heading-copy h2 {
      font-size: 1.05rem;
      font-weight: 650;
      line-height: 1.3;
    }

    .wire-drawer__heading-copy p {
      color: var(--wire-color-text-muted);
      font-size: 0.8rem;
      line-height: 1.55;
    }

    .wire-drawer[data-variant="solid"] .wire-drawer__heading-copy p {
      color: color-mix(in srgb, currentColor 76%, transparent);
    }

    /*
     * padding is reset explicitly: an app-level `button { padding: ... }` rule
     * outranks the browser default and leaves this fixed-size button with a
     * content box of a couple of pixels, which squeezes the icon to a sliver
     * and reads as "the close button has no icon". Same trap as Modal.
     */
    .wire-drawer__close {
      appearance: none;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex: 0 0 auto;
      padding: 0;
      width: 2.35rem;
      height: 2.35rem;
      color: var(--wire-color-text-muted);
      background: var(--wire-color-surface-soft);
      border: 1px solid var(--wire-color-border);
      border-radius: 0.75rem;
      cursor: pointer;
    }

    .wire-drawer__close:hover,
    .wire-drawer__close:focus-visible {
      color: var(--drawer-accent);
      border-color: color-mix(in srgb, var(--drawer-accent) 34%, var(--wire-color-border));
      outline: none;
    }

    /* Never let the glyph be shrunk by the flex container. */
    .wire-drawer__close svg {
      flex: 0 0 auto;
      width: 1rem;
      height: 1rem;
    }

    /*
     * Slot content is authored by the host app, so the app global stylesheet
     * styles it too. A bare element selector there (p { color: ... }) beats
     * anything the panel merely *inherits*, which is how modal body copy ended
     * up muted grey on a saturated background. State the colour explicitly;
     * :where() keeps the specificity low enough that any class the app puts on
     * its own slot content still wins.
     */
    .wire-drawer__body {
      flex: 1 1 auto;
      min-height: 0;
      padding: 1.35rem;
      color: var(--wire-color-text);
    }

    .wire-drawer__body :where(p, li, dd, dt, h1, h2, h3, h4, h5, h6, span, label, code) {
      color: inherit;
    }

    .wire-drawer[data-scrollable="true"] .wire-drawer__body {
      overflow: auto;
      overscroll-behavior: contain;
    }

    .wire-drawer__footer {
      padding: 1rem 1.35rem 1.35rem;
      border-top: 1px solid var(--wire-color-border);
    }

    .wire-drawer__footer:empty {
      display: none;
    }

    @media (max-width: 639px) {
      .wire-drawer[data-placement="right"] .wire-drawer__panel,
      .wire-drawer[data-placement="left"] .wire-drawer__panel {
        width: min(24rem, 100vw);
      }

      .wire-drawer__header,
      .wire-drawer__body,
      .wire-drawer__footer {
        padding-inline: 1rem;
      }
    }
  }
}
```

---

## Dropdown

Showcase: https://component.wrnexusjs.dev/
Mount: <Dropdown /> (legacy: data-component="Dropdown")
Category: overlays
Purpose: Open an accessible anchored menu with keyboard navigation, item selection, actions, and responsive placement.
Props: items: unknown[] = [], open: boolean = false, defaultOpen: boolean = false, label: string = "Open menu", icon: string = "", showChevron: boolean = true, menuLabel: string = "Dropdown menu", placement: string = "bottom-start", width: string = "md", size: string = "default", color: string = "primary", variant: string = "raised", closeOnSelect: boolean = true, closeOnOutside: boolean = true, disabled: boolean = false, emptyLabel: string = "No menu items", class: string = ""
Slots: trigger, header, default, footer
Events: toggle, open, close, select, action

### Complete .wrn source contract

```wrn
component Dropdown {
  outputs {
    toggle(payload: { open: boolean; sourceEvent: Event; reason?: object })
    open(payload: { sourceEvent: Event })
    close(payload: { reason: string; sourceEvent: Event })
    select(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
    action(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
  }

  props {
items: unknown[] = []
    open: boolean = false
    defaultOpen: boolean = false
    label: string = "Open menu"
    icon: string = ""
    showChevron: boolean = true
    menuLabel: string = "Dropdown menu"
    placement: string = "bottom-start"
    width: string = "md"
    size: string = "default"
    color: string = "primary"
    variant: string = "raised"
    closeOnSelect: boolean = true
    closeOnOutside: boolean = true
    disabled: boolean = false
    emptyLabel: string = "No menu items"
    class: string = ""
  }

  state visible = defaultOpen

  functions {
    shared function isOpen() {
      return open || visible
    }

    client function showMenu(sourceEvent) {
      if (disabled) {
        return
      }
      visible = true
      output.open({ sourceEvent: sourceEvent })
      output.toggle({ open: true, sourceEvent: sourceEvent })
    }

    client function hideMenu(reason, sourceEvent) {
      visible = false
      output.close({ reason: reason, sourceEvent: sourceEvent })
      output.toggle({ open: false, reason: reason, sourceEvent: sourceEvent })
    }

    client function toggleMenu(sourceEvent) {
      if (isOpen()) {
        hideMenu("toggle", sourceEvent)
      } else {
        showMenu(sourceEvent)
      }
    }

    client function chooseItem(item, itemIndex, sourceEvent) {
      if (disabled || item.disabled || item.type === "divider" || item.type === "header") {
        sourceEvent.preventDefault()
        return
      }
      output.select({
        item: item,
        itemIndex: itemIndex,
        value: item.value || "",
        sourceEvent: sourceEvent
      })
      if (item.action) {
        output.action({
          item: item,
          itemIndex: itemIndex,
          value: item.value || "",
          sourceEvent: sourceEvent
        })
      }
      if (closeOnSelect) {
        hideMenu("select", sourceEvent)
      }
    }

    client function moveFocus(sourceEvent, direction) {
      const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
      const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
      if (!options.length) {
        return
      }
      const activeIndex = options.indexOf(document.activeElement)
      const nextIndex = (activeIndex + direction + options.length) % options.length
      options[nextIndex].focus()
    }

    client function handleKeydown(sourceEvent) {
      if (sourceEvent.key === "Escape") {
        sourceEvent.preventDefault()
        hideMenu("escape", sourceEvent)
      } else if (sourceEvent.key === "ArrowDown") {
        sourceEvent.preventDefault()
        if (!isOpen()) {
          showMenu(sourceEvent)
        }
        moveFocus(sourceEvent, 1)
      } else if (sourceEvent.key === "ArrowUp") {
        sourceEvent.preventDefault()
        if (!isOpen()) {
          showMenu(sourceEvent)
        }
        moveFocus(sourceEvent, -1)
      } else if (sourceEvent.key === "Home" && isOpen()) {
        sourceEvent.preventDefault()
        const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
        const first = root.querySelector("[data-dropdown-item]:not([disabled])")
        if (first) {
          first.focus()
        }
      } else if (sourceEvent.key === "End" && isOpen()) {
        sourceEvent.preventDefault()
        const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
        const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
        if (options.length) {
          options[options.length - 1].focus()
        }
      }
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="Dropdown"
      data-open='{open || visible ? "true" : "false"}'
      data-placement='{placement}'
      data-width='{width}'
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      class='wire-dropdown {class}'
      @keydown='handleKeydown(event)'
    >
      <button
        type="button"
        class="wire-dropdown__trigger"
        disabled='{disabled}'
        aria-haspopup="menu"
        aria-expanded='{open || visible ? "true" : "false"}'
        @click='toggleMenu(event)'
      >
        <slot name="trigger"></slot>
        {#if icon}
          <span class='wire-dropdown__trigger-icon {icon}' aria-hidden="true"></span>
        {/if}
        {#if label}
          <span class="wire-dropdown__trigger-label">{label}</span>
        {/if}
        {#if showChevron}
          <span class="icon-[lucide--chevron-down] wire-dropdown__chevron" aria-hidden="true"></span>
        {/if}
      </button>

      {#if closeOnOutside}
        <button
          type="button"
          class="wire-dropdown__dismiss-layer"
          data-show='{open || visible}'
          aria-label="Close menu"
          @click='hideMenu("outside", event)'
        ></button>
      {/if}

      <div
        class="wire-dropdown__panel"
        data-wrn-anchored="true"
        data-show='{open || visible}'
        role="menu"
        aria-label='{menuLabel}'
      >
        <slot name="header"></slot>

        <div class="wire-dropdown__items">
          {#each items as item, itemIndex}
            {#if item.type === "divider"}
              <div class="wire-dropdown__divider" role="separator"></div>
            {:else if item.type === "header"}
              <div class="wire-dropdown__section-label">{item.label || item.title}</div>
            {:else if item.href}
              <a
                href='{item.href}'
                target='{item.target || ""}'
                rel='{item.external || item.target === "_blank" ? "noopener noreferrer" : (item.rel || "")}'
                role="menuitem"
                data-dropdown-item
                data-danger='{item.danger ? "true" : "false"}'
                data-selected='{item.selected || item.checked ? "true" : "false"}'
                aria-disabled='{item.disabled ? "true" : "false"}'
                class="wire-dropdown__item"
                @click='chooseItem(item, itemIndex, event)'
              >
                {#if item.icon}
                  <span class='wire-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
                {/if}
                <span class="wire-dropdown__copy">
                  <strong>{item.label || item.title}</strong>
                  {#if item.description}
                    <small>{item.description}</small>
                  {/if}
                </span>
                {#if item.badge}
                  <span class="wire-dropdown__badge">{item.badge}</span>
                {:else if item.shortcut}
                  <kbd>{item.shortcut}</kbd>
                {:else if item.checked || item.selected}
                  <span class="icon-[lucide--check] wire-dropdown__status" aria-hidden="true"></span>
                {:else if item.external}
                  <span class="icon-[lucide--arrow-up-right] wire-dropdown__status" aria-hidden="true"></span>
                {/if}
              </a>
            {:else}
              <button
                type="button"
                role="menuitem"
                data-dropdown-item
                data-danger='{item.danger ? "true" : "false"}'
                data-selected='{item.selected || item.checked ? "true" : "false"}'
                disabled='{item.disabled}'
                class="wire-dropdown__item"
                @click='chooseItem(item, itemIndex, event)'
              >
                {#if item.icon}
                  <span class='wire-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
                {/if}
                <span class="wire-dropdown__copy">
                  <strong>{item.label || item.title}</strong>
                  {#if item.description}
                    <small>{item.description}</small>
                  {/if}
                </span>
                {#if item.badge}
                  <span class="wire-dropdown__badge">{item.badge}</span>
                {:else if item.shortcut}
                  <kbd>{item.shortcut}</kbd>
                {:else if item.checked || item.selected}
                  <span class="icon-[lucide--check] wire-dropdown__status" aria-hidden="true"></span>
                {/if}
              </button>
            {/if}
          {:empty}
            <div class="wire-dropdown__empty">{emptyLabel}</div>
          {/each}
        </div>

        <slot></slot>
        <slot name="footer"></slot>
      </div>
    </div>
  }

  style {
    .wire-dropdown {
      --dropdown-accent: var(--wire-color-primary);
      --dropdown-soft: var(--wire-color-primary-soft);
      position: relative;
      display: inline-flex;
      min-width: 0;
    }

    .wire-dropdown[data-color="secondary"] {
      --dropdown-accent: var(--wire-color-secondary);
      --dropdown-soft: var(--wire-color-secondary-soft);
    }

    .wire-dropdown[data-color="info"] {
      --dropdown-accent: var(--wire-color-info);
      --dropdown-soft: var(--wire-color-info-soft);
    }

    .wire-dropdown[data-color="success"] {
      --dropdown-accent: var(--wire-color-success);
      --dropdown-soft: var(--wire-color-success-soft);
    }

    .wire-dropdown[data-color="warning"] {
      --dropdown-accent: var(--wire-color-warning);
      --dropdown-soft: var(--wire-color-warning-soft);
    }

    .wire-dropdown[data-color="danger"] {
      --dropdown-accent: var(--wire-color-danger);
      --dropdown-soft: var(--wire-color-danger-soft);
    }

    .wire-dropdown__trigger {
      appearance: none;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 0.55rem;
      min-height: 2.55rem;
      padding: 0.65rem 0.9rem;
      color: var(--wire-color-text);
      background: var(--wire-color-surface-raised);
      border: 1px solid var(--wire-color-border);
      border-radius: 0.78rem;
      font: inherit;
      font-size: 0.84rem;
      font-weight: 600;
      cursor: pointer;
      transition: border-color 150ms ease, background-color 150ms ease, color 150ms ease;
    }

    .wire-dropdown__trigger:hover,
    .wire-dropdown__trigger:focus-visible,
    .wire-dropdown[data-open="true"] .wire-dropdown__trigger {
      color: var(--dropdown-accent);
      background: var(--dropdown-soft);
      border-color: color-mix(in srgb, var(--dropdown-accent) 38%, var(--wire-color-border));
      outline: none;
    }

    .wire-dropdown__trigger:disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }

    .wire-dropdown__trigger-icon,
    .wire-dropdown__chevron {
      width: 1rem;
      height: 1rem;
    }

    .wire-dropdown__chevron {
      transition: transform 160ms ease;
    }

    .wire-dropdown[data-open="true"] .wire-dropdown__chevron {
      transform: rotate(180deg);
    }

    .wire-dropdown[data-size="sm"] .wire-dropdown__trigger {
      min-height: 2.2rem;
      padding: 0.5rem 0.72rem;
      font-size: 0.78rem;
    }

    .wire-dropdown[data-size="lg"] .wire-dropdown__trigger {
      min-height: 2.9rem;
      padding: 0.78rem 1.05rem;
      font-size: 0.9rem;
    }

    .wire-dropdown__dismiss-layer {
      position: fixed;
      inset: 0;
      z-index: 1100;
      appearance: none;
      padding: 0;
      background: transparent;
      border: 0;
    }

    .wire-dropdown__panel {
      position: absolute;
      z-index: 1101;
      top: calc(100% + 0.55rem);
      left: 0;
      width: max-content;
      min-width: 12rem;
      max-width: min(22rem, calc(100vw - 1.5rem));
      padding: 0.45rem;
      color: var(--wire-color-text);
      background:
        linear-gradient(145deg, color-mix(in srgb, var(--dropdown-accent) 5%, transparent), transparent 60%),
        color-mix(in srgb, var(--wire-color-surface-raised) 97%, transparent);
      border: 1px solid color-mix(in srgb, var(--dropdown-accent) 18%, var(--wire-color-border));
      border-radius: 1rem;
      box-shadow:
        0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
        0 24px 64px color-mix(in srgb, black 22%, transparent);
      backdrop-filter: blur(18px);
    }

    .wire-dropdown[data-width="trigger"] .wire-dropdown__panel {
      width: 100%;
      min-width: 100%;
    }

    .wire-dropdown[data-width="sm"] .wire-dropdown__panel {
      width: 12rem;
    }

    .wire-dropdown[data-width="md"] .wire-dropdown__panel {
      width: 17rem;
    }

    .wire-dropdown[data-width="lg"] .wire-dropdown__panel {
      width: 22rem;
    }

    .wire-dropdown[data-placement="bottom-end"] .wire-dropdown__panel {
      right: 0;
      left: auto;
    }

    .wire-dropdown[data-placement="top-start"] .wire-dropdown__panel {
      top: auto;
      bottom: calc(100% + 0.55rem);
    }

    .wire-dropdown[data-placement="top-end"] .wire-dropdown__panel {
      top: auto;
      right: 0;
      bottom: calc(100% + 0.55rem);
      left: auto;
    }

    .wire-dropdown[data-variant="soft"] .wire-dropdown__panel {
      background:
        linear-gradient(145deg, var(--dropdown-soft), transparent 70%),
        var(--wire-color-surface-raised);
      box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
    }

    .wire-dropdown[data-variant="outline"] .wire-dropdown__panel {
      background: var(--wire-color-surface-raised);
      box-shadow: none;
    }

    .wire-dropdown__items {
      display: grid;
      gap: 0.15rem;
    }

    .wire-dropdown__section-label {
      padding: 0.55rem 0.72rem 0.3rem;
      color: var(--wire-color-text-muted);
      font-size: 0.68rem;
      font-weight: 700;
      letter-spacing: 0.12em;
      text-transform: uppercase;
    }

    .wire-dropdown__divider {
      height: 1px;
      margin: 0.3rem 0.4rem;
      background: var(--wire-color-border);
    }

    .wire-dropdown__item {
      appearance: none;
      display: grid;
      grid-template-columns: auto minmax(0, 1fr) auto;
      align-items: center;
      gap: 0.7rem;
      width: 100%;
      min-width: 0;
      padding: 0.68rem 0.72rem;
      color: var(--wire-color-text);
      background: transparent;
      border: 0;
      border-radius: 0.72rem;
      font: inherit;
      text-align: left;
      text-decoration: none;
      cursor: pointer;
      transition: background-color 150ms ease, color 150ms ease, transform 150ms ease;
    }

    .wire-dropdown__item:hover,
    .wire-dropdown__item:focus-visible,
    .wire-dropdown__item[data-selected="true"] {
      color: var(--dropdown-accent);
      background: var(--dropdown-soft);
      outline: none;
    }

    .wire-dropdown__item:active {
      transform: scale(0.985);
    }

    .wire-dropdown__item[data-danger="true"] {
      color: var(--wire-color-danger);
    }

    .wire-dropdown__item[data-danger="true"]:hover,
    .wire-dropdown__item[data-danger="true"]:focus-visible {
      background: var(--wire-color-danger-soft);
    }

    .wire-dropdown__item[disabled],
    .wire-dropdown__item[aria-disabled="true"] {
      opacity: 0.48;
      pointer-events: none;
    }

    .wire-dropdown__item-icon,
    .wire-dropdown__status {
      width: 1rem;
      height: 1rem;
    }

    .wire-dropdown__copy {
      display: grid;
      gap: 0.12rem;
      min-width: 0;
    }

    .wire-dropdown__copy strong {
      overflow: hidden;
      font-size: 0.82rem;
      font-weight: 600;
      line-height: 1.3;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .wire-dropdown__copy small {
      overflow: hidden;
      color: var(--wire-color-text-muted);
      font-size: 0.7rem;
      line-height: 1.35;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .wire-dropdown__badge {
      padding: 0.18rem 0.45rem;
      color: var(--dropdown-accent);
      background: var(--dropdown-soft);
      border-radius: 999px;
      font-size: 0.64rem;
      font-weight: 700;
    }

    .wire-dropdown kbd {
      padding: 0.16rem 0.38rem;
      color: var(--wire-color-text-muted);
      background: var(--wire-color-surface-soft);
      border: 1px solid var(--wire-color-border);
      border-radius: 0.36rem;
      font-size: 0.64rem;
      font-family: inherit;
    }

    .wire-dropdown__empty {
      padding: 1rem;
      color: var(--wire-color-text-muted);
      font-size: 0.8rem;
      text-align: center;
    }

    @media (max-width: 639px) {
      .wire-dropdown__panel {
        position: fixed;
        right: 0.75rem;
        bottom: 0.75rem;
        left: 0.75rem;
        top: auto;
        width: auto;
        min-width: 0;
        max-width: none;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .wire-dropdown__trigger,
      .wire-dropdown__chevron,
      .wire-dropdown__item {
        transition: none;
      }
    }
  }
}
```

---

## FeatureCard

Showcase: https://component.wrnexusjs.dev/
Mount: <FeatureCard /> (legacy: data-component="FeatureCard")
Category: marketing
Purpose: Present one linked feature or service with media, icon, badge, description, and action.
Props: icon: string = "", iconStyle: string = "soft", iconSize: string = "default", eyebrow: string = "", title: string = "Feature", description: string = "", image: string = "", imageAlt: string = "", imagePosition: string = "top", imageAspect: string = "wide", imageLoading: string = "lazy", href: string = "", target: string = "", rel: string = "", external: boolean = false, actionLabel: string = "Learn more", actionIcon: string = "", showArrow: boolean = true, stretchedLink: boolean = true, badge: string = "", badgeColor: string = "primary", size: string = "default", color: string = "primary", variant: string = "default", hover: string = "lift", align: string = "left", disabled: boolean = false, class: string = ""
Slots: media, icon, default, footer
Events: none

### Complete .wrn source contract

```wrn
component FeatureCard {
  props {
    icon: string = ""
    iconStyle: string = "soft"
    iconSize: string = "default"
    eyebrow: string = ""
    title: string = "Feature"
    description: string = ""

    image: string = ""
    imageAlt: string = ""
    imagePosition: string = "top"
    imageAspect: string = "wide"
    imageLoading: string = "lazy"

    href: string = ""
    target: string = ""
    rel: string = ""
    external: boolean = false
    actionLabel: string = "Learn more"
    actionIcon: string = ""
    showArrow: boolean = true
    stretchedLink: boolean = true

    badge: string = ""
    badgeColor: string = "primary"
    size: string = "default"
    color: string = "primary"
    variant: string = "default"
    hover: string = "lift"
    align: string = "left"
    disabled: boolean = false
    class: string = ""
  }

  view {
    <article
      {...attrs}
      data-ui-component="FeatureCard"
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      data-hover='{hover}'
      data-align='{align}'
      data-icon-style='{iconStyle}'
      data-icon-size='{iconSize}'
      data-image-position='{imagePosition}'
      data-image-aspect='{imageAspect}'
      data-disabled='{disabled ? "true" : "false"}'
      data-stretched-link='{stretchedLink ? "true" : "false"}'
      class='wire-feature-card {class}'
      role="listitem"
      aria-disabled='{disabled ? "true" : "false"}'
    >
      {#if image}
        <div class="wire-feature-card__media">
          <img
            src='{image}'
            alt='{imageAlt}'
            loading='{imageLoading}'
            class="wire-feature-card__image"
          />
          <div class="wire-feature-card__media-overlay" aria-hidden="true"></div>
        </div>
      {/if}

      <slot name="media"></slot>

      <div class="wire-feature-card__body">
        <div class="wire-feature-card__top">
          <div class="wire-feature-card__leading">
            <slot name="icon"></slot>

            {#if icon}
              <div class="wire-feature-card__icon" aria-hidden="true">
                <span class='{icon}'></span>
              </div>
            {/if}
          </div>

          {#if badge}
            <span class="wire-feature-card__badge" data-color='{badgeColor}'>
              {badge}
            </span>
          {/if}
        </div>

        <div class="wire-feature-card__copy">
          {#if eyebrow}
            <p class="wire-feature-card__eyebrow">{eyebrow}</p>
          {/if}

          <h3 class="wire-feature-card__title">{title}</h3>

          {#if description}
            <p class="wire-feature-card__description">{description}</p>
          {/if}
        </div>

        <div class="wire-feature-card__content">
          <slot></slot>
        </div>

        <div class="wire-feature-card__footer">
          <div class="wire-feature-card__footer-content">
            <slot name="footer"></slot>
          </div>

          {#if href && actionLabel && !disabled}
            <a
              href='{href}'
              target='{target}'
              rel='{external || target === "_blank" ? "noopener noreferrer" : rel}'
              class="wire-feature-card__action"
              aria-label='{actionLabel + ": " + title}'
            >
              <span>{actionLabel}</span>

              {#if actionIcon}
                <span class='wire-feature-card__action-icon {actionIcon}' aria-hidden="true"></span>
              {:else if external}
                <span class="wire-feature-card__action-icon icon-[lucide--external-link]" aria-hidden="true"></span>
              {:else if showArrow}
                <span class="wire-feature-card__action-icon icon-[lucide--arrow-right]" aria-hidden="true"></span>
              {/if}
            </a>
          {:else if actionLabel}
            <span class="wire-feature-card__action wire-feature-card__action--disabled">
              <span>{actionLabel}</span>
            </span>
          {/if}
        </div>
      </div>
    </article>
  }

  style {
    .wire-feature-card {
      --feature-accent: var(--wire-color-primary);
      --feature-accent-hover: var(--wire-color-primary-hover);
      --feature-accent-soft: var(--wire-color-primary-soft);
      --feature-accent-muted: var(--wire-color-primary-muted);
      --feature-contrast: var(--wire-color-primary-contrast);
      --feature-border: color-mix(in srgb, var(--feature-accent) 16%, var(--wire-color-border));

      position: relative;
      isolation: isolate;
      display: flex;
      flex-direction: column;
      width: 100%;
      min-width: 0;
      height: 100%;
      overflow: hidden;
      color: var(--wire-color-text);
      background:
        radial-gradient(
          circle at 100% 0%,
          color-mix(in srgb, var(--feature-accent) 7%, transparent),
          transparent 34%
        ),
        var(--wire-color-surface-raised);
      border: 1px solid var(--feature-border);
      border-radius: 1.25rem;
      box-shadow:
        0 1px 0 color-mix(in srgb, white 4%, transparent) inset,
        0 10px 30px color-mix(in srgb, black 9%, transparent);
      transition:
        transform 180ms ease,
        border-color 180ms ease,
        box-shadow 180ms ease,
        background-color 180ms ease;
    }

    .wire-feature-card[data-color="secondary"] {
      --feature-accent: var(--wire-color-secondary);
      --feature-accent-hover: var(--wire-color-secondary-hover);
      --feature-accent-soft: var(--wire-color-secondary-soft);
      --feature-accent-muted: var(--wire-color-secondary-muted);
      --feature-contrast: var(--wire-color-secondary-contrast);
    }

    .wire-feature-card[data-color="info"] {
      --feature-accent: var(--wire-color-info);
      --feature-accent-soft: var(--wire-color-info-soft);
      --feature-accent-muted: var(--wire-color-info-muted);
      --feature-contrast: var(--wire-color-info-contrast);
    }

    .wire-feature-card[data-color="success"] {
      --feature-accent: var(--wire-color-success);
      --feature-accent-soft: var(--wire-color-success-soft);
      --feature-accent-muted: var(--wire-color-success-muted);
      --feature-contrast: var(--wire-color-success-contrast);
    }

    .wire-feature-card[data-color="warning"] {
      --feature-accent: var(--wire-color-warning);
      --feature-accent-soft: var(--wire-color-warning-soft);
      --feature-accent-muted: var(--wire-color-warning-muted);
      --feature-contrast: var(--wire-color-warning-contrast);
    }

    .wire-feature-card[data-color="danger"] {
      --feature-accent: var(--wire-color-danger);
      --feature-accent-soft: var(--wire-color-danger-soft);
      --feature-accent-muted: var(--wire-color-danger-muted);
      --feature-contrast: var(--wire-color-danger-contrast);
    }

    .wire-feature-card::before {
      position: absolute;
      top: 0;
      right: 0;
      left: 0;
      z-index: -1;
      height: 3px;
      content: "";
      background: linear-gradient(90deg, var(--feature-accent), transparent 72%);
      opacity: 0.8;
    }

    .wire-feature-card__media {
      position: relative;
      overflow: hidden;
      background: var(--wire-color-surface-soft);
      border-bottom: 1px solid var(--feature-border);
    }

    .wire-feature-card[data-image-aspect="square"] .wire-feature-card__media {
      aspect-ratio: 1;
    }

    .wire-feature-card[data-image-aspect="portrait"] .wire-feature-card__media {
      aspect-ratio: 4 / 5;
    }

    .wire-feature-card[data-image-aspect="standard"] .wire-feature-card__media {
      aspect-ratio: 4 / 3;
    }

    .wire-feature-card[data-image-aspect="wide"] .wire-feature-card__media {
      aspect-ratio: 16 / 9;
    }

    .wire-feature-card[data-image-aspect="cinema"] .wire-feature-card__media {
      aspect-ratio: 21 / 9;
    }

    .wire-feature-card__image {
      display: block;
      width: 100%;
      height: 100%;
      object-fit: cover;
      transition: transform 300ms ease;
    }

    .wire-feature-card__media-overlay {
      position: absolute;
      inset: 0;
      pointer-events: none;
      background: linear-gradient(180deg, transparent 58%, color-mix(in srgb, black 18%, transparent));
    }

    .wire-feature-card__body {
      display: flex;
      flex: 1;
      flex-direction: column;
      min-width: 0;
      padding: 1.5rem;
    }

    .wire-feature-card[data-size="sm"] .wire-feature-card__body {
      padding: 1.1rem;
    }

    .wire-feature-card[data-size="lg"] .wire-feature-card__body {
      padding: 1.85rem;
    }

    .wire-feature-card__top {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 1rem;
      min-width: 0;
    }

    .wire-feature-card__leading {
      display: flex;
      align-items: center;
      min-width: 0;
    }

    .wire-feature-card__icon {
      display: inline-flex;
      flex: 0 0 auto;
      align-items: center;
      justify-content: center;
      width: 3rem;
      height: 3rem;
      color: var(--feature-accent);
      background: var(--feature-accent-soft);
      border: 1px solid color-mix(in srgb, var(--feature-accent) 18%, transparent);
      border-radius: 0.95rem;
      box-shadow: 0 1px 0 color-mix(in srgb, white 7%, transparent) inset;
    }

    .wire-feature-card__icon > span {
      width: 1.35rem;
      height: 1.35rem;
    }

    .wire-feature-card[data-icon-size="sm"] .wire-feature-card__icon {
      width: 2.5rem;
      height: 2.5rem;
      border-radius: 0.8rem;
    }

    .wire-feature-card[data-icon-size="sm"] .wire-feature-card__icon > span {
      width: 1.1rem;
      height: 1.1rem;
    }

    .wire-feature-card[data-icon-size="lg"] .wire-feature-card__icon {
      width: 3.6rem;
      height: 3.6rem;
      border-radius: 1.05rem;
    }

    .wire-feature-card[data-icon-size="lg"] .wire-feature-card__icon > span {
      width: 1.65rem;
      height: 1.65rem;
    }

    .wire-feature-card[data-icon-style="solid"] .wire-feature-card__icon {
      color: var(--feature-contrast);
      background: var(--feature-accent);
      border-color: color-mix(in srgb, white 18%, transparent);
      box-shadow: 0 10px 24px color-mix(in srgb, var(--feature-accent) 24%, transparent);
    }

    .wire-feature-card[data-icon-style="outline"] .wire-feature-card__icon {
      color: var(--feature-accent);
      background: transparent;
      border-color: color-mix(in srgb, var(--feature-accent) 38%, var(--wire-color-border));
      box-shadow: none;
    }

    .wire-feature-card[data-icon-style="ghost"] .wire-feature-card__icon {
      width: auto;
      height: auto;
      padding: 0.25rem;
      background: transparent;
      border-color: transparent;
      border-radius: 0;
      box-shadow: none;
    }

    .wire-feature-card__badge {
      display: inline-flex;
      flex: 0 0 auto;
      align-items: center;
      min-height: 1.65rem;
      padding: 0.25rem 0.65rem;
      color: var(--feature-accent);
      font-size: 0.7rem;
      font-weight: 600;
      line-height: 1;
      white-space: nowrap;
      background: var(--feature-accent-soft);
      border: 1px solid color-mix(in srgb, var(--feature-accent) 18%, transparent);
      border-radius: 999px;
    }

    .wire-feature-card__badge[data-color="secondary"] {
      color: var(--wire-color-secondary);
      background: var(--wire-color-secondary-soft);
      border-color: color-mix(in srgb, var(--wire-color-secondary) 20%, transparent);
    }

    .wire-feature-card__badge[data-color="info"] {
      color: var(--wire-color-info);
      background: var(--wire-color-info-soft);
      border-color: color-mix(in srgb, var(--wire-color-info) 20%, transparent);
    }

    .wire-feature-card__badge[data-color="success"] {
      color: var(--wire-color-success);
      background: var(--wire-color-success-soft);
      border-color: color-mix(in srgb, var(--wire-color-success) 20%, transparent);
    }

    .wire-feature-card__badge[data-color="warning"] {
      color: var(--wire-color-warning-text);
      background: var(--wire-color-warning-soft);
      border-color: color-mix(in srgb, var(--wire-color-warning) 22%, transparent);
    }

    .wire-feature-card__badge[data-color="danger"] {
      color: var(--wire-color-danger);
      background: var(--wire-color-danger-soft);
      border-color: color-mix(in srgb, var(--wire-color-danger) 20%, transparent);
    }

    .wire-feature-card__copy {
      min-width: 0;
      margin-top: 1.25rem;
    }

    .wire-feature-card__eyebrow {
      margin: 0 0 0.5rem;
      color: var(--feature-accent);
      font-size: 0.72rem;
      font-weight: 600;
      line-height: 1.3;
      letter-spacing: 0.12em;
      text-transform: uppercase;
    }

    .wire-feature-card__title {
      margin: 0;
      color: var(--wire-color-text);
      font-size: 1.1rem;
      font-weight: 600;
      line-height: 1.35;
      letter-spacing: -0.015em;
      text-wrap: balance;
      transition: color 180ms ease;
    }

    .wire-feature-card[data-size="sm"] .wire-feature-card__title {
      font-size: 1rem;
    }

    .wire-feature-card[data-size="lg"] .wire-feature-card__title {
      font-size: 1.3rem;
    }

    .wire-feature-card__description {
      margin: 0.75rem 0 0;
      color: var(--wire-color-text-muted);
      font-size: 0.9rem;
      line-height: 1.65;
      text-wrap: pretty;
    }

    .wire-feature-card[data-size="sm"] .wire-feature-card__description {
      margin-top: 0.6rem;
      font-size: 0.82rem;
      line-height: 1.55;
    }

    .wire-feature-card[data-size="lg"] .wire-feature-card__description {
      font-size: 0.98rem;
    }

    /*
     * Slot content is app-authored, so a bare `p { color: ... }` in the app
     * stylesheet beats anything this container merely passes down by
     * inheritance. State it, at a specificity the app can still override.
     */
    .wire-feature-card__content {
      color: var(--wire-color-text);
      min-width: 0;
      margin-top: 1rem;
    }

    .wire-feature-card__content:empty {
      display: none;
    }

    .wire-feature-card__footer {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
      min-width: 0;
      margin-top: auto;
      padding-top: 1.35rem;
    }

    .wire-feature-card__footer-content {
      position: relative;
      z-index: 3;
      min-width: 0;
    }

    .wire-feature-card__footer-content:empty {
      display: none;
    }

    .wire-feature-card__action {
      position: relative;
      z-index: 2;
      display: inline-flex;
      flex: 0 0 auto;
      align-items: center;
      justify-content: center;
      gap: 0.45rem;
      min-height: 2.25rem;
      margin-left: auto;
      color: var(--feature-accent);
      font-size: 0.84rem;
      font-weight: 600;
      line-height: 1.2;
      text-decoration: none;
      transition:
        color 180ms ease,
        gap 180ms ease;
    }

    .wire-feature-card__action::before {
      position: absolute;
      inset: -0.4rem -0.55rem;
      content: "";
      border-radius: 0.75rem;
    }

    .wire-feature-card[data-stretched-link="true"] .wire-feature-card__action::after {
      position: absolute;
      inset: auto;
      z-index: 1;
      content: "";
    }

    .wire-feature-card[data-stretched-link="true"] .wire-feature-card__action {
      position: static;
    }

    .wire-feature-card[data-stretched-link="true"] .wire-feature-card__action::after {
      position: absolute;
      inset: 0;
      border-radius: inherit;
    }

    .wire-feature-card__action:hover {
      color: var(--feature-accent-hover, var(--feature-accent));
      gap: 0.65rem;
    }

    .wire-feature-card__action:focus-visible {
      outline: 2px solid var(--wire-color-focus);
      outline-offset: 4px;
      border-radius: 0.55rem;
    }

    .wire-feature-card__action-icon {
      width: 1rem;
      height: 1rem;
      transition: transform 180ms ease;
    }

    .wire-feature-card__action:hover .wire-feature-card__action-icon {
      transform: translateX(0.15rem);
    }

    .wire-feature-card__action--disabled {
      color: var(--wire-color-text-muted);
      opacity: 0.7;
    }

    .wire-feature-card[data-align="center"] .wire-feature-card__body {
      align-items: center;
      text-align: center;
    }

    .wire-feature-card[data-align="center"] .wire-feature-card__top,
    .wire-feature-card[data-align="center"] .wire-feature-card__footer {
      width: 100%;
      justify-content: center;
    }

    .wire-feature-card[data-align="center"] .wire-feature-card__action {
      margin-inline: auto;
    }

    .wire-feature-card[data-align="right"] .wire-feature-card__body {
      align-items: flex-end;
      text-align: right;
    }

    .wire-feature-card[data-align="right"] .wire-feature-card__top,
    .wire-feature-card[data-align="right"] .wire-feature-card__footer {
      width: 100%;
      justify-content: flex-end;
    }

    .wire-feature-card[data-variant="raised"] {
      box-shadow:
        0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
        0 20px 50px color-mix(in srgb, black 17%, transparent);
    }

    .wire-feature-card[data-variant="soft"] {
      background:
        linear-gradient(135deg, var(--feature-accent-soft), transparent 68%),
        var(--wire-color-surface-raised);
      border-color: color-mix(in srgb, var(--feature-accent) 22%, var(--wire-color-border));
      box-shadow: none;
    }

    .wire-feature-card[data-variant="outline"] {
      background: transparent;
      box-shadow: none;
    }

    .wire-feature-card[data-variant="ghost"] {
      background: transparent;
      border-color: transparent;
      box-shadow: none;
    }

    .wire-feature-card[data-variant="minimal"] {
      background: transparent;
      border-color: transparent;
      border-radius: 0;
      box-shadow: none;
    }

    .wire-feature-card[data-variant="solid"] {
      color: var(--feature-contrast);
      background:
        linear-gradient(135deg, color-mix(in srgb, white 9%, transparent), transparent 60%),
        var(--feature-accent);
      border-color: color-mix(in srgb, white 20%, transparent);
      box-shadow: 0 22px 54px color-mix(in srgb, var(--feature-accent) 28%, transparent);
    }

    .wire-feature-card[data-variant="solid"] .wire-feature-card__title,
    .wire-feature-card[data-variant="solid"] .wire-feature-card__description,
    .wire-feature-card[data-variant="solid"] .wire-feature-card__eyebrow,
    .wire-feature-card[data-variant="solid"] .wire-feature-card__action {
      color: var(--feature-contrast);
    }

    .wire-feature-card[data-variant="solid"] .wire-feature-card__description {
      opacity: 0.8;
    }

    .wire-feature-card[data-variant="solid"] .wire-feature-card__icon,
    .wire-feature-card[data-variant="solid"] .wire-feature-card__badge {
      color: var(--feature-contrast);
      background: color-mix(in srgb, white 12%, transparent);
      border-color: color-mix(in srgb, white 20%, transparent);
    }

    .wire-feature-card[data-variant="gradient"] {
      background:
        radial-gradient(circle at 100% 0%, color-mix(in srgb, var(--feature-accent) 24%, transparent), transparent 44%),
        linear-gradient(145deg, var(--wire-color-surface-raised), color-mix(in srgb, var(--feature-accent) 8%, var(--wire-color-surface-raised)));
      border-color: color-mix(in srgb, var(--feature-accent) 26%, var(--wire-color-border));
    }

    .wire-feature-card[data-hover="lift"]:not([data-disabled="true"]):hover {
      transform: translateY(-0.3rem);
      border-color: color-mix(in srgb, var(--feature-accent) 48%, var(--wire-color-border));
      box-shadow:
        0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
        0 24px 58px color-mix(in srgb, black 20%, transparent);
    }

    .wire-feature-card[data-hover="shadow"]:not([data-disabled="true"]):hover {
      box-shadow:
        0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
        0 24px 58px color-mix(in srgb, black 20%, transparent);
    }

    .wire-feature-card[data-hover="border"]:not([data-disabled="true"]):hover {
      border-color: color-mix(in srgb, var(--feature-accent) 58%, var(--wire-color-border));
    }

    .wire-feature-card[data-hover="glow"]:not([data-disabled="true"]):hover {
      border-color: color-mix(in srgb, var(--feature-accent) 54%, var(--wire-color-border));
      box-shadow: 0 20px 60px color-mix(in srgb, var(--feature-accent) 22%, transparent);
    }

    .wire-feature-card:not([data-disabled="true"]):hover .wire-feature-card__image {
      transform: scale(1.035);
    }

    .wire-feature-card:not([data-disabled="true"]):hover .wire-feature-card__title {
      color: var(--feature-accent);
    }

    .wire-feature-card[data-variant="solid"]:not([data-disabled="true"]):hover .wire-feature-card__title {
      color: var(--feature-contrast);
    }

    .wire-feature-card[data-disabled="true"] {
      pointer-events: none;
      opacity: 0.55;
      filter: saturate(0.65);
    }

    @media (min-width: 48rem) {
      .wire-feature-card[data-image-position="left"],
      .wire-feature-card[data-image-position="right"] {
        display: grid;
        grid-template-columns: minmax(0, 0.88fr) minmax(0, 1.12fr);
      }

      .wire-feature-card[data-image-position="right"] {
        grid-template-columns: minmax(0, 1.12fr) minmax(0, 0.88fr);
      }

      .wire-feature-card[data-image-position="right"] .wire-feature-card__media {
        order: 2;
        border-right: 0;
        border-bottom: 0;
        border-left: 1px solid var(--feature-border);
      }

      .wire-feature-card[data-image-position="left"] .wire-feature-card__media {
        border-right: 1px solid var(--feature-border);
        border-bottom: 0;
      }

      .wire-feature-card[data-image-position="left"] .wire-feature-card__media,
      .wire-feature-card[data-image-position="right"] .wire-feature-card__media {
        height: 100%;
        min-height: 15rem;
        aspect-ratio: auto;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .wire-feature-card,
      .wire-feature-card__image,
      .wire-feature-card__action,
      .wire-feature-card__action-icon {
        transition: none;
      }
    }
  }
}
```

---

## FeatureGrid

Showcase: https://component.wrnexusjs.dev/
Mount: <FeatureGrid /> (legacy: data-component="FeatureGrid")
Category: layout
Purpose: Arrange feature cards, services, solutions, or benefits in a responsive equal-height grid.
Props: size: string = "default", color: string = "primary", items: unknown[] = [], columns: number = 3, tabletColumns: number = 2, mobileColumns: number = 1, gap: string = "md", minItemWidth: string = "", equalHeight: boolean = true, align: string = "stretch", maxWidth: string = "full", variant: string = "default", label: string = "Features", class: string = ""
Slots: default
Events: none

### Complete .wrn source contract

```wrn
import FeatureCard from "./FeatureCard.wrn"

component FeatureGrid {
  props {
    size: string = "default"
    color: string = "primary"
    items: unknown[] = []
    columns: number = 3
    tabletColumns: number = 2
    mobileColumns: number = 1
    gap: string = "md"
    minItemWidth: string = ""
    equalHeight: boolean = true
    align: string = "stretch"
    maxWidth: string = "full"
    variant: string = "default"
    label: string = "Features"
    class: string = ""
  }

  view {
    <section
      {...attrs}
      data-ui-component="FeatureGrid"
      data-size='{size}'
      data-color='{color}'
      data-columns='{columns}'
      data-tablet-columns='{tabletColumns}'
      data-mobile-columns='{mobileColumns}'
      data-gap='{gap}'
      data-autofit='{minItemWidth !== "" ? "true" : "false"}'
      data-equal-height='{equalHeight ? "true" : "false"}'
      data-align='{align}'
      data-max-width='{maxWidth}'
      data-variant='{variant}'
      class='wire-feature-grid wire-component--color-{color} wire-component--size-{size} {class}'
      aria-label='{label}'
      style='--wire-feature-grid-columns: {columns}; --wire-feature-grid-tablet-columns: {tabletColumns}; --wire-feature-grid-mobile-columns: {mobileColumns}; --wire-feature-grid-min-item-width: {minItemWidth || "17rem"};'
    >
      <div class="wire-feature-grid__inner">
        <div class="wire-feature-grid__grid" role="list">
          {#if items.length > 0}
            {#each items as item, itemIndex}
              <FeatureCard
                icon='{item.icon || ""}'
                iconStyle='{item.iconStyle || "soft"}'
                iconSize='{item.iconSize || "default"}'
                eyebrow='{item.eyebrow || ""}'
                title='{item.title || item.label || "Feature"}'
                description='{item.description || ""}'
                image='{item.image || item.imageSrc || ""}'
                imageAlt='{item.imageAlt || item.title || item.label || ""}'
                imagePosition='{item.imagePosition || "top"}'
                imageAspect='{item.imageAspect || "wide"}'
                href='{item.href || ""}'
                target='{item.target || ""}'
                rel='{item.rel || ""}'
                external='{item.external || false}'
                actionLabel='{item.actionLabel || "Learn more"}'
                actionIcon='{item.actionIcon || ""}'
                badge='{item.badge || ""}'
                badgeColor='{item.badgeColor || item.color || "primary"}'
                size='{item.size || "default"}'
                color='{item.color || "primary"}'
                variant='{item.variant || "default"}'
                hover='{item.hover || "lift"}'
                align='{item.align || "left"}'
                disabled='{item.disabled || false}'
                stretchedLink='{item.stretchedLink !== false}'
                showArrow='{item.showArrow !== false}'
                class='{item.class || ""}'
              />
            {/each}
          {/if}

          <slot></slot>
        </div>
      </div>
    </section>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-feature-grid {
      --feature-grid-gap: 1.25rem;

      position: relative;
      width: 100%;
      min-width: 0;
      color: var(--wire-color-text);
    }

    .wire-feature-grid[data-gap="none"] {
      --feature-grid-gap: 0;
    }

    .wire-feature-grid[data-gap="xs"] {
      --feature-grid-gap: 0.5rem;
    }

    .wire-feature-grid[data-gap="sm"] {
      --feature-grid-gap: 0.75rem;
    }

    .wire-feature-grid[data-gap="md"] {
      --feature-grid-gap: 1.25rem;
    }

    .wire-feature-grid[data-gap="lg"] {
      --feature-grid-gap: 1.75rem;
    }

    .wire-feature-grid[data-gap="xl"] {
      --feature-grid-gap: 2.25rem;
    }

    .wire-feature-grid__inner {
      width: 100%;
      margin-inline: auto;
    }

    .wire-feature-grid[data-max-width="compact"] .wire-feature-grid__inner {
      max-width: 64rem;
    }

    .wire-feature-grid[data-max-width="lg"] .wire-feature-grid__inner {
      max-width: 72rem;
    }

    .wire-feature-grid[data-max-width="xl"] .wire-feature-grid__inner {
      max-width: 80rem;
    }

    .wire-feature-grid[data-max-width="wide"] .wire-feature-grid__inner {
      max-width: 90rem;
    }

    .wire-feature-grid[data-max-width="2xl"] .wire-feature-grid__inner {
      max-width: 96rem;
    }

    .wire-feature-grid[data-max-width="full"] .wire-feature-grid__inner {
      max-width: none;
    }

    .wire-feature-grid__grid {
      display: grid;
      grid-template-columns: repeat(
        var(--wire-feature-grid-mobile-columns),
        minmax(0, 1fr)
      );
      align-items: stretch;
      gap: var(--feature-grid-gap);
      width: 100%;
      min-width: 0;
    }

    .wire-feature-grid[data-align="start"] .wire-feature-grid__grid {
      align-items: start;
    }

    .wire-feature-grid[data-align="center"] .wire-feature-grid__grid {
      align-items: center;
    }

    .wire-feature-grid[data-align="end"] .wire-feature-grid__grid {
      align-items: end;
    }

    .wire-feature-grid__grid > * {
      min-width: 0;
      max-width: 100%;
    }

    .wire-feature-grid[data-equal-height="true"] .wire-feature-grid__grid > * {
      height: 100%;
    }

    .wire-feature-grid[data-variant="panel"] .wire-feature-grid__inner {
      padding: 1rem;
      background: var(--wire-color-surface-raised);
      border: 1px solid var(--wire-color-border);
      border-radius: 1.5rem;
    }

    .wire-feature-grid[data-variant="soft"] .wire-feature-grid__inner {
      padding: 1rem;
      background: color-mix(in srgb, var(--wire-color-primary) 5%, transparent);
      border: 1px solid color-mix(in srgb, var(--wire-color-primary) 14%, var(--wire-color-border));
      border-radius: 1.5rem;
    }

    @media (min-width: 40rem) {
      .wire-feature-grid__grid {
        grid-template-columns: repeat(
          var(--wire-feature-grid-tablet-columns),
          minmax(0, 1fr)
        );
      }

      .wire-feature-grid[data-autofit="true"] .wire-feature-grid__grid {
        grid-template-columns: repeat(
          auto-fit,
          minmax(min(100%, var(--wire-feature-grid-min-item-width)), 1fr)
        );
      }

      .wire-feature-grid[data-variant="panel"] .wire-feature-grid__inner,
      .wire-feature-grid[data-variant="soft"] .wire-feature-grid__inner {
        padding: 1.25rem;
      }
    }

    @media (min-width: 64rem) {
      .wire-feature-grid__grid {
        grid-template-columns: repeat(
          var(--wire-feature-grid-columns),
          minmax(0, 1fr)
        );
      }

      .wire-feature-grid[data-autofit="true"] .wire-feature-grid__grid {
        grid-template-columns: repeat(
          auto-fit,
          minmax(min(100%, var(--wire-feature-grid-min-item-width)), 1fr)
        );
      }

      .wire-feature-grid[data-variant="panel"] .wire-feature-grid__inner,
      .wire-feature-grid[data-variant="soft"] .wire-feature-grid__inner {
        padding: 1.5rem;
      }
    }
  }
}
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"
 import FeatureCard from "./FeatureCard.wrn"
```

---

## FeatureIconCard

Showcase: https://component.wrnexusjs.dev/
Mount: <FeatureIconCard /> (legacy: data-component="FeatureIconCard")
Category: marketing
Purpose: Present a compact feature or benefit with a styled icon, title, description, badge, and optional link.
Props: icon: string = "icon-[lucide--sparkles]", iconSize: string = "md", iconVariant: string = "soft", title: string = "Feature", description: string = "", href: string = "", actionLabel: string = "Explore", badge: string = "", size: string = "default", color: string = "primary", variant: string = "default", align: string = "left", hover: string = "lift", class: string = ""
Slots: default, footer
Events: none

### Complete .wrn source contract

```wrn
import TextLink from "./TextLink.wrn"

component FeatureIconCard {
  props {
    icon: string = "icon-[lucide--sparkles]"
    iconSize: string = "md"
    iconVariant: string = "soft"
    title: string = "Feature"
    description: string = ""
    href: string = ""
    actionLabel: string = "Explore"
    badge: string = ""
    size: string = "default"
    color: string = "primary"
    variant: string = "default"
    align: string = "left"
    hover: string = "lift"
    class: string = ""
  }

  view {
    <article
      data-ui-component="FeatureIconCard"
      data-size='{size}' data-variant='{variant}' data-hover='{hover}' data-align='{align}'
      class='wire-feature-icon-card {class}'
    >
      <div
        class="wire-feature-icon-card__icon"
        data-size='{iconSize}' data-variant='{iconVariant}' data-color='{color}'
      >
        <span class='{icon + " wire-feature-icon-card__icon-glyph"}' aria-hidden="true"></span>

        {#if badge}
          <span class="wire-feature-icon-card__badge">
            {badge}
          </span>
        {/if}
      </div>

      <h3 class="wire-feature-icon-card__title">{title}</h3>

      {#if description}
        <p class="wire-feature-icon-card__description">{description}</p>
      {/if}

      <slot></slot>

      {#if href}
        <TextLink
          label='{actionLabel}'
          href='{href}'
          color='{color}'
          size="sm"
          class="wire-feature-icon-card__action"
        />
      {/if}

      <slot name="footer"></slot>
    </article>
  }

  style {
    .wire-feature-icon-card { position: relative; display: flex; height: 100%; flex-direction: column; padding: 1.5rem; border: 1px solid var(--wire-color-border); border-radius: calc(var(--wire-radius) * 2); background: var(--wire-color-surface-raised); box-shadow: var(--wire-shadow-1); transition: transform var(--wire-motion-base), border-color var(--wire-motion-base), box-shadow var(--wire-motion-base); }
    .wire-feature-icon-card[data-size="sm"] { padding: 1.25rem; } .wire-feature-icon-card[data-size="lg"] { padding: 2rem; }
    .wire-feature-icon-card[data-variant="soft"] { background: var(--wire-color-surface-soft); }
    .wire-feature-icon-card[data-variant="ghost"] { border-color: transparent; background: transparent; box-shadow: none; }
    .wire-feature-icon-card[data-hover="lift"]:hover { transform: translateY(-0.25rem); }
    .wire-feature-icon-card:is([data-hover="lift"], [data-hover="shadow"]):hover { box-shadow: var(--wire-shadow-3); }
    .wire-feature-icon-card:is([data-hover="lift"], [data-hover="border"]):hover { border-color: var(--wire-color-primary); }
    .wire-feature-icon-card[data-align="center"] { align-items: center; text-align: center; }
    .wire-feature-icon-card__icon { position: relative; display: flex; width: 3.5rem; height: 3.5rem; align-items: center; justify-content: center; border: 1px solid var(--wire-color-primary-muted); border-radius: calc(var(--wire-radius) * 1.5); color: var(--wire-color-primary); background: var(--wire-color-primary-soft); }
    .wire-feature-icon-card__icon[data-size="sm"] { width: 3rem; height: 3rem; } .wire-feature-icon-card__icon[data-size="lg"] { width: 4rem; height: 4rem; }
    .wire-feature-icon-card__icon[data-variant="solid"] { color: var(--wire-color-on-primary); background: var(--wire-color-primary); }
    .wire-feature-icon-card__icon[data-color="success"]:not([data-variant="solid"]) { color: var(--wire-color-success); background: var(--wire-color-success-soft); }
    .wire-feature-icon-card__icon[data-color="warning"]:not([data-variant="solid"]) { color: var(--wire-color-warning-text); background: var(--wire-color-warning-soft); }
    .wire-feature-icon-card__icon[data-color="danger"]:not([data-variant="solid"]) { color: var(--wire-color-danger); background: var(--wire-color-danger-soft); }
    .wire-feature-icon-card__icon-glyph { width: 1.75rem; height: 1.75rem; }
    .wire-feature-icon-card__badge { position: absolute; top: -0.5rem; right: -0.5rem; padding: 0.25rem 0.5rem; border: 1px solid var(--wire-color-border); border-radius: 999px; color: var(--wire-color-primary); background: var(--wire-color-surface-raised); box-shadow: var(--wire-shadow-1); font-size: 0.625rem; font-weight: 700; letter-spacing: 0.025em; text-transform: uppercase; }
    .wire-feature-icon-card__title { margin-top: 1.5rem; color: var(--wire-color-text); font-size: 1.25rem; font-weight: 700; letter-spacing: -0.025em; }
    .wire-feature-icon-card__description { flex: 1; margin-top: 0.75rem; color: var(--wire-color-text-muted); font-size: 0.875rem; line-height: 1.5rem; }
    .wire-feature-icon-card__action { margin-top: 1.5rem; }
  }
}
```

---

## FileInput

Showcase: https://component.wrnexusjs.dev/
Mount: <FileInput /> (legacy: data-component="FileInput")
Category: forms
Purpose: Theme-aware, responsive file input component.
Props: size: string = "default", color: string = "primary", id: string = "", name: string = "", label: string = "File", hiddenLabel: boolean = false, placeholder: string = "Choose a file", value: string = "", icon: string = "icon-[lucide--upload]", iconPosition: string = "start", accept: string = "", multiple: boolean = false, helperText: string = "", cornerHint: string = "", error: string = "", inline: boolean = false, variant: string = "normal", readonly: boolean = false, disabled: boolean = false, required: boolean = false, class: string = ""
Slots: none
Events: input, change, focus, blur, select, clear, invalid

### Complete .wrn source contract

```wrn
import FieldStyles from "../styles/FieldStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component FileInput {
  outputs {
    input(payload: { files: File[]; name: string; sourceEvent: Event })
    change(payload: { files: File[]; name: string; sourceEvent: Event })
    focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    select(payload: { files: File[]; name: string; sourceEvent: Event })
    clear(payload: { name: string; sourceEvent: Event })
    invalid(payload: { message?: string; value: string | number | boolean | null | object; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
size: string = "default"
    color: string = "primary"
    id: string = ""
    name: string = ""
    label: string = "File"
    hiddenLabel: boolean = false
    placeholder: string = "Choose a file"
    value: string = ""
    icon: string = "icon-[lucide--upload]"
    iconPosition: string = "start"
    accept: string = ""
    multiple: boolean = false
    helperText: string = ""
    cornerHint: string = ""
    error: string = ""
    inline: boolean = false
    variant: string = "normal"
    readonly: boolean = false
    disabled: boolean = false
    required: boolean = false
    class: string = ""
  }
  state selectedName: string = ""
  functions {
    client function handleChange(sourceEvent) {
      sourceEvent.stopPropagation()
      selectedName = sourceEvent.currentTarget.files && sourceEvent.currentTarget.files.length ? sourceEvent.currentTarget.files[0].name : ""
      output.input({ files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
      output.change({ files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
      if (selectedName) output.select({ files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
      else output.clear({ name: name, sourceEvent: sourceEvent })
    }
    client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ name: name, sourceEvent: sourceEvent }) }
    client function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
  }
  view {
    <div {...attrs} class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--field wire-next--file-input {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
      <div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
      <div class="wire-next__file-control" data-icon-position="{iconPosition}">{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}<span>{selectedName || value || placeholder}</span><input id="{id || name}" type="file" name="{name}" accept="{accept}" multiple="{multiple}" disabled="{disabled}" required="{required}" aria-readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" @click="preventReadonly(event)" @input="handleChange(event)" @change="handleChange(event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @invalid="emitField('invalid', event)" /></div>
      {#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
    </div>
    <FieldStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next__file-control {
      position: relative;
      min-height: 2.75rem;
      padding: 0.65rem 0.85rem;
      border: 1px dashed var(--wire-color-border);
      border-radius: var(--wire-radius-sm);
      background: var(--wire-color-surface);
    }

    .wire-next__file-control input {
      position: absolute;
      cursor: pointer;
      opacity: 0;
      inset: 0;
    }
  }
}
```

---

## FileUploadProgress

Showcase: https://component.wrnexusjs.dev/
Mount: <FileUploadProgress /> (legacy: data-component="FileUploadProgress")
Category: base
Purpose: Theme-aware, responsive file upload progress component.
Props: size: string = "default", color: string = "primary", label: string = "Progress", value: number = 50, max: number = 100, showValue: boolean = true, fileName: string = "", fileSize: string = "", uploadedSize: string = "", status: string = "uploading", cancelLabel: string = "Cancel upload", retryLabel: string = "Retry upload", class: string = ""
Slots: none
Events: cancel, retry, complete

### Complete .wrn source contract

```wrn
import FieldStyles from "../styles/FieldStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component FileUploadProgress {
  outputs {
    cancel(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
    retry(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
    complete(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
  }

  props {
size: string = "default"
    color: string = "primary"
    label: string = "Progress"
    value: number = 50
    max: number = 100
    showValue: boolean = true
    fileName: string = ""
    fileSize: string = ""
    uploadedSize: string = ""
    status: string = "uploading"
    cancelLabel: string = "Cancel upload"
    retryLabel: string = "Retry upload"
    class: string = ""
  }
  state currentValue = value
  state currentStatus = status
  functions {
    shared function percent() { if (!Number(max)) { return 0 } return Math.max(0, Math.min(100, Math.round(Number(currentValue) / Number(max) * 100))) }
    client function detail(sourceEvent) { return { value: currentValue, max: max, percent: percent(), status: currentStatus, fileName: fileName, sourceEvent: sourceEvent } }
    client function cancelUpload(sourceEvent) { currentStatus = "cancelled"; output.cancel(detail(sourceEvent)) }
    client function retryUpload(sourceEvent) { currentValue = 0; currentStatus = "uploading"; output.retry(detail(sourceEvent)) }
    client function completeUpload(sourceEvent) { currentValue = max; currentStatus = "complete"; output.complete(detail(sourceEvent)) }
  }
  view {
    <div {...attrs} class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--file-upload-progress {class}" data-status="{currentStatus}" aria-live="polite">
      <div class="wire-next__row"><span>{#if fileName}<strong>{fileName}</strong><small>{#if uploadedSize || fileSize}{uploadedSize}{#if uploadedSize && fileSize} of {/if}{fileSize}{:else}{label}{/if}</small>{:else}<strong>{label}</strong>{/if}</span>{#if showValue}<strong>{percent()}%</strong>{/if}</div>
      <progress value="{currentValue}" max="{max}" aria-label="{label}"></progress>
      <div class="wire-next__upload-status"><span class="wire-next__upload-status-icon" aria-hidden="true"></span><span>{#if currentStatus === "uploading"}Uploading securely…{:else if currentStatus === "complete"}Upload complete{:else if currentStatus === "error"}Upload failed{:else}Upload cancelled{/if}</span></div>
      <div class="wire-next__upload-actions">{#if currentStatus === "uploading"}<button type="button" @click="cancelUpload(event)">{cancelLabel}</button><button type="button" @click="completeUpload(event)">Complete</button>{/if}{#if currentStatus === "error" || currentStatus === "cancelled"}<button type="button" @click="retryUpload(event)">{retryLabel}</button>{/if}</div>
    </div>
    <FieldStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next__row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
    }

    .wire-next--file-upload-progress progress {
      width: 100%;
      accent-color: var(--wire-color-primary);
    }

    .wire-next--file-upload-progress {
      display: grid;
      width: min(100%, 36rem);
      gap: 0.75rem;
      padding: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-md);
      background: var(--wire-color-surface);
      box-shadow: var(--wire-shadow-1);
    }

    .wire-next--file-upload-progress progress {
      width: 100%;
      height: 0.55rem;
      overflow: hidden;
      border: 0;
      border-radius: 999px;
      accent-color: var(--wire-component-color);
    }

    .wire-next__upload-actions {
      justify-content: flex-end;
    }

    .wire-next__upload-actions button {
      min-height: 2.25rem;
      padding-inline: 0.8rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-sm);
      background: var(--wire-color-surface-2);
      color: inherit;
    }
  }
}
```

---

## Footer

Showcase: https://component.wrnexusjs.dev/
Mount: <Footer /> (legacy: data-component="Footer")
Category: layout
Purpose: Render structured responsive footer navigation, pre and post content, copyright content, links, and public events.
Props: size: string = "default", color: string = "primary", label: string = "Footer navigation", items: unknown[] = [], columns: number = 3, maxWidth: string = "compact", copyright: string = "", class: string = ""
Slots: pre-footer, post-footer, copyright-left, copyright-right
Events: select, action

### Complete .wrn source contract

```wrn
component Footer {
  outputs {
    select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    action(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {

    size: string = "default"
    color: string = "primary"
    label: string = "Footer navigation"
    items: unknown[] = []
    columns: number = 3
    maxWidth: string = "compact"
    copyright: string = ""
    class: string = ""
  }

  view {
    <footer
      {...attrs}
      data-ui-component="Footer"
      data-size='{size}'
      data-color='{color}'
      class='wire-footer {maxWidth === "full" ? "wire-footer--width-full" : ""} {class}'
    >
      <div
        class="wire-footer__grid"
      >
        <div
          class="wire-footer__pre"
        >
          <slot
            name="pre-footer"
          >
          </slot>
        </div>

        {#if items.length > 0}
          <nav
            class='wire-footer__links wire-footer--columns-{columns}'
            aria-label='{label}'
          >
            {#each items as item, itemIndex}
              {#if item.type === "header"}
                <section
                  class="wire-footer__column"
                  aria-labelledby='footer-heading-{itemIndex}'
                >
                  {#if item.icon}
                    <span
                      class='{item.icon}'
                      aria-hidden="true"
                    >
                    </span>
                  {/if}

                  <h2
                    id='footer-heading-{itemIndex}'
                    class="wire-footer__heading"
                  >
                    {item.label || item.title}
                  </h2>

                  {#if item.description}
                    <p>
                      {item.description}
                    </p>
                  {/if}

                  {#if (item.items || item.links || []).length > 0}
                    <div
                      class="wire-footer__column-links"
                    >
                      {#each item.items || item.links || [] as child, childIndex}
                        <a
                          href='{child.href || "#"}'
                          target='{child.target || ""}'
                          rel='{child.external ? "noopener noreferrer" : (child.rel || "")}'
                          aria-current='{child.active ? "page" : ""}'
                          class="wire-footer__link"
                          @click='output.select({ item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex }); child.action && output.action({ item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex })'
                        >
                          {#if child.icon}
                            <span
                              class='{child.icon}'
                              aria-hidden="true"
                            >
                            </span>
                          {/if}

                          <span>
                            {child.label || child.title}
                          </span>

                          {#if child.badge}
                            <span
                              class="wire-badge wire-badge--default"
                            >
                              {child.badge}
                            </span>
                          {/if}

                          {#if child.external}
                            <span
                              class="icon-[lucide--arrow-up-right]"
                              aria-hidden="true"
                            >
                            </span>
                          {/if}
                        </a>
                      {/each}
                    </div>
                  {/if}
                </section>

              {:else if item.type === "link" || item.href}
                <section
                  class="wire-footer__column"
                >
                  <div
                    class="wire-footer__column-links"
                  >
                    <a
                      href='{item.href || "#"}'
                      target='{item.target || ""}'
                      rel='{item.external ? "noopener noreferrer" : (item.rel || "")}'
                      aria-current='{item.active ? "page" : ""}'
                      class="wire-footer__link"
                      @click='output.select({ item: item, itemIndex: itemIndex }); item.action && output.action({ item: item, itemIndex: itemIndex })'
                    >
                      {#if item.icon}
                        <span
                          class='{item.icon}'
                          aria-hidden="true"
                        >
                        </span>
                      {/if}

                      <span>
                        {item.label || item.title}
                      </span>

                      {#if item.badge}
                        <span
                          class="wire-badge wire-badge--default"
                        >
                          {item.badge}
                        </span>
                      {/if}

                      {#if item.external}
                        <span
                          class="icon-[lucide--arrow-up-right]"
                          aria-hidden="true"
                        >
                        </span>
                      {/if}
                    </a>
                  </div>
                </section>

              {:else}
                <section
                  class="wire-footer__column"
                  aria-labelledby='footer-heading-{itemIndex}'
                >
                  {#if item.icon}
                    <span
                      class='{item.icon}'
                      aria-hidden="true"
                    >
                    </span>
                  {/if}

                  {#if item.title || item.label}
                    <h2
                      id='footer-heading-{itemIndex}'
                      class="wire-footer__heading"
                    >
                      {item.title || item.label}
                    </h2>
                  {/if}

                  {#if item.description}
                    <p>
                      {item.description}
                    </p>
                  {/if}

                  {#if (item.items || item.links || []).length > 0}
                    <div
                      class="wire-footer__column-links"
                    >
                      {#each item.items || item.links || [] as child, childIndex}
                        <a
                          href='{child.href || "#"}'
                          target='{child.target || ""}'
                          rel='{child.external ? "noopener noreferrer" : (child.rel || "")}'
                          aria-current='{child.active ? "page" : ""}'
                          class="wire-footer__link"
                          @click='output.select({ item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex }); child.action && output.action({ item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex })'
                        >
                          {#if child.icon}
                            <span
                              class='{child.icon}'
                              aria-hidden="true"
                            >
                            </span>
                          {/if}

                          <span>
                            {child.label || child.title}
                          </span>

                          {#if child.badge}
                            <span
                              class="wire-badge wire-badge--default"
                            >
                              {child.badge}
                            </span>
                          {/if}

                          {#if child.external}
                            <span
                              class="icon-[lucide--arrow-up-right]"
                              aria-hidden="true"
                            >
                            </span>
                          {/if}
                        </a>
                      {/each}
                    </div>
                  {/if}
                </section>
              {/if}
            {/each}
          </nav>
        {/if}

        <div
          class="wire-footer__post"
        >
          <slot
            name="post-footer"
          >
          </slot>
        </div>
      </div>

      <div
        class="wire-footer__bottom wire-footer__bottom--slots"
      >
        <div
          class="wire-footer__copyright-left"
        >
          <slot
            name="copyright-left"
          >
          </slot>
        </div>

        {#if copyright}
          <div
            class="wire-footer__bottom"
          >
            <div></div>
            <p
              class="wire-footer__copyright"
            >
              {copyright}
            </p>
            <div></div>
          </div>
        {/if}

        <div
          class="wire-footer__copyright-right"
        >
          <slot
            name="copyright-right"
          >
          </slot>
        </div>
      </div>
    </footer>
  }

  style {
    .wire-footer--columns-1 {
      --wire-footer-columns: 1;
    }

    .wire-footer--columns-2 {
      --wire-footer-columns: 2;
    }

    .wire-footer--columns-3 {
      --wire-footer-columns: 3;
    }

    .wire-footer--columns-4 {
      --wire-footer-columns: 4;
    }

    .wire-footer--columns-5 {
      --wire-footer-columns: 5;
    }

    .wire-footer--columns-6 {
      --wire-footer-columns: 6;
    }

    @media (max-width: 900px) {
      .wire-footer--columns-1 .wire-footer__links {
        grid-template-columns: 1fr;
      }

      .wire-footer--columns-2 .wire-footer__links {
        grid-template-columns: repeat(2, minmax(0, 1fr));
      }
    }

    @media (max-width: 600px) {
      .wire-footer--columns-1 .wire-footer__links {
        grid-template-columns: 1fr;
      }
    }

    /* --- Badge ---------------------------------------------------------------- */
    .wire-badge {
      display: inline-flex;
      align-items: center;
      font-size: 0.75rem;
      font-weight: 700;
      line-height: 1;
      padding: 0.25rem 0.5rem;
      border-radius: 999px;
    }

    .wire-badge--default {
      background: var(--wire-color-surface-2);
      color: var(--wire-color-text);
    }

    /* Structured responsive footer */
    .wire-footer {
      width: 100%;
      color: var(--wire-color-text);
      background: var(--wire-color-surface);
      border-top: 1px solid var(--wire-color-border);
      font-size: 0.875rem;
      font-weight: 400;
    }

    .wire-footer__grid {
      display: grid;
      grid-template-columns: minmax(13rem, 1.2fr) minmax(0, 3fr) minmax(13rem, 1fr);
      width: min(100%, 90rem);
      margin-inline: auto;
      padding: clamp(2rem, 5vw, 4rem) clamp(1rem, 4vw, 3rem);
      gap: clamp(1.5rem, 4vw, 3.5rem);
    }

    .wire-footer--width-full .wire-footer__grid {
      width: 100%;
      max-width: none;
    }

    .wire-footer__pre:empty,
    .wire-footer__post:empty,
    .wire-footer__pre:not(:has(*)),
    .wire-footer__post:not(:has(*)) {
      display: none;
    }

    .wire-footer__pre,
    .wire-footer__post {
      min-width: 0;
    }

    .wire-footer__pre strong,
    .wire-footer__post strong {
      font-size: 0.875rem;
      font-weight: 600;
    }

    .wire-footer__pre p,
    .wire-footer__post p {
      font-size: 0.8125rem;
      font-weight: 400;
      line-height: 1.6;
    }

    .wire-footer__links {
      display: grid;
      grid-template-columns: repeat(var(--wire-footer-columns, 3), minmax(0, 1fr));
      align-content: start;
      gap: 1.5rem;
    }

    .wire-footer__column {
      display: grid;
      min-width: 0;
      align-content: start;
      gap: 0.8rem;
    }

    .wire-footer__column-links {
      display: grid;
      align-content: start;
      gap: 0.55rem;
    }

    .wire-footer__heading {
      display: block;
      color: var(--wire-color-text);
      font-size: 0.8125rem;
      font-weight: 600;
    }

    .wire-footer__link {
      display: flex;
      width: fit-content;
      min-height: 2rem;
      align-items: center;
      gap: 0.4rem;
      color: var(--wire-color-muted);
      font-size: 0.8125rem;
      font-weight: 400;
      text-decoration: none;
    }

    .wire-footer__link:hover {
      color: var(--wire-component-color, var(--wire-color-primary));
      text-decoration: underline;
      text-underline-offset: 0.2em;
    }

    .wire-footer__bottom {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
      align-items: center;
      gap: 1rem;
      padding: 1rem clamp(1rem, 4vw, 3rem);
      color: var(--wire-color-muted);
      border-top: 1px solid var(--wire-color-border);
      font-size: 0.75rem;
      font-weight: 400;
    }

    .wire-footer__bottom--slots {
      grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    }

    .wire-footer__bottom--slots:not(
      :has(.wire-footer__copyright-left > *, .wire-footer__copyright-right > *)
    ) {
      display: none;
    }

    .wire-footer__copyright {
      text-align: center;
    }

    .wire-footer__copyright-left,
    .wire-footer__copyright-right {
      display: flex;
      min-width: 0;
      align-items: center;
      gap: 0.75rem;
    }

    .wire-footer__copyright-right {
      justify-content: flex-end;
      text-align: right;
    }

    .wire-footer__copyright-left:empty,
    .wire-footer__copyright-right:empty,
    .wire-footer__copyright-left:not(:has(*)),
    .wire-footer__copyright-right:not(:has(*)) {
      min-height: 0;
    }

    @media (max-width: 900px) {
    .wire-footer__grid {
        grid-template-columns: 1fr;
      }
    .wire-footer__links {
        grid-template-columns: repeat(3, minmax(0, 1fr));
      }
    }

    @media (max-width: 600px) {
    .wire-footer__links {
        grid-template-columns: repeat(2, minmax(0, 1fr));
      }
    .wire-footer__bottom,
      .wire-footer__bottom--slots {
        grid-template-columns: 1fr;
        justify-items: center;
        text-align: center;
      }
    .wire-footer__copyright-left,
      .wire-footer__copyright-right {
        justify-content: center;
        text-align: center;
      }
    }

    @media (max-width: 420px) {
    .wire-footer__links {
        grid-template-columns: 1fr;
      }
    }
  }
}
```

---

## Grid

Showcase: https://component.wrnexusjs.dev/
Mount: <Grid /> (legacy: data-component="Grid")
Category: layout
Purpose: Arrange arbitrary content in a responsive configurable CSS grid with stable columns, gaps, alignment, and width.
Props: size: string = "default", color: string = "primary", columns: number = 2, gap: string = "md", maxWidth: string = "xl", minItemWidth: string = "", class: string = ""
Slots: default
Events: none

### Complete .wrn source contract

```wrn
// Grid -- an equal-track grid that collapses on small screens.
//
//   <Grid columns={3} gap="lg">...</Grid>
//
// Pass minItemWidth to let the browser decide the count instead: the track
// list becomes auto-fit, which reflows continuously rather than at fixed
// breakpoints and suits card decks whose item count is not known.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Grid {
  props {
    size: string = "default"
    color: string = "primary"
    columns: number = 2
    gap: string = "md"
    maxWidth: string = "xl"
    // When set, the count is derived from the available width instead of the
    // columns prop. Accepts any CSS length.
    minItemWidth: string = ""
    class: string = ""
  }

  functions {
    shared function columnCount() {
      var value = Number(columns)
      if (!value || value < 1) {
        return 1
      }
      return Math.min(6, value)
    }

    shared function autoTrack() {
      return minItemWidth ? "repeat(auto-fit, minmax(" + minItemWidth + ", 1fr))" : ""
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="Grid"
      class='wire-grid {class}'
      data-size='{size}'
      data-color='{color}'
      data-gap='{gap}'
      data-max-width='{maxWidth}'
      data-columns='{columnCount()}'
      data-auto='{minItemWidth ? "true" : "false"}'
      style='--grid-auto-track:{autoTrack()};'
    >
      <slot></slot>
    </div>
  }

  style {
    .wire-grid {
      --grid-gap: 1.25rem;
      display: grid;
      width: 100%;
      min-width: 0;
      gap: var(--grid-gap);
      grid-template-columns: 1fr;
      align-items: stretch;
    }

    .wire-grid[data-size="compact"] {
      align-items: start;
    }

    .wire-grid[data-max-width="md"] {
      max-width: 48rem;
    }

    .wire-grid[data-max-width="lg"] {
      max-width: 64rem;
    }

    .wire-grid[data-max-width="xl"] {
      max-width: 80rem;
    }

    .wire-grid[data-max-width="2xl"] {
      max-width: 96rem;
    }

    .wire-grid[data-max-width="full"] {
      max-width: none;
    }

    .wire-grid[data-gap="xs"] {
      --grid-gap: 0.5rem;
    }

    .wire-grid[data-gap="sm"] {
      --grid-gap: 0.75rem;
    }

    .wire-grid[data-gap="lg"] {
      --grid-gap: 2rem;
    }

    .wire-grid[data-gap="xl"] {
      --grid-gap: 2.5rem;
    }

    /*
     * auto-fit needs no breakpoints, so it wins over the fixed counts below
     * and applies at every width.
     */
    .wire-grid[data-auto="true"] {
      grid-template-columns: var(--grid-auto-track);
    }

    @media (min-width: 640px) {
      .wire-grid[data-auto="false"]:not([data-columns="1"]) {
        grid-template-columns: repeat(2, minmax(0, 1fr));
      }
    }

    @media (min-width: 1024px) {
      .wire-grid[data-auto="false"][data-columns="3"] {
        grid-template-columns: repeat(3, minmax(0, 1fr));
      }

      .wire-grid[data-auto="false"][data-columns="4"] {
        grid-template-columns: repeat(4, minmax(0, 1fr));
      }

      .wire-grid[data-auto="false"][data-columns="5"] {
        grid-template-columns: repeat(5, minmax(0, 1fr));
      }

      .wire-grid[data-auto="false"][data-columns="6"] {
        grid-template-columns: repeat(6, minmax(0, 1fr));
      }
    }
  }
}
```

---

## Hero

Showcase: https://component.wrnexusjs.dev/
Mount: <Hero /> (legacy: data-component="Hero")
Category: marketing
Purpose: Build a full-width responsive hero with constrained content, actions, trust signals, and a structured or custom visual panel.
Props: eyebrow: string = "", eyebrowIcon: string = "", icon: string = "", title: string = "Build something remarkable", highlight: string = "", description: string = "", align: string = "left", size: string = "default", color: string = "primary", variant: string = "default", layout: string = "split", visualPosition: string = "right", visualStyle: string = "plain", showDecorations: boolean = true, fullBleed: boolean = true, visualEyebrow: string = "", visualTitle: string = "", visualDescription: string = "", visualIcon: string = "", visualImage: string = "", visualAlt: string = "", visualItems: unknown[] = [], primaryLabel: string = "", primaryHref: string = "", primaryIcon: string = "", primaryTarget: string = "", secondaryLabel: string = "", secondaryHref: string = "", secondaryIcon: string = "", secondaryTarget: string = "", tertiaryLabel: string = "", tertiaryHref: string = "", tertiaryIcon: string = "", tertiaryTarget: string = "", badges: unknown[] = [], trustItems: unknown[] = [], maxWidth: string = "xl", class: string = ""
Slots: eyebrow, actions, trust, default, visual, footer
Events: none

### Complete .wrn source contract

```wrn
import Badge from "./Badge.wrn"

component Hero {
  props {
    eyebrow: string = ""
    eyebrowIcon: string = ""
    icon: string = ""
    title: string = "Build something remarkable"
    highlight: string = ""
    description: string = ""

    align: string = "left"
    size: string = "default"
    color: string = "primary"
    variant: string = "default"
    layout: string = "split"
    visualPosition: string = "right"
    visualStyle: string = "plain"
    showDecorations: boolean = true
    fullBleed: boolean = true

    visualEyebrow: string = ""
    visualTitle: string = ""
    visualDescription: string = ""
    visualIcon: string = ""
    visualImage: string = ""
    visualAlt: string = ""
    visualItems: unknown[] = []

    primaryLabel: string = ""
    primaryHref: string = ""
    primaryIcon: string = ""
    primaryTarget: string = ""

    secondaryLabel: string = ""
    secondaryHref: string = ""
    secondaryIcon: string = ""
    secondaryTarget: string = ""

    tertiaryLabel: string = ""
    tertiaryHref: string = ""
    tertiaryIcon: string = ""
    tertiaryTarget: string = ""

    badges: unknown[] = []
    trustItems: unknown[] = []
    maxWidth: string = "xl"
    class: string = ""
  }

  view {
    <section
      {...attrs}
      data-ui-component="Hero"
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      data-layout='{layout}'
      data-align='{align}'
      data-visual-position='{visualPosition}'
      data-visual-style='{visualStyle}'
      data-max-width='{maxWidth}'
      data-full-bleed='{fullBleed}'
      aria-label='{title}'
      class='wire-hero {class}'
    >
        {#if showDecorations}
          <div class="wire-hero__decorations" aria-hidden="true">
            <span class="wire-hero__glow wire-hero__glow--one"></span>
            <span class="wire-hero__glow wire-hero__glow--two"></span>
            <span class="wire-hero__grid-pattern"></span>
          </div>
        {/if}

        <div class="wire-hero__inner">
          <div class="wire-hero__layout">
            <div class="wire-hero__content">
              <slot name="eyebrow"></slot>

              {#if eyebrow}
                <div class="wire-hero__eyebrow">
                  {#if eyebrowIcon}
                    <span class='wire-hero__eyebrow-icon {eyebrowIcon}' aria-hidden="true"></span>
                  {:else}
                    <span class="wire-hero__eyebrow-dot" aria-hidden="true"></span>
                  {/if}
                  <span>{eyebrow}</span>
                </div>
              {/if}

              <div class="wire-hero__heading-row">
                {#if icon}
                  <div class="wire-hero__icon" aria-hidden="true">
                    <span class='{icon}'></span>
                  </div>
                {/if}

                <div class="wire-hero__heading-copy">
                  <h1 class="wire-hero__title">
                    <span>{title}</span>
                    {#if highlight}
                      <span class="wire-hero__highlight">{highlight}</span>
                    {/if}
                  </h1>

                  {#if description}
                    <p class="wire-hero__description">{description}</p>
                  {/if}
                </div>
              </div>

              {#if badges.length > 0}
                <div class="wire-hero__badges">
                  {#each badges as badge}
                    <Badge
                      label='{badge.label || badge.title || badge}'
                      icon='{badge.icon || ""}'
                      color='{badge.color || color}'
                      variant='{badge.variant || "soft"}'
                      size='{badge.size || "sm"}'
                    />
                  {/each}
                </div>
              {/if}

              <div class="wire-hero__actions">
                {#if primaryLabel}
                  <a
                    href='{primaryHref || "#"}'
                    target='{primaryTarget}'
                    rel='{primaryTarget === "_blank" ? "noopener noreferrer" : ""}'
                    class="wire-hero__action wire-hero__action--primary"
                  >
                    <span>{primaryLabel}</span>
                    {#if primaryIcon}
                      <span class='wire-hero__action-icon {primaryIcon}' aria-hidden="true"></span>
                    {/if}
                  </a>
                {/if}

                {#if secondaryLabel}
                  <a
                    href='{secondaryHref || "#"}'
                    target='{secondaryTarget}'
                    rel='{secondaryTarget === "_blank" ? "noopener noreferrer" : ""}'
                    class="wire-hero__action wire-hero__action--secondary"
                  >
                    {#if secondaryIcon}
                      <span class='wire-hero__action-icon {secondaryIcon}' aria-hidden="true"></span>
                    {/if}
                    <span>{secondaryLabel}</span>
                  </a>
                {/if}

                {#if tertiaryLabel}
                  <a
                    href='{tertiaryHref || "#"}'
                    target='{tertiaryTarget}'
                    rel='{tertiaryTarget === "_blank" ? "noopener noreferrer" : ""}'
                    class="wire-hero__action wire-hero__action--tertiary"
                  >
                    <span>{tertiaryLabel}</span>
                    {#if tertiaryIcon}
                      <span class='wire-hero__action-icon {tertiaryIcon}' aria-hidden="true"></span>
                    {/if}
                  </a>
                {/if}

                <slot name="actions"></slot>
              </div>

              {#if trustItems.length > 0}
                <div class="wire-hero__trust" aria-label="Highlights">
                  {#each trustItems as item}
                    <span class="wire-hero__trust-item">
                      <span
                        class='wire-hero__trust-icon {(item.icon || "icon-[lucide--check-circle-2]")}'
                        aria-hidden="true"
                      ></span>
                      <span>{item.label || item.title || item}</span>
                    </span>
                  {/each}
                </div>
              {/if}

              <slot name="trust"></slot>
              <slot></slot>
            </div>

            <div class="wire-hero__visual">
              <slot name="visual"></slot>

              {#if visualImage || visualTitle || visualDescription || visualItems.length > 0}
                <div class="wire-hero__visual-card">
                  {#if visualImage}
                    <div class="wire-hero__visual-media">
                      <img
                        src='{visualImage}'
                        alt='{visualAlt || visualTitle || title}'
                        class="wire-hero__visual-image"
                      />
                    </div>
                  {/if}

                  {#if visualIcon || visualEyebrow || visualTitle || visualDescription}
                    <div class="wire-hero__visual-header">
                      {#if visualIcon}
                        <span class="wire-hero__visual-icon" aria-hidden="true">
                          <span class='{visualIcon}'></span>
                        </span>
                      {/if}

                      <div class="wire-hero__visual-copy">
                        {#if visualEyebrow}
                          <p class="wire-hero__visual-eyebrow">{visualEyebrow}</p>
                        {/if}

                        {#if visualTitle}
                          <h2 class="wire-hero__visual-title">{visualTitle}</h2>
                        {/if}

                        {#if visualDescription}
                          <p class="wire-hero__visual-description">{visualDescription}</p>
                        {/if}
                      </div>
                    </div>
                  {/if}

                  {#if visualItems.length > 0}
                    <div class="wire-hero__visual-items">
                      {#each visualItems as item}
                        <div class="wire-hero__visual-item">
                          {#if item.icon}
                            <span class='wire-hero__visual-item-icon {item.icon}' aria-hidden="true"></span>
                          {/if}

                          <div class="wire-hero__visual-item-copy">
                            <span class="wire-hero__visual-item-label">{item.label || item.title}</span>
                            {#if item.description}
                              <span class="wire-hero__visual-item-description">{item.description}</span>
                            {/if}
                          </div>

                          {#if item.value}
                            <strong class="wire-hero__visual-item-value">{item.value}</strong>
                          {/if}
                        </div>
                      {/each}
                    </div>
                  {/if}
                </div>
              {/if}
            </div>
          </div>

          <div class="wire-hero__footer">
            <slot name="footer"></slot>
          </div>
      </div>
    </section>
  }

  style {
    .wire-hero {
      --wire-hero-accent: var(--wire-color-primary);
      --wire-hero-accent-soft: var(--wire-color-primary-soft);
      --wire-hero-accent-muted: var(--wire-color-primary-muted);
      --wire-hero-contrast: var(--wire-color-primary-contrast);

      position: relative;
      isolation: isolate;
      overflow: hidden;
      width: 100%;
      max-width: 100%;
      box-sizing: border-box;
      color: var(--wire-color-text);
      background: transparent;
      border: 1px solid transparent;
      border-radius: 2rem;
    }

    .wire-hero[data-full-bleed="true"] {
      width: 100vw;
      max-width: 100vw;
      margin-inline: calc(50% - 50vw);
      border-right-width: 0;
      border-left-width: 0;
      border-radius: 0;
    }

    @supports (width: 100dvw) {
      .wire-hero[data-full-bleed="true"] {
        width: 100dvw;
        max-width: 100dvw;
        margin-inline: calc(50% - 50dvw);
      }
    }

    .wire-hero[data-color="secondary"] {
      --wire-hero-accent: var(--wire-color-secondary);
      --wire-hero-accent-soft: var(--wire-color-secondary-soft, var(--wire-color-primary-soft));
      --wire-hero-accent-muted: var(--wire-color-secondary-muted, var(--wire-color-primary-muted));
      --wire-hero-contrast: var(--wire-color-secondary-contrast, var(--wire-color-primary-contrast));
    }

    .wire-hero[data-color="success"] {
      --wire-hero-accent: var(--wire-color-success);
      --wire-hero-accent-soft: var(--wire-color-success-soft, var(--wire-color-primary-soft));
      --wire-hero-accent-muted: var(--wire-color-success-muted, var(--wire-color-primary-muted));
      --wire-hero-contrast: var(--wire-color-success-contrast, var(--wire-color-primary-contrast));
    }

    .wire-hero[data-color="info"] {
      --wire-hero-accent: var(--wire-color-info);
      --wire-hero-accent-soft: var(--wire-color-info-soft, var(--wire-color-primary-soft));
      --wire-hero-accent-muted: var(--wire-color-info-muted, var(--wire-color-primary-muted));
      --wire-hero-contrast: var(--wire-color-info-contrast, var(--wire-color-primary-contrast));
    }

    .wire-hero[data-color="warning"] {
      --wire-hero-accent: var(--wire-color-warning);
      --wire-hero-accent-soft: var(--wire-color-warning-soft, var(--wire-color-primary-soft));
      --wire-hero-accent-muted: var(--wire-color-warning-muted, var(--wire-color-primary-muted));
      --wire-hero-contrast: var(--wire-color-warning-contrast, var(--wire-color-primary-contrast));
    }

    .wire-hero[data-color="danger"] {
      --wire-hero-accent: var(--wire-color-danger);
      --wire-hero-accent-soft: var(--wire-color-danger-soft, var(--wire-color-primary-soft));
      --wire-hero-accent-muted: var(--wire-color-danger-muted, var(--wire-color-primary-muted));
      --wire-hero-contrast: var(--wire-color-danger-contrast, var(--wire-color-primary-contrast));
    }

    .wire-hero[data-variant="soft"] {
      background:
        linear-gradient(135deg, var(--wire-hero-accent-soft), transparent 58%),
        var(--wire-color-surface-raised);
      border-color: var(--wire-color-border);
    }

    .wire-hero[data-variant="raised"] {
      background: var(--wire-color-surface-raised);
      border-color: var(--wire-color-border);
      box-shadow: 0 1.5rem 4rem rgb(0 0 0 / 0.14);
    }

    .wire-hero[data-variant="outline"] {
      background: var(--wire-color-surface);
      border-color: color-mix(in srgb, var(--wire-hero-accent) 35%, var(--wire-color-border));
    }

    .wire-hero[data-variant="gradient"] {
      background:
        radial-gradient(circle at 10% 15%, var(--wire-hero-accent-muted), transparent 32%),
        radial-gradient(circle at 90% 85%, color-mix(in srgb, var(--wire-color-secondary) 24%, transparent), transparent 34%),
        linear-gradient(145deg, var(--wire-color-surface-raised), var(--wire-color-surface));
      border-color: color-mix(in srgb, var(--wire-hero-accent) 24%, var(--wire-color-border));
    }

    .wire-hero[data-variant="solid"] {
      color: var(--wire-hero-contrast);
      background:
        radial-gradient(circle at 85% 15%, rgb(255 255 255 / 0.16), transparent 30%),
        linear-gradient(135deg, var(--wire-hero-accent), color-mix(in srgb, var(--wire-hero-accent) 76%, black));
      border-color: transparent;
    }

    .wire-hero[data-variant="minimal"] {
      border-radius: 0;
    }

    .wire-hero__decorations {
      position: absolute;
      inset: 0;
      z-index: -1;
      overflow: hidden;
      pointer-events: none;
    }

    .wire-hero__glow {
      position: absolute;
      width: 22rem;
      height: 22rem;
      border-radius: 999px;
      background: var(--wire-hero-accent-muted);
      filter: blur(5rem);
      opacity: 0.42;
    }

    .wire-hero__glow--one {
      top: -12rem;
      right: -8rem;
    }

    .wire-hero__glow--two {
      bottom: -15rem;
      left: -10rem;
      opacity: 0.22;
    }

    .wire-hero__grid-pattern {
      position: absolute;
      inset: 0;
      opacity: 0.035;
      background-image:
        linear-gradient(var(--wire-color-text) 1px, transparent 1px),
        linear-gradient(90deg, var(--wire-color-text) 1px, transparent 1px);
      background-size: 2.5rem 2.5rem;
      mask-image: linear-gradient(to bottom, black, transparent 84%);
    }

    .wire-hero__inner {
      width: 100%;
      max-width: 80rem;
      margin-inline: auto;
      padding: 3rem 1.25rem;
      box-sizing: border-box;
    }

    .wire-hero[data-max-width="compact"] .wire-hero__inner,
    .wire-hero[data-max-width="sm"] .wire-hero__inner {
      max-width: 64rem;
    }

    .wire-hero[data-max-width="default"] .wire-hero__inner,
    .wire-hero[data-max-width="lg"] .wire-hero__inner {
      max-width: 72rem;
    }

    .wire-hero[data-max-width="wide"] .wire-hero__inner,
    .wire-hero[data-max-width="xl"] .wire-hero__inner {
      max-width: 80rem;
    }

    .wire-hero[data-max-width="2xl"] .wire-hero__inner {
      max-width: 90rem;
    }

    .wire-hero[data-max-width="full"] .wire-hero__inner {
      max-width: none;
    }

    .wire-hero[data-size="compact"] .wire-hero__inner,
    .wire-hero[data-size="sm"] .wire-hero__inner {
      padding-block: 2rem;
    }

    .wire-hero[data-size="lg"] .wire-hero__inner,
    .wire-hero[data-size="large"] .wire-hero__inner {
      padding-block: 4rem;
    }

    .wire-hero[data-size="xl"] .wire-hero__inner {
      padding-block: 5rem;
    }

    .wire-hero__layout {
      display: grid;
      grid-template-columns: minmax(0, 1fr);
      align-items: center;
      gap: 2.5rem;
    }

    .wire-hero__content {
      min-width: 0;
      max-width: 46rem;
    }

    .wire-hero[data-align="center"] .wire-hero__content {
      margin-inline: auto;
      text-align: center;
    }

    .wire-hero[data-align="right"] .wire-hero__content {
      margin-left: auto;
      text-align: right;
    }

    .wire-hero__eyebrow {
      display: inline-flex;
      align-items: center;
      gap: 0.6rem;
      width: fit-content;
      margin-bottom: 1.4rem;
      padding: 0.55rem 0.9rem;
      color: var(--wire-hero-accent);
      font-size: 0.75rem;
      font-weight: 600;
      line-height: 1;
      letter-spacing: 0.04em;
      background: var(--wire-hero-accent-soft);
      border: 1px solid color-mix(in srgb, var(--wire-hero-accent) 24%, transparent);
      border-radius: 999px;
    }

    .wire-hero[data-align="center"] .wire-hero__eyebrow {
      margin-inline: auto;
    }

    .wire-hero[data-align="right"] .wire-hero__eyebrow {
      margin-left: auto;
    }

    .wire-hero__eyebrow-dot {
      width: 0.45rem;
      height: 0.45rem;
      flex: 0 0 auto;
      background: currentColor;
      border-radius: 999px;
      box-shadow: 0 0 0 0.25rem color-mix(in srgb, currentColor 12%, transparent);
    }

    .wire-hero__eyebrow-icon {
      width: 0.9rem;
      height: 0.9rem;
      flex: 0 0 auto;
    }

    .wire-hero__heading-row {
      display: flex;
      align-items: flex-start;
      gap: 1.25rem;
    }

    .wire-hero[data-align="center"] .wire-hero__heading-row {
      justify-content: center;
    }

    .wire-hero[data-align="right"] .wire-hero__heading-row {
      justify-content: flex-end;
    }

    .wire-hero__heading-copy {
      min-width: 0;
    }

    .wire-hero__icon {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 3.5rem;
      height: 3.5rem;
      flex: 0 0 auto;
      color: var(--wire-hero-accent);
      font-size: 1.5rem;
      background: var(--wire-hero-accent-soft);
      border-radius: 1.1rem;
    }

    .wire-hero__title {
      max-width: 18ch;
      margin: 0;
      color: var(--wire-color-text);
      font-size: clamp(2.35rem, 6vw, 4.8rem);
      font-weight: 650;
      line-height: 1.02;
      letter-spacing: -0.045em;
      text-wrap: balance;
    }

    .wire-hero[data-size="compact"] .wire-hero__title,
    .wire-hero[data-size="sm"] .wire-hero__title {
      font-size: clamp(2rem, 5vw, 3.4rem);
    }

    .wire-hero[data-size="lg"] .wire-hero__title,
    .wire-hero[data-size="large"] .wire-hero__title {
      font-size: clamp(2.8rem, 7vw, 5.6rem);
    }

    .wire-hero[data-size="xl"] .wire-hero__title {
      font-size: clamp(3.1rem, 8vw, 6.4rem);
    }

    .wire-hero[data-variant="solid"] .wire-hero__title,
    .wire-hero[data-variant="solid"] .wire-hero__description,
    .wire-hero[data-variant="solid"] .wire-hero__trust {
      color: var(--wire-hero-contrast);
    }

    .wire-hero__highlight {
      display: block;
      color: var(--wire-hero-accent);
      background: linear-gradient(90deg, var(--wire-hero-accent), var(--wire-color-secondary));
      background-clip: text;
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
    }

    .wire-hero[data-variant="solid"] .wire-hero__highlight {
      color: inherit;
      background: none;
      -webkit-text-fill-color: currentColor;
      opacity: 0.9;
    }

    .wire-hero__description {
      max-width: 42rem;
      margin: 1.5rem 0 0;
      color: var(--wire-color-text-muted);
      font-size: 1.05rem;
      line-height: 1.8;
      text-wrap: pretty;
    }

    .wire-hero[data-align="center"] .wire-hero__title,
    .wire-hero[data-align="center"] .wire-hero__description {
      margin-inline: auto;
    }

    .wire-hero[data-align="right"] .wire-hero__title,
    .wire-hero[data-align="right"] .wire-hero__description {
      margin-left: auto;
    }

    .wire-hero__badges,
    .wire-hero__actions,
    .wire-hero__trust {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
    }

    .wire-hero__badges {
      gap: 0.5rem;
      margin-top: 1.5rem;
    }

    .wire-hero__actions {
      gap: 0.75rem;
      margin-top: 2rem;
    }

    .wire-hero__actions:empty {
      display: none;
    }

    .wire-hero__action {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 0.6rem;
      min-height: 3rem;
      padding: 0.75rem 1.15rem;
      font-size: 0.9rem;
      font-weight: 650;
      line-height: 1.2;
      text-decoration: none;
      border: 1px solid transparent;
      border-radius: 0.85rem;
      transition:
        transform 160ms ease,
        background-color 160ms ease,
        border-color 160ms ease,
        color 160ms ease,
        box-shadow 160ms ease;
    }

    .wire-hero__action:hover {
      transform: translateY(-2px);
    }

    .wire-hero__action:focus-visible {
      outline: 3px solid color-mix(in srgb, var(--wire-hero-accent) 30%, transparent);
      outline-offset: 3px;
    }

    .wire-hero__action--primary {
      color: var(--wire-hero-contrast);
      background: var(--wire-hero-accent);
      box-shadow: 0 0.75rem 2rem color-mix(in srgb, var(--wire-hero-accent) 24%, transparent);
    }

    .wire-hero__action--primary:hover {
      box-shadow: 0 1rem 2.4rem color-mix(in srgb, var(--wire-hero-accent) 34%, transparent);
    }

    .wire-hero__action--secondary {
      color: var(--wire-hero-accent);
      background: color-mix(in srgb, var(--wire-color-surface-raised) 76%, transparent);
      border-color: color-mix(in srgb, var(--wire-hero-accent) 45%, var(--wire-color-border));
    }

    .wire-hero__action--secondary:hover {
      background: var(--wire-hero-accent-soft);
    }

    .wire-hero__action--tertiary {
      color: var(--wire-color-text-muted);
      background: transparent;
    }

    .wire-hero__action--tertiary:hover {
      color: var(--wire-hero-accent);
      background: var(--wire-hero-accent-soft);
    }

    .wire-hero__action-icon {
      width: 1rem;
      height: 1rem;
      flex: 0 0 auto;
    }

    .wire-hero__trust {
      gap: 0.75rem 1.25rem;
      margin-top: 1.75rem;
      color: var(--wire-color-text-muted);
      font-size: 0.82rem;
      line-height: 1.5;
    }

    .wire-hero__trust-item {
      display: inline-flex;
      align-items: center;
      gap: 0.45rem;
    }

    .wire-hero__trust-icon {
      width: 1rem;
      height: 1rem;
      flex: 0 0 auto;
      color: var(--wire-hero-accent);
    }

    .wire-hero[data-align="center"] .wire-hero__badges,
    .wire-hero[data-align="center"] .wire-hero__actions,
    .wire-hero[data-align="center"] .wire-hero__trust {
      justify-content: center;
    }

    .wire-hero[data-align="right"] .wire-hero__badges,
    .wire-hero[data-align="right"] .wire-hero__actions,
    .wire-hero[data-align="right"] .wire-hero__trust {
      justify-content: flex-end;
    }

    .wire-hero__visual {
      position: relative;
      min-width: 0;
      width: 100%;
    }

    .wire-hero__visual:not(:has(> *)) {
      display: none;
    }

    .wire-hero__layout:not(:has(.wire-hero__visual > *)) {
      grid-template-columns: minmax(0, 1fr);
    }

    .wire-hero[data-visual-style="card"] .wire-hero__visual {
      padding: 0.75rem;
      background: color-mix(in srgb, var(--wire-color-surface-raised) 88%, transparent);
      border: 1px solid var(--wire-color-border);
      border-radius: 1.75rem;
      box-shadow: 0 1.5rem 4rem rgb(0 0 0 / 0.14);
    }

    .wire-hero[data-visual-style="soft"] .wire-hero__visual {
      padding: 1.25rem;
      background: var(--wire-hero-accent-soft);
      border-radius: 1.75rem;
    }

    .wire-hero__visual > * {
      width: 100%;
      max-width: 100%;
    }

    .wire-hero__visual-card {
      width: 100%;
      overflow: hidden;
      padding: 1.25rem;
      color: var(--wire-color-text);
      background: color-mix(in srgb, var(--wire-color-surface-raised) 92%, transparent);
      border: 1px solid color-mix(in srgb, var(--wire-hero-accent) 18%, var(--wire-color-border));
      border-radius: 1.75rem;
      box-shadow: 0 1.5rem 4rem rgb(0 0 0 / 0.14);
      backdrop-filter: blur(1rem);
    }

    .wire-hero__visual-media {
      overflow: hidden;
      margin: -0.5rem -0.5rem 1.25rem;
      border-radius: 1.25rem;
    }

    .wire-hero__visual-image {
      display: block;
      width: 100%;
      aspect-ratio: 16 / 10;
      object-fit: cover;
    }

    .wire-hero__visual-header {
      display: flex;
      align-items: flex-start;
      gap: 1rem;
    }

    .wire-hero__visual-icon {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 3rem;
      height: 3rem;
      flex: 0 0 auto;
      color: var(--wire-hero-accent);
      background: var(--wire-hero-accent-soft);
      border-radius: 1rem;
    }

    .wire-hero__visual-icon > span {
      width: 1.35rem;
      height: 1.35rem;
    }

    .wire-hero__visual-copy {
      min-width: 0;
    }

    .wire-hero__visual-eyebrow {
      margin: 0 0 0.35rem;
      color: var(--wire-hero-accent);
      font-size: 0.72rem;
      font-weight: 600;
      letter-spacing: 0.12em;
      text-transform: uppercase;
    }

    .wire-hero__visual-title {
      margin: 0;
      color: var(--wire-color-text);
      font-size: 1.35rem;
      font-weight: 650;
      line-height: 1.25;
    }

    .wire-hero__visual-description {
      margin: 0.55rem 0 0;
      color: var(--wire-color-text-muted);
      font-size: 0.9rem;
      line-height: 1.65;
    }

    .wire-hero__visual-items {
      display: grid;
      gap: 0.75rem;
      margin-top: 1.25rem;
    }

    .wire-hero__visual-item {
      display: flex;
      align-items: center;
      gap: 0.8rem;
      min-width: 0;
      padding: 0.9rem 1rem;
      background: color-mix(in srgb, var(--wire-color-surface) 82%, transparent);
      border: 1px solid var(--wire-color-border);
      border-radius: 1rem;
    }

    .wire-hero__visual-item-icon {
      width: 1.1rem;
      height: 1.1rem;
      flex: 0 0 auto;
      color: var(--wire-hero-accent);
    }

    .wire-hero__visual-item-copy {
      display: flex;
      flex: 1 1 auto;
      flex-direction: column;
      min-width: 0;
    }

    .wire-hero__visual-item-label {
      color: var(--wire-color-text);
      font-size: 0.88rem;
      font-weight: 600;
      line-height: 1.35;
    }

    .wire-hero__visual-item-description {
      margin-top: 0.15rem;
      color: var(--wire-color-text-muted);
      font-size: 0.76rem;
      line-height: 1.4;
    }

    .wire-hero__visual-item-value {
      flex: 0 0 auto;
      color: var(--wire-hero-accent);
      font-size: 0.9rem;
      font-weight: 700;
    }

    .wire-hero[data-variant="solid"] .wire-hero__visual-card,
    .wire-hero[data-variant="solid"] .wire-hero__visual-title,
    .wire-hero[data-variant="solid"] .wire-hero__visual-item-label {
      color: var(--wire-hero-contrast);
    }

    .wire-hero[data-variant="solid"] .wire-hero__visual-description,
    .wire-hero[data-variant="solid"] .wire-hero__visual-item-description {
      color: color-mix(in srgb, var(--wire-hero-contrast) 74%, transparent);
    }

    .wire-hero__footer:not(:has(> *)) {
      display: none;
    }

    .wire-hero__footer {
      margin-top: 2rem;
      padding-top: 1.5rem;
      border-top: 1px solid var(--wire-color-border);
    }

    @media (min-width: 640px) {
      .wire-hero__inner {
        padding-inline: 2rem;
      }
    }

    @media (min-width: 768px) {
      .wire-hero__actions {
        gap: 0.9rem;
      }

      .wire-hero__action {
        padding-inline: 1.35rem;
      }
    }

    @media (min-width: 1024px) {
      .wire-hero__inner {
        padding-inline: 3rem;
      }

      .wire-hero[data-layout="split"] .wire-hero__layout {
        grid-template-columns: minmax(0, 1.08fr) minmax(20rem, 0.92fr);
        gap: 4rem;
      }

      .wire-hero[data-layout="stacked"] .wire-hero__layout,
      .wire-hero[data-layout="content"] .wire-hero__layout {
        grid-template-columns: minmax(0, 1fr);
      }

      .wire-hero[data-layout="stacked"] .wire-hero__content {
        max-width: 58rem;
      }

      .wire-hero[data-layout="stacked"] .wire-hero__visual {
        max-width: 68rem;
      }

      .wire-hero[data-layout="content"] .wire-hero__visual {
        display: none;
      }

      .wire-hero[data-visual-position="left"] .wire-hero__visual {
        grid-column: 1;
        grid-row: 1;
      }

      .wire-hero[data-visual-position="left"] .wire-hero__content {
        grid-column: 2;
        grid-row: 1;
      }

      .wire-hero[data-layout="stacked"][data-visual-position="left"] .wire-hero__visual,
      .wire-hero[data-layout="stacked"][data-visual-position="left"] .wire-hero__content {
        grid-column: 1;
      }

      .wire-hero[data-layout="stacked"][data-visual-position="left"] .wire-hero__visual {
        grid-row: 1;
      }

      .wire-hero[data-layout="stacked"][data-visual-position="left"] .wire-hero__content {
        grid-row: 2;
      }
    }

    @media (max-width: 639px) {
      .wire-hero:not([data-full-bleed="true"]) {
        border-radius: 1.25rem;
      }

      .wire-hero__inner {
        padding: 2.25rem 1rem;
      }

      .wire-hero__heading-row {
        gap: 0.85rem;
      }

      .wire-hero__icon {
        width: 2.75rem;
        height: 2.75rem;
        font-size: 1.2rem;
        border-radius: 0.85rem;
      }

      .wire-hero__description {
        font-size: 0.95rem;
        line-height: 1.7;
      }

      .wire-hero__actions {
        align-items: stretch;
      }

      .wire-hero__action {
        width: 100%;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .wire-hero__action {
        transition: none;
      }

      .wire-hero__action:hover {
        transform: none;
      }
    }
  }
}
```

---

## HeroActions

Showcase: https://component.wrnexusjs.dev/
Mount: <HeroActions /> (legacy: data-component="HeroActions")
Category: marketing
Purpose: Group hero and campaign actions with consistent alignment, orientation, sizing, and responsive mobile stacking.
Props: actions: unknown[] = [], align: string = "left", orientation: string = "horizontal", stackOnMobile: boolean = true, fullWidthMobile: boolean = true, size: string = "default", color: string = "primary", class: string = ""
Slots: default
Events: none

### Complete .wrn source contract

```wrn
import Button from "./button.wrn"

component HeroActions {
  props {
    actions: unknown[] = []
    align: string = "left"
    orientation: string = "horizontal"
    stackOnMobile: boolean = true
    fullWidthMobile: boolean = true
    size: string = "default"
    color: string = "primary"
    class: string = ""
  }

  view {
    <div
      data-ui-component="HeroActions"
      role="group"
      aria-label="Page actions"
      data-align='{align}'
      data-orientation='{orientation}'
      data-stack-mobile='{stackOnMobile ? "true" : "false"}'
      data-full-mobile='{fullWidthMobile ? "true" : "false"}'
      class='wire-hero-actions {class}'
    >
      {#each actions as action, index}
        {#if action.label}
          <Button
            label='{action.label}'
            href='{action.href || ""}'
            target='{action.target || ""}'
            rel='{action.rel || ""}'
            type='{action.type || "button"}'
            variant='{action.variant || (index === 0 ? "default" : "outline")}'
            color='{action.color || color}'
            size='{action.size || size}'
            disabled='{action.disabled || false}'
            loading='{action.loading || false}'
            icon='{action.icon || ""}'
            iconPosition='{action.iconPosition || "start"}'
            ariaLabel='{action.ariaLabel || action.label}'
            fullWidth='{fullWidthMobile}'
            controlClass='{action.controlClass || ""}'
            class='{"wire-hero-actions__action " + (action.class || "")}'
          />
        {/if}
      {/each}

      <slot></slot>
    </div>
  }

  style {
    .wire-hero-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; }
    .wire-hero-actions[data-align="center"] { justify-content: center; }
    .wire-hero-actions[data-align="right"] { justify-content: flex-end; }
    .wire-hero-actions[data-orientation="vertical"] { flex-direction: column; align-items: stretch; }
    .wire-hero-actions[data-full-mobile="true"] { width: 100%; }
    @media (max-width: 39.999rem) {
      .wire-hero-actions[data-stack-mobile="true"] { flex-direction: column; align-items: stretch; }
    }
    @media (min-width: 40rem) {
      .wire-hero-actions[data-full-mobile="true"] { width: auto; }
      .wire-hero-actions__action { width: auto; }
    }
  }
}
```

---

## Image

Showcase: https://component.wrnexusjs.dev/
Mount: <Image /> (legacy: data-component="Image")
Category: layout
Purpose: Render a responsive image with explicit dimensions, loading behavior, alternative text, sizing, and rounded treatment.
Props: size: string = "default", color: string = "primary", src: string = "", alt: string = "", width: string = "", height: string = "", loading: string = "lazy", fit: string = "cover", rounded: boolean = false, class: string = ""
Slots: default
Events: none

### Complete .wrn source contract

```wrn
// Image -- a framed image with a reserved aspect ratio.
//
//   <Image src="/hero.jpg" alt="Dashboard" size="video" rounded={true} />
//
// The frame keeps its ratio whether or not the image has loaded, so the page
// does not jump when it arrives. Without a src it renders a labelled
// placeholder of the same shape rather than collapsing.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Image {
  props {
    size: string = "default"
    color: string = "primary"
    src: string = ""
    alt: string = ""
    width: string = ""
    height: string = ""
    loading: string = "lazy"
    fit: string = "cover"
    rounded: boolean = false
    class: string = ""
  }

  view {
    <figure
      {...attrs}
      data-ui-component="Image"
      class='wire-image {class}'
      data-size='{size}'
      data-color='{color}'
      data-fit='{fit}'
      data-rounded='{rounded}'
    >
      <img
        class="wire-image__img"
        data-show="src"
        src='{src}'
        alt='{alt}'
        width='{width}'
        height='{height}'
        loading='{loading}'
        decoding="async"
      />

      <div
        class="wire-image__placeholder"
        data-show="!src"
        role="img"
        aria-label='{alt || "Image placeholder"}'
      >
        <span class="icon-[lucide--image] wire-image__placeholder-icon" aria-hidden="true"></span>
      </div>

      <slot></slot>
    </figure>
  }

  style {
    .wire-image {
      position: relative;
      margin: 0;
      overflow: hidden;
      width: 100%;
      min-width: 0;
      background: var(--wire-color-surface-soft);
    }

    .wire-image[data-rounded="true"] {
      border-radius: var(--wire-radius-lg);
    }

    /*
     * The ratio sits on the frame rather than the image, so the space is
     * reserved before the file arrives and the page does not jump.
     */
    .wire-image[data-size="square"] {
      aspect-ratio: 1 / 1;
    }

    .wire-image[data-size="video"] {
      aspect-ratio: 16 / 9;
    }

    .wire-image[data-size="landscape"] {
      aspect-ratio: 4 / 3;
    }

    .wire-image[data-size="portrait"] {
      aspect-ratio: 3 / 4;
    }

    .wire-image__img {
      display: block;
      width: 100%;
      height: 100%;
      object-fit: cover;
    }

    .wire-image[data-fit="contain"] .wire-image__img {
      object-fit: contain;
    }

    .wire-image[data-fit="fill"] .wire-image__img {
      object-fit: fill;
    }

    .wire-image[data-size="default"] .wire-image__img {
      height: auto;
    }

    .wire-image__placeholder {
      display: flex;
      align-items: center;
      justify-content: center;
      width: 100%;
      min-height: 12rem;
      height: 100%;
      color: var(--wire-color-text-muted);
    }

    .wire-image__placeholder-icon {
      width: 2rem;
      height: 2rem;
    }
  }
}
```

---

## Input

Showcase: https://component.wrnexusjs.dev/
Mount: <Input /> (legacy: data-component="Input")
Category: forms
Purpose: Theme-aware, responsive input component.
Props: size: string = "default", color: string = "primary", id: string = "", name: string = "", label: string = "Input", hiddenLabel: boolean = false, placeholder: string = "", value: string = "", type: string = "text", variant: string = "normal", icon: string = "", iconPosition: string = "start", helperText: string = "", cornerHint: string = "", error: string = "", inline: boolean = false, readonly: boolean = false, disabled: boolean = false, required: boolean = false, autocomplete: string = "", inputmode: string = "", minlength: string = "", maxlength: string = "", pattern: string = "", min: string = "", max: string = "", step: string = "", class: string = ""
Slots: none
Events: input, change, focus, blur, invalid, keydown, keyup

### Complete .wrn source contract

```wrn
import FieldStyles from "../styles/FieldStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component Input {
  outputs {
    input(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
    change(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
    focus(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
    blur(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
    invalid(payload: { value: string | number | boolean | null | object; name: string; message: string; sourceEvent: Event })
    keydown(payload: { key: string; value: string | number | boolean | null | object; name: string; sourceEvent: Event })
    keyup(payload: { key: string; value: string | number | boolean | null | object; name: string; sourceEvent: Event })
  }

  props {
size: string = "default"
    color: string = "primary"
    id: string = ""
    name: string = ""
    label: string = "Input"
    hiddenLabel: boolean = false
    placeholder: string = ""
    value: string = ""
    type: string = "text"
    variant: string = "normal"
    icon: string = ""
    iconPosition: string = "start"
    helperText: string = ""
    cornerHint: string = ""
    error: string = ""
    inline: boolean = false
    readonly: boolean = false
    disabled: boolean = false
    required: boolean = false
    autocomplete: string = ""
    inputmode: string = ""
    minlength: string = ""
    maxlength: string = ""
    pattern: string = ""
    min: string = ""
    max: string = ""
    step: string = ""
    class: string = ""
  }
  functions {
    client function detail(sourceEvent) {
      return { value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }
    }
    client function handleInput(sourceEvent) { sourceEvent.stopPropagation(); output.input(detail(sourceEvent)) }
    client function handleChange(sourceEvent) { sourceEvent.stopPropagation(); output.change(detail(sourceEvent)) }
    client function handleFocus(sourceEvent) { sourceEvent.stopPropagation(); output.focus(detail(sourceEvent)) }
    client function handleBlur(sourceEvent) { sourceEvent.stopPropagation(); output.blur(detail(sourceEvent)) }
    client function handleInvalid(sourceEvent) { sourceEvent.stopPropagation(); output.invalid({ value: sourceEvent.currentTarget.value, name: name, message: sourceEvent.currentTarget.validationMessage, sourceEvent: sourceEvent }) }
    client function handleKeydown(sourceEvent) { sourceEvent.stopPropagation(); output.keydown({ key: sourceEvent.key, value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
    client function handleKeyup(sourceEvent) { sourceEvent.stopPropagation(); output.keyup({ key: sourceEvent.key, value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
  }
  view {
    <div {...attrs} class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--field wire-next--input {class}" data-variant="{variant}" data-inline="{inline}" data-floating="{variant === 'floating'}" data-invalid="{error ? 'true' : 'false'}">
      <div class="wire-next__field-heading">
        <label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>
        {#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}
      </div>
      <div class="wire-next__field-control" data-icon-position="{iconPosition}">
        {#if icon}<span class="{icon} wire-next__field-icon" aria-hidden="true"></span>{/if}
        <input
          id="{id || name}"
          type="{type}"
          name="{name}"
          value="{value}"
          placeholder="{variant === 'floating' ? ' ' : placeholder}"
          readonly="{readonly}"
          disabled="{disabled}"
          required="{required}"
          autocomplete="{autocomplete}"
          inputmode="{inputmode}"
          minlength="{minlength}"
          maxlength="{maxlength}"
          pattern="{pattern}"
          min="{min}"
          max="{max}"
          step="{step}"
          aria-invalid="{error ? 'true' : 'false'}"
          aria-describedby="{error ? (id || name) + '-error' : (helperText ? (id || name) + '-help' : '')}"
          @input="handleInput(event)"
          @change="handleChange(event)"
          @focus="handleFocus(event)"
          @blur="handleBlur(event)"
          @invalid="handleInvalid(event)"
          @keydown="handleKeydown(event)"
          @keyup="handleKeyup(event)"
        />
        {#if variant === "floating"}<span class="wire-next__field-floating-label">{label}</span>{/if}
      </div>
      {#if helperText}<small id="{(id || name) + '-help'}" class="wire-next__field-help">{helperText}</small>{/if}
      <small id="{(id || name) + '-error'}" class="wire-next__field-error" data-error="{name}">{error}</small>
    </div>
    <FieldStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--input { width: 100%; min-width: 0; }
    .wire-next--input[data-invalid="true"] { --wire-component-color: var(--wire-color-danger); }
  }
}
```

---

## InputGroup

Showcase: https://component.wrnexusjs.dev/
Mount: <InputGroup /> (legacy: data-component="InputGroup")
Category: forms
Purpose: Theme-aware, responsive input group component.
Props: size: string = "default", color: string = "primary", id: string = "", name: string = "", label: string = "Input group", hiddenLabel: boolean = false, value: string = "", placeholder: string = "", type: string = "text", startText: string = "", endText: string = "", icon: string = "", iconPosition: string = "start", actionLabel: string = "", helperText: string = "", cornerHint: string = "", error: string = "", inline: boolean = false, variant: string = "normal", readonly: boolean = false, disabled: boolean = false, required: boolean = false, class: string = ""
Slots: none
Events: input, change, focus, blur, submit, action

### Complete .wrn source contract

```wrn
import FieldStyles from "../styles/FieldStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component InputGroup {
  outputs {
    input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    submit(payload: { value: string | number | boolean | null | object; name: string; sourceEvent: Event })
    action(payload: { name: string; sourceEvent: Event })
  }

  props {
    size: string = "default"
    color: string = "primary"
    id: string = ""
    name: string = ""
    label: string = "Input group"
    hiddenLabel: boolean = false
    value: string = ""
    placeholder: string = ""
    type: string = "text"
    startText: string = ""
    endText: string = ""
    icon: string = ""
    iconPosition: string = "start"
    actionLabel: string = ""
    helperText: string = ""
    cornerHint: string = ""
    error: string = ""
    inline: boolean = false
    variant: string = "normal"
    readonly: boolean = false
    disabled: boolean = false
    required: boolean = false
    class: string = ""
  }
  functions {
    client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
    client function handleKeydown(sourceEvent) { if (sourceEvent.key === "Enter") { sourceEvent.stopPropagation(); output.submit({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) } }
    client function handleAction(sourceEvent) { output.action({ name: name, sourceEvent: sourceEvent }) }
  }
  view {
    <div {...attrs} class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--field wire-next--input-group {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
      <div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
      <div class="wire-next__input-group-control">
        {#if startText}<span class="wire-next__input-addon">{startText}</span>{/if}
        {#if icon}<span class="{icon} wire-next__field-icon" data-position="{iconPosition}" aria-hidden="true"></span>{/if}
        <input id="{id || name}" type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" readonly="{readonly}" disabled="{disabled}" required="{required}" aria-invalid="{error ? 'true' : 'false'}" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @keydown="handleKeydown(event)" />
        {#if endText}<span class="wire-next__input-addon">{endText}</span>{/if}
        {#if actionLabel}<button type="button" disabled="{disabled}" @click="handleAction(event)">{actionLabel}</button>{/if}
      </div>
      {#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
    </div>
    <FieldStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next__input-group-control {
      overflow: hidden;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-sm);
      background: var(--wire-color-surface);
    }

    .wire-next__input-group-control input {
      flex: 1;
      min-width: 0;
      border: 0;
      box-shadow: none;
    }

    .wire-next__input-addon,
    .wire-next__input-group-control button {
      padding: 0.7rem 0.85rem;
      white-space: nowrap;
    }

    .wire-next__input-group-control button {
      align-self: stretch;
      border: 0;
      background: var(--wire-field-color);
      color: var(--wire-color-primary-contrast, #fff);
      font: inherit;
      font-weight: 700;
      cursor: pointer;
    }
  }
}
```

---

## InputNumber

Showcase: https://component.wrnexusjs.dev/
Mount: <InputNumber /> (legacy: data-component="InputNumber")
Category: advanced-forms
Purpose: Theme-aware, responsive input number component.
Props: size: string = "default", color: string = "primary", variant: string = "default", class: string = "", id: string = "", name: string = "quantity", value: number = 0, min: string = "", max: string = "", step: number = 1, precision: string = "auto", label: string = "", description: string = "", helpText: string = "", error: string = "", invalid: boolean = false, prefix: string = "", suffix: string = "", placeholder: string = "", autocomplete: string = "off", inputMode: string = "decimal", ariaLabel: string = "", required: boolean = false, disabled: boolean = false, inputDisabled: boolean = false, buttonsDisabled: boolean = false, readonly: boolean = false, allowInput: boolean = true, keyboard: boolean = true, wheel: boolean = false, clamp: boolean = true, fullWidth: boolean = false, showButtons: boolean = true, showValidationMessage: boolean = true, decrementLabel: string = "Decrease value", incrementLabel: string = "Increase value", controlsLabel: string = "Quantity controls", requiredMessage: string = "A value is required.", minMessage: string = "Value is below the minimum.", maxMessage: string = "Value is above the maximum."
Slots: none
Events: input, change, increment, decrement

### Complete .wrn source contract

```wrn
component InputNumber {
    outputs {
      input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
      change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
      increment(payload: { value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
      decrement(payload: { value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    }

    props {
        size: string = "default"
        color: string = "primary"
        variant: string = "default"
        class: string = ""

        id: string = ""
        name: string = "quantity"
        value: number = 0
        min: string = ""
        max: string = ""
        step: number = 1
        precision: string = "auto"

        label: string = ""
        description: string = ""
        helpText: string = ""
        error: string = ""
        invalid: boolean = false

        prefix: string = ""
        suffix: string = ""
        placeholder: string = ""
        autocomplete: string = "off"
        inputMode: string = "decimal"
        ariaLabel: string = ""

        required: boolean = false
        disabled: boolean = false
        inputDisabled: boolean = false
        buttonsDisabled: boolean = false
        readonly: boolean = false
        allowInput: boolean = true
        keyboard: boolean = true
        wheel: boolean = false
        clamp: boolean = true
        fullWidth: boolean = false
        showButtons: boolean = true
        showValidationMessage: boolean = true

        decrementLabel: string = "Decrease value"
        incrementLabel: string = "Increase value"
        controlsLabel: string = "Quantity controls"
        requiredMessage: string = "A value is required."
        minMessage: string = "Value is below the minimum."
        maxMessage: string = "Value is above the maximum."

    }

    state currentValue = value
    state committedValue = value

    functions {
        shared function inputId() {
            if (id !== "") {
                return id
            }

            if (name !== "") {
                return name
            }

            return "input-number"
        }

        shared function descriptionId() {
            return inputId() + "-description"
        }

        shared function messageId() {
            return inputId() + "-message"
        }

        shared function componentColor() {
            if (color === "secondary") {
                return "var(--wire-color-secondary)"
            }

            if (color === "success") {
                return "var(--wire-color-success)"
            }

            if (color === "warning") {
                return "var(--wire-color-warning)"
            }

            if (color === "danger") {
                return "var(--wire-color-danger)"
            }

            if (color === "info") {
                return "var(--wire-color-info)"
            }

            return "var(--wire-color-primary)"
        }

        shared function hasMin() {
            return min !== "" && min !== null && min !== undefined
        }

        shared function hasMax() {
            return max !== "" && max !== null && max !== undefined
        }

        shared function isBlank() {
            return currentValue === "" || currentValue === null || currentValue === undefined
        }

        shared function normalizedStep() {
            return Number(step) > 0 ? Number(step) : 1
        }

        shared function inferredPrecision() {
            if (precision !== "auto" && precision !== "") {
                return Math.max(0, Number(precision) || 0)
            }

            if (String(normalizedStep()).includes(".")) {
                return String(normalizedStep()).split(".")[1].length
            }

            return 0
        }

        shared function precisionFactor() {
            return Math.pow(10, inferredPrecision())
        }

        shared function roundValue(nextValue) {
            return Math.round(Number(nextValue) * precisionFactor()) / precisionFactor()
        }

        shared function clampValue(nextValue) {
            nextValue = Number(nextValue)

            if (hasMin() && nextValue < Number(min)) {
                nextValue = Number(min)
            }

            if (hasMax() && nextValue > Number(max)) {
                nextValue = Number(max)
            }

            return roundValue(nextValue)
        }

        shared function isBelowMin() {
            return !isBlank() && hasMin() && Number(currentValue) < Number(min)
        }

        shared function isAboveMax() {
            return !isBlank() && hasMax() && Number(currentValue) > Number(max)
        }

        shared function isInvalid() {
            return (
                invalid ||
                error !== "" ||
                (required && isBlank()) ||
                isBelowMin() ||
                isAboveMax()
            )
        }

        shared function validationMessage() {
            if (error !== "") {
                return error
            }

            if (required && isBlank()) {
                return requiredMessage
            }

            if (isBelowMin()) {
                return minMessage
            }

            if (isAboveMax()) {
                return maxMessage
            }

            return ""
        }

        shared function hasMessage() {
            return (
                helpText !== "" ||
                (showValidationMessage && isInvalid() && validationMessage() !== "")
            )
        }

        shared function describedBy() {
            if (description !== "" && hasMessage()) {
                return descriptionId() + " " + messageId()
            }

            if (description !== "") {
                return descriptionId()
            }

            if (hasMessage()) {
                return messageId()
            }

            return ""
        }

        shared function decrementDisabled() {
            return (
                disabled ||
                readonly ||
                buttonsDisabled ||
                (hasMin() && !isBlank() && Number(currentValue) <= Number(min))
            )
        }

        shared function incrementDisabled() {
            return (
                disabled ||
                readonly ||
                buttonsDisabled ||
                (hasMax() && !isBlank() && Number(currentValue) >= Number(max))
            )
        }

        // Outputs are resolved by name, so each one is written out. Raw
        // CustomEvents dispatched on the root -- what this did before -- never
        // reach a parent @binding.
        client function dispatchInputNumberEvent(
            sourceEvent,
            eventName,
            action,
            previousValue,
            payload
        ) {
            payload = {
                component: "InputNumber",
                name: name,
                value: currentValue,
                previousValue: previousValue,
                action: action,
                min: hasMin() ? Number(min) : null,
                max: hasMax() ? Number(max) : null,
                step: normalizedStep(),
                valid: !isInvalid()
            }

            if (eventName === "input") {
                output.input(payload)
            } else if (eventName === "change") {
                output.change(payload)
            } else if (eventName === "increment") {
                output.increment(payload)
            } else if (eventName === "decrement") {
                output.decrement(payload)
            }
        }

        client function applyControlValue(nextValue, action, sourceEvent, previousValue) {
            previousValue = currentValue
            currentValue = clampValue(nextValue)
            committedValue = currentValue

            dispatchInputNumberEvent(
                sourceEvent,
                "input",
                action,
                previousValue
            )
            dispatchInputNumberEvent(
                sourceEvent,
                "change",
                action,
                previousValue
            )
            dispatchInputNumberEvent(
                sourceEvent,
                action,
                action,
                previousValue
            )
        }

        client function incrementValue(sourceEvent, nextValue) {
            if (incrementDisabled()) {
                return
            }

            if (isBlank()) {
                nextValue = hasMin() ? Number(min) : normalizedStep()
            } else {
                nextValue =
                    Number(currentValue) +
                    normalizedStep() * (sourceEvent.shiftKey ? 10 : 1)
            }

            applyControlValue(nextValue, "increment", sourceEvent)
        }

        client function decrementValue(sourceEvent, nextValue) {
            if (decrementDisabled()) {
                return
            }

            if (isBlank()) {
                nextValue = hasMax() ? Number(max) : -normalizedStep()
            } else {
                nextValue =
                    Number(currentValue) -
                    normalizedStep() * (sourceEvent.shiftKey ? 10 : 1)
            }

            applyControlValue(nextValue, "decrement", sourceEvent)
        }

        client function handleInput(sourceEvent, previousValue, nextValue) {
            sourceEvent.stopPropagation()
            previousValue = currentValue
            nextValue = sourceEvent.target.value

            if (nextValue === "") {
                currentValue = ""
            } else if (!Number.isNaN(Number(nextValue))) {
                currentValue = roundValue(Number(nextValue))
            }

            dispatchInputNumberEvent(
                sourceEvent,
                "input",
                "input",
                previousValue
            )
        }

        client function handleChange(sourceEvent, previousValue) {
            sourceEvent.stopPropagation()
            previousValue = committedValue

            if (!isBlank()) {
                currentValue = clamp
                    ? clampValue(currentValue)
                    : roundValue(currentValue)
            }

            committedValue = currentValue

            dispatchInputNumberEvent(
                sourceEvent,
                "change",
                "change",
                previousValue
            )
        }

        client function handleKeydown(sourceEvent) {
            if (
                !keyboard ||
                disabled ||
                readonly ||
                inputDisabled ||
                !allowInput
            ) {
                return
            }

            if (sourceEvent.key === "ArrowUp") {
                sourceEvent.preventDefault()
                incrementValue(sourceEvent)
            } else if (sourceEvent.key === "ArrowDown") {
                sourceEvent.preventDefault()
                decrementValue(sourceEvent)
            } else if (sourceEvent.key === "Home" && hasMin()) {
                sourceEvent.preventDefault()
                applyControlValue(Number(min), "decrement", sourceEvent)
            } else if (sourceEvent.key === "End" && hasMax()) {
                sourceEvent.preventDefault()
                applyControlValue(Number(max), "increment", sourceEvent)
            }
        }

        client function handleWheel(sourceEvent) {
            if (
                !wheel ||
                disabled ||
                readonly ||
                inputDisabled ||
                !allowInput
            ) {
                return
            }

            sourceEvent.preventDefault()

            if (sourceEvent.deltaY < 0) {
                incrementValue(sourceEvent)
            } else if (sourceEvent.deltaY > 0) {
                decrementValue(sourceEvent)
            }
        }
    }

    view {
        <div
            {...attrs}
            data-wrn-input-number
            data-variant='{variant}'
            data-size='{size}'
            data-color='{color}'
            data-value='{currentValue}'
            data-invalid='{isInvalid() ? "true" : "false"}'
            data-disabled='{disabled ? "true" : "false"}'
            data-full-width='{fullWidth ? "true" : "false"}'
            style='--input-number-accent: {componentColor()};'
            class='wire-input-number {class}'
        >
            {#if label !== "" && variant !== "labeled" && variant !== "seat"}
            <label
                for='{inputId()}'
                class='wire-input-number__label'
            >
                <span>{label}</span>
                {#if required}
                <span
                    aria-hidden="true"
                    class='wire-input-number__required'
                >*</span>
                {/if}
            </label>
            {/if}

            {#if description !== "" && variant !== "labeled" && variant !== "seat"}
            <p
                id='{descriptionId()}'
                class='wire-input-number__description'
            >
                {description}
            </p>
            {/if}

            <div
                class='wire-input-number__control'
            >
                {#if variant === "horizontal" && showButtons}
                <button
                    type="button"
                    aria-label='{decrementLabel}'
                    aria-controls='{inputId()}'
                    disabled='{decrementDisabled()}'
                    @click='decrementValue(event)'
                    class='wire-input-number__button wire-input-number__button--start'
                >
                    <span
                        aria-hidden="true"
                        class='icon-[lucide--minus] wire-input-number__icon'
                    ></span>
                </button>
                {/if}

                <div
                    class='wire-input-number__content'
                >
                    {#if variant === "labeled" || variant === "seat"}
                    <div class='wire-input-number__copy'>
                        {#if label !== ""}
                        <label
                            for='{inputId()}'
                            class='wire-input-number__inner-label'
                        >
                            {label}
                            {#if required}
                            <span
                                aria-hidden="true"
                                class='wire-input-number__required'
                            >*</span>
                            {/if}
                        </label>
                        {/if}

                        {#if description !== ""}
                        <span
                            id='{descriptionId()}'
                            class='wire-input-number__inner-description'
                        >
                            {description}
                        </span>
                        {/if}
                    </div>
                    {/if}

                    <div
                        class='wire-input-number__field'
                    >
                        {#if prefix !== ""}
                        <span
                            aria-hidden="true"
                            class='wire-input-number__prefix'
                        >
                            {prefix}
                        </span>
                        {/if}

                        <input
                            id='{inputId()}'
                            name='{name}'
                            type="number"
                            value='{currentValue}'
                            min='{min}'
                            max='{max}'
                            step='{normalizedStep()}'
                            placeholder='{placeholder}'
                            autocomplete='{autocomplete}'
                            inputmode='{inputMode}'
                            aria-label='{ariaLabel !== "" ? ariaLabel : label !== "" ? label : name}'
                            aria-describedby='{describedBy()}'
                            aria-invalid='{isInvalid() ? "true" : "false"}'
                            aria-required='{required ? "true" : "false"}'
                            aria-disabled='{disabled || inputDisabled ? "true" : "false"}'
                            required='{required}'
                            disabled='{disabled}'
                            readonly='{readonly || inputDisabled || !allowInput}'
                            tabindex='{inputDisabled ? "-1" : "0"}'
                            @input='handleInput(event)'
                            @change='handleChange(event)'
                            @keydown='handleKeydown(event)'
                            @wheel='handleWheel(event)'
                            class='wire-input-number__input'
                        />

                        {#if suffix !== ""}
                        <span
                            aria-hidden="true"
                            class='wire-input-number__suffix'
                        >
                            {suffix}
                        </span>
                        {/if}
                    </div>
                </div>

                {#if variant === "horizontal" && showButtons}
                <button
                    type="button"
                    aria-label='{incrementLabel}'
                    aria-controls='{inputId()}'
                    disabled='{incrementDisabled()}'
                    @click='incrementValue(event)'
                    class='wire-input-number__button wire-input-number__button--end'
                >
                    <span
                        aria-hidden="true"
                        class='icon-[lucide--plus] wire-input-number__icon'
                    ></span>
                </button>
                {:else}
                {#if showButtons}
                <div
                    role="group"
                    aria-label='{controlsLabel}'
                    class='wire-input-number__buttons'
                >
                    <button
                        type="button"
                        aria-label='{decrementLabel}'
                        aria-controls='{inputId()}'
                        disabled='{decrementDisabled()}'
                        @click='decrementValue(event)'
                        class='wire-input-number__button wire-input-number__button--decrement'
                    >
                        <span
                            aria-hidden="true"
                            class='icon-[lucide--minus] wire-input-number__icon'
                        ></span>
                    </button>

                    <button
                        type="button"
                        aria-label='{incrementLabel}'
                        aria-controls='{inputId()}'
                        disabled='{incrementDisabled()}'
                        @click='incrementValue(event)'
                        class='wire-input-number__button wire-input-number__button--increment'
                    >
                        <span
                            aria-hidden="true"
                            class='icon-[lucide--plus] wire-input-number__icon'
                        ></span>
                    </button>
                </div>
                {/if}
                {/if}
            </div>

            {#if showValidationMessage && isInvalid() && validationMessage() !== ""}
            <p
                id='{messageId()}'
                role="alert"
                aria-live="polite"
                class='wire-input-number__message wire-input-number__message--error'
            >
                <span
                    aria-hidden="true"
                    class='icon-[lucide--circle-alert] wire-input-number__error-icon'
                ></span>
                <span>{validationMessage()}</span>
            </p>
            {/if}

            {#if (!showValidationMessage || !isInvalid() || validationMessage() === "") && helpText !== ""}
            <p
                id='{messageId()}'
                class='wire-input-number__message'
            >
                {helpText}
            </p>
            {/if}
        </div>
    }

    style {
        .wire-input-number { position: relative; display: flex; width: 100%; max-width: 24rem; flex-direction: column; gap: 0.375rem; color: var(--wire-color-text); }
        .wire-input-number[data-full-width="true"] { max-width: none; }
        .wire-input-number[data-variant="compact"] { width: fit-content; max-width: 100%; }
        .wire-input-number[data-disabled="true"] { opacity: 0.6; }
        .wire-input-number__label { display: inline-flex; align-items: center; gap: 0.25rem; color: var(--wire-color-text); font-size: 0.875rem; font-weight: 600; line-height: 1.25rem; }
        .wire-input-number__required { margin-left: 0.125rem; color: var(--wire-color-danger); }
        .wire-input-number__description, .wire-input-number__message { margin: 0; color: var(--wire-color-muted); font-size: 0.75rem; line-height: 1.25rem; }
        .wire-input-number__control { display: flex; min-width: 0; min-height: 2.5rem; overflow: hidden; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius-sm); color: var(--wire-color-text); background: var(--wire-color-surface); box-shadow: var(--wire-shadow-1); transition: border-color var(--wire-motion-base), box-shadow var(--wire-motion-base), background var(--wire-motion-base); }
        .wire-input-number__control:focus-within { border-color: var(--input-number-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--input-number-accent) 18%, transparent); }
        .wire-input-number[data-invalid="true"] .wire-input-number__control { border-color: var(--wire-color-danger); }
        .wire-input-number[data-invalid="true"] .wire-input-number__control:focus-within { box-shadow: 0 0 0 3px color-mix(in srgb, var(--wire-color-danger) 18%, transparent); }
        .wire-input-number[data-disabled="true"] .wire-input-number__control { cursor: not-allowed; background: var(--wire-color-surface-2); }
        .wire-input-number[data-variant="compact"] .wire-input-number__control { border-radius: 999px; }
        .wire-input-number[data-size="xs"] .wire-input-number__control { min-height: 2rem; font-size: 0.75rem; }
        .wire-input-number[data-size="sm"] .wire-input-number__control { min-height: 2.25rem; font-size: 0.875rem; }
        .wire-input-number[data-size="lg"] .wire-input-number__control { min-height: 3rem; font-size: 1rem; }
        .wire-input-number[data-size="xl"] .wire-input-number__control { min-height: 3.5rem; font-size: 1.125rem; }
        .wire-input-number__content { display: flex; min-width: 0; flex: 1; align-items: center; padding-inline: 0.75rem; }
        .wire-input-number[data-size="xs"] .wire-input-number__content { padding-inline: 0.5rem; }
        .wire-input-number[data-size="sm"] .wire-input-number__content { padding-inline: 0.625rem; }
        .wire-input-number[data-size="lg"] .wire-input-number__content { padding-inline: 1rem; }
        .wire-input-number[data-size="xl"] .wire-input-number__content { padding-inline: 1.25rem; }
        .wire-input-number[data-variant="labeled"] .wire-input-number__content { flex-direction: column; align-items: stretch; justify-content: center; gap: 0.125rem; }
        .wire-input-number[data-variant="seat"] .wire-input-number__content { justify-content: space-between; gap: 0.75rem; }
        .wire-input-number__copy { min-width: 0; flex: 1; }
        .wire-input-number__inner-label, .wire-input-number__inner-description { display: block; overflow: hidden; color: var(--wire-color-text); font-size: 0.875rem; font-weight: 600; line-height: 1rem; text-overflow: ellipsis; white-space: nowrap; }
        .wire-input-number__inner-description { color: var(--wire-color-muted); font-size: 0.75rem; font-weight: 400; }
        .wire-input-number[data-variant="labeled"] .wire-input-number__inner-label { color: var(--wire-color-muted); font-size: 0.75rem; font-weight: 500; }
        .wire-input-number__field { display: flex; width: 100%; min-width: 0; align-items: center; }
        .wire-input-number[data-variant="seat"] .wire-input-number__field { width: auto; flex: none; }
        .wire-input-number__prefix, .wire-input-number__suffix { flex: none; color: var(--wire-color-muted); }
        .wire-input-number__prefix { padding-right: 0.375rem; } .wire-input-number__suffix { padding-left: 0.375rem; }
        .wire-input-number__input { width: 100%; min-width: 0; flex: 1; padding: 0; border: 0; outline: none; color: var(--wire-color-text); background: transparent; font: inherit; font-weight: 500; line-height: 1; appearance: textfield; }
        .wire-input-number__input::-webkit-inner-spin-button, .wire-input-number__input::-webkit-outer-spin-button { appearance: none; }
        .wire-input-number__input::placeholder { color: var(--wire-color-muted); }
        .wire-input-number__input:read-only { cursor: default; }
        .wire-input-number__input:disabled { cursor: not-allowed; }
        .wire-input-number:is([data-variant="horizontal"], [data-variant="compact"], [data-variant="seat"]) .wire-input-number__input { text-align: center; }
        .wire-input-number[data-variant="seat"] .wire-input-number__input { width: 2.5rem; flex: none; }
        .wire-input-number__buttons { display: flex; flex: none; border-left: 1px solid var(--wire-color-border); }
        .wire-input-number[data-variant="vertical"] .wire-input-number__buttons { flex-direction: column; }
        .wire-input-number__button { display: inline-flex; width: 2.5rem; flex: none; align-items: center; justify-content: center; padding: 0; border: 0; color: var(--wire-color-muted); background: transparent; transition: color var(--wire-motion-fast), background var(--wire-motion-fast); }
        .wire-input-number[data-size="xs"] .wire-input-number__button { width: 2rem; } .wire-input-number[data-size="sm"] .wire-input-number__button { width: 2.25rem; } .wire-input-number[data-size="lg"] .wire-input-number__button { width: 3rem; } .wire-input-number[data-size="xl"] .wire-input-number__button { width: 3.5rem; }
        .wire-input-number__button--start { border-right: 1px solid var(--wire-color-border); } .wire-input-number__button--end { border-left: 1px solid var(--wire-color-border); }
        .wire-input-number__button--decrement { border-right: 1px solid var(--wire-color-border); }
        .wire-input-number[data-variant="vertical"] .wire-input-number__button--decrement { border-right: 0; border-bottom: 1px solid var(--wire-color-border); }
        .wire-input-number__button:hover { color: var(--input-number-accent); background: var(--wire-color-surface-2); }
        .wire-input-number__button:focus-visible { z-index: 1; outline: 2px solid var(--input-number-accent); outline-offset: -2px; }
        .wire-input-number__button:disabled { cursor: not-allowed; opacity: 0.4; }
        .wire-input-number__icon { width: 1rem; height: 1rem; }
        .wire-input-number__message--error { display: flex; align-items: center; gap: 0.375rem; color: var(--wire-color-danger); }
        .wire-input-number__error-icon { width: 0.875rem; height: 0.875rem; flex: none; }
    }
}
```

---

## Kbd

Showcase: https://component.wrnexusjs.dev/
Mount: <Kbd /> (legacy: data-component="Kbd")
Category: layout
Purpose: Theme-aware, responsive kbd component.
Props: size: string = "default", color: string = "primary", label: string = "K", class: string = ""
Slots: default
Events: none

### Complete .wrn source contract

```wrn
// Kbd -- a keyboard key.
//
//   <Kbd label="Ctrl" />
//
// Deliberately small. A kbd element is a kbd element; the work here is making
// it look like a key and follow the theme.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Kbd {
  props {
    size: string = "default"
    color: string = "primary"
    label: string = "K"
    class: string = ""
  }

  view {
    <kbd {...attrs} class='wire-kbd {class}' data-size='{size}' data-color='{color}'>
      <slot>{label}</slot>
    </kbd>
  }

  style {
    .wire-kbd {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-width: 1.6rem;
      padding: 0.15rem 0.4rem;
      border: 1px solid var(--wire-color-border);
      /* The lower edge is what reads as a physical key. */
      border-bottom-width: 2px;
      border-radius: var(--wire-radius-sm);
      background: var(--wire-color-surface-soft);
      color: var(--wire-color-text);
      font-family: inherit;
      font-size: 0.78rem;
      font-weight: 600;
      line-height: 1;
    }

    .wire-kbd[data-size="sm"] {
      min-width: 1.4rem;
      padding: 0.1rem 0.3rem;
      font-size: 0.7rem;
    }

    .wire-kbd[data-size="lg"] {
      min-width: 2rem;
      padding: 0.3rem 0.55rem;
      font-size: 0.9rem;
    }
  }
}
```

---

## LayoutSplitter

Showcase: https://component.wrnexusjs.dev/
Mount: <LayoutSplitter /> (legacy: data-component="LayoutSplitter")
Category: layout
Purpose: Theme-aware, responsive layout splitter component.
Props: color: string = "primary", size: number = 50, orientation: string = "horizontal", minSize: number = 15, step: number = 5, label: string = "Resize panels", class: string = ""
Slots: start, end, default
Events: sizeChange

### Complete .wrn source contract

```wrn
// LayoutSplitter -- two panes with a divider the reader can move.
//
//   <LayoutSplitter size={40} minSize={20}>
//     <div data-slot="start">...</div>
//     <div data-slot="end">...</div>
//   </LayoutSplitter>
//
// The dragging lives in the reactive runtime behind data-wrn-splitter. Pointer
// moves fire far too often to route through a client function, and a state
// write made inside a pointermove callback is dropped, so the resolved size is
// held in the DOM as the --wrn-split custom property and these styles read it.
//
// This component previously declared resizeStart, resize and resizeEnd with no
// pointer handling whatsoever: a caller wired up @resize and received nothing,
// for ever, with no error.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component LayoutSplitter {
  outputs {
    // Named sizeChange rather than resize so it cannot be confused with the
    // native window event a caller may already be listening for. An earlier
    // comment here claimed a natively-named output could never reach a parent
    // binding; that was wrong, and the rename was never what fixed anything.
    sizeChange(payload: { size: number })
  }

  props {
    color: string = "primary"
    size: number = 50
    orientation: string = "horizontal"
    // Smallest share either pane may take, as a percentage. It also fixes the
    // upper bound at 100 - minSize, so neither pane can be dragged away to
    // nothing and left unrecoverable by pointer.
    minSize: number = 15
    step: number = 5
    label: string = "Resize panels"
    class: string = ""
  }

  functions {
    // The runtime resolves the size and announces it; this turns that into the
    // declared output so a parent @resize binding receives it.
    client function reportResize(sourceEvent) {
      var detail = sourceEvent ? sourceEvent.detail : null
      if (!detail) {
        return
      }
      output.sizeChange({ size: detail.size })
    }

    shared function isVertical() {
      return orientation === "vertical"
    }

    shared function lowerBound() {
      var value = Number(minSize)
      if (!value || value < 0) {
        return 15
      }
      return Math.min(45, value)
    }

    shared function upperBound() {
      return 100 - lowerBound()
    }

    // Sizes arrive as HTML attributes, so a value outside the bounds is
    // routine rather than exceptional. Clamp instead of rendering something
    // the reader cannot undo.
    shared function currentSize() {
      var value = Number(size)
      if (!value || value < 0) {
        return 50
      }
      return Math.min(upperBound(), Math.max(lowerBound(), value))
    }

    shared function stepSize() {
      var value = Number(step)
      return value && value > 0 ? value : 5
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="LayoutSplitter"
      class='wire-splitter {class}'
      data-color='{color}'
      data-orientation='{orientation}'
      data-wrn-splitter='{isVertical() ? "vertical" : "horizontal"}'
      data-wrn-splitter-min='{lowerBound()}'
      data-wrn-splitter-step='{stepSize()}'
      style='--wrn-split:{currentSize()}%;'
      @wrnexus:splitter:resize='reportResize(event)'
    >
      <div class="wire-splitter__pane wire-splitter__pane--start">
        <slot name="start"></slot>
      </div>

      <div
        class="wire-splitter__handle"
        data-wrn-splitter-handle="true"
        role="separator"
        tabindex="0"
        aria-label='{label}'
        aria-orientation='{isVertical() ? "horizontal" : "vertical"}'
        aria-valuenow='{currentSize()}'
        aria-valuemin='{lowerBound()}'
        aria-valuemax='{upperBound()}'
      >
        <span class="wire-splitter__grip" aria-hidden="true"></span>
      </div>

      <div class="wire-splitter__pane wire-splitter__pane--end">
        <slot name="end"></slot>
      </div>

      <slot />
    </div>
  }

  style {
    .wire-splitter {
      --splitter-accent: var(--wire-color-primary);
      display: grid;
      /* The first track follows the size the runtime resolves. */
      grid-template-columns: var(--wrn-split, 50%) auto minmax(0, 1fr);
      align-items: stretch;
      width: 100%;
      min-width: 0;
    }

    .wire-splitter[data-color="secondary"] {
      --splitter-accent: var(--wire-color-secondary);
    }

    .wire-splitter[data-color="success"] {
      --splitter-accent: var(--wire-color-success);
    }

    .wire-splitter[data-color="danger"] {
      --splitter-accent: var(--wire-color-danger);
    }

    .wire-splitter[data-color="info"] {
      --splitter-accent: var(--wire-color-info);
    }

    .wire-splitter[data-orientation="vertical"] {
      grid-template-columns: minmax(0, 1fr);
      grid-template-rows: var(--wrn-split, 50%) auto minmax(0, 1fr);
    }

    .wire-splitter__pane {
      min-width: 0;
      min-height: 0;
      overflow: auto;
    }

    .wire-splitter__handle {
      display: flex;
      align-items: center;
      justify-content: center;
      flex: 0 0 auto;
      padding: 0 0.25rem;
      border: 0;
      background: transparent;
      cursor: col-resize;
      /* Without this the pointer drag selects the text in both panes. */
      touch-action: none;
      user-select: none;
    }

    .wire-splitter[data-orientation="vertical"] .wire-splitter__handle {
      padding: 0.25rem 0;
      cursor: row-resize;
    }

    .wire-splitter__grip {
      display: block;
      width: 2px;
      height: 100%;
      min-height: 1.5rem;
      border-radius: 999px;
      background: var(--wire-color-border);
      transition: background 140ms ease;
    }

    .wire-splitter[data-orientation="vertical"] .wire-splitter__grip {
      width: 100%;
      min-width: 1.5rem;
      height: 2px;
    }

    .wire-splitter__handle:hover .wire-splitter__grip,
    .wire-splitter[data-wrn-splitter-dragging="true"] .wire-splitter__grip {
      background: var(--splitter-accent);
    }

    .wire-splitter__handle:focus-visible {
      outline: 2px solid var(--splitter-accent);
      outline-offset: -2px;
      border-radius: var(--wire-radius-sm);
    }

    /*
     * Two panes side by side stop making sense on a phone. Stacking them keeps
     * both readable, and the divider stops being draggable because the grid
     * no longer has a second track to trade against.
     */
    @media (max-width: 639px) {
      .wire-splitter,
      .wire-splitter[data-orientation="vertical"] {
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: auto auto auto;
      }

      .wire-splitter__handle {
        cursor: default;
      }
    }
  }
}
```

---

## LegendIndicator

Showcase: https://component.wrnexusjs.dev/
Mount: <LegendIndicator /> (legacy: data-component="LegendIndicator")
Category: base
Purpose: Theme-aware, responsive legend indicator component.
Props: size: string = "default", color: string = "primary", title: string = "Legend Indicator", description: string = "", items: unknown[] = [], variant: string = "default", class: string = ""
Slots: default
Events: toggle, select

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component LegendIndicator {
  outputs {
    toggle(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
  }

  props {
    size: string = "default"
    color: string = "primary"
    title: string = "Legend Indicator"
    description: string = ""
    items: unknown[] = []
    variant: string = "default"
    class: string = ""
  }
  view {
    <section class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--legend-indicator wire-next--variant-{variant} {class}">
      {#if title}<strong>{title}</strong>{/if}
      {#if description}<p>{description}</p>{/if}
      {#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
      <slot />
    </section>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--legend-indicator {
      display: grid;
      gap: 0.75rem;
      padding: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius);
      color: var(--wire-color-text);
      background: var(--wire-color-surface);
    }
    .wire-next--legend-indicator > p { margin: 0; color: var(--wire-color-muted); }
    .wire-next--legend-indicator > .wire-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
    .wire-next--legend-indicator > .wire-next__items > span { padding: 0.35rem 0.6rem; border-radius: var(--wire-radius-sm); background: var(--wire-color-surface-2); }
  }
}
```

---

## Link

Showcase: https://component.wrnexusjs.dev/
Mount: <Link /> (legacy: data-component="Link")
Category: layout
Purpose: Render an accessible internal or external link with target, relation, size, color, and public focus or click events.
Props: size: string = "default", color: string = "primary", label: string = "Link", href: string = "#", target: string = "", rel: string = "", underline: string = "hover", external: boolean = false, class: string = ""
Slots: default
Events: none

### Complete .wrn source contract

```wrn
// Link -- a text link that follows the theme.
//
//   <Link href="/docs" label="Documentation" />
//   <Link href="https://example.com" label="Spec" external={true} />
//
// external adds rel="noopener noreferrer" and an outbound icon, so the reader
// is told the link leaves the site before they follow it.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Link {
  props {
    size: string = "default"
    color: string = "primary"
    label: string = "Link"
    href: string = "#"
    target: string = ""
    rel: string = ""
    underline: string = "hover"
    external: boolean = false
    class: string = ""
  }

  view {
    <a
      {...attrs}
      data-ui-component="Link"
      class='wire-link {class}'
      href='{href}'
      target='{target}'
      rel='{external ? (rel || "noopener noreferrer") : rel}'
      data-size='{size}'
      data-color='{color}'
      data-underline='{underline}'
      data-external='{external}'
    >
      <span class="wire-link__label">{label}</span>
      <span
        class="icon-[lucide--external-link] wire-link__icon"
        data-show="external"
        aria-hidden="true"
      ></span>
      <slot></slot>
    </a>
  }

  style {
    .wire-link {
      --link-tone: var(--wire-color-primary);
      --link-tone-hover: var(--wire-color-primary-hover);
      display: inline-flex;
      align-items: center;
      gap: 0.35rem;
      min-width: 0;
      border-radius: var(--wire-radius-sm);
      color: var(--link-tone);
      font-size: 0.875rem;
      font-weight: 600;
      text-decoration: none;
      text-underline-offset: 4px;
      transition: color 140ms ease;
    }

    .wire-link[data-color="secondary"] {
      --link-tone: var(--wire-color-secondary);
      --link-tone-hover: var(--wire-color-secondary-hover);
    }

    .wire-link[data-color="success"] {
      --link-tone: var(--wire-color-success);
      --link-tone-hover: var(--wire-color-success);
    }

    .wire-link[data-color="warning"] {
      --link-tone: var(--wire-color-warning-text);
      --link-tone-hover: var(--wire-color-warning-text);
    }

    .wire-link[data-color="danger"] {
      --link-tone: var(--wire-color-danger);
      --link-tone-hover: var(--wire-color-danger);
    }

    .wire-link[data-color="info"] {
      --link-tone: var(--wire-color-info);
      --link-tone-hover: var(--wire-color-info);
    }

    .wire-link[data-color="neutral"] {
      --link-tone: var(--wire-color-text);
      --link-tone-hover: var(--wire-color-text);
    }

    .wire-link[data-size="xs"] {
      font-size: 0.75rem;
    }

    .wire-link[data-size="md"] {
      font-size: 1rem;
    }

    .wire-link[data-size="lg"] {
      font-size: 1.125rem;
    }

    .wire-link[data-underline="always"] {
      text-decoration: underline;
    }

    .wire-link[data-underline="hover"]:hover {
      text-decoration: underline;
    }

    .wire-link:hover {
      color: var(--link-tone-hover);
    }

    .wire-link:focus-visible {
      outline: 2px solid var(--wire-color-focus);
      outline-offset: 2px;
    }

    /* The label truncates; the outbound icon must not shrink with it. */
    .wire-link__label {
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .wire-link__icon {
      flex: 0 0 auto;
      width: 0.875rem;
      height: 0.875rem;
    }
  }
}
```

---

## List

Showcase: https://component.wrnexusjs.dev/
Mount: <List /> (legacy: data-component="List")
Category: base
Purpose: Present structured responsive linked or status items with icons, descriptions, actions, and selection events.
Props: size: string = "default", color: string = "primary", title: string = "List", description: string = "", items: unknown[] = [], variant: string = "default", class: string = ""
Slots: default
Events: select

### Complete .wrn source contract

```wrn
component List {
  outputs {
    select(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
    size: string = "default"
    color: string = "primary"
    title: string = "List"
    description: string = ""
    items: unknown[] = []
    variant: string = "default"
    class: string = ""
  }

  view {
    <section
      data-ui-component="List"
      aria-label='{title}'
      class='wire-list {class}'
    >
      {#if title || description}
        <header class="wire-list__header">
          {#if title}
            <h3 class="wire-list__title">{title}</h3>
          {/if}
          {#if description}
            <p class="wire-list__description">{description}</p>
          {/if}
        </header>
      {/if}

      <ul
        class="wire-list__items"
        data-variant='{variant}'
      >
        {#each items as item, index}
          <li
            class="wire-list__item"
            data-variant='{variant}'
            @click='if (!item.href) { output.select({ item: item, index: index }) }'
          >
            {#if item.href}
              <a
                href='{item.href}'
                class="wire-list__row"
                data-size='{size}'
                @click='output.select({ item: item, index: index })'
              >
                {#if item.icon}
                  <span class="wire-list__leading-icon"><span class='{item.icon}' aria-hidden="true"></span>
                  </span>
                {/if}
                {#if item.imageSrc}
                  <img src='{item.imageSrc}' alt='{item.imageAlt || ""}' class="wire-list__image" loading="lazy" />
                {/if}
                <div class="wire-list__body">
                  <div class="wire-list__heading">
                    <p class="wire-list__item-title">{item.title || item.label}</p>
                    {#if item.meta}
                      <span class="wire-list__meta">{item.meta}</span>
                    {/if}
                  </div>
                  {#if item.description}
                    <p class="wire-list__item-description">{item.description}</p>
                  {/if}
                  {#if item.badge}
                    <span class="wire-list__badge">{item.badge}</span>
                  {/if}
                </div>
                <span class="icon-[lucide--chevron-right] wire-list__chevron" aria-hidden="true"></span>
              </a>
            {:else}
              <div class="wire-list__row" data-size='{size}'>
                {#if item.icon}
                  <span class="wire-list__leading-icon"><span class='{item.icon}' aria-hidden="true"></span>
                  </span>
                {/if}
                <div class="wire-list__body">
                  <p class="wire-list__item-title">{item.title || item.label}</p>
                  {#if item.description}
                    <p class="wire-list__item-description">{item.description}</p>
                  {/if}
                </div>
              </div>
            {/if}
          </li>
        {:empty}
          <li class="wire-list__empty">
            No items available.
          </li>
        {/each}
      </ul>

      <slot></slot>
    </section>
  }

  style {
    .wire-list { width: 100%; }
    .wire-list__header { margin-bottom: 1rem; }
    .wire-list__title { color: var(--wire-color-text); font-size: 1.125rem; font-weight: 700; }
    .wire-list__description { margin-top: 0.25rem; color: var(--wire-color-text-muted); font-size: 0.875rem; line-height: 1.5rem; }
    .wire-list__items { overflow: hidden; }
    .wire-list__items:is([data-variant="default"], [data-variant="divided"]) > .wire-list__item + .wire-list__item { border-top: 1px solid var(--wire-color-border); }
    .wire-list__items[data-variant="card"] { border: 1px solid var(--wire-color-border); border-radius: calc(var(--wire-radius) * 1.5); background: var(--wire-color-surface-raised); }
    .wire-list__items[data-variant="separated"] { display: grid; gap: 0.75rem; }
    .wire-list__item { min-width: 0; }
    .wire-list__item[data-variant="separated"] { border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius); background: var(--wire-color-surface-raised); }
    .wire-list__row { display: flex; min-width: 0; align-items: flex-start; gap: 0.75rem; padding: 0.75rem 1rem; outline: none; transition: background var(--wire-motion-base); }
    a.wire-list__row:hover { background: var(--wire-color-surface-soft); }
    .wire-list__row:focus-visible { box-shadow: inset 0 0 0 2px var(--wire-color-focus); }
    .wire-list__row[data-size="sm"] { padding: 0.625rem 0.75rem; }
    .wire-list__row[data-size="lg"] { padding: 1rem 1.25rem; }
    .wire-list__leading-icon { display: flex; width: 2.5rem; height: 2.5rem; flex: none; align-items: center; justify-content: center; border-radius: var(--wire-radius); color: var(--wire-color-primary); background: var(--wire-color-primary-soft); }
    .wire-list__leading-icon > span { width: 1.25rem; height: 1.25rem; }
    .wire-list__image { width: 3rem; height: 3rem; flex: none; border-radius: var(--wire-radius); object-fit: cover; }
    .wire-list__body { min-width: 0; flex: 1; }
    .wire-list__heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.75rem; }
    .wire-list__item-title { overflow: hidden; color: var(--wire-color-text); font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
    .wire-list__meta { flex: none; color: var(--wire-color-text-muted); font-size: 0.75rem; }
    .wire-list__item-description { display: -webkit-box; margin-top: 0.25rem; overflow: hidden; color: var(--wire-color-text-muted); font-size: 0.875rem; line-height: 1.25rem; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
    .wire-list__badge { display: inline-flex; margin-top: 0.5rem; padding: 0.25rem 0.625rem; border-radius: 999px; color: var(--wire-color-primary); background: var(--wire-color-primary-soft); font-size: 0.75rem; font-weight: 600; }
    .wire-list__chevron { width: 1rem; height: 1rem; flex: none; margin-top: 0.25rem; color: var(--wire-color-text-muted); transition: color var(--wire-motion-base), transform var(--wire-motion-base); }
    .wire-list__row:hover .wire-list__chevron { color: var(--wire-color-primary); transform: translateX(0.125rem); }
    .wire-list__empty { padding: 1.5rem; border: 1px dashed var(--wire-color-border); border-radius: var(--wire-radius); color: var(--wire-color-text-muted); font-size: 0.875rem; text-align: center; }
  }
}
```

---

## ListGroup

Showcase: https://component.wrnexusjs.dev/
Mount: <ListGroup /> (legacy: data-component="ListGroup")
Category: base
Purpose: Theme-aware, responsive list group component.
Props: size: string = "default", color: string = "primary", title: string = "List Group", description: string = "", items: unknown[] = [], variant: string = "default", class: string = ""
Slots: default
Events: select, change

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component ListGroup {
  outputs {
    select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
  }

  props {
    size: string = "default"
    color: string = "primary"
    title: string = "List Group"
    description: string = ""
    items: unknown[] = []
    variant: string = "default"
    class: string = ""
  }
  view {
    <section class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--list-group wire-next--variant-{variant} {class}">
      {#if title}<strong>{title}</strong>{/if}
      {#if description}<p>{description}</p>{/if}
      {#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
      <slot />
    </section>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--list-group {
      display: grid;
      gap: 0.75rem;
      padding: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius);
      color: var(--wire-color-text);
      background: var(--wire-color-surface);
    }
    .wire-next--list-group > p { margin: 0; color: var(--wire-color-muted); }
    .wire-next--list-group > .wire-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
    .wire-next--list-group > .wire-next__items > span { padding: 0.35rem 0.6rem; border-radius: var(--wire-radius-sm); background: var(--wire-color-surface-2); }
  }
}
```

---

## Map

Showcase: https://component.wrnexusjs.dev/
Mount: <Map /> (legacy: data-component="Map")
Category: integrations
Purpose: Present responsive location information and markers with map-ready metadata and movement or marker events.
Props: size: string = "default", color: string = "primary", title: string = "Map", description: string = "", items: unknown[] = [], variant: string = "default", class: string = ""
Slots: default
Events: markerClick, select, zoom

### Complete .wrn source contract

```wrn
component Map {
  outputs {
    // markerClick and select both fire for a marker press; select is the
    // generic name callers reach for, markerClick the explicit one.
    markerClick(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    select(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    zoom(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
    size: string = "default"
    color: string = "primary"
    title: string = "Map"
    description: string = ""
    items: unknown[] = []
    variant: string = "default"
    class: string = ""
  }

  view {
    <section
      data-ui-component="Map"
      aria-label='{title}'
      class='wire-map {class}'
    >
      {#if title || description}
        <header class="wire-map__header">
          <div>
            {#if title}
              <h3 class="wire-map__title">{title}</h3>
            {/if}
            {#if description}
              <p class="wire-map__description">{description}</p>
            {/if}
          </div>
          <span class="wire-map__header-icon"><span class="icon-[lucide--map]" aria-hidden="true"></span>
          </span>
        </header>
      {/if}

      <div
        class="wire-map__viewport"
        data-size='{size}'
      >
        <div
          class="wire-map__grid"
          style="background-image: linear-gradient(var(--wire-color-border) 1px, transparent 1px), linear-gradient(90deg, var(--wire-color-border) 1px, transparent 1px); background-size: 32px 32px;"
          aria-hidden="true"
        ></div>

        <div class="wire-map__content">
          <slot></slot>
        </div>

        {#if items.length > 0}
          {#each items as item, index}
            <button
              type="button"
              aria-label='{item.label || item.title || "Map marker"}'
              class="wire-map__marker"
              style='left: {item.x || (20 + index * 12)}%; top: {item.y || (30 + (index % 3) * 18)}%;'
              @click='output.markerClick(item); output.select(item)'
            >
              <span class='{item.icon || "icon-[lucide--map-pin]"}' aria-hidden="true"></span>
            </button>
          {/each}
        {/if}

        <div class="wire-map__controls">
          <button
            type="button"
            aria-label="Zoom in"
            class="wire-map__control"
            @click='output.zoom({ direction: "in" })'
          >
            <span class="icon-[lucide--plus] wire-map__control-icon" aria-hidden="true"></span>
          </button>
          <button
            type="button"
            aria-label="Zoom out"
            class="wire-map__control"
            @click='output.zoom({ direction: "out" })'
          >
            <span class="icon-[lucide--minus] wire-map__control-icon" aria-hidden="true"></span>
          </button>
        </div>
      </div>
    </section>
  }

  style {
    .wire-map { overflow: hidden; border: 1px solid var(--wire-color-border); border-radius: calc(var(--wire-radius) * 1.5); background: var(--wire-color-surface-raised); box-shadow: var(--wire-shadow-1); }
    .wire-map__header { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; padding: 1.25rem; border-bottom: 1px solid var(--wire-color-border); }
    .wire-map__title { color: var(--wire-color-text); font-size: 1.125rem; font-weight: 700; }
    .wire-map__description { margin-top: 0.25rem; color: var(--wire-color-text-muted); font-size: 0.875rem; line-height: 1.5rem; }
    .wire-map__header-icon { display: flex; width: 2.5rem; height: 2.5rem; flex: none; align-items: center; justify-content: center; border-radius: var(--wire-radius); color: var(--wire-color-primary); background: var(--wire-color-primary-soft); }
    .wire-map__header-icon > span { width: 1.25rem; height: 1.25rem; }
    .wire-map__viewport { position: relative; isolation: isolate; height: 20rem; overflow: hidden; background: var(--wire-color-surface-soft); }
    .wire-map__viewport[data-size="sm"] { height: 16rem; }
    .wire-map__viewport[data-size="lg"] { height: 28rem; }
    .wire-map__grid { position: absolute; inset: 0; opacity: 0.5; pointer-events: none; }
    .wire-map__content { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 1.5rem; }
    .wire-map__marker { position: absolute; display: inline-flex; width: 2.5rem; height: 2.5rem; align-items: center; justify-content: center; padding: 0; border: 4px solid var(--wire-color-surface-raised); border-radius: 50%; color: var(--wire-color-on-primary); background: var(--wire-color-primary); box-shadow: var(--wire-shadow-3); transition: transform var(--wire-motion-base) var(--wire-ease-standard); }
    .wire-map__marker:hover { transform: scale(1.1); }
    .wire-map__controls { position: absolute; right: 1rem; bottom: 1rem; display: flex; flex-direction: column; gap: 0.5rem; }
    .wire-map__control { display: inline-flex; width: 2.5rem; height: 2.5rem; align-items: center; justify-content: center; padding: 0; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius); color: var(--wire-color-text); background: var(--wire-color-surface-raised); box-shadow: var(--wire-shadow-1); transition: background var(--wire-motion-base); }
    .wire-map__control:hover { background: var(--wire-color-surface-soft); }
    .wire-map__marker:focus-visible, .wire-map__control:focus-visible { outline: 2px solid var(--wire-color-focus); outline-offset: 2px; }
    .wire-map__control-icon { width: 1rem; height: 1rem; }
  }
}
```

---

## MarketingSectionHeader

Showcase: https://component.wrnexusjs.dev/
Mount: <MarketingSectionHeader /> (legacy: data-component="MarketingSectionHeader")
Category: marketing
Purpose: Introduce marketing content with an eyebrow, title, description, and optional linked action.
Props: id: string = "", eyebrow: string = "", title: string = "", description: string = "", align: string = "split", size: string = "default", color: string = "primary", actionLabel: string = "", actionHref: string = "", actionIcon: string = "", actionExternal: boolean = false, class: string = ""
Slots: icon, actions, default
Events: none

### Complete .wrn source contract

```wrn
import SectionHeader from "./SectionHeader.wrn"
import TextLink from "./TextLink.wrn"

component MarketingSectionHeader {
  props {
    id: string = ""
    eyebrow: string = ""
    title: string = ""
    description: string = ""
    align: string = "split"
    size: string = "default"
    color: string = "primary"
    actionLabel: string = ""
    actionHref: string = ""
    actionIcon: string = ""
    actionExternal: boolean = false
    class: string = ""
  }

  view {
    <div
      {...attrs}
      data-ui-component="MarketingSectionHeader"
      data-size='{size}'
      data-color='{color}'
      class='wire-marketing-section-header {class}'
    >
      <SectionHeader
        id='{id}'
        eyebrow='{eyebrow}'
        title='{title}'
        description='{description}'
        align='{align}'
        size='{size}'
        color='{color}'
        headingLevel="2"
        maxWidth="3xl"
      >
        <div data-slot="icon">
          <slot name="icon"></slot>
        </div>

        <div data-slot="actions">
          {#if actionLabel}
            <TextLink
              label='{actionLabel}'
              href='{actionHref}'
              external='{actionExternal}'
              icon='{actionIcon}'
              iconPosition="start"
              showArrow="true"
              color='{color}'
            />
          {/if}
          <slot name="actions"></slot>
        </div>

        <slot></slot>
      </SectionHeader>
    </div>
  }

  style {
    .wire-marketing-section-header { width: 100%; min-width: 0; }
  }
}
```

---

## Marquee

Showcase: https://component.wrnexusjs.dev/
Mount: <Marquee /> (legacy: data-component="Marquee")
Category: base
Purpose: Continuously present responsive labels, partners, notices, or capabilities with pause and resume behavior.
Props: size: string = "default", color: string = "primary", title: string = "Marquee", description: string = "", items: unknown[] = [], variant: string = "default", class: string = ""
Slots: default
Events: pause, resume

### Complete .wrn source contract

```wrn
component Marquee {
  outputs {
    // Fired when the reader pauses the scroll, by hover, focus or the button.
    pause(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    resume(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
    size: string = "default"
    color: string = "primary"
    title: string = "Marquee"
    description: string = ""
    items: unknown[] = []
    variant: string = "default"
    class: string = ""
  }

  state paused: boolean = false

  view {
    <section
      data-ui-component="Marquee"
      aria-label='{title}'
      class='wire-marquee {class}'
    >
      <div class="wire-marquee__layout">
        {#if title}
          <div class="wire-marquee__title">
            <span class="icon-[lucide--megaphone] wire-marquee__icon" aria-hidden="true"></span>
            <span>{title}</span>
          </div>
        {/if}

        <div
          class="wire-marquee__window"
          @mouseenter='paused = true; output.pause({})'
          @mouseleave='paused = false; output.resume({})'
          @focusin='paused = true'
          @focusout='paused = false'
        >
          <div
            class="wire-marquee__track"
            class:wire-marquee-paused='paused'
          >
            {#each items as item}
              <a
                href='{item.href || "#"}'
                class="wire-marquee__item"
              >
                <span
                  class="wire-marquee__dot"
                  data-color='{item.color || "primary"}'
                  aria-hidden="true"
                ></span>
                <span>{item.label || item.title}</span>
                {#if item.meta}
                  <span class="wire-marquee__meta">{item.meta}</span>
                {/if}
              </a>
            {:empty}
              <p class="wire-marquee__empty">{description || "No announcements available."}</p>
            {/each}

            {#if items.length > 0}
              {#each items as item}
                <a
                  href='{item.href || "#"}'
                  aria-hidden="true"
                  tabindex="-1"
                  class="wire-marquee__item"
                >
                  <span class="wire-marquee__dot" aria-hidden="true"></span>
                  <span>{item.label || item.title}</span>
                </a>
              {/each}
            {/if}
          </div>
        </div>

        <button
          type="button"
          aria-label='{paused ? "Resume announcements" : "Pause announcements"}'
          class="wire-marquee__toggle"
          @click='paused = !paused; paused ? output.pause({}) : output.resume({})'
        >
          <span class="icon-[lucide--pause] wire-marquee__icon" data-show='!paused' aria-hidden="true"></span>
          <span class="icon-[lucide--play] wire-marquee__icon" data-show='paused' aria-hidden="true"></span>
        </button>
      </div>

      <slot></slot>
    </section>
  }

  style {
    .wire-marquee { overflow: hidden; border-block: 1px solid var(--wire-color-border); background: var(--wire-color-surface-raised); }
    .wire-marquee__layout { display: flex; align-items: stretch; }
    .wire-marquee__title { position: relative; z-index: 1; display: flex; flex: none; align-items: center; gap: 0.5rem; padding: 0.75rem 1rem; border-right: 1px solid var(--wire-color-border); color: var(--wire-color-on-primary); background: var(--wire-color-primary); font-size: 0.875rem; font-weight: 700; }
    .wire-marquee__window { position: relative; min-width: 0; flex: 1; overflow: hidden; }
    .wire-marquee__track {
      display: flex; width: max-content; min-width: 100%; align-items: center;
      animation: wire-marquee 32s linear infinite;
    }
    .wire-marquee-paused { animation-play-state: paused; }
    .wire-marquee__item { display: flex; flex: none; align-items: center; gap: 0.75rem; padding: 0.75rem 1.25rem; color: var(--wire-color-text); font-size: 0.875rem; font-weight: 500; transition: color var(--wire-motion-base), background var(--wire-motion-base); }
    .wire-marquee__item:hover { color: var(--wire-color-primary); background: var(--wire-color-primary-soft); }
    .wire-marquee__item:focus-visible, .wire-marquee__toggle:focus-visible { outline: 2px solid var(--wire-color-focus); outline-offset: -2px; }
    .wire-marquee__dot { width: 0.5rem; height: 0.5rem; flex: none; border-radius: 50%; background: var(--wire-color-primary); }
    .wire-marquee__dot[data-color="danger"] { background: var(--wire-color-danger); }
    .wire-marquee__dot[data-color="warning"] { background: var(--wire-color-warning); }
    .wire-marquee__dot[data-color="success"] { background: var(--wire-color-success); }
    .wire-marquee__meta { color: var(--wire-color-text-muted); font-size: 0.75rem; }
    .wire-marquee__empty { padding: 0.75rem 1.25rem; color: var(--wire-color-text-muted); font-size: 0.875rem; }
    .wire-marquee__toggle { display: flex; flex: none; align-items: center; justify-content: center; padding-inline: 1rem; border: 0; border-left: 1px solid var(--wire-color-border); color: var(--wire-color-text-muted); background: transparent; transition: color var(--wire-motion-base), background var(--wire-motion-base); }
    .wire-marquee__toggle:hover { color: var(--wire-color-text); background: var(--wire-color-surface-soft); }
    .wire-marquee__icon { width: 1rem; height: 1rem; }
    @media (min-width: 40rem) { .wire-marquee__title { padding-inline: 1.25rem; } }

    .wire-marquee-paused {
      animation-play-state: paused;
    }

    @keyframes wire-marquee {
      from { transform: translateX(0); }
      to { transform: translateX(-50%); }
    }

    @media (prefers-reduced-motion: reduce) {
      .wire-marquee__track {
        animation: none;
      }
    }
  }
}
```

---

## MegaMenu

Showcase: https://component.wrnexusjs.dev/
Mount: <MegaMenu /> (legacy: data-component="MegaMenu")
Category: navigation
Purpose: Theme-aware, responsive mega menu component.
Props: color: string = "primary", size: string = "default", label: string = "Menu", icon: string = "", columns: unknown[] = [], footer: string = "", defaultOpen: boolean = false, class: string = ""
Slots: default
Events: open, close, select

### Complete .wrn source contract

```wrn
// MegaMenu -- a trigger and a wide panel of grouped links.
//
//   <MegaMenu label="Products" columns='[{"heading":"Platform",
//     "items":[{"label":"Runtime","href":"/runtime"}]}]' />
//
// Deliberately one level deep. A mega menu exists to show breadth flat, so
// everything is one click away; nesting inside the panel buries content behind
// hover-within-hover and is close to unusable by keyboard and touch. Use Nav
// when you actually want cascading submenus.
//
// The panel carries data-wrn-anchored so the runtime clamp keeps it inside the
// viewport instead of hanging off the edge of a wide layout.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component MegaMenu {
  outputs {
    open(payload: { sourceEvent: Event })
    close(payload: { reason: string })
    select(payload: { item: object; value: string })
  }

  props {
    color: string = "primary"
    size: string = "default"
    label: string = "Menu"
    icon: string = ""
    columns: unknown[] = []
    footer: string = ""
    defaultOpen: boolean = false
    class: string = ""
  }

  state visible = defaultOpen

  functions {
    shared function columnList() {
      return Array.isArray(columns) ? columns : []
    }

    shared function itemsOf(column) {
      return column && Array.isArray(column.items) ? column.items : []
    }

    client function showPanel(sourceEvent) {
      visible = true
      output.open({ sourceEvent: sourceEvent })
    }

    client function hidePanel(reason) {
      visible = false
      output.close({ reason: reason })
    }

    client function togglePanel(sourceEvent) {
      if (visible) {
        hidePanel("toggle")
      } else {
        showPanel(sourceEvent)
      }
    }

    client function handleKeydown(sourceEvent) {
      if (sourceEvent.key === "Escape" && visible) {
        sourceEvent.preventDefault()
        hidePanel("escape")
      }
    }

    client function choose(item) {
      if (item.disabled) {
        return
      }
      output.select({ item: item, value: item.value || item.label || "" })
      hidePanel("select")
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="MegaMenu"
      class='wire-mega {class}'
      data-color='{color}'
      data-size='{size}'
      data-open='{visible}'
      @keydown='handleKeydown(event)'
      @mouseleave='hidePanel("pointer-leave")'
    >
      <button
        type="button"
        class="wire-mega__trigger"
        aria-haspopup="true"
        aria-expanded='{visible}'
        @click='togglePanel(event)'
        @mouseenter='showPanel(event)'
        @focus='showPanel(event)'
      >
        <span class='wire-mega__trigger-icon {icon}' data-show="icon" aria-hidden="true"></span>
        <span class="wire-mega__trigger-label">{label}</span>
        <span class="wire-mega__arrow" aria-hidden="true">&#8250;</span>
      </button>

      <div
        class="wire-mega__panel"
        data-wrn-anchored="true"
        data-show="visible"
        data-wrn-roving="both"
      >
        <div class="wire-mega__columns">
          {#each columnList() as column}
            <section class="wire-mega__column">
              <h3 class="wire-mega__heading" data-show="column.heading">{column.heading}</h3>
              <ul class="wire-mega__list">
                {#each itemsOf(column) as item}
                  <li>
                    <a
                      class="wire-mega__link"
                      href='{item.href || "#"}'
                      data-wrn-roving-item="true"
                      aria-disabled='{item.disabled ? "true" : "false"}'
                      @click='choose(item)'
                    >
                      <span
                        class='wire-mega__link-icon {item.icon}'
                        data-show="item.icon"
                        aria-hidden="true"
                      ></span>
                      <span class="wire-mega__link-body">
                        <span class="wire-mega__link-label">{item.label}</span>
                        <span class="wire-mega__link-description" data-show="item.description">
                          {item.description}
                        </span>
                      </span>
                    </a>
                  </li>
                {/each}
              </ul>
            </section>
          {/each}
        </div>

        <p class="wire-mega__footer" data-show="footer">{footer}</p>
        <slot />
      </div>
    </div>
  }

  style {
    .wire-mega {
      --mega-accent: var(--wire-color-primary);
      position: relative;
      display: inline-block;
      max-width: 100%;
    }

    .wire-mega[data-color="secondary"] {
      --mega-accent: var(--wire-color-secondary);
    }

    .wire-mega[data-color="success"] {
      --mega-accent: var(--wire-color-success);
    }

    .wire-mega[data-color="danger"] {
      --mega-accent: var(--wire-color-danger);
    }

    .wire-mega[data-color="info"] {
      --mega-accent: var(--wire-color-info);
    }

    .wire-mega[data-size="sm"] {
      font-size: 0.82rem;
    }

    .wire-mega[data-size="lg"] {
      font-size: 1rem;
    }

    .wire-mega__trigger {
      appearance: none;
      display: inline-flex;
      align-items: center;
      gap: 0.4rem;
      padding: 0.45rem 0.7rem;
      border: 1px solid transparent;
      border-radius: var(--wire-radius-sm);
      background: transparent;
      color: var(--wire-color-text-muted);
      font: inherit;
      font-size: 0.9rem;
      font-weight: 600;
      cursor: pointer;
    }

    .wire-mega__trigger:hover,
    .wire-mega[data-open="true"] .wire-mega__trigger {
      background: var(--wire-color-surface-soft);
      color: var(--wire-color-text);
    }

    .wire-mega__trigger:focus-visible {
      outline: 2px solid var(--mega-accent);
      outline-offset: 2px;
    }

    .wire-mega__arrow {
      display: inline-block;
      transition: transform 160ms ease;
    }

    .wire-mega[data-open="true"] .wire-mega__arrow {
      transform: rotate(90deg);
    }

    /*
     * The panel is offset below the trigger for looks, which used to close the
     * menu on the way to it: that offset is dead space belonging to neither
     * element, so crossing it fired mouseleave on the root. The bridge below
     * covers the gap with a descendant, so the pointer never actually leaves.
     */
    .wire-mega__panel::before {
      content: "";
      position: absolute;
      left: 0;
      right: 0;
      bottom: 100%;
      height: 0.6rem;
    }

    .wire-mega__panel {
      position: absolute;
      z-index: 40;
      top: calc(100% + 0.4rem);
      left: 0;
      width: max-content;
      max-width: min(60rem, calc(100vw - 2rem));
      padding: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-lg);
      background: var(--wire-color-surface);
      box-shadow: var(--wire-shadow-1);
    }

    .wire-mega__columns {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
      gap: 1.25rem;
    }

    .wire-mega__column {
      min-width: 0;
    }

    .wire-mega__heading {
      margin: 0 0 0.5rem;
      color: var(--wire-color-text-muted);
      font-size: 0.72rem;
      font-weight: 700;
      letter-spacing: 0.08em;
      text-transform: uppercase;
    }

    .wire-mega__list {
      display: grid;
      gap: 0.15rem;
      margin: 0;
      padding: 0;
      list-style: none;
    }

    .wire-mega__link {
      display: flex;
      align-items: flex-start;
      gap: 0.5rem;
      padding: 0.45rem 0.5rem;
      border-radius: var(--wire-radius-sm);
      color: var(--wire-color-text);
      text-decoration: none;
    }

    .wire-mega__link:hover {
      background: var(--wire-color-surface-soft);
    }

    .wire-mega__link:focus-visible {
      outline: 2px solid var(--mega-accent);
      outline-offset: 2px;
    }

    .wire-mega__link[aria-disabled="true"] {
      opacity: 0.5;
      pointer-events: none;
    }

    .wire-mega__link-body {
      display: flex;
      flex-direction: column;
      min-width: 0;
    }

    .wire-mega__link-label {
      font-size: 0.88rem;
      font-weight: 600;
    }

    .wire-mega__link-description {
      color: var(--wire-color-text-muted);
      font-size: 0.78rem;
      line-height: 1.5;
    }

    .wire-mega__footer {
      margin: 1rem 0 0;
      padding-top: 0.75rem;
      border-top: 1px solid var(--wire-color-border);
      color: var(--wire-color-text-muted);
      font-size: 0.8rem;
    }

    /*
     * On a phone the panel stops floating and becomes part of the flow. A
     * pointer-opened overlay pinned to a trigger is unreachable on touch, and
     * a 60rem grid has nowhere to go on a 360px screen.
     */
    @media (max-width: 767px) {
      .wire-mega {
        display: block;
      }

      .wire-mega__panel {
        position: static;
        width: auto;
        max-width: 100%;
        box-shadow: none;
      }

      .wire-mega__columns {
        grid-template-columns: 1fr;
      }
    }
  }
}
```

---

## MetricCard

Showcase: https://component.wrnexusjs.dev/
Mount: <MetricCard /> (legacy: data-component="MetricCard")
Category: data
Purpose: Display one operational metric with value, suffix, description, icon, trend, progress, and optional action.
Props: label: string = "Metric", value: string = "0", description: string = "", icon: string = "", iconStyle: string = "soft", prefix: string = "", suffix: string = "", badge: string = "", trend: string = "", trendLabel: string = "", trendDirection: string = "neutral", progress: number = -1, progressLabel: string = "", href: string = "", target: string = "", rel: string = "", external: boolean = false, actionLabel: string = "", actionIcon: string = "", showArrow: boolean = true, selectable: boolean = false, disabled: boolean = false, size: string = "default", color: string = "primary", variant: string = "default", hover: string = "lift", align: string = "left", class: string = ""
Slots: default, footer
Events: select, action

### Complete .wrn source contract

```wrn
component MetricCard {
  outputs {
    select(payload: { label: string; value: string; href: string; sourceEvent: Event })
    action(payload: { label: string; value: string; href: string; sourceEvent: Event })
  }

  props {

    label: string = "Metric"
    value: string = "0"
    description: string = ""
    icon: string = ""
    iconStyle: string = "soft"
    prefix: string = ""
    suffix: string = ""
    badge: string = ""
    trend: string = ""
    trendLabel: string = ""
    trendDirection: string = "neutral"
    progress: number = -1
    progressLabel: string = ""
    href: string = ""
    target: string = ""
    rel: string = ""
    external: boolean = false
    actionLabel: string = ""
    actionIcon: string = ""
    showArrow: boolean = true
    selectable: boolean = false
    disabled: boolean = false
    size: string = "default"
    color: string = "primary"
    variant: string = "default"
    hover: string = "lift"
    align: string = "left"
    class: string = ""
  }

  functions {
    client function selectCard(sourceEvent) {
      if (disabled || !selectable) {
        return
      }
      if (sourceEvent.target.closest(".wire-metric-card__action")) {
        return
      }
      output.select({
        label: label,
        value: value,
        href: href,
        sourceEvent: sourceEvent
      })
    }

    client function activateAction(sourceEvent) {
      if (disabled) {
        sourceEvent.preventDefault()
        return
      }
      output.action({
        label: label,
        value: value,
        href: href,
        sourceEvent: sourceEvent
      })
    }
  }

  view {
    <article
      {...attrs}
      data-ui-component="MetricCard"
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      data-hover='{hover}'
      data-align='{align}'
      data-icon-style='{iconStyle}'
      data-disabled='{disabled ? "true" : "false"}'
      data-selectable='{selectable ? "true" : "false"}'
      class='wire-metric-card {class}'
      role='{selectable ? "button" : ""}'
      tabindex='{selectable && !disabled ? "0" : "-1"}'
      @click='selectCard(event)'
      @keydown='if (selectable && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); selectCard(event) }'
      style='--wire-metric-progress: {progress}%;'
    >
      <div class="wire-metric-card__top">
        <div class="wire-metric-card__heading">
          {#if label}
            <p class="wire-metric-card__label">{label}</p>
          {/if}

          {#if badge}
            <span class="wire-metric-card__badge">{badge}</span>
          {/if}
        </div>

        {#if icon}
          <div class="wire-metric-card__icon" aria-hidden="true">
            <span class='{icon}'></span>
          </div>
        {/if}
      </div>

      <div class="wire-metric-card__value-row">
        {#if prefix}
          <span class="wire-metric-card__prefix">{prefix}</span>
        {/if}

        <strong class="wire-metric-card__value">{value}</strong>

        {#if suffix}
          <span class="wire-metric-card__suffix">{suffix}</span>
        {/if}
      </div>

      {#if description}
        <p class="wire-metric-card__description">{description}</p>
      {/if}

      <slot></slot>

      {#if progress >= 0}
        <div class="wire-metric-card__progress-block">
          <div class="wire-metric-card__progress-meta">
            <span>{progressLabel || "Progress"}</span>
            <strong>{progress}%</strong>
          </div>
          <div
            class="wire-metric-card__progress-track"
            role="progressbar"
            aria-label='{progressLabel || label}'
            aria-valuemin="0"
            aria-valuemax="100"
            aria-valuenow='{progress}'
          >
            <span class="wire-metric-card__progress-value"></span>
          </div>
        </div>
      {/if}

      {#if trend || trendLabel}
        <div
          class="wire-metric-card__trend"
          data-direction='{trendDirection}'
        >
          {#if trendDirection === "up" || trendDirection === "positive"}
            <span class="icon-[lucide--trending-up] wire-metric-card__trend-icon" aria-hidden="true"></span>
          {:else if trendDirection === "down" || trendDirection === "negative"}
            <span class="icon-[lucide--trending-down] wire-metric-card__trend-icon" aria-hidden="true"></span>
          {:else}
            <span class="icon-[lucide--minus] wire-metric-card__trend-icon" aria-hidden="true"></span>
          {/if}

          {#if trend}
            <strong class="wire-metric-card__trend-value">{trend}</strong>
          {/if}

          {#if trendLabel}
            <span class="wire-metric-card__trend-label">{trendLabel}</span>
          {/if}
        </div>
      {/if}

      <div class="wire-metric-card__footer">
        <slot name="footer"></slot>

        {#if href}
          <a
            href='{href}'
            target='{target}'
            rel='{external || target === "_blank" ? "noopener noreferrer" : rel}'
            class="wire-metric-card__action"
            aria-disabled='{disabled ? "true" : "false"}'
            @click='activateAction(event)'
          >
            <span>{actionLabel || "View details"}</span>

            {#if actionIcon}
              <span class='wire-metric-card__action-icon {actionIcon}' aria-hidden="true"></span>
            {:else if external}
              <span class="wire-metric-card__action-icon icon-[lucide--external-link]" aria-hidden="true"></span>
            {:else if showArrow}
              <span class="wire-metric-card__action-icon icon-[lucide--arrow-right]" aria-hidden="true"></span>
            {/if}
          </a>
        {:else if actionLabel}
          <button
            type="button"
            class="wire-metric-card__action"
            disabled='{disabled}'
            @click='activateAction(event)'
          >
            <span>{actionLabel}</span>
            {#if actionIcon}
              <span class='wire-metric-card__action-icon {actionIcon}' aria-hidden="true"></span>
            {:else if showArrow}
              <span class="wire-metric-card__action-icon icon-[lucide--arrow-right]" aria-hidden="true"></span>
            {/if}
          </button>
        {/if}
      </div>
    </article>
  }

  style {
    .wire-metric-card {
      --metric-accent: var(--wire-color-primary);
      --metric-accent-hover: var(--wire-color-primary-hover);
      --metric-accent-soft: var(--wire-color-primary-soft);
      --metric-accent-muted: var(--wire-color-primary-muted);
      --metric-contrast: var(--wire-color-primary-contrast);
      --metric-border: color-mix(in srgb, var(--metric-accent) 16%, var(--wire-color-border));

      position: relative;
      isolation: isolate;
      display: flex;
      flex-direction: column;
      width: 100%;
      min-width: 0;
      height: 100%;
      padding: 1.5rem;
      color: var(--wire-color-text);
      background:
        radial-gradient(
          circle at 100% 0%,
          color-mix(in srgb, var(--metric-accent) 8%, transparent),
          transparent 36%
        ),
        var(--wire-color-surface-raised);
      border: 1px solid var(--metric-border);
      border-radius: 1.25rem;
      box-shadow:
        0 1px 0 color-mix(in srgb, white 4%, transparent) inset,
        0 14px 36px color-mix(in srgb, black 10%, transparent);
      overflow: hidden;
      transition:
        transform 180ms ease,
        border-color 180ms ease,
        box-shadow 180ms ease,
        background-color 180ms ease;
    }

    .wire-metric-card[data-color="secondary"] {
      --metric-accent: var(--wire-color-secondary);
      --metric-accent-hover: var(--wire-color-secondary-hover);
      --metric-accent-soft: var(--wire-color-secondary-soft);
      --metric-accent-muted: var(--wire-color-secondary-muted);
      --metric-contrast: var(--wire-color-secondary-contrast);
    }

    .wire-metric-card[data-color="info"] {
      --metric-accent: var(--wire-color-info);
      --metric-accent-hover: var(--wire-color-info-hover);
      --metric-accent-soft: var(--wire-color-info-soft);
      --metric-accent-muted: var(--wire-color-info-muted);
      --metric-contrast: var(--wire-color-info-contrast);
    }

    .wire-metric-card[data-color="success"] {
      --metric-accent: var(--wire-color-success);
      --metric-accent-hover: var(--wire-color-success-hover);
      --metric-accent-soft: var(--wire-color-success-soft);
      --metric-accent-muted: var(--wire-color-success-muted);
      --metric-contrast: var(--wire-color-success-contrast);
    }

    .wire-metric-card[data-color="warning"] {
      --metric-accent: var(--wire-color-warning);
      --metric-accent-hover: var(--wire-color-warning-hover);
      --metric-accent-soft: var(--wire-color-warning-soft);
      --metric-accent-muted: var(--wire-color-warning-muted);
      --metric-contrast: var(--wire-color-warning-contrast);
    }

    .wire-metric-card[data-color="danger"] {
      --metric-accent: var(--wire-color-danger);
      --metric-accent-hover: var(--wire-color-danger-hover);
      --metric-accent-soft: var(--wire-color-danger-soft);
      --metric-accent-muted: var(--wire-color-danger-muted);
      --metric-contrast: var(--wire-color-danger-contrast);
    }

    .wire-metric-card[data-size="sm"] {
      padding: 1.1rem;
      border-radius: 1rem;
    }

    .wire-metric-card[data-size="lg"] {
      padding: 1.9rem;
      border-radius: 1.5rem;
    }

    .wire-metric-card[data-variant="soft"] {
      background:
        linear-gradient(135deg, var(--metric-accent-soft), transparent 68%),
        var(--wire-color-surface-raised);
      box-shadow: none;
    }

    .wire-metric-card[data-variant="raised"] {
      box-shadow:
        0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
        0 24px 64px color-mix(in srgb, black 16%, transparent);
    }

    .wire-metric-card[data-variant="outline"] {
      background: transparent;
      box-shadow: none;
    }

    .wire-metric-card[data-variant="minimal"] {
      padding-inline: 0;
      background: transparent;
      border-color: transparent;
      border-radius: 0;
      box-shadow: none;
    }

    .wire-metric-card[data-variant="solid"] {
      color: var(--metric-contrast);
      background:
        linear-gradient(135deg, color-mix(in srgb, white 10%, transparent), transparent 58%),
        var(--metric-accent);
      border-color: color-mix(in srgb, white 22%, transparent);
      box-shadow: 0 20px 52px color-mix(in srgb, var(--metric-accent) 28%, transparent);
    }

    .wire-metric-card[data-variant="gradient"] {
      background:
        linear-gradient(
          145deg,
          color-mix(in srgb, var(--metric-accent) 20%, var(--wire-color-surface-raised)),
          var(--wire-color-surface-raised) 58%
        );
    }

    .wire-metric-card[data-disabled="true"] {
      cursor: not-allowed;
      opacity: 0.58;
      pointer-events: none;
    }

    .wire-metric-card[data-selectable="true"]:not([data-disabled="true"]) {
      cursor: pointer;
    }

    .wire-metric-card[data-hover="lift"]:hover {
      transform: translateY(-4px);
      border-color: color-mix(in srgb, var(--metric-accent) 42%, var(--wire-color-border));
      box-shadow: 0 24px 56px color-mix(in srgb, black 16%, transparent);
    }

    .wire-metric-card[data-hover="border"]:hover {
      border-color: var(--metric-accent);
    }

    .wire-metric-card[data-hover="glow"]:hover {
      border-color: color-mix(in srgb, var(--metric-accent) 52%, var(--wire-color-border));
      box-shadow: 0 22px 58px color-mix(in srgb, var(--metric-accent) 18%, transparent);
    }

    .wire-metric-card[data-align="center"] {
      align-items: center;
      text-align: center;
    }

    .wire-metric-card[data-align="right"] {
      align-items: flex-end;
      text-align: right;
    }

    .wire-metric-card__top {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 1rem;
      width: 100%;
    }

    .wire-metric-card__heading {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 0.55rem;
      min-width: 0;
    }

    .wire-metric-card__label {
      margin: 0;
      color: var(--wire-color-text-muted);
      font-size: 0.84rem;
      font-weight: 600;
      line-height: 1.4;
    }

    .wire-metric-card__badge {
      display: inline-flex;
      align-items: center;
      min-height: 1.45rem;
      padding: 0.2rem 0.55rem;
      color: var(--metric-accent);
      background: var(--metric-accent-soft);
      border: 1px solid color-mix(in srgb, var(--metric-accent) 24%, transparent);
      border-radius: 999px;
      font-size: 0.69rem;
      font-weight: 700;
      line-height: 1;
    }

    .wire-metric-card[data-variant="solid"] .wire-metric-card__label,
    .wire-metric-card[data-variant="solid"] .wire-metric-card__description,
    .wire-metric-card[data-variant="solid"] .wire-metric-card__trend-label,
    .wire-metric-card[data-variant="solid"] .wire-metric-card__progress-meta {
      color: color-mix(in srgb, currentColor 78%, transparent);
    }

    .wire-metric-card[data-variant="solid"] .wire-metric-card__badge {
      color: currentColor;
      background: color-mix(in srgb, white 14%, transparent);
      border-color: color-mix(in srgb, white 22%, transparent);
    }

    .wire-metric-card__icon {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex: 0 0 auto;
      width: 2.75rem;
      height: 2.75rem;
      color: var(--metric-accent);
      background: var(--metric-accent-soft);
      border: 1px solid color-mix(in srgb, var(--metric-accent) 18%, transparent);
      border-radius: 0.9rem;
      font-size: 1.25rem;
    }

    .wire-metric-card[data-icon-style="ghost"] .wire-metric-card__icon {
      background: transparent;
      border-color: transparent;
    }

    .wire-metric-card[data-icon-style="outline"] .wire-metric-card__icon {
      background: transparent;
      border-color: color-mix(in srgb, var(--metric-accent) 34%, var(--wire-color-border));
    }

    .wire-metric-card[data-icon-style="solid"] .wire-metric-card__icon,
    .wire-metric-card[data-variant="solid"] .wire-metric-card__icon {
      color: var(--metric-contrast);
      background: var(--metric-accent);
      border-color: transparent;
    }

    .wire-metric-card[data-variant="solid"] .wire-metric-card__icon {
      color: currentColor;
      background: color-mix(in srgb, white 14%, transparent);
      border-color: color-mix(in srgb, white 20%, transparent);
    }

    .wire-metric-card__value-row {
      display: flex;
      align-items: baseline;
      gap: 0.2rem;
      width: 100%;
      margin-top: 1.15rem;
    }

    .wire-metric-card[data-align="center"] .wire-metric-card__value-row {
      justify-content: center;
    }

    .wire-metric-card[data-align="right"] .wire-metric-card__value-row {
      justify-content: flex-end;
    }

    .wire-metric-card__value {
      color: var(--wire-color-text);
      font-size: clamp(2rem, 5vw, 2.8rem);
      font-weight: 700;
      line-height: 0.98;
      letter-spacing: -0.045em;
      white-space: nowrap;
      font-variant-numeric: tabular-nums;
    }

    .wire-metric-card[data-size="sm"] .wire-metric-card__value {
      font-size: clamp(1.65rem, 4vw, 2.15rem);
    }

    .wire-metric-card[data-size="lg"] .wire-metric-card__value {
      font-size: clamp(2.45rem, 6vw, 3.5rem);
    }

    .wire-metric-card[data-variant="solid"] .wire-metric-card__value {
      color: currentColor;
    }

    .wire-metric-card__prefix,
    .wire-metric-card__suffix {
      color: var(--metric-accent);
      font-size: 1rem;
      font-weight: 700;
    }

    .wire-metric-card[data-variant="solid"] .wire-metric-card__prefix,
    .wire-metric-card[data-variant="solid"] .wire-metric-card__suffix {
      color: currentColor;
    }

    .wire-metric-card__description {
      max-width: 38rem;
      margin: 0.8rem 0 0;
      color: var(--wire-color-text-muted);
      font-size: 0.86rem;
      line-height: 1.7;
    }

    .wire-metric-card__progress-block {
      width: 100%;
      margin-top: 1.25rem;
    }

    .wire-metric-card__progress-meta {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
      margin-bottom: 0.45rem;
      color: var(--wire-color-text-muted);
      font-size: 0.75rem;
    }

    .wire-metric-card__progress-track {
      width: 100%;
      height: 0.42rem;
      overflow: hidden;
      background: color-mix(in srgb, var(--wire-color-border) 72%, transparent);
      border-radius: 999px;
    }

    .wire-metric-card__progress-value {
      display: block;
      width: clamp(0%, var(--wire-metric-progress), 100%);
      height: 100%;
      background: var(--metric-accent);
      border-radius: inherit;
    }

    .wire-metric-card[data-variant="solid"] .wire-metric-card__progress-track {
      background: color-mix(in srgb, white 20%, transparent);
    }

    .wire-metric-card[data-variant="solid"] .wire-metric-card__progress-value {
      background: currentColor;
    }

    .wire-metric-card__trend {
      display: inline-flex;
      align-items: center;
      align-self: flex-start;
      gap: 0.42rem;
      margin-top: 1.15rem;
      color: var(--wire-color-text-muted);
      font-size: 0.76rem;
      line-height: 1.4;
    }

    .wire-metric-card[data-align="center"] .wire-metric-card__trend {
      align-self: center;
    }

    .wire-metric-card[data-align="right"] .wire-metric-card__trend {
      align-self: flex-end;
    }

    .wire-metric-card__trend[data-direction="up"],
    .wire-metric-card__trend[data-direction="positive"] {
      color: var(--wire-color-success);
    }

    .wire-metric-card__trend[data-direction="down"],
    .wire-metric-card__trend[data-direction="negative"] {
      color: var(--wire-color-danger);
    }

    .wire-metric-card__trend-icon {
      width: 0.95rem;
      height: 0.95rem;
    }

    .wire-metric-card__trend-label {
      color: var(--wire-color-text-muted);
      font-weight: 500;
    }

    .wire-metric-card__footer {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      justify-content: space-between;
      gap: 0.8rem;
      width: 100%;
      margin-top: auto;
      padding-top: 1.35rem;
    }

    .wire-metric-card__action {
      appearance: none;
      padding: 0;
      background: transparent;
      border: 0;
      font: inherit;
      cursor: pointer;
      position: relative;
      z-index: 1;
      display: inline-flex;
      align-items: center;
      gap: 0.45rem;
      color: var(--metric-accent);
      font-size: 0.82rem;
      font-weight: 700;
      line-height: 1.3;
      text-decoration: none;
      transition:
        color 160ms ease,
        gap 160ms ease;
    }

    .wire-metric-card__action:hover {
      gap: 0.62rem;
      color: var(--metric-accent-hover);
    }

    .wire-metric-card[data-variant="solid"] .wire-metric-card__action {
      color: currentColor;
    }

    .wire-metric-card__action-icon {
      width: 1rem;
      height: 1rem;
    }

    .wire-metric-card__action:focus-visible {
      outline: 2px solid var(--wire-color-focus);
      outline-offset: 4px;
      border-radius: 0.35rem;
    }

    @media (max-width: 639px) {
      .wire-metric-card {
        padding: 1.2rem;
      }

      .wire-metric-card__value {
        font-size: 2.05rem;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .wire-metric-card,
      .wire-metric-card__action {
        transition: none;
      }

      .wire-metric-card[data-hover="lift"]:hover {
        transform: none;
      }
    }
  }
}
```

---

## MetricGrid

Showcase: https://component.wrnexusjs.dev/
Mount: <MetricGrid /> (legacy: data-component="MetricGrid")
Category: data
Purpose: Arrange operational metrics, KPIs, public statistics, or service indicators in a responsive equal-height grid.
Props: items: unknown[] = [], columns: number = 4, tabletColumns: number = 2, mobileColumns: number = 1, gap: string = "md", equalHeight: boolean = true, dividers: boolean = false, size: string = "default", color: string = "primary", variant: string = "default", maxWidth: string = "full", minItemWidth: string = "", class: string = ""
Slots: default
Events: select, action

### Complete .wrn source contract

```wrn
import MetricCard from "./MetricCard.wrn"

component MetricGrid {
  outputs {
    select(payload: { item: string | number | boolean | null | object; itemIndex: number; sourceEvent: Event })
    action(payload: { item: string | number | boolean | null | object; itemIndex: number; sourceEvent: Event })
  }

  props {

    items: unknown[] = []
    columns: number = 4
    tabletColumns: number = 2
    mobileColumns: number = 1
    gap: string = "md"
    equalHeight: boolean = true
    dividers: boolean = false
    size: string = "default"
    color: string = "primary"
    variant: string = "default"
    maxWidth: string = "full"
    minItemWidth: string = ""
    class: string = ""
  }

  functions {
    client function forwardSelect(item, itemIndex, sourceEvent) {
      output.select({
        item: item,
        itemIndex: itemIndex,
        sourceEvent: sourceEvent
      })
    }

    client function forwardAction(item, itemIndex, sourceEvent) {
      output.action({
        item: item,
        itemIndex: itemIndex,
        sourceEvent: sourceEvent
      })
    }
  }

  view {
    <section
      {...attrs}
      data-ui-component="MetricGrid"
      data-gap='{gap}'
      data-equal-height='{equalHeight ? "true" : "false"}'
      data-dividers='{dividers ? "true" : "false"}'
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      data-max-width='{maxWidth}'
      data-auto-fit='{minItemWidth ? "true" : "false"}'
      class='wire-metric-grid {class}'
      style='display:block;width:100%;min-width:0;--wire-metric-columns:{columns};--wire-metric-tablet-columns:{tabletColumns};--wire-metric-mobile-columns:{mobileColumns};--wire-metric-min-item-width:{minItemWidth || "18rem"};'
    >
      <div class="wire-metric-grid__frame">
        <div class="wire-metric-grid__surface">
          {#each items as item, itemIndex}
            <MetricCard
              label='{item.label || item.title || "Metric"}'
              value='{item.value}'
              description='{item.description || ""}'
              icon='{item.icon || ""}'
              iconStyle='{item.iconStyle || "soft"}'
              prefix='{item.prefix || ""}'
              suffix='{item.suffix || ""}'
              badge='{item.badge || ""}'
              trend='{item.trend || ""}'
              trendLabel='{item.trendLabel || ""}'
              trendDirection='{item.trendDirection || "neutral"}'
              progress='{item.progress === undefined ? -1 : item.progress}'
              progressLabel='{item.progressLabel || ""}'
              href='{item.href || ""}'
              target='{item.target || ""}'
              rel='{item.rel || ""}'
              external='{item.external || false}'
              actionLabel='{item.actionLabel || ""}'
              actionIcon='{item.actionIcon || ""}'
              showArrow='{item.showArrow === undefined ? true : item.showArrow}'
              selectable='{item.selectable || false}'
              disabled='{item.disabled || false}'
              size='{item.size || size}'
              color='{item.color || color}'
              variant='{dividers ? "minimal" : (item.variant || variant)}'
              hover='{item.hover || (dividers ? "none" : "lift")}'
              align='{item.align || "left"}'
              class="wire-metric-grid__card"
              @select='forwardSelect(item, itemIndex, event)'
              @action='forwardAction(item, itemIndex, event)'
            />
          {:empty}
            <slot></slot>
          {/each}
        </div>
      </div>
    </section>
  }

  style {
    .wire-metric-grid__frame {
      width: 100%;
      min-width: 0;
      margin-inline: auto;
    }

    .wire-metric-grid[data-max-width="compact"] .wire-metric-grid__frame {
      max-width: 64rem;
    }

    .wire-metric-grid[data-max-width="lg"] .wire-metric-grid__frame {
      max-width: 72rem;
    }

    .wire-metric-grid[data-max-width="xl"] .wire-metric-grid__frame {
      max-width: 80rem;
    }

    .wire-metric-grid[data-max-width="wide"] .wire-metric-grid__frame,
    .wire-metric-grid[data-max-width="2xl"] .wire-metric-grid__frame {
      max-width: 90rem;
    }

    .wire-metric-grid[data-max-width="full"] .wire-metric-grid__frame {
      max-width: none;
    }

    .wire-metric-grid__surface {
      display: grid;
      grid-template-columns: repeat(var(--wire-metric-mobile-columns), minmax(0, 1fr));
      gap: 1.25rem;
      width: 100%;
      min-width: 0;
      align-items: stretch;
    }

    .wire-metric-grid[data-auto-fit="true"] .wire-metric-grid__surface {
      grid-template-columns: repeat(
        auto-fit,
        minmax(min(100%, var(--wire-metric-min-item-width)), 1fr)
      );
    }

    .wire-metric-grid[data-gap="none"] .wire-metric-grid__surface {
      gap: 0;
    }

    .wire-metric-grid[data-gap="sm"] .wire-metric-grid__surface {
      gap: 0.75rem;
    }

    .wire-metric-grid[data-gap="lg"] .wire-metric-grid__surface {
      gap: 1.75rem;
    }

    .wire-metric-grid[data-gap="xl"] .wire-metric-grid__surface {
      gap: 2.25rem;
    }

    .wire-metric-grid[data-equal-height="true"] .wire-metric-grid__surface > * {
      min-width: 0;
      height: 100%;
    }

    .wire-metric-grid[data-variant="panel"] .wire-metric-grid__surface,
    .wire-metric-grid[data-variant="soft"] .wire-metric-grid__surface,
    .wire-metric-grid[data-dividers="true"] .wire-metric-grid__surface {
      overflow: hidden;
      padding: 0.75rem;
      background: var(--wire-color-surface-raised);
      border: 1px solid var(--wire-color-border);
      border-radius: 1.4rem;
    }

    .wire-metric-grid[data-variant="soft"] .wire-metric-grid__surface {
      background:
        linear-gradient(
          135deg,
          color-mix(in srgb, var(--wire-color-primary) 8%, transparent),
          transparent 62%
        ),
        var(--wire-color-surface-raised);
    }

    .wire-metric-grid[data-dividers="true"] .wire-metric-grid__surface {
      gap: 0;
      padding: 0;
    }

    .wire-metric-grid[data-dividers="true"] .wire-metric-grid__card {
      border-radius: 0;
    }

    @media (min-width: 640px) {
      .wire-metric-grid[data-auto-fit="false"] .wire-metric-grid__surface {
        grid-template-columns: repeat(var(--wire-metric-tablet-columns), minmax(0, 1fr));
      }
    }

    @media (min-width: 1024px) {
      .wire-metric-grid[data-auto-fit="false"] .wire-metric-grid__surface {
        grid-template-columns: repeat(var(--wire-metric-columns), minmax(0, 1fr));
      }
    }

    @media (max-width: 639px) {
      .wire-metric-grid[data-dividers="true"] .wire-metric-grid__surface > * + * {
        border-top: 1px solid var(--wire-color-border);
      }
    }

    @media (min-width: 640px) and (max-width: 1023px) {
      .wire-metric-grid[data-dividers="true"] .wire-metric-grid__surface > * {
        border-top: 1px solid var(--wire-color-border);
      }

      .wire-metric-grid[data-dividers="true"] .wire-metric-grid__surface > *:nth-child(-n + 2) {
        border-top: 0;
      }

      .wire-metric-grid[data-dividers="true"] .wire-metric-grid__surface > *:nth-child(2n) {
        border-left: 1px solid var(--wire-color-border);
      }
    }

    @media (min-width: 1024px) {
      .wire-metric-grid[data-dividers="true"] .wire-metric-grid__surface > * + * {
        border-left: 1px solid var(--wire-color-border);
      }
    }
  }
}
```

---

## Modal

Showcase: https://component.wrnexusjs.dev/
Mount: <Modal /> (legacy: data-component="Modal")
Category: overlays
Purpose: Present an accessible modal dialog with focus management, confirmation, cancellation, slots, and responsive sizing.
Props: open: boolean = false, defaultOpen: boolean = false, title: string = "Modal", description: string = "", icon: string = "", label: string = "Modal dialog", size: string = "md", placement: string = "center", color: string = "primary", variant: string = "default", showClose: boolean = true, closeLabel: string = "Close modal", closeOnBackdrop: boolean = true, closeOnEscape: boolean = true, closeOnCancel: boolean = true, closeOnConfirm: boolean = false, showFooter: boolean = true, cancelLabel: string = "Cancel", cancelIcon: string = "", confirmLabel: string = "Confirm", confirmIcon: string = "", confirmDisabled: boolean = false, confirmLoading: boolean = false, destructive: boolean = false, triggerLabel: string = "", triggerIcon: string = "", scrollable: boolean = true, scrollBehavior: string = "inside", class: string = ""
Slots: trigger, header, default, footer
Events: open, close, cancel, confirm

### Complete .wrn source contract

```wrn
component Modal {
  outputs {
    open(payload: { sourceEvent: Event })
    close(payload: { reason: string; sourceEvent: Event })
    cancel(payload: { sourceEvent: Event })
    confirm(payload: { sourceEvent: Event })
  }

  props {

    open: boolean = false
    defaultOpen: boolean = false
    title: string = "Modal"
    description: string = ""
    icon: string = ""
    label: string = "Modal dialog"
    size: string = "md"
    placement: string = "center"
    color: string = "primary"
    variant: string = "default"
    showClose: boolean = true
    closeLabel: string = "Close modal"
    closeOnBackdrop: boolean = true
    closeOnEscape: boolean = true
    closeOnCancel: boolean = true
    closeOnConfirm: boolean = false
    showFooter: boolean = true
    cancelLabel: string = "Cancel"
    cancelIcon: string = ""
    confirmLabel: string = "Confirm"
    confirmIcon: string = ""
    confirmDisabled: boolean = false
    confirmLoading: boolean = false
    destructive: boolean = false
    triggerLabel: string = ""
    triggerIcon: string = ""
    scrollable: boolean = true
    scrollBehavior: string = "inside"
    class: string = ""
  }

  state visible = defaultOpen

  functions {
    shared function isOpen() {
      return open || visible
    }

    client function showModal(sourceEvent) {
      visible = true
      output.open({ sourceEvent: sourceEvent })
    }

    // Slot content can close the modal it sits in by dispatching a bubbling
    // wrnexus:modal:close event, e.g. from a form success handler:
    //
    //   event.target.dispatchEvent(
    //     new CustomEvent("wrnexus:modal:close", { bubbles: true })
    //   )
    //
    // The listener is on the modal root, so the event only ever closes the
    // modal the dispatching element is actually inside -- no ids to wire up
    // and no way to close somebody else's modal by accident.
    client function hideModal(reason, sourceEvent) {
      visible = false
      output.close({
        reason: reason,
        sourceEvent: sourceEvent
      })
    }

    client function cancelModal(sourceEvent) {
      output.cancel({ sourceEvent: sourceEvent })
      if (closeOnCancel) {
        hideModal("cancel", sourceEvent)
      }
    }

    client function confirmModal(sourceEvent) {
      if (confirmDisabled || confirmLoading) {
        return
      }
      output.confirm({ sourceEvent: sourceEvent })
      if (closeOnConfirm) {
        hideModal("confirm", sourceEvent)
      }
    }

    client function handleKeydown(sourceEvent) {
      if (closeOnEscape && sourceEvent.key === "Escape") {
        sourceEvent.preventDefault()
        cancelModal(sourceEvent)
      }
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="Modal"
      data-open='{isOpen() ? "true" : "false"}'
      data-size='{size}'
      data-placement='{placement}'
      data-color='{color}'
      data-variant='{variant}'
      data-scrollable='{scrollable ? "true" : "false"}'
      data-scroll='{scrollBehavior}'
      data-destructive='{destructive ? "true" : "false"}'
      class='wire-modal {class}'
      @wrnexus:modal:close='hideModal("api", event)'
    >
      {#if triggerLabel}
        <button
          type="button"
          class="wire-modal__trigger"
          aria-haspopup="dialog"
          aria-expanded='{isOpen() ? "true" : "false"}'
          @click='showModal(event)'
        >
          {#if triggerIcon}
            <span
              class='{triggerIcon}'
              aria-hidden="true"
            >
            </span>
          {/if}
          <span>{triggerLabel}</span>
        </button>
      {/if}

      <span
        class="wire-modal__trigger-slot"
        @click='showModal(event)'
      >
        <slot
          name="trigger"
        >
        </slot>
      </span>

      <div
        class="wire-modal__layer"
        data-show="isOpen()"
        role="presentation"
        @keydown='handleKeydown(event)'
      >
        <button
          type="button"
          class="wire-modal__backdrop"
          aria-label='{closeLabel}'
          @click='if (closeOnBackdrop) { cancelModal(event) }'
        >
        </button>

        <section
          class="wire-modal__panel"
          role="dialog"
          aria-modal="true"
          aria-label='{label || title}'
          tabindex="-1"
        >
          <header
            class="wire-modal__header"
          >
            <div
              class="wire-modal__heading"
            >
              {#if icon}
                <span
                  class='wire-modal__icon {icon}'
                  aria-hidden="true"
                >
                </span>
              {/if}

              <div
                class="wire-modal__heading-copy"
              >
                <slot
                  name="header"
                >
                </slot>
                {#if title}
                  <h2>{title}</h2>
                {/if}
                {#if description}
                  <p>{description}</p>
                {/if}
              </div>
            </div>

            {#if showClose}
              <button
                type="button"
                class="wire-modal__close"
                aria-label='{closeLabel}'
                @click='hideModal("close-button", event)'
              >
                <svg
                  viewBox="0 0 24 24"
                  width="16"
                  height="16"
                  fill="none"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  aria-hidden="true"
                >
                  <path d="M18 6 6 18" />
                  <path d="M6 6 18 18" />
                </svg>
              </button>
            {/if}
          </header>

          <div
            class="wire-modal__body"
          >
            <slot></slot>
          </div>

          {#if showFooter}
            <footer
              class="wire-modal__footer"
            >
              <div
                class="wire-modal__custom-footer"
              >
                <slot
                  name="footer"
                >
                </slot>
              </div>

              <div
                class="wire-modal__actions"
              >
                {#if cancelLabel}
                  <button
                    type="button"
                    class="wire-modal__button wire-modal__button--secondary"
                    @click='cancelModal(event)'
                  >
                    {#if cancelIcon}
                      <span
                        class='{cancelIcon}'
                        aria-hidden="true"
                      >
                      </span>
                    {/if}
                    <span>{cancelLabel}</span>
                  </button>
                {/if}

                {#if confirmLabel}
                  <button
                    type="button"
                    class="wire-modal__button wire-modal__button--primary"
                    disabled='{confirmDisabled || confirmLoading}'
                    @click='confirmModal(event)'
                  >
                    {#if confirmLoading}
                      <span
                        class="wire-modal__spinner"
                        aria-hidden="true"
                      >
                      </span>
                    {:else if confirmIcon}
                      <span
                        class='{confirmIcon}'
                        aria-hidden="true"
                      >
                      </span>
                    {/if}
                    <span>{confirmLabel}</span>
                  </button>
                {/if}
              </div>
            </footer>
          {/if}
        </section>
      </div>
    </div>
  }

  style {
    .wire-modal {
      --modal-accent: var(--wire-color-primary);
      --modal-soft: var(--wire-color-primary-soft);
      --modal-contrast: var(--wire-color-primary-contrast);
      position: relative;
      display: inline-flex;
    }

    .wire-modal[data-color="secondary"] {
      --modal-accent: var(--wire-color-secondary);
      --modal-soft: var(--wire-color-secondary-soft);
      --modal-contrast: var(--wire-color-secondary-contrast);
    }

    /*
     * Fallbacks below (the second var() argument): the theme token generator
     * (packages/styles/src/theme.ts) only emits a real -contrast token for
     * primary and secondary. info, success, and danger have no contrast
     * token defined at all, so var(--wire-color-info-contrast) with no
     * fallback resolves to nothing, and --modal-contrast becomes invalid --
     * which made confirm-button and solid-panel text unreadable. White is a
     * safe default against these saturated colors until the theme package
     * defines real tokens for them.
     */
    .wire-modal[data-color="info"] {
      --modal-accent: var(--wire-color-info);
      --modal-soft: var(--wire-color-info-soft);
      --modal-contrast: var(--wire-color-info-contrast, white);
    }

    .wire-modal[data-color="success"] {
      --modal-accent: var(--wire-color-success);
      --modal-soft: var(--wire-color-success-soft);
      --modal-contrast: var(--wire-color-success-contrast, white);
    }

    .wire-modal[data-color="warning"] {
      --modal-accent: var(--wire-color-warning);
      --modal-soft: var(--wire-color-warning-soft);
      --modal-contrast: var(--wire-color-warning-text);
    }

    .wire-modal[data-color="danger"],
    .wire-modal[data-destructive="true"] {
      --modal-accent: var(--wire-color-danger);
      --modal-soft: var(--wire-color-danger-soft);
      --modal-contrast: var(--wire-color-on-danger, white);
    }

    .wire-modal__trigger,
    .wire-modal__trigger-slot {
      display: inline-flex;
      align-items: center;
      gap: 0.55rem;
    }

    .wire-modal__trigger {
      appearance: none;
      min-height: 2.55rem;
      padding: 0.65rem 1rem;
      color: var(--modal-contrast);
      background: var(--modal-accent);
      border: 0;
      border-radius: 0.8rem;
      font: inherit;
      font-size: 0.85rem;
      font-weight: 650;
      cursor: pointer;
      transition: opacity 150ms ease, transform 150ms ease, box-shadow 150ms ease;
    }

    .wire-modal__trigger:hover {
      opacity: 0.92;
    }

    .wire-modal__trigger:active {
      transform: scale(0.97);
    }

    .wire-modal__trigger:focus-visible {
      outline: none;
      box-shadow: 0 0 0 3px color-mix(in srgb, var(--modal-accent) 40%, transparent);
    }

    /*
     * Hidden by default so the SSR-rendered HTML never paints the layer before
     * hydration runs. data-open on the wire-modal root is evaluated and
     * serialized to a real true or false string at render time -- a plain
     * bind, not the raw-expression data-show directive -- so this selector
     * is correct on first paint with zero flash, with no dependency on
     * client JS having run yet. The data-show attribute and client directive
     * still run after hydration to keep things in sync for state changes,
     * but visibility itself is driven by CSS here. visibility (rather than
     * display) is used so the open and close transitions below can actually
     * animate -- a box that starts at display: none has no prior frame to
     * transition from.
     */
    .wire-modal__layer {
      position: fixed;
      inset: 0;
      z-index: 1250;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 1rem;
      visibility: hidden;
      opacity: 0;
      transition: opacity 180ms ease, visibility 0s linear 180ms;
    }

    .wire-modal[data-open="true"] .wire-modal__layer {
      visibility: visible;
      opacity: 1;
      transition: opacity 180ms ease, visibility 0s linear 0s;
    }

    .wire-modal[data-placement="top"] .wire-modal__layer {
      align-items: flex-start;
      padding-top: clamp(1rem, 8vh, 5rem);
    }

    .wire-modal[data-scroll="page"] .wire-modal__layer {
      align-items: flex-start;
      overflow-y: auto;
      padding: 2.5rem 1rem;
    }

    .wire-modal__backdrop {
      position: absolute;
      inset: 0;
      z-index: 0;
      appearance: none;
      padding: 0;
      background: color-mix(in srgb, black 62%, transparent);
      border: 0;
      backdrop-filter: blur(9px);
    }

    .wire-modal__panel {
      position: relative;
      z-index: 1;
      display: flex;
      flex-direction: column;
      width: min(32rem, calc(100vw - 2rem));
      max-height: min(88vh, 52rem);
      color: var(--wire-color-text);
      background:
      radial-gradient(
      circle at 100% 0%,
      color-mix(in srgb, var(--modal-accent) 9%, transparent),
      transparent 34%
      ),
      var(--wire-color-surface-raised);
      border: 1px solid color-mix(in srgb, var(--modal-accent) 18%, var(--wire-color-border));
      border-radius: 1.35rem;
      box-shadow:
      0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
      0 40px 110px color-mix(in srgb, black 38%, transparent);
      overflow: hidden;
      opacity: 0;
      transform: scale(0.96) translateY(10px);
      transition: opacity 180ms ease, transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
    }

    .wire-modal[data-open="true"] .wire-modal__panel {
      opacity: 1;
      transform: none;
    }

    .wire-modal[data-size="xs"] .wire-modal__panel {
      width: min(19rem, calc(100vw - 2rem));
    }

    .wire-modal[data-size="sm"] .wire-modal__panel {
      width: min(25rem, calc(100vw - 2rem));
    }

    .wire-modal[data-size="lg"] .wire-modal__panel {
      width: min(44rem, calc(100vw - 2rem));
    }

    .wire-modal[data-size="xl"] .wire-modal__panel {
      width: min(64rem, calc(100vw - 2rem));
    }

    .wire-modal[data-size="full"] .wire-modal__panel {
      width: calc(100vw - 2rem);
      height: calc(100vh - 2rem);
      max-height: none;
    }

    .wire-modal[data-scroll="page"] .wire-modal__panel {
      max-height: none;
    }

    .wire-modal[data-variant="soft"] .wire-modal__panel {
      background:
      linear-gradient(145deg, var(--modal-soft), transparent 68%),
      var(--wire-color-surface-raised);
    }

    .wire-modal[data-variant="outline"] .wire-modal__panel {
      background: var(--wire-color-surface-raised);
      box-shadow: 0 24px 72px color-mix(in srgb, black 24%, transparent);
    }

    .wire-modal[data-variant="solid"] .wire-modal__panel {
      color: var(--modal-contrast);
      background: var(--modal-accent);
      border-color: color-mix(in srgb, white 20%, transparent);
    }

    .wire-modal__header {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 1rem;
      padding: 1.4rem 1.4rem 1.15rem;
      border-bottom: 1px solid var(--wire-color-border);
    }

    .wire-modal__heading {
      display: flex;
      align-items: flex-start;
      gap: 0.9rem;
      min-width: 0;
    }

    .wire-modal__icon {
      flex: 0 0 auto;
      width: 1.3rem;
      height: 1.3rem;
      margin-top: 0.15rem;
      color: var(--modal-accent);
    }

    .wire-modal[data-variant="solid"] .wire-modal__icon {
      color: currentColor;
    }

    .wire-modal__heading-copy {
      display: grid;
      gap: 0.3rem;
      min-width: 0;
    }

    .wire-modal__heading-copy h2,
    .wire-modal__heading-copy p {
      margin: 0;
    }

    .wire-modal__heading-copy h2 {
      color: inherit;
      font-size: 1.08rem;
      font-weight: 650;
      line-height: 1.3;
    }

    .wire-modal__heading-copy p {
      color: var(--wire-color-text-muted);
      font-size: 0.8rem;
      line-height: 1.55;
    }

    .wire-modal[data-variant="solid"] .wire-modal__heading-copy p {
      color: color-mix(in srgb, currentColor 76%, transparent);
    }

    /*
     * padding is reset explicitly: an app-level `button { padding: … }` rule
     * outranks the browser default, and 1rem of horizontal padding left this
     * 2.15rem button with a ~2px content box -- which squeezed the icon to
     * 0.4px wide and read as "the close button has no icon".
     */
    .wire-modal__close {
      appearance: none;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex: 0 0 auto;
      padding: 0;
      width: 2.15rem;
      height: 2.15rem;
      color: var(--wire-color-text-muted);
      background: transparent;
      border: 1px solid transparent;
      border-radius: 9999px;
      cursor: pointer;
      transition: background 150ms ease, color 150ms ease, border-color 150ms ease, transform 150ms ease;
    }

    /*
     * On a solid panel the background is the accent color, so the muted-grey
     * default is close to invisible -- the dismiss affordance reads as
     * missing rather than subtle. Derive it from the panel contrast color
     * instead, and give it a faint ring so it is unmistakably a control.
     */
    .wire-modal[data-variant="solid"] .wire-modal__close {
      color: color-mix(in srgb, currentColor 82%, transparent);
      border-color: color-mix(in srgb, currentColor 35%, transparent);
    }

    .wire-modal[data-variant="solid"] .wire-modal__close:hover {
      color: currentColor;
      background: color-mix(in srgb, black 18%, transparent);
      border-color: color-mix(in srgb, currentColor 55%, transparent);
    }

    .wire-modal__close:hover {
      color: var(--wire-color-text);
      background: var(--wire-color-surface-soft);
    }

    .wire-modal__close:focus-visible {
      color: var(--modal-accent);
      border-color: color-mix(in srgb, var(--modal-accent) 45%, transparent);
      outline: none;
    }

    .wire-modal__close:active {
      transform: scale(0.92);
    }

    /* Never let the glyph be shrunk by the flex container. */
    .wire-modal__close svg {
      flex: 0 0 auto;
      width: 1rem;
      height: 1rem;
    }

    /*
     * Slot content is authored by the host app, so the app global stylesheet
     * styles it too. A bare element selector there (p { color: ... }) beats
     * anything the panel merely *inherits*, which is how solid-variant modals
     * ended up with muted grey body copy on a saturated accent background --
     * unreadable, and worst exactly where contrast matters most (the
     * destructive confirm). Setting the color on the body makes the panel
     * choice explicit instead of leaving it to inheritance.
     *
     * NOTE: apostrophes are avoided in .wrn style comments on purpose -- the
     * block scanner treats a quote as a string delimiter while it counts
     * braces, so a stray one breaks parsing of the whole component.
     */
    .wire-modal__body {
      flex: 1 1 auto;
      min-height: 0;
      padding: 1.4rem;
      color: var(--wire-color-text);
    }

    /*
     * :where() keeps this at the specificity of .wire-modal__body alone, so it
     * outranks a global element selector but still yields to any class the app
     * puts on its own slot content (an error message, a muted caption). A
     * plain .wire-modal__body p list would have quietly overridden those.
     */
    .wire-modal__body :where(p, li, dd, dt, h1, h2, h3, h4, h5, h6, span, label, code) {
      color: inherit;
    }

    .wire-modal[data-variant="solid"] .wire-modal__body {
      color: var(--modal-contrast);
    }

    .wire-modal[data-scrollable="true"] .wire-modal__body {
      overflow: auto;
      overscroll-behavior: contain;
    }

    .wire-modal__footer {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
      padding: 1rem 1.4rem 1.4rem;
      border-top: 1px solid var(--wire-color-border);
    }

    .wire-modal__custom-footer:empty {
      display: none;
    }

    .wire-modal__actions {
      display: flex;
      align-items: center;
      justify-content: flex-end;
      gap: 0.65rem;
      margin-left: auto;
    }

    .wire-modal__button {
      appearance: none;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 0.5rem;
      min-height: 2.55rem;
      padding: 0.65rem 1rem;
      border-radius: 0.78rem;
      font: inherit;
      font-size: 0.83rem;
      font-weight: 650;
      cursor: pointer;
      transition: transform 150ms ease, opacity 150ms ease, border-color 150ms ease;
    }

    .wire-modal__button:active {
      transform: scale(0.985);
    }

    .wire-modal__button--secondary {
      color: var(--wire-color-text);
      background: transparent;
      border: 1px solid var(--wire-color-border);
    }

    .wire-modal__button--primary {
      color: var(--modal-contrast);
      background: var(--modal-accent);
      border: 1px solid transparent;
    }

    /*
     * On a solid-variant panel the panel background is also --modal-accent,
     * so a plain primary button (same color) has no visible edge against it.
     * Darken the fill slightly and add a light border so the button still
     * reads as a distinct, clickable pill instead of blending into the panel.
     */
    .wire-modal[data-variant="solid"] .wire-modal__button--primary {
      background: color-mix(in srgb, black 18%, var(--modal-accent));
      border-color: color-mix(in srgb, white 32%, transparent);
      box-shadow: 0 1px 0 color-mix(in srgb, white 12%, transparent) inset;
    }

    .wire-modal__button:disabled {
      opacity: 0.55;
      cursor: not-allowed;
    }

    .wire-modal__spinner {
      width: 0.95rem;
      height: 0.95rem;
      border: 2px solid color-mix(in srgb, currentColor 35%, transparent);
      border-top-color: currentColor;
      border-radius: 50%;
      animation: wire-modal-spin 750ms linear infinite;
    }

    @keyframes wire-modal-spin {
      to {
        transform: rotate(360deg);
      }
    }

    @media (max-width: 639px) {
      .wire-modal__layer {
        align-items: flex-end;
        padding: 0;
      }

      .wire-modal__panel,
      .wire-modal[data-size="xs"] .wire-modal__panel,
      .wire-modal[data-size="sm"] .wire-modal__panel,
      .wire-modal[data-size="lg"] .wire-modal__panel,
      .wire-modal[data-size="xl"] .wire-modal__panel {
        width: 100%;
        max-height: 92vh;
        border-radius: 1.25rem 1.25rem 0 0;
      }

      .wire-modal__footer {
        align-items: stretch;
        flex-direction: column;
      }

      .wire-modal__actions {
        width: 100%;
      }

      .wire-modal__button {
        flex: 1 1 0;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .wire-modal__button,
      .wire-modal__spinner,
      .wire-modal__layer,
      .wire-modal__panel,
      .wire-modal__close {
        animation: none;
        transition: none;
      }

      .wire-modal__panel {
        transform: none;
      }
    }
  }
}
```

---

## Nav

Showcase: https://component.wrnexusjs.dev/
Mount: <Nav /> (legacy: data-component="Nav")
Category: navigation
Purpose: Theme-aware, responsive nav component.
Props: color: string = "primary", size: string = "default", items: unknown[] = [], active: string = "", orientation: string = "horizontal", label: string = "Main", collapsible: boolean = true, toggleLabel: string = "Menu", class: string = ""
Slots: default
Events: select

### Complete .wrn source contract

```wrn
// Nav -- a navigation link list, flat or with submenus.
//
//   <Nav items='[{"label":"Home","href":"/","value":"home"}]' active="home" />
//
// An item carrying its own items array becomes a submenu. Depth is capped at
// three levels: this template language has no component recursion, so each
// level is written out, and three covers any realistic navigation.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Nav {
  outputs {
    select(payload: { item: object; value: string })
  }

  props {
    color: string = "primary"
    size: string = "default"
    items: unknown[] = []
    active: string = ""
    orientation: string = "horizontal"
    label: string = "Main"
    collapsible: boolean = true
    toggleLabel: string = "Menu"
    class: string = ""
  }

  state expanded = false

  functions {
    shared function itemList() {
      return Array.isArray(items) ? items : []
    }

    shared function childrenOf(item) {
      return item && Array.isArray(item.items) ? item.items : []
    }

    shared function isActive(item) {
      return Boolean(item.value) && item.value === active
    }

    shared function rovingAxis() {
      return orientation === "vertical" ? "vertical" : "horizontal"
    }

    client function choose(item) {
      if (item.disabled) {
        return
      }
      output.select({ item: item, value: item.value || "" })
    }

    client function toggleMenu() {
      expanded = !expanded
    }
  }

  view {
    <nav
      {...attrs}
      data-ui-component="Nav"
      class='wire-nav {class}'
      data-orientation='{orientation}'
      data-color='{color}'
      data-size='{size}'
      data-expanded='{expanded}'
      role="navigation"
      aria-label='{label}'
    >
      <button
        type="button"
        class="wire-nav__toggle"
        data-show="collapsible"
        aria-expanded='{expanded}'
        aria-label='{toggleLabel}'
        @click='toggleMenu()'
      >
        <span class="wire-nav__toggle-bar" aria-hidden="true"></span>
        <span class="wire-nav__toggle-text">{toggleLabel}</span>
      </button>

      <ul class="wire-nav__list" data-wrn-roving='{rovingAxis()}'>
        {#each itemList() as item}
          <li class="wire-nav__item" data-has-children='{childrenOf(item).length > 0}'>
            <a
              class="wire-nav__link"
              href='{item.href || "#"}'
              data-wrn-roving-item="true"
              data-active='{isActive(item)}'
              aria-current='{isActive(item) ? "page" : "false"}'
              aria-disabled='{item.disabled ? "true" : "false"}'
              @click='choose(item)'
            >
              <span
                class='wire-nav__icon {item.icon}'
                data-show="item.icon"
                aria-hidden="true"
              ></span>
              <span class="wire-nav__label">{item.label}</span>
              <span class="wire-nav__badge" data-show="item.badge">{item.badge}</span>
              <span
                class="wire-nav__arrow"
                data-show="childrenOf(item).length > 0"
                aria-hidden="true"
              >&#8250;</span>
            </a>

            <ul
              class="wire-nav__submenu"
              data-wrn-anchored="true"
              data-show="childrenOf(item).length > 0"
            >
              {#each childrenOf(item) as child}
                <li class="wire-nav__item" data-has-children='{childrenOf(child).length > 0}'>
                  <a
                    class="wire-nav__link"
                    href='{child.href || "#"}'
                    data-active='{isActive(child)}'
                    aria-current='{isActive(child) ? "page" : "false"}'
                    aria-disabled='{child.disabled ? "true" : "false"}'
                    @click='choose(child)'
                  >
                    <span
                      class='wire-nav__icon {child.icon}'
                      data-show="child.icon"
                      aria-hidden="true"
                    ></span>
                    <span class="wire-nav__label">{child.label}</span>
                    <span class="wire-nav__badge" data-show="child.badge">{child.badge}</span>
                    <span
                      class="wire-nav__arrow"
                      data-show="childrenOf(child).length > 0"
                      aria-hidden="true"
                    >&#8250;</span>
                  </a>

                  <ul
                    class="wire-nav__submenu wire-nav__submenu--level3"
                    data-wrn-anchored="true"
                    data-show="childrenOf(child).length > 0"
                  >
                    {#each childrenOf(child) as leaf}
                      <li class="wire-nav__item">
                        <a
                          class="wire-nav__link"
                          href='{leaf.href || "#"}'
                          data-active='{isActive(leaf)}'
                          aria-current='{isActive(leaf) ? "page" : "false"}'
                          aria-disabled='{leaf.disabled ? "true" : "false"}'
                          @click='choose(leaf)'
                        >
                          <span
                            class='wire-nav__icon {leaf.icon}'
                            data-show="leaf.icon"
                            aria-hidden="true"
                          ></span>
                          <span class="wire-nav__label">{leaf.label}</span>
                        </a>
                      </li>
                    {/each}
                  </ul>
                </li>
              {/each}
            </ul>
          </li>
        {/each}
      </ul>

      <slot />
    </nav>
  }

  style {
    .wire-nav {
      --nav-accent: var(--wire-color-primary);
      max-width: 100%;
    }

    .wire-nav[data-color="secondary"] {
      --nav-accent: var(--wire-color-secondary);
    }

    .wire-nav[data-color="success"] {
      --nav-accent: var(--wire-color-success);
    }

    .wire-nav[data-color="danger"] {
      --nav-accent: var(--wire-color-danger);
    }

    .wire-nav[data-color="info"] {
      --nav-accent: var(--wire-color-info);
    }

    .wire-nav[data-size="sm"] {
      font-size: 0.82rem;
    }

    .wire-nav[data-size="lg"] {
      font-size: 1rem;
    }

    .wire-nav__list {
      display: flex;
      align-items: center;
      gap: 0.25rem;
      margin: 0;
      padding: 0;
      list-style: none;
    }

    .wire-nav[data-orientation="vertical"] .wire-nav__list {
      flex-direction: column;
      align-items: stretch;
    }

    .wire-nav__item {
      position: relative;
    }

    .wire-nav__link {
      display: flex;
      align-items: center;
      gap: 0.45rem;
      padding: 0.45rem 0.7rem;
      border-radius: var(--wire-radius-sm);
      color: var(--wire-color-text-muted);
      font-size: 0.9rem;
      font-weight: 600;
      text-decoration: none;
    }

    .wire-nav__link:hover {
      background: var(--wire-color-surface-soft);
      color: var(--wire-color-text);
    }

    .wire-nav__link:focus-visible {
      outline: 2px solid var(--nav-accent);
      outline-offset: 2px;
    }

    .wire-nav__link[data-active="true"] {
      background: color-mix(in srgb, var(--nav-accent) 16%, transparent);
      color: var(--nav-accent);
    }

    .wire-nav__link[aria-disabled="true"] {
      opacity: 0.5;
      pointer-events: none;
    }

    .wire-nav__badge {
      padding: 0.05rem 0.4rem;
      border-radius: 999px;
      background: color-mix(in srgb, var(--nav-accent) 16%, transparent);
      color: var(--nav-accent);
      font-size: 0.72rem;
    }

    .wire-nav__arrow {
      display: inline-block;
      transition: transform 160ms ease;
    }

    .wire-nav__item:hover > .wire-nav__link > .wire-nav__arrow,
    .wire-nav__item:focus-within > .wire-nav__link > .wire-nav__arrow {
      transform: rotate(90deg);
    }

    .wire-nav__submenu {
      position: absolute;
      z-index: 30;
      top: 100%;
      left: 0;
      min-width: 12rem;
      margin: 0;
      padding: 0.35rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-md);
      background: var(--wire-color-surface);
      box-shadow: var(--wire-shadow-1);
      list-style: none;
      opacity: 0;
      visibility: hidden;
      transition: opacity 160ms ease;
    }

    .wire-nav__item:hover > .wire-nav__submenu,
    .wire-nav__item:focus-within > .wire-nav__submenu {
      opacity: 1;
      visibility: visible;
    }

    .wire-nav__submenu--level3 {
      top: 0;
      left: 100%;
    }

    .wire-nav__toggle {
      display: none;
      align-items: center;
      gap: 0.5rem;
      padding: 0.45rem 0.7rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-sm);
      background: var(--wire-color-surface);
      color: var(--wire-color-text);
      font: inherit;
      font-size: 0.9rem;
      cursor: pointer;
    }

    .wire-nav__toggle-bar {
      width: 1rem;
      height: 2px;
      background: currentColor;
      box-shadow:
        0 -5px 0 currentColor,
        0 5px 0 currentColor;
    }

    /*
     * On a phone the bar becomes a disclosure: submenus stop being floating
     * overlays and stack inline, because a hover-opened overlay is
     * unreachable on touch.
     */
    @media (max-width: 767px) {
      .wire-nav__toggle {
        display: inline-flex;
      }

      .wire-nav__list {
        flex-direction: column;
        align-items: stretch;
      }

      .wire-nav[data-expanded="false"] .wire-nav__list {
        display: none;
      }

      .wire-nav__submenu,
      .wire-nav__submenu--level3 {
        position: static;
        opacity: 1;
        visibility: visible;
        border: 0;
        box-shadow: none;
        padding-left: 1rem;
      }
    }
  }
}
```

---

## Navbar

Showcase: https://component.wrnexusjs.dev/
Mount: <Navbar /> (legacy: data-component="Navbar")
Category: navigation
Purpose: Theme-aware, responsive navbar component.
Props: size: string = "default", color: string = "primary", label: string = "Primary navigation", topbarLabel: string = "Utility navigation", brand: Record<string, unknown> = {}, items: unknown[] = [], actions: unknown[] = [], active: string = "", sticky: boolean = false, openOnHover: boolean = false, maxWidth: string = "full", mobileLabel: string = "Toggle navigation", class: string = ""
Slots: topbar, actions
Events: toggle, open, close, select, action

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component Navbar {
  outputs {
    toggle(payload: { open: boolean })
    open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    select(payload: { item: string | number | boolean | null | object; value: string | number | boolean; level: string })
    action(payload: { item: string | number | boolean | null | object; value: string | number | boolean })
  }

  props {
size: string = "default"
    color: string = "primary"
    label: string = "Primary navigation"
    topbarLabel: string = "Utility navigation"
    brand: Record<string, unknown> = {}
    items: unknown[] = []
    actions: unknown[] = []
    active: string = ""
    sticky: boolean = false
    openOnHover: boolean = false
    maxWidth: string = "full"
    mobileLabel: string = "Toggle navigation"
    class: string = ""
  }

  state mobileOpen: boolean = false

  functions {
    client function toggleNavigation() {
      mobileOpen = !mobileOpen
      output.toggle({ open: mobileOpen })
      output[mobileOpen ? "open" : "close"]({ source: "mobile" })
    }

    client function selectItem(item, level) {
      mobileOpen = false
      output.select({ item: item, value: item.value || "", level: level })
    }

    client function selectAction(item) {
      mobileOpen = false
      output.action({ item: item, value: item.value || "" })
    }

    shared function isItemActive(item) {
      if (!item) return false
      if (item.value && item.value === active) return true
      if (!item.children || !item.children.length) return false
      return item.children.some(function (child) {
        return isItemActive(child)
      })
    }

    client function toggleDropdown(event, item) {
      output[event.currentTarget.open ? "open" : "close"]({
        source: "menu",
        item: item
      })
    }
  }

  view {
    <header class="wire-navbar wire-navbar--width-{maxWidth} wire-component--color-{color} wire-component--size-{size} {sticky ? 'wire-navbar--sticky' : ''} {class}" data-wrn-navbar data-open-on-hover="{openOnHover}">
      <div class="wire-navbar__topbar" aria-label="{topbarLabel}">
        <slot name="topbar" />
      </div>

      <div class="wire-navbar__main">
        <a class="wire-navbar__brand" href="{brand.href || '/'}" aria-label="{brand.ariaLabel || brand.label || label}">
          {#if brand.logo}
            <img class="wire-navbar__brand-logo" src="{brand.logo}" alt="{brand.logoAlt || brand.label || ''}" width="{brand.logoWidth || ''}" height="{brand.logoHeight || ''}" />
          {/if}
          {#if brand.icon}
            <span class="wire-navbar__brand-icon {brand.icon}" aria-hidden="true"></span>
          {/if}
          {#if brand.label || brand.description}
            <span class="wire-navbar__brand-copy">
              {#if brand.label}<strong>{brand.label}</strong>{/if}
              {#if brand.description}<small>{brand.description}</small>{/if}
            </span>
          {/if}
        </a>

        <button type="button" class="wire-navbar__toggle" aria-label="{mobileLabel}" aria-expanded="{mobileOpen}" @click="toggleNavigation()">
          <span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
        </button>

        <div class="wire-navbar__collapse {mobileOpen ? 'is-open' : ''}">
          <nav class="wire-navbar__menus" aria-label="{label}" data-wrn-roving="horizontal">
            {#each items as item}
              {#if item.children && item.children.length}
                <details class="wire-navbar__dropdown wire-navbar__dropdown--{item.type || 'dropdown'}" name="wire-navbar-menu" @toggle="toggleDropdown(event, item)">
                  <summary data-wrn-roving-item="true" aria-current="{isItemActive(item) ? 'page' : 'false'}">
                    {#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
                    <span>{item.label}</span>
                    <span class="wire-navbar__chevron" aria-hidden="true"></span>
                  </summary>
                  <div class="wire-navbar__panel wire-navbar__panel--columns-{item.columns || 1}">
                    {#if item.description}<p class="wire-navbar__panel-intro">{item.description}</p>{/if}
                    {#each item.children as child}
                      <div class="wire-navbar__group">
                        {#if child.children && child.children.length}
                          {#if child.label}<strong class="wire-navbar__group-title">{child.label}</strong>{/if}
                          {#if child.description}<small>{child.description}</small>{/if}
                          {#each child.children as nested}
                            <a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : 'false'}" @click="selectItem(nested, 3)">
                              {#if nested.icon}<span class="{nested.icon}" aria-hidden="true"></span>{/if}
                              <span><strong>{nested.label}</strong>{#if nested.description}<small>{nested.description}</small>{/if}</span>
                            </a>
                          {/each}
                        {:else}
                          <a href="{child.href || '#'}" target="{child.target || ''}" rel="{child.rel || ''}" aria-current="{child.value === active ? 'page' : 'false'}" @click="selectItem(child, 2)">
                            {#if child.icon}<span class="{child.icon}" aria-hidden="true"></span>{/if}
                            <span><strong>{child.label}</strong>{#if child.description}<small>{child.description}</small>{/if}</span>
                          </a>
                        {/if}
                      </div>
                    {/each}
                  </div>
                </details>
              {:else}
                <a class="wire-navbar__menu-link" data-wrn-roving-item="true" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{item.value === active ? 'page' : 'false'}" @click="selectItem(item, 1)">
                  {#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
                  <span>{item.label}</span>
                </a>
              {/if}
            {/each}
          </nav>

          <div class="wire-navbar__actions">
            {#each actions as item}
              <a class="wire-navbar__action wire-navbar__action--{item.variant || 'link'}" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" @click="selectAction(item)">
                {#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
                <span>{item.label}</span>
              </a>
            {/each}
            <slot name="actions" />
          </div>
        </div>
      </div>
    </header>
    <ComponentBaseStyles hidden />
  }

  style {
    /* Full application navigation */
    .wire-navbar {
      position: relative;
      z-index: 40;
      width: 100%;
      color: var(--wire-color-text);
      background: color-mix(in srgb, var(--wire-color-surface) 96%, transparent);
      border-bottom: 1px solid var(--wire-color-border);
    }

    .wire-navbar--sticky {
      position: sticky;
      top: 0;
      backdrop-filter: blur(16px);
    }

    .wire-navbar__topbar:empty,
    .wire-navbar__topbar:not(:has(*)) {
      display: none;
    }

    .wire-navbar__topbar:not(:empty) {
      display: flex;
      min-height: 2.25rem;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
      padding: 0.35rem clamp(1rem, 4vw, 3rem);
      color: var(--wire-color-muted);
      background: var(--wire-color-surface-2);
      border-bottom: 1px solid var(--wire-color-border);
      font-size: 0.75rem;
      font-weight: 400;
    }

    .wire-navbar__main {
      display: flex;
      min-height: 4.25rem;
      width: min(100%, 90rem);
      margin-inline: auto;
      padding: 0.75rem clamp(1rem, 4vw, 3rem);
      align-items: center;
      gap: 1.5rem;
    }

    .wire-navbar--width-full .wire-navbar__main {
      width: 100%;
      max-width: none;
    }

    .wire-navbar--width-compact .wire-navbar__main {
      width: min(86%, 80rem);
    }

    .wire-navbar__brand,
    .wire-navbar__menu-link,
    .wire-navbar__dropdown summary,
    .wire-navbar__action,
    .wire-navbar__panel a {
      color: inherit;
      text-decoration: none;
    }

    .wire-navbar__brand {
      display: inline-flex;
      min-width: 0;
      align-items: center;
      gap: 0.75rem;
      flex: 0 0 auto;
    }

    .wire-navbar__brand-logo {
      display: block;
      width: auto;
      max-width: 11rem;
      height: 2.5rem;
      object-fit: contain;
    }

    .wire-navbar__brand-icon {
      width: 2.5rem;
      height: 2.5rem;
      color: var(--wire-component-color, var(--wire-color-primary));
    }

    .wire-navbar__brand-copy {
      display: grid;
      min-width: 0;
      line-height: 1.2;
    }

    .wire-navbar__brand-copy strong {
      font-size: 0.9rem;
      font-weight: 600;
    }

    .wire-navbar__brand-copy small {
      margin-top: 0.2rem;
      color: var(--wire-color-muted);
      font-size: 0.6875rem;
      font-weight: 400;
    }

    .wire-navbar__collapse,
    .wire-navbar__menus,
    .wire-navbar__actions {
      display: flex;
      align-items: center;
    }

    .wire-navbar__collapse {
      min-width: 0;
      flex: 1;
      gap: 1rem;
    }

    .wire-navbar__menus {
      justify-content: center;
      flex: 1;
      gap: 0.25rem;
    }

    .wire-navbar__actions {
      justify-content: flex-end;
      gap: 0.5rem;
    }

    .wire-navbar__menu-link,
    .wire-navbar__dropdown > summary,
    .wire-navbar__action {
      display: inline-flex;
      min-height: 2.75rem;
      padding: 0.65rem 0.8rem;
      align-items: center;
      gap: 0.45rem;
      border-radius: 0.65rem;
      cursor: pointer;
      font-size: 0.8125rem;
      font-weight: 500;
      white-space: nowrap;
    }

    .wire-navbar__menu-link:hover,
    .wire-navbar__dropdown > summary:hover {
      color: var(--wire-color-text);
      background: var(--wire-color-surface-2);
    }

    .wire-navbar__menu-link[aria-current="page"],
    .wire-navbar__dropdown > summary[aria-current="page"] {
      color: var(--wire-component-color, var(--wire-color-primary));
      font-weight: 600;
      background: color-mix(
        in srgb,
        var(--wire-component-color, var(--wire-color-primary)) 12%,
        transparent
      );
      box-shadow: inset 0 -2px 0 var(--wire-component-color, var(--wire-color-primary));
    }

    .wire-navbar__menu-link[aria-current="page"]:hover,
    .wire-navbar__dropdown > summary[aria-current="page"]:hover {
      color: var(--wire-component-color, var(--wire-color-primary));
      background: color-mix(
        in srgb,
        var(--wire-component-color, var(--wire-color-primary)) 16%,
        transparent
      );
    }

    .wire-navbar__dropdown {
      position: relative;
    }

    .wire-navbar__dropdown > summary {
      list-style: none;
    }

    .wire-navbar__dropdown > summary::-webkit-details-marker {
      display: none;
    }

    .wire-navbar__chevron {
      width: 0.45rem;
      height: 0.45rem;
      border-right: 1.5px solid currentColor;
      border-bottom: 1.5px solid currentColor;
      transform: rotate(45deg) translateY(-0.15rem);
      transition: transform var(--wire-motion-fast, 150ms) ease;
    }

    .wire-navbar__dropdown[open] .wire-navbar__chevron {
      transform: rotate(225deg) translate(-0.1rem, -0.1rem);
    }

    .wire-navbar__panel {
      position: absolute;
      top: calc(100% + 0.55rem);
      left: 50%;
      display: grid;
      width: max-content;
      min-width: 15rem;
      max-width: min(90vw, 64rem);
      padding: 0.65rem;
      gap: 0.45rem;
      border: 1px solid var(--wire-color-border);
      border-radius: 0.9rem;
      color: var(--wire-color-text);
      background: var(--wire-color-surface);
      box-shadow: 0 18px 48px rgb(15 23 42 / 0.16);
      opacity: 1;
      transform: translateX(-50%) translateY(0) scale(1);
      transform-origin: top center;
      transition:
        opacity 180ms ease,
        transform 180ms cubic-bezier(0.22, 1, 0.36, 1),
        display 180ms allow-discrete;
    }

    .wire-navbar__panel::before {
      position: absolute;
      right: 0;
      bottom: 100%;
      left: 0;
      height: 0.65rem;
      content: "";
    }

    .wire-navbar__dropdown:not([open]) > .wire-navbar__panel {
      display: none;
      opacity: 0;
      transform: translateX(-50%) translateY(-0.5rem) scale(0.98);
    }

    @starting-style {
      .wire-navbar__dropdown[open] > .wire-navbar__panel {
        opacity: 0;
        transform: translateX(-50%) translateY(-0.5rem) scale(0.98);
      }
    }

    .wire-navbar__dropdown--mega {
      position: static;
    }

    .wire-navbar__dropdown--mega .wire-navbar__panel {
      right: auto;
      left: 50%;
      width: min(calc(100vw - 2rem), 64rem);
    }

    .wire-navbar__panel--columns-2 {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .wire-navbar__panel--columns-3 {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }

    .wire-navbar__panel--columns-4 {
      grid-template-columns: repeat(4, minmax(0, 1fr));
    }

    .wire-navbar__panel-intro {
      grid-column: 1 / -1;
      margin: 0;
      padding: 0.5rem 0.65rem;
      color: var(--wire-color-muted);
      font-size: 0.8125rem;
      font-weight: 400;
    }

    .wire-navbar__group {
      display: grid;
      align-content: start;
      gap: 0.25rem;
    }

    .wire-navbar__group-title {
      padding: 0.55rem 0.65rem 0.25rem;
      font-size: 0.7rem;
      font-weight: 600;
      letter-spacing: 0.04em;
      text-transform: uppercase;
    }

    .wire-navbar__group > small {
      padding: 0 0.65rem 0.4rem;
      color: var(--wire-color-muted);
      font-size: 0.7rem;
      font-weight: 400;
    }

    .wire-navbar__panel a {
      display: flex;
      min-height: 2.75rem;
      padding: 0.65rem;
      align-items: flex-start;
      gap: 0.65rem;
      border-radius: 0.6rem;
      font-size: 0.8rem;
      font-weight: 450;
    }

    .wire-navbar__panel a:hover {
      background: var(--wire-color-surface-2);
    }

    .wire-navbar__panel a span:last-child {
      display: grid;
      gap: 0.15rem;
    }

    .wire-navbar__panel a strong {
      font-weight: 500;
    }

    .wire-navbar__panel a small {
      color: var(--wire-color-muted);
      font-size: 0.7rem;
      font-weight: 400;
    }

    .wire-navbar__action {
      border: 1px solid transparent;
    }

    .wire-navbar__action--primary {
      color: var(--wire-color-on-primary, #fff);
      background: var(--wire-component-color, var(--wire-color-primary));
      font-weight: 600;
    }

    .wire-navbar__action--outline {
      border-color: var(--wire-color-border);
    }

    .wire-navbar__toggle {
      display: none;
      width: 2.75rem;
      height: 2.75rem;
      margin-left: auto;
      padding: 0.65rem;
      border: 1px solid var(--wire-color-border);
      border-radius: 0.65rem;
      color: inherit;
      background: var(--wire-color-surface);
    }

    .wire-navbar__toggle span {
      display: block;
      height: 2px;
      margin: 0.25rem 0;
      background: currentColor;
    }

    @media (max-width: 900px) {
    .wire-navbar--width-compact .wire-navbar__main {
        width: 100%;
      }
    .wire-navbar__toggle {
        display: block;
      }
    .wire-navbar__collapse {
        position: absolute;
        top: 100%;
        right: 0;
        left: 0;
        display: none;
        padding: 0.75rem 1rem 1rem;
        align-items: stretch;
        flex-direction: column;
        background: var(--wire-color-surface);
        border-bottom: 1px solid var(--wire-color-border);
        box-shadow: 0 16px 32px rgb(15 23 42 / 0.12);
        max-height: calc(100dvh - 4rem);
        overflow-x: hidden;
        overflow-y: auto;
        overscroll-behavior: contain;
      }
    .wire-navbar__collapse.is-open {
        display: flex;
      }
    .wire-navbar__menus,
      .wire-navbar__actions {
        width: 100%;
        align-items: stretch;
        flex-direction: column;
      }
    .wire-navbar__menu-link,
      .wire-navbar__dropdown > summary,
      .wire-navbar__action {
        width: 100%;
      }
    .wire-navbar__panel,
      .wire-navbar__dropdown--mega .wire-navbar__panel {
        position: static;
        width: 100%;
        max-width: none;
        margin-top: 0.25rem;
        box-shadow: none;
        transform: none;
        overflow: hidden;
        animation: wire-navbar-mobile-panel 180ms ease both;
      }
    .wire-navbar__dropdown:not([open]) > .wire-navbar__panel,
      .wire-navbar__dropdown[open] > .wire-navbar__panel {
        transform: none;
      }
    .wire-navbar__panel[class*="wire-navbar__panel--columns-"] {
        grid-template-columns: 1fr;
      }
    .wire-navbar__panel::before {
        display: none;
      }
    }

    @media (max-width: 600px) {
    .wire-navbar__brand-copy small {
        display: none;
      }
    }

    @keyframes wire-navbar-mobile-panel {
      from {
        opacity: 0;
        transform: translateY(-0.35rem);
      }
      to {
        opacity: 1;
        transform: translateY(0);
      }
    }
  }
}
```

---

## PageHeader

Showcase: https://component.wrnexusjs.dev/
Mount: <PageHeader /> (legacy: data-component="PageHeader")
Category: marketing
Purpose: Introduce an internal or public page with breadcrumbs, icon, title, description, and primary or secondary actions.
Props: eyebrow: string = "", title: string = "", description: string = "", icon: string = "", id: string = "page-title", align: string = "left", centered: boolean = false, compact: boolean = false, size: string = "default", maxWidth: string = "xl", showBreadcrumbs: boolean = false, breadcrumbs: unknown[] = [], breadcrumbParent: string = "", breadcrumbParentHref: string = "", breadcrumbCurrent: string = "", primaryLabel: string = "", primaryHref: string = "", primaryIcon: string = "", secondaryLabel: string = "", secondaryHref: string = "", secondaryIcon: string = "", color: string = "primary", variant: string = "default", borderBottom: boolean = true, class: string = ""
Slots: meta, actions, default
Events: none

### Complete .wrn source contract

```wrn
import Breadcrumb from "./Breadcrumb.wrn"
import Button from "./button.wrn"

component PageHeader {
  props {
    eyebrow: string = ""
    title: string = ""
    description: string = ""
    icon: string = ""
    id: string = "page-title"
    align: string = "left"
    centered: boolean = false
    compact: boolean = false
    size: string = "default"
    maxWidth: string = "xl"
    showBreadcrumbs: boolean = false
    breadcrumbs: unknown[] = []
    breadcrumbParent: string = ""
    breadcrumbParentHref: string = ""
    breadcrumbCurrent: string = ""
    primaryLabel: string = ""
    primaryHref: string = ""
    primaryIcon: string = ""
    secondaryLabel: string = ""
    secondaryHref: string = ""
    secondaryIcon: string = ""
    color: string = "primary"
    variant: string = "default"
    borderBottom: boolean = true
    class: string = ""
  }

  view {
    <section
      {...attrs}
      data-ui-component="PageHeader"
      data-size='{size}'
      data-align='{centered ? "center" : align}'
      data-color='{color}'
      data-variant='{variant}'
      data-compact='{compact ? "true" : "false"}'
      data-max-width='{maxWidth}'
      data-border-bottom='{borderBottom ? "true" : "false"}'
      aria-labelledby='{id}'
      class='wire-page-header {class}'
    >
      <div
        class="wire-page-header__decoration"
        aria-hidden="true"
      >
        <span
          class="wire-page-header__glow wire-page-header__glow--one"
        >
        </span>
        <span
          class="wire-page-header__glow wire-page-header__glow--two"
        >
        </span>
        <span
          class="wire-page-header__grid-pattern"
        >
        </span>
      </div>

      <div
        class="wire-page-header__inner"
      >
        {#if showBreadcrumbs && breadcrumbs.length > 0}
          <div
            class="wire-page-header__breadcrumb"
          >
            <Breadcrumb
              label="Breadcrumb"
              items='{breadcrumbs}'
              active='{breadcrumbs[breadcrumbs.length - 1]?.value || breadcrumbs[breadcrumbs.length - 1]?.label || breadcrumbs[breadcrumbs.length - 1]?.title || ""}'
              color='{color}'
              size='{compact ? "sm" : "default"}'
              variant='{variant === "solid" ? "contrast" : "minimal"}'
            />
          </div>
        {:else if showBreadcrumbs && breadcrumbParent}
          <div
            class="wire-page-header__breadcrumb"
          >
            <Breadcrumb
              label="Breadcrumb"
              items='{[
{
label: breadcrumbParent,
href: breadcrumbParentHref,
value: breadcrumbParent
},
{
label: breadcrumbCurrent || title,
value: breadcrumbCurrent || title,
current: true
}
]}'
              active='{breadcrumbCurrent || title}'
              color='{color}'
              size='{compact ? "sm" : "default"}'
              variant='{variant === "solid" ? "contrast" : "minimal"}'
            />
          </div>
        {:else if showBreadcrumbs && (breadcrumbCurrent || title)}
          <div
            class="wire-page-header__breadcrumb"
          >
            <Breadcrumb
              label="Breadcrumb"
              items='{[
{
label: breadcrumbCurrent || title,
value: breadcrumbCurrent || title,
current: true
}
]}'
              active='{breadcrumbCurrent || title}'
              color='{color}'
              size='{compact ? "sm" : "default"}'
              variant='{variant === "solid" ? "contrast" : "minimal"}'
            />
          </div>
        {/if}

        <div
          class="wire-page-header__layout"
        >
          <div
            class="wire-page-header__content"
          >
            <div
              class="wire-page-header__intro"
            >
              {#if icon}
                <span
                  class="wire-page-header__icon-wrap"
                >
                  {#if icon === "sparkles"}
                    <span
                      class="icon-[lucide--sparkles] wire-page-header__icon"
                      aria-hidden="true"
                    >
                    </span>
                  {:else if icon === "component"}
                    <span
                      class="icon-[lucide--component] wire-page-header__icon"
                      aria-hidden="true"
                    >
                    </span>
                  {:else}
                    <span
                      class='{icon} wire-page-header__icon'
                      aria-hidden="true"
                    >
                    </span>
                  {/if}
                </span>
              {/if}

              <div
                class="wire-page-header__copy"
              >
                {#if eyebrow}
                  <div
                    class="wire-page-header__eyebrow-row"
                  >
                    <span
                      class="wire-page-header__eyebrow-mark"
                      aria-hidden="true"
                    >
                    </span>
                    <p
                      class="wire-page-header__eyebrow"
                    >
                      {eyebrow}
                    </p>
                  </div>
                {/if}

                {#if title}
                  <h1
                    id='{id}'
                    class="wire-page-header__title"
                  >
                    {title}
                  </h1>
                {/if}

                {#if description}
                  <p
                    class="wire-page-header__description"
                  >
                    {description}
                  </p>
                {/if}

                <div
                  class="wire-page-header__meta"
                >
                  <slot
                    name="meta"
                  >
                  </slot>
                </div>
              </div>
            </div>
          </div>

          {#if primaryLabel || secondaryLabel}
            <div
              class="wire-page-header__actions"
            >
              {#if secondaryLabel}
                <Button
                  label='{secondaryLabel}'
                  href='{secondaryHref || "#"}'
                  icon='{secondaryIcon}'
                  size='{compact ? "sm" : "default"}'
                  variant="outline"
                  color='{color}'
                  class="wire-page-header__action wire-page-header__action--secondary"
                />
              {/if}

              {#if primaryLabel}
                <Button
                  label='{primaryLabel}'
                  href='{primaryHref || "#"}'
                  icon='{primaryIcon}'
                  size='{compact ? "sm" : "default"}'
                  variant="default"
                  color='{color}'
                  class="wire-page-header__action wire-page-header__action--primary"
                />
              {/if}

              <slot
                name="actions"
              >
              </slot>
            </div>
          {:else}
            <div
              class="wire-page-header__actions wire-page-header__actions--slot-only"
            >
              <slot
                name="actions"
              >
              </slot>
            </div>
          {/if}
        </div>

        <div
          class="wire-page-header__body"
        >
          <slot></slot>
        </div>
      </div>
    </section>
  }

  style {
    .wire-page-header {
      --page-header-accent: var(--wire-color-primary);
      --page-header-soft: var(--wire-color-primary-soft);
      --page-header-muted: var(--wire-color-primary-muted);
      --page-header-accent-text: var(--wire-color-primary-text);

      position: relative;
      isolation: isolate;
      width: 100%;
      overflow: hidden;
      color: var(--wire-color-text);
      background: var(--wire-color-background);
    }

    .wire-page-header[data-color="secondary"] {
      --page-header-accent: var(--wire-color-secondary);
      --page-header-soft: var(--wire-color-secondary-soft);
      --page-header-muted: var(--wire-color-secondary-muted);
      --page-header-accent-text: var(--wire-color-secondary-text);
    }

    .wire-page-header[data-color="info"] {
      --page-header-accent: var(--wire-color-info);
      --page-header-soft: var(--wire-color-info-soft);
      --page-header-muted: var(--wire-color-info-muted);
      --page-header-accent-text: var(--wire-color-info-text);
    }

    .wire-page-header[data-color="success"] {
      --page-header-accent: var(--wire-color-success);
      --page-header-soft: var(--wire-color-success-soft);
      --page-header-muted: var(--wire-color-success-muted);
      --page-header-accent-text: var(--wire-color-success-text);
    }

    .wire-page-header[data-color="warning"] {
      --page-header-accent: var(--wire-color-warning);
      --page-header-soft: var(--wire-color-warning-soft);
      --page-header-muted: var(--wire-color-warning-muted);
      --page-header-accent-text: var(--wire-color-warning-text);
    }

    .wire-page-header[data-color="danger"] {
      --page-header-accent: var(--wire-color-danger);
      --page-header-soft: var(--wire-color-danger-soft);
      --page-header-muted: var(--wire-color-danger-muted);
      --page-header-accent-text: var(--wire-color-danger-text);
    }

    .wire-page-header[data-variant="soft"] {
      background: var(--wire-color-surface);
    }

    .wire-page-header[data-variant="raised"] {
      background: var(--wire-color-surface-raised);
      box-shadow: var(--wire-shadow-sm);
    }

    .wire-page-header[data-variant="tinted"] {
      background:
      linear-gradient(135deg, var(--page-header-soft), transparent 72%),
      var(--wire-color-background);
    }

    .wire-page-header[data-variant="solid"] {
      color: #ffffff;
      background: var(--page-header-accent);
    }

    .wire-page-header[data-variant="minimal"] {
      background: transparent;
    }

    .wire-page-header[data-border-bottom="true"] {
      border-bottom: 1px solid var(--wire-color-border);
    }

    .wire-page-header[data-variant="solid"][data-border-bottom="true"] {
      border-bottom-color: rgba(255, 255, 255, 0.18);
    }

    .wire-page-header__decoration {
      position: absolute;
      inset: 0;
      z-index: -1;
      overflow: hidden;
      pointer-events: none;
    }

    .wire-page-header__glow {
      position: absolute;
      display: block;
      border-radius: 999px;
      filter: blur(60px);
      opacity: 0.17;
    }

    .wire-page-header__glow--one {
      top: -9rem;
      right: -5rem;
      width: 24rem;
      height: 24rem;
      background: var(--page-header-accent);
    }

    .wire-page-header__glow--two {
      bottom: -10rem;
      left: 18%;
      width: 18rem;
      height: 18rem;
      background: var(--page-header-muted);
      opacity: 0.13;
    }

    .wire-page-header__grid-pattern {
      position: absolute;
      inset: 0;
      opacity: 0.2;
      background-image:
      linear-gradient(to right, var(--wire-color-border) 1px, transparent 1px),
      linear-gradient(to bottom, var(--wire-color-border) 1px, transparent 1px);
      background-size: 32px 32px;
      mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.35), transparent 78%);
    }

    .wire-page-header[data-variant="minimal"] .wire-page-header__decoration,
    .wire-page-header[data-compact="true"] .wire-page-header__grid-pattern {
      display: none;
    }

    .wire-page-header[data-variant="solid"] .wire-page-header__grid-pattern {
      opacity: 0.1;
      background-image:
      linear-gradient(to right, rgba(255, 255, 255, 0.28) 1px, transparent 1px),
      linear-gradient(to bottom, rgba(255, 255, 255, 0.28) 1px, transparent 1px);
    }

    .wire-page-header__inner {
      position: relative;
      width: 100%;
      max-width: 80rem;
      margin-inline: auto;
      padding: 1.25rem 1rem 2.75rem;
    }

    .wire-page-header[data-max-width="compact"] .wire-page-header__inner,
    .wire-page-header[data-max-width="lg"] .wire-page-header__inner {
      max-width: 64rem;
    }

    .wire-page-header[data-max-width="wide"] .wire-page-header__inner,
    .wire-page-header[data-max-width="2xl"] .wire-page-header__inner {
      max-width: 90rem;
    }

    .wire-page-header[data-max-width="full"] .wire-page-header__inner {
      max-width: none;
    }

    .wire-page-header[data-compact="true"] .wire-page-header__inner {
      padding-top: 1rem;
      padding-bottom: 1.75rem;
    }

    .wire-page-header[data-size="lg"] .wire-page-header__inner {
      padding-bottom: 3.5rem;
    }

    .wire-page-header__breadcrumb {
      display: flex;
      align-items: center;
      min-width: 0;
      margin-bottom: 1.75rem;
      padding-bottom: 0.9rem;
      border-bottom: 1px solid var(--wire-color-border);
    }

    .wire-page-header[data-variant="solid"] .wire-page-header__breadcrumb {
      border-bottom-color: rgba(255, 255, 255, 0.18);
    }

    .wire-page-header[data-compact="true"] .wire-page-header__breadcrumb {
      margin-bottom: 1.25rem;
      padding-bottom: 0.7rem;
    }

    .wire-page-header__layout {
      display: grid;
      grid-template-columns: minmax(0, 1fr);
      gap: 1.75rem;
      align-items: start;
    }

    .wire-page-header__content,
    .wire-page-header__copy {
      min-width: 0;
    }

    .wire-page-header__intro {
      display: flex;
      align-items: flex-start;
      gap: 1rem;
      min-width: 0;
    }

    .wire-page-header__icon-wrap {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex: 0 0 auto;
      width: 3.25rem;
      height: 3.25rem;
      margin-top: 0.15rem;
      color: var(--page-header-accent);
      background: var(--page-header-soft);
      border: 1px solid var(--page-header-muted);
      border-radius: 1rem;
      box-shadow: var(--wire-shadow-sm);
    }

    .wire-page-header[data-compact="true"] .wire-page-header__icon-wrap {
      width: 2.75rem;
      height: 2.75rem;
      border-radius: 0.8rem;
    }

    .wire-page-header[data-variant="solid"] .wire-page-header__icon-wrap {
      color: #ffffff;
      background: rgba(255, 255, 255, 0.12);
      border-color: rgba(255, 255, 255, 0.22);
    }

    .wire-page-header__icon {
      width: 1.35rem;
      height: 1.35rem;
    }

    .wire-page-header__eyebrow-row {
      display: flex;
      align-items: center;
      gap: 0.65rem;
      margin-bottom: 0.65rem;
    }

    .wire-page-header__eyebrow-mark {
      width: 1.75rem;
      height: 2px;
      flex: 0 0 auto;
      background: var(--page-header-accent);
      border-radius: 999px;
    }

    .wire-page-header__eyebrow {
      margin: 0;
      color: var(--page-header-accent-text);
      font-size: 0.72rem;
      font-weight: 800;
      line-height: 1.3;
      letter-spacing: 0.16em;
      text-transform: uppercase;
    }

    .wire-page-header[data-variant="solid"] .wire-page-header__eyebrow,
    .wire-page-header[data-variant="solid"] .wire-page-header__title,
    .wire-page-header[data-variant="solid"] .wire-page-header__description {
      color: #ffffff;
    }

    .wire-page-header[data-variant="solid"] .wire-page-header__eyebrow-mark {
      background: rgba(255, 255, 255, 0.8);
    }

    .wire-page-header__title {
      max-width: 54rem;
      margin: 0;
      color: var(--wire-color-text);
      font-size: clamp(2rem, 4vw, 3.75rem);
      font-weight: 800;
      line-height: 1.08;
      letter-spacing: -0.035em;
      text-wrap: balance;
    }

    .wire-page-header[data-compact="true"] .wire-page-header__title {
      font-size: clamp(1.75rem, 3vw, 2.65rem);
    }

    .wire-page-header[data-size="lg"] .wire-page-header__title {
      font-size: clamp(2.35rem, 5vw, 4.5rem);
    }

    .wire-page-header__description {
      max-width: 48rem;
      margin: 0.9rem 0 0;
      color: var(--wire-color-text-muted);
      font-size: 1rem;
      line-height: 1.75;
      text-wrap: pretty;
    }

    .wire-page-header[data-size="lg"] .wire-page-header__description {
      font-size: 1.075rem;
    }

    .wire-page-header[data-variant="solid"] .wire-page-header__description {
      opacity: 0.82;
    }

    .wire-page-header__meta:empty,
    .wire-page-header__body:empty,
    .wire-page-header__actions--slot-only:empty {
      display: none;
    }

    .wire-page-header__meta {
      margin-top: 1.1rem;
    }

    .wire-page-header__actions {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 0.75rem;
      min-width: 0;
    }

    /*
     * Slot content is app-authored, so a bare `p { color: ... }` in the app
     * stylesheet beats anything this container merely passes down by
     * inheritance. State it, at a specificity the app can still override.
     */
    .wire-page-header__body {
      color: var(--wire-color-text);
      margin-top: 1.75rem;
    }

    .wire-page-header[data-align="center"] .wire-page-header__layout {
      justify-items: center;
      text-align: center;
    }

    .wire-page-header[data-align="center"] .wire-page-header__intro {
      flex-direction: column;
      align-items: center;
    }

    .wire-page-header[data-align="center"] .wire-page-header__eyebrow-row,
    .wire-page-header[data-align="center"] .wire-page-header__actions {
      justify-content: center;
    }

    .wire-page-header[data-align="center"] .wire-page-header__title,
    .wire-page-header[data-align="center"] .wire-page-header__description {
      margin-inline: auto;
    }

    .wire-page-header[data-align="right"] .wire-page-header__layout {
      justify-items: end;
      text-align: right;
    }

    .wire-page-header[data-align="right"] .wire-page-header__intro {
      flex-direction: row-reverse;
    }

    .wire-page-header[data-align="right"] .wire-page-header__eyebrow-row,
    .wire-page-header[data-align="right"] .wire-page-header__actions {
      justify-content: flex-end;
    }

    .wire-page-header[data-align="right"] .wire-page-header__title,
    .wire-page-header[data-align="right"] .wire-page-header__description {
      margin-left: auto;
    }

    @media (min-width: 640px) {
      .wire-page-header__inner {
        padding-inline: 1.5rem;
      }

      .wire-page-header__intro {
        gap: 1.25rem;
      }
    }

    @media (min-width: 1024px) {
      .wire-page-header__inner {
        padding-inline: 2rem;
      }

      .wire-page-header__layout {
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: start;
        gap: 2.5rem;
      }

      .wire-page-header[data-align="center"] .wire-page-header__layout,
      .wire-page-header[data-align="right"] .wire-page-header__layout {
        grid-template-columns: minmax(0, 1fr);
      }

      .wire-page-header__actions {
        align-self: start;
        justify-self: end;
        justify-content: flex-end;
        padding-top: 0;
        padding-bottom: 0;
      }
    }

    @media (max-width: 639px) {
      .wire-page-header__intro {
        flex-direction: column;
      }

      .wire-page-header__actions {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
      }

      .wire-page-header__action {
        width: 100%;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .wire-page-header__glow {
        filter: blur(52px);
      }
    }
  }
}
```

---

## Pagination

Showcase: https://component.wrnexusjs.dev/
Mount: <Pagination /> (legacy: data-component="Pagination")
Category: navigation
Purpose: Theme-aware, responsive pagination component.
Props: color: string = "primary", size: string = "default", page: number = 1, pageSize: number = 10, total: number = 0, variant: string = "compact", siblingCount: number = 1, showSummary: boolean = true, label: string = "Pagination", previousLabel: string = "Previous", nextLabel: string = "Next", class: string = ""
Slots: default
Events: change, previous, next

### Complete .wrn source contract

```wrn
// Pagination -- page controls over a known total.
//
//   <Pagination page={2} pageSize={10} total={137} variant="numbered" />
//
// The component owns no data. It reports the requested page through its
// change output and lets the caller fetch or slice.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Pagination {
  outputs {
    change(payload: { page: number; pageSize: number })
    previous(payload: { page: number })
    next(payload: { page: number })
  }

  props {
    color: string = "primary"
    size: string = "default"
    page: number = 1
    pageSize: number = 10
    total: number = 0
    variant: string = "compact"
    siblingCount: number = 1
    showSummary: boolean = true
    label: string = "Pagination"
    previousLabel: string = "Previous"
    nextLabel: string = "Next"
    class: string = ""
  }

  functions {
    shared function lastPage() {
      var size = Number(pageSize) > 0 ? Number(pageSize) : 10
      var count = Number(total) > 0 ? Number(total) : 0
      return Math.max(1, Math.ceil(count / size))
    }

    // Out-of-range values arrive routinely: page travels as an HTML attribute
    // and callers compute it from data that may have shrunk underneath them.
    shared function currentPage() {
      var value = Number(page)
      if (!value || value < 1) {
        return 1
      }
      return Math.min(value, lastPage())
    }

    shared function firstShown() {
      if (Number(total) < 1) {
        return 0
      }
      return (currentPage() - 1) * (Number(pageSize) || 10) + 1
    }

    shared function lastShown() {
      return Math.min(currentPage() * (Number(pageSize) || 10), Number(total) || 0)
    }

    shared function pageNumbers() {
      var last = lastPage()
      var current = currentPage()
      var siblings = Math.max(0, Number(siblingCount) || 0)
      var start = Math.max(1, current - siblings)
      var end = Math.min(last, current + siblings)
      var pages = []
      if (start > 1) {
        pages.push({ value: 1, label: "1", gap: false })
        if (start > 2) {
          pages.push({ value: 0, label: "...", gap: true })
        }
      }
      for (var index = start; index <= end; index += 1) {
        pages.push({ value: index, label: String(index), gap: false })
      }
      if (end < last) {
        if (end < last - 1) {
          pages.push({ value: 0, label: "...", gap: true })
        }
        pages.push({ value: last, label: String(last), gap: false })
      }
      return pages
    }

    client function goToPage(target) {
      var next = Math.min(Math.max(1, Number(target) || 1), lastPage())
      output.change({ page: next, pageSize: Number(pageSize) || 10 })
    }

    client function goPrevious() {
      var target = Math.max(1, currentPage() - 1)
      output.previous({ page: target })
      output.change({ page: target, pageSize: Number(pageSize) || 10 })
    }

    client function goNext() {
      var target = Math.min(lastPage(), currentPage() + 1)
      output.next({ page: target })
      output.change({ page: target, pageSize: Number(pageSize) || 10 })
    }
  }

  view {
    <nav
      {...attrs}
      data-ui-component="Pagination"
      class='wire-pagination {class}'
      data-variant='{variant}'
      data-color='{color}'
      data-size='{size}'
      role="navigation"
      aria-label='{label}'
    >
      <p class="wire-pagination__summary" data-show="showSummary">
        {firstShown()} to {lastShown()} of {total}
      </p>

      <div class="wire-pagination__controls">
        <button
          type="button"
          class="wire-pagination__step"
          aria-label='{previousLabel}'
          @click='goPrevious()'
        >
          <span class="wire-pagination__step-icon" aria-hidden="true">&#8249;</span>
          <span class="wire-pagination__step-label">{previousLabel}</span>
        </button>

        <ol class="wire-pagination__pages" data-show="variant === 'numbered'">
          {#each pageNumbers() as entry}
            <li class="wire-pagination__slot">
              <span class="wire-pagination__gap" data-show="entry.gap">{entry.label}</span>
              <button
                type="button"
                class="wire-pagination__page"
                data-show="!entry.gap"
                data-active='{entry.value === currentPage()}'
                aria-current='{entry.value === currentPage() ? "page" : "false"}'
                @click='goToPage(entry.value)'
              >
                {entry.label}
              </button>
            </li>
          {/each}
        </ol>

        <p class="wire-pagination__compact" data-show="variant !== 'numbered'">
          {currentPage()} / {lastPage()}
        </p>

        <button
          type="button"
          class="wire-pagination__step"
          aria-label='{nextLabel}'
          @click='goNext()'
        >
          <span class="wire-pagination__step-label">{nextLabel}</span>
          <span class="wire-pagination__step-icon" aria-hidden="true">&#8250;</span>
        </button>
      </div>

      <slot />
    </nav>
  }

  style {
    .wire-pagination {
      --pagination-accent: var(--wire-color-primary);
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      justify-content: space-between;
      gap: 0.75rem;
      max-width: 100%;
    }

    .wire-pagination[data-color="secondary"] {
      --pagination-accent: var(--wire-color-secondary);
    }

    .wire-pagination[data-color="success"] {
      --pagination-accent: var(--wire-color-success);
    }

    .wire-pagination[data-color="danger"] {
      --pagination-accent: var(--wire-color-danger);
    }

    .wire-pagination[data-color="info"] {
      --pagination-accent: var(--wire-color-info);
    }

    .wire-pagination[data-size="sm"] {
      font-size: 0.82rem;
    }

    .wire-pagination[data-size="lg"] {
      font-size: 1rem;
    }

    .wire-pagination__summary {
      margin: 0;
      color: var(--wire-color-text-muted);
      font-size: 0.82rem;
    }

    .wire-pagination__controls {
      display: flex;
      align-items: center;
      gap: 0.35rem;
      flex-wrap: wrap;
    }

    .wire-pagination__pages {
      display: flex;
      align-items: center;
      gap: 0.25rem;
      margin: 0;
      padding: 0;
      list-style: none;
    }

    .wire-pagination__slot {
      display: inline-flex;
    }

    .wire-pagination__page,
    .wire-pagination__step {
      appearance: none;
      display: inline-flex;
      align-items: center;
      gap: 0.35rem;
      min-width: 2.25rem;
      justify-content: center;
      padding: 0.4rem 0.6rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-sm);
      background: var(--wire-color-surface);
      color: var(--wire-color-text);
      font: inherit;
      font-size: 0.85rem;
      cursor: pointer;
    }

    .wire-pagination__page:hover,
    .wire-pagination__step:hover {
      background: var(--wire-color-surface-soft);
    }

    .wire-pagination__page:focus-visible,
    .wire-pagination__step:focus-visible {
      outline: 2px solid var(--pagination-accent);
      outline-offset: 2px;
    }

    .wire-pagination__page[data-active="true"] {
      border-color: var(--pagination-accent);
      background: var(--pagination-accent);
      color: var(--wire-color-primary-contrast);
      font-weight: 700;
    }

    .wire-pagination__gap {
      padding: 0 0.35rem;
      color: var(--wire-color-text-muted);
    }

    .wire-pagination__compact {
      margin: 0;
      padding: 0 0.5rem;
      color: var(--wire-color-text-muted);
      font-size: 0.85rem;
    }

    /* Below the small breakpoint the word labels crowd the arrows out. */
    @media (max-width: 639px) {
      .wire-pagination {
        justify-content: center;
      }

      .wire-pagination__step-label {
        display: none;
      }

      .wire-pagination__summary {
        width: 100%;
        text-align: center;
      }
    }
  }
}
```

---

## PinInput

Showcase: https://component.wrnexusjs.dev/
Mount: <PinInput /> (legacy: data-component="PinInput")
Category: advanced-forms
Purpose: Secure multi-cell PIN and verification-code input with regex and paste support.
Props: size: string = "default", color: string = "primary", label: string = "Verification code", name: string = "pin", value: string = "", length: number = 4, pattern: string = "[0-9]", type: string = "text", inputMode: string = "numeric", placeholder: string = "○", autocomplete: string = "one-time-code", masked: boolean = false, disabled: boolean = false, readonly: boolean = false, required: boolean = false, autoFocus: boolean = false, autoSubmit: boolean = false, allowPaste: boolean = true, clearable: boolean = true, clearLabel: string = "Clear code", separator: string = "", groupSize: number = 0, helpText: string = "", invalid: boolean = false, validationMessage: string = "", class: string = ""
Slots: none
Events: input, change, complete, paste, clear, error

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component PinInput {
  outputs {
    input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    complete(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    paste(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
    clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
  }

  props {
    size: string = "default"
    color: string = "primary"
    label: string = "Verification code"
    name: string = "pin"
    value: string = ""
    length: number = 4
    pattern: string = "[0-9]"
    type: string = "text"
    inputMode: string = "numeric"
    placeholder: string = "○"
    autocomplete: string = "one-time-code"
    masked: boolean = false
    disabled: boolean = false
    readonly: boolean = false
    required: boolean = false
    autoFocus: boolean = false
    autoSubmit: boolean = false
    allowPaste: boolean = true
    clearable: boolean = true
    clearLabel: string = "Clear code"
    separator: string = ""
    groupSize: number = 0
    helpText: string = ""
    invalid: boolean = false
    validationMessage: string = ""
    class: string = ""
  }

  functions {
    shared function inputIndexes() {
      return [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].slice(
        0,
        Math.max(1, Math.min(12, Number(length)))
      )
    }
  }

  view {
    <fieldset
      {...attrs}
      class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--pin-input {invalid ? 'wire-next--invalid' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
      data-wrn-pin-input
      data-length="{length}"
      data-pattern="{pattern}"
      data-allow-paste="{allowPaste ? 'true' : 'false'}"
      data-auto-submit="{autoSubmit ? 'true' : 'false'}"
      data-value="{value}"
      disabled="{disabled}"
      aria-describedby="{validationMessage ? `${name}-validation` : helpText ? `${name}-help` : ''}"
    >
      <div class="wire-next__pin-heading">
        <legend>{label}</legend>
        <button
          type="button"
          class="wire-next__pin-clear"
          data-pin-clear
          aria-label="{clearLabel}"
          title="{clearLabel}"
          style="{clearable && value ? '' : 'display: none'}"
        >
          <span class="icon-[lucide--rotate-ccw]" aria-hidden="true"></span>
          <span>{clearLabel}</span>
        </button>
      </div>

      <div class="wire-next__pin-cells" role="group" aria-label="{label}">
        {#each inputIndexes() as index}
          <input
            data-pin-cell
            data-pin-index="{index}"
            id="{name}-{index}"
            type="{masked ? 'password' : type}"
            inputmode="{inputMode}"
            maxlength="1"
            value="{value[index] || ''}"
            placeholder="{placeholder}"
            autocomplete="{index === 0 ? autocomplete : 'off'}"
            aria-label="{label} digit {index + 1} of {length}"
            aria-required="{required ? 'true' : 'false'}"
            readonly="{readonly}"
            disabled="{disabled}"
            autofocus="{autoFocus && index === 0}"
          />
          {#if separator && groupSize !== 0 && (index + 1) % groupSize === 0 && index + 1 !== length}
            <span class="wire-next__pin-separator" aria-hidden="true">{separator}</span>
          {/if}
        {/each}
      </div>

      <input
        type="hidden"
        name="{name}"
        value="{value}"
        required="{required}"
        data-pin-value
      />

      {#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
      <small
        id="{name}-validation"
        class="wire-next__validation"
        data-error="{name}"
      >{validationMessage}</small>
    </fieldset>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--pin-input.wire-next--invalid .wire-next__pin-cells input,
    .wire-next--pin-input:has(input.wire-invalid) .wire-next__pin-cells input {
      border-color: var(--wire-color-danger);
    }

    .wire-next--pin-input {
      display: grid;
      width: 100%;
      min-width: 0;
      gap: 0.65rem;
      padding: 0;
      border: 0;
      color: var(--wire-color-text);
    }

    .wire-next__pin-heading {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
    }

    .wire-next__pin-heading legend {
      padding: 0;
      font-size: 0.82em;
      font-weight: 700;
    }

    .wire-next__pin-clear {
      display: inline-flex;
      align-items: center;
      gap: 0.35rem;
      padding: 0;
      border: 0;
      color: var(--wire-component-color);
      background: transparent;
      font: inherit;
      font-size: 0.7em;
      font-weight: 750;
      cursor: pointer;
    }

    .wire-next__pin-clear > span:first-child {
      width: 0.9rem;
      height: 0.9rem;
    }

    .wire-next__pin-cells {
      display: flex;
      max-width: 100%;
      align-items: center;
      gap: clamp(0.35rem, 1.5vw, 0.65rem);
      overflow-x: auto;
      padding: 0.2rem;
      scrollbar-width: thin;
    }

    .wire-next--pin-input .wire-next__pin-cells input {
      flex: 0 0 clamp(2.4rem, 8vw, 3.25rem);
      width: clamp(2.4rem, 8vw, 3.25rem);
      height: clamp(2.75rem, 9vw, 3.5rem);
      min-width: clamp(2.4rem, 8vw, 3.25rem);
      padding: 0;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-sm);
      color: var(--wire-color-text);
      background: var(--wire-color-bg);
      font: inherit;
      font-size: clamp(1rem, 3vw, 1.25rem);
      font-weight: 750;
      text-align: center;
      caret-color: var(--wire-component-color);
      transition:
        border-color var(--wire-motion-base) var(--wire-ease-standard),
        box-shadow var(--wire-motion-base) var(--wire-ease-standard),
        transform var(--wire-motion-fast) var(--wire-ease-standard);
    }

    .wire-next--pin-input .wire-next__pin-cells input::placeholder {
      color: var(--wire-color-muted);
      opacity: 0.45;
    }

    .wire-next--pin-input .wire-next__pin-cells input:focus-visible {
      border-color: var(--wire-component-color);
      outline: 0;
      box-shadow: 0 0 0 3px color-mix(in srgb, var(--wire-component-color) 18%, transparent);
      transform: translateY(-1px);
    }

    .wire-next--pin-input .wire-next__pin-cells input:disabled,
    .wire-next--pin-input .wire-next__pin-cells input:read-only {
      cursor: not-allowed;
      opacity: 0.55;
    }

    .wire-next__pin-separator {
      color: var(--wire-color-muted);
      font-size: 1.1rem;
      font-weight: 800;
    }

    .wire-next--pin-input[data-complete="true"] .wire-next__pin-cells input {
      border-color: color-mix(in srgb, var(--wire-component-color) 72%, var(--wire-color-border));
      background: color-mix(in srgb, var(--wire-component-color) 7%, var(--wire-color-bg));
    }

    @media (max-width: 480px) {
    .wire-next__pin-cells {
        gap: 0.3rem;
      }
    .wire-next--pin-input .wire-next__pin-cells input {
        flex-basis: min(2.7rem, 12vw);
        width: min(2.7rem, 12vw);
        min-width: min(2.7rem, 12vw);
        height: min(3rem, 14vw);
      }
    }
  }
}
```

---

## Popover

Showcase: https://component.wrnexusjs.dev/
Mount: <Popover /> (legacy: data-component="Popover")
Category: overlays
Purpose: Display anchored supporting content with configurable trigger, placement, responsive sizing, and open or close events.
Props: open: boolean = false, defaultOpen: boolean = false, triggerLabel: string = "Open popover", triggerIcon: string = "", title: string = "", description: string = "", icon: string = "", placement: string = "bottom-start", width: string = "md", size: string = "default", color: string = "primary", variant: string = "raised", showArrow: boolean = true, showClose: boolean = false, closeLabel: string = "Close popover", closeOnOutside: boolean = true, closeOnEscape: boolean = true, actionLabel: string = "", actionHref: string = "", actionIcon: string = "", closeOnAction: boolean = true, disabled: boolean = false, class: string = ""
Slots: trigger, header, default, footer
Events: toggle, open, close, action

### Complete .wrn source contract

```wrn
component Popover {
  outputs {
    toggle(payload: { open: boolean; sourceEvent: Event; reason?: object })
    open(payload: { sourceEvent: Event })
    close(payload: { reason: string; sourceEvent: Event })
    action(payload: { href: string; sourceEvent: Event })
  }

  props {

    open: boolean = false
    defaultOpen: boolean = false
    triggerLabel: string = "Open popover"
    triggerIcon: string = ""
    title: string = ""
    description: string = ""
    icon: string = ""
    placement: string = "bottom-start"
    width: string = "md"
    size: string = "default"
    color: string = "primary"
    variant: string = "raised"
    showArrow: boolean = true
    showClose: boolean = false
    closeLabel: string = "Close popover"
    closeOnOutside: boolean = true
    closeOnEscape: boolean = true
    actionLabel: string = ""
    actionHref: string = ""
    actionIcon: string = ""
    closeOnAction: boolean = true
    disabled: boolean = false
    class: string = ""
  }

  state visible = defaultOpen

  functions {
    shared function isOpen() {
      return open || visible
    }

    client function showPopover(sourceEvent) {
      if (disabled) {
        return
      }
      visible = true
      output.open({ sourceEvent: sourceEvent })
      output.toggle({ open: true, sourceEvent: sourceEvent })
    }

    client function hidePopover(reason, sourceEvent) {
      visible = false
      output.close({ reason: reason, sourceEvent: sourceEvent })
      output.toggle({ open: false, reason: reason, sourceEvent: sourceEvent })
    }

    client function togglePopover(sourceEvent) {
      if (isOpen()) {
        hidePopover("toggle", sourceEvent)
      } else {
        showPopover(sourceEvent)
      }
    }

    client function activateAction(sourceEvent) {
      output.action({
        href: actionHref,
        sourceEvent: sourceEvent
      })
      if (closeOnAction) {
        hidePopover("action", sourceEvent)
      }
    }

    client function handleKeydown(sourceEvent) {
      if (closeOnEscape && sourceEvent.key === "Escape") {
        sourceEvent.preventDefault()
        hidePopover("escape", sourceEvent)
      }
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="Popover"
      data-open='{open || visible ? "true" : "false"}'
      data-placement='{placement}'
      data-width='{width}'
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      class='wire-popover {class}'
      @keydown='handleKeydown(event)'
    >
      <button
        type="button"
        class="wire-popover__trigger"
        disabled='{disabled}'
        aria-haspopup="dialog"
        aria-expanded='{open || visible ? "true" : "false"}'
        @click='togglePopover(event)'
      >
        <slot name="trigger"></slot>
        {#if triggerIcon}
          <span class='{triggerIcon}' aria-hidden="true"></span>
        {/if}
        {#if triggerLabel}
          <span>{triggerLabel}</span>
        {/if}
      </button>

      {#if closeOnOutside}
        <button
          type="button"
          class="wire-popover__dismiss-layer"
          data-show='{open || visible}'
          aria-label='{closeLabel}'
          @click='hidePopover("outside", event)'
        ></button>
      {/if}

      <section
        class="wire-popover__panel"
        data-wrn-anchored="true"
        data-show='{open || visible}'
        role="dialog"
        aria-label='{title || triggerLabel}'
      >
        {#if showArrow}
          <span class="wire-popover__arrow" data-wrn-anchor-arrow="true" aria-hidden="true"></span>
        {/if}

        {#if title || description || icon || showClose}
          <header class="wire-popover__header">
            <div class="wire-popover__heading">
              {#if icon}
                <span class='wire-popover__icon {icon}' aria-hidden="true"></span>
              {/if}
              <div class="wire-popover__heading-copy">
                <slot name="header"></slot>
                {#if title}
                  <h3>{title}</h3>
                {/if}
                {#if description}
                  <p>{description}</p>
                {/if}
              </div>
            </div>

            {#if showClose}
              <button
                type="button"
                class="wire-popover__close"
                aria-label='{closeLabel}'
                @click='hidePopover("close-button", event)'
              >
                <span class="icon-[lucide--x]" aria-hidden="true"></span>
              </button>
            {/if}
          </header>
        {/if}

        <div class="wire-popover__body">
          <slot></slot>
        </div>

        {#if actionLabel}
          <footer class="wire-popover__footer">
            {#if actionHref}
              <a
                href='{actionHref}'
                class="wire-popover__action"
                @click='activateAction(event)'
              >
                <span>{actionLabel}</span>
                {#if actionIcon}
                  <span class='{actionIcon}' aria-hidden="true"></span>
                {:else}
                  <span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
                {/if}
              </a>
            {:else}
              <button
                type="button"
                class="wire-popover__action"
                @click='activateAction(event)'
              >
                <span>{actionLabel}</span>
                {#if actionIcon}
                  <span class='{actionIcon}' aria-hidden="true"></span>
                {:else}
                  <span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
                {/if}
              </button>
            {/if}
          </footer>
        {/if}

        <slot name="footer"></slot>
      </section>
    </div>
  }

  style {
    .wire-popover {
      --popover-accent: var(--wire-color-primary);
      --popover-soft: var(--wire-color-primary-soft);
      --popover-contrast: var(--wire-color-primary-contrast);
      position: relative;
      display: inline-flex;
    }

    .wire-popover[data-color="secondary"] {
      --popover-accent: var(--wire-color-secondary);
      --popover-soft: var(--wire-color-secondary-soft);
      --popover-contrast: var(--wire-color-secondary-contrast);
    }

    .wire-popover[data-color="info"] {
      --popover-accent: var(--wire-color-info);
      --popover-soft: var(--wire-color-info-soft);
      --popover-contrast: var(--wire-color-info-contrast);
    }

    .wire-popover[data-color="success"] {
      --popover-accent: var(--wire-color-success);
      --popover-soft: var(--wire-color-success-soft);
      --popover-contrast: var(--wire-color-success-contrast);
    }

    .wire-popover[data-color="warning"] {
      --popover-accent: var(--wire-color-warning);
      --popover-soft: var(--wire-color-warning-soft);
      --popover-contrast: var(--wire-color-warning-text);
    }

    .wire-popover[data-color="danger"] {
      --popover-accent: var(--wire-color-danger);
      --popover-soft: var(--wire-color-danger-soft);
      --popover-contrast: var(--wire-color-on-danger);
    }

    .wire-popover__trigger {
      appearance: none;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 0.55rem;
      min-height: 2.55rem;
      padding: 0.65rem 0.9rem;
      color: var(--wire-color-text);
      background: var(--wire-color-surface-raised);
      border: 1px solid var(--wire-color-border);
      border-radius: 0.78rem;
      font: inherit;
      font-size: 0.84rem;
      font-weight: 600;
      cursor: pointer;
      transition: color 150ms ease, border-color 150ms ease, background-color 150ms ease;
    }

    .wire-popover__trigger:hover,
    .wire-popover__trigger:focus-visible,
    .wire-popover[data-open="true"] .wire-popover__trigger {
      color: var(--popover-accent);
      background: var(--popover-soft);
      border-color: color-mix(in srgb, var(--popover-accent) 38%, var(--wire-color-border));
      outline: none;
    }

    .wire-popover__trigger:disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }

    .wire-popover[data-size="sm"] .wire-popover__trigger {
      min-height: 2.2rem;
      padding: 0.5rem 0.72rem;
      font-size: 0.78rem;
    }

    .wire-popover[data-size="lg"] .wire-popover__trigger {
      min-height: 2.9rem;
      padding: 0.78rem 1.05rem;
      font-size: 0.9rem;
    }

    .wire-popover__dismiss-layer {
      position: fixed;
      inset: 0;
      z-index: 1120;
      appearance: none;
      padding: 0;
      background: transparent;
      border: 0;
    }

    .wire-popover__panel {
      position: absolute;
      z-index: 1121;
      top: calc(100% + 0.72rem);
      left: 0;
      width: 18rem;
      max-width: calc(100vw - 1.5rem);
      color: var(--wire-color-text);
      background:
        linear-gradient(145deg, color-mix(in srgb, var(--popover-accent) 5%, transparent), transparent 62%),
        color-mix(in srgb, var(--wire-color-surface-raised) 97%, transparent);
      border: 1px solid color-mix(in srgb, var(--popover-accent) 18%, var(--wire-color-border));
      border-radius: 1rem;
      box-shadow:
        0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
        0 24px 64px color-mix(in srgb, black 22%, transparent);
      backdrop-filter: blur(18px);
    }

    .wire-popover[data-width="sm"] .wire-popover__panel {
      width: 14rem;
    }

    .wire-popover[data-width="lg"] .wire-popover__panel {
      width: 24rem;
    }

    .wire-popover[data-width="xl"] .wire-popover__panel {
      width: 32rem;
    }

    .wire-popover[data-width="trigger"] .wire-popover__panel {
      width: 100%;
      min-width: 100%;
    }

    .wire-popover[data-placement="bottom-end"] .wire-popover__panel {
      right: 0;
      left: auto;
    }

    .wire-popover[data-placement="top-start"] .wire-popover__panel {
      top: auto;
      bottom: calc(100% + 0.72rem);
    }

    .wire-popover[data-placement="top-end"] .wire-popover__panel {
      top: auto;
      right: 0;
      bottom: calc(100% + 0.72rem);
      left: auto;
    }

    .wire-popover[data-placement="left"] .wire-popover__panel {
      top: 50%;
      right: calc(100% + 0.72rem);
      left: auto;
      transform: translateY(-50%);
    }

    .wire-popover[data-placement="right"] .wire-popover__panel {
      top: 50%;
      left: calc(100% + 0.72rem);
      transform: translateY(-50%);
    }

    .wire-popover[data-variant="soft"] .wire-popover__panel {
      background:
        linear-gradient(145deg, var(--popover-soft), transparent 70%),
        var(--wire-color-surface-raised);
      box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
    }

    .wire-popover[data-variant="outline"] .wire-popover__panel {
      background: var(--wire-color-surface-raised);
      box-shadow: none;
    }

    .wire-popover[data-variant="solid"] .wire-popover__panel {
      color: var(--popover-contrast);
      background: var(--popover-accent);
      border-color: color-mix(in srgb, white 20%, transparent);
    }

    .wire-popover__arrow {
      position: absolute;
      top: -0.35rem;
      left: 1.25rem;
      width: 0.7rem;
      height: 0.7rem;
      background: inherit;
      border-top: 1px solid color-mix(in srgb, var(--popover-accent) 18%, var(--wire-color-border));
      border-left: 1px solid color-mix(in srgb, var(--popover-accent) 18%, var(--wire-color-border));
      transform: rotate(45deg);
    }

    .wire-popover[data-placement="bottom-end"] .wire-popover__arrow {
      right: 1.25rem;
      left: auto;
    }

    .wire-popover[data-placement="top-start"] .wire-popover__arrow,
    .wire-popover[data-placement="top-end"] .wire-popover__arrow {
      top: auto;
      bottom: -0.35rem;
      transform: rotate(225deg);
    }

    .wire-popover[data-placement="left"] .wire-popover__arrow {
      top: 50%;
      right: -0.35rem;
      left: auto;
      transform: translateY(-50%) rotate(135deg);
    }

    .wire-popover[data-placement="right"] .wire-popover__arrow {
      top: 50%;
      left: -0.35rem;
      transform: translateY(-50%) rotate(-45deg);
    }

    .wire-popover__header {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 0.8rem;
      padding: 1rem 1rem 0.8rem;
      border-bottom: 1px solid var(--wire-color-border);
    }

    .wire-popover__heading {
      display: flex;
      align-items: flex-start;
      gap: 0.7rem;
      min-width: 0;
    }

    .wire-popover__icon {
      flex: 0 0 auto;
      width: 1.05rem;
      height: 1.05rem;
      margin-top: 0.15rem;
      color: var(--popover-accent);
    }

    .wire-popover[data-variant="solid"] .wire-popover__icon {
      color: currentColor;
    }

    .wire-popover__heading-copy {
      display: grid;
      gap: 0.2rem;
      min-width: 0;
    }

    .wire-popover__heading-copy h3,
    .wire-popover__heading-copy p {
      margin: 0;
    }

    .wire-popover__heading-copy h3 {
      font-size: 0.9rem;
      font-weight: 650;
    }

    .wire-popover__heading-copy p {
      color: var(--wire-color-text-muted);
      font-size: 0.74rem;
      line-height: 1.5;
    }

    .wire-popover[data-variant="solid"] .wire-popover__heading-copy p {
      color: color-mix(in srgb, currentColor 76%, transparent);
    }

    /*
     * padding is reset explicitly: an app-level `button { padding: ... }` rule
     * outranks the browser default and crushes the icon inside this
     * fixed-size button. Same trap as Modal and Drawer.
     */
    .wire-popover__close {
      padding: 0;
      appearance: none;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex: 0 0 auto;
      width: 2rem;
      height: 2rem;
      color: var(--wire-color-text-muted);
      background: var(--wire-color-surface-soft);
      border: 1px solid var(--wire-color-border);
      border-radius: 0.65rem;
      cursor: pointer;
    }

    .wire-popover__body {
      padding: 1rem;
      color: var(--wire-color-text-muted);
      font-size: 0.8rem;
      line-height: 1.6;
    }

    .wire-popover__footer {
      padding: 0 1rem 1rem;
    }

    .wire-popover__action {
      appearance: none;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 0.45rem;
      min-height: 2.35rem;
      padding: 0.58rem 0.8rem;
      color: var(--popover-contrast);
      background: var(--popover-accent);
      border: 0;
      border-radius: 0.7rem;
      font: inherit;
      font-size: 0.78rem;
      font-weight: 650;
      text-decoration: none;
      cursor: pointer;
    }

    @media (max-width: 639px) {
      .wire-popover__panel {
        position: fixed;
        right: 0.75rem;
        bottom: 0.75rem;
        left: 0.75rem;
        top: auto;
        width: auto;
        max-width: none;
        transform: none;
      }

      .wire-popover__arrow {
        display: none;
      }
    }
  }
}
```

---

## PortalDashboard

Showcase: https://component.wrnexusjs.dev/
Mount: <PortalDashboard /> (legacy: data-component="PortalDashboard")
Category: core
Purpose: Reusable portal dashboard component.
Props: size: string = "default", color: string = "primary", eyebrow: string = "Overview", eyebrowKey: string = "", title: string = "Dashboard", titleKey: string = "", description: string = "", descriptionKey: string = "", userName: string = "", metrics: unknown[] = [], actions: unknown[] = [], updates: unknown[] = [], tasks: unknown[] = [], class: string = ""
Slots: hero-action
Events: action, navigate

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component PortalDashboard {
  outputs {
    action(payload: { item: string | number | boolean | null | object; value: string | number | boolean; index: number })
    navigate(payload: { item: string | number | boolean | null | object; value: string | number | boolean; index: number })
  }

  props {
    size: string = "default"
    color: string = "primary"
    eyebrow: string = "Overview"
    eyebrowKey: string = ""
    title: string = "Dashboard"
    titleKey: string = ""
    description: string = ""
    descriptionKey: string = ""
    userName: string = ""
    metrics: unknown[] = []
    actions: unknown[] = []
    updates: unknown[] = []
    tasks: unknown[] = []
    class: string = ""
  }

  functions {
    client function chooseAction(item, index) {
      output.action({ item: item, value: item.value || "", index: index })
    }

    client function chooseNavigation(item, index) {
      output.navigate({ item: item, value: item.value || "", index: index })
    }
  }

  view {
    <main class="wire-portal-dashboard wire-component--color-{color} wire-component--size-{size} {class}">
      <header class="wire-portal-dashboard__hero">
        <div>
          {#if eyebrowKey}<span class="wire-portal-dashboard__eyebrow" data-t="{eyebrowKey}">{eyebrow}</span>{/if}
          {#if !eyebrowKey}<span class="wire-portal-dashboard__eyebrow">{eyebrow}</span>{/if}
          <h1>
            {#if titleKey}<span data-t="{titleKey}">{title}</span>{/if}
            {#if !titleKey}<span>{title}</span>{/if}
            {#if userName}, {userName}{/if}
          </h1>
          {#if descriptionKey}<p data-t="{descriptionKey}">{description}</p>{/if}
          {#if !descriptionKey && description}<p>{description}</p>{/if}
        </div>
        <div class="wire-portal-dashboard__hero-slot"><slot name="hero-action" /></div>
      </header>

      <section class="wire-portal-dashboard__metrics" aria-label="Summary">
        {#each metrics as item}
          <article class="wire-portal-dashboard__metric">
            <span class="wire-portal-dashboard__metric-icon {item.icon || 'icon-[lucide--activity]'}" aria-hidden="true"></span>
            <div><small>{item.label}</small><strong>{item.value}</strong>{#if item.detail}<span>{item.detail}</span>{/if}</div>
          </article>
        {/each}
      </section>

      <div class="wire-portal-dashboard__grid">
        <section class="wire-portal-dashboard__panel wire-portal-dashboard__panel--actions">
          <div class="wire-portal-dashboard__panel-heading"><div><small>Shortcuts</small><h2>Quick actions</h2></div></div>
          <div class="wire-portal-dashboard__actions">
            {#each actions as item, index}
              <a href="{item.href || '#'}" @click="chooseAction(item, index)">
                <span class="{item.icon || 'icon-[lucide--arrow-up-right]'}" aria-hidden="true"></span>
                <span><strong>{item.label}</strong>{#if item.description}<small>{item.description}</small>{/if}</span>
                <span class="icon-[lucide--chevron-right]" aria-hidden="true"></span>
              </a>
            {/each}
          </div>
        </section>

        <section class="wire-portal-dashboard__panel">
          <div class="wire-portal-dashboard__panel-heading"><div><small>Priority</small><h2>Tasks requiring attention</h2></div></div>
          <div class="wire-portal-dashboard__list">
            {#each tasks as item, index}
              <a href="{item.href || '#'}" @click="chooseNavigation(item, index)">
                <span class="wire-portal-dashboard__status wire-portal-dashboard__status--{item.status || 'default'}"></span>
                <span><strong>{item.label}</strong>{#if item.detail}<small>{item.detail}</small>{/if}</span>
                {#if item.meta}<em>{item.meta}</em>{/if}
              </a>
            {/each}
          </div>
        </section>

        <section class="wire-portal-dashboard__panel wire-portal-dashboard__panel--updates">
          <div class="wire-portal-dashboard__panel-heading"><div><small>Latest</small><h2>Recent updates</h2></div></div>
          <div class="wire-portal-dashboard__timeline">
            {#each updates as item}
              <article>
                <span class="{item.icon || 'icon-[lucide--bell]'}" aria-hidden="true"></span>
                <div><strong>{item.label}</strong>{#if item.detail}<p>{item.detail}</p>{/if}<small>{item.time || ''}</small></div>
              </article>
            {/each}
          </div>
        </section>
      </div>
    </main>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-portal-dashboard__status--urgent {
      background: var(--wire-color-danger);
    }

    .wire-portal-dashboard__status--warning {
      background: var(--wire-color-warning);
    }

    .wire-portal-dashboard__status--success {
      background: var(--wire-color-success);
    }

    /* Reusable responsive portal dashboard */
    .wire-portal-dashboard {
      width: min(100%, 96rem);
      margin-inline: auto;
      color: var(--wire-color-text);
    }

    .wire-portal-dashboard__hero {
      display: flex;
      padding: clamp(1.25rem, 3vw, 2rem);
      align-items: flex-end;
      justify-content: space-between;
      gap: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: 1rem;
      background:
        radial-gradient(
          circle at 85% 0%,
          color-mix(in srgb, var(--wire-color-primary) 20%, transparent),
          transparent 34%
        ),
        var(--wire-color-surface);
      box-shadow: var(--wire-shadow-sm);
    }

    .wire-portal-dashboard__eyebrow,
    .wire-portal-dashboard__panel-heading small {
      color: var(--wire-color-primary);
      font-size: 0.7rem;
      font-weight: 650;
      letter-spacing: 0.09em;
      text-transform: uppercase;
    }

    .wire-portal-dashboard__hero h1 {
      margin: 0.35rem 0 0;
      font-size: clamp(1.4rem, 3vw, 2.25rem);
      font-weight: 650;
      letter-spacing: -0.035em;
    }

    .wire-portal-dashboard__hero p {
      max-width: 48rem;
      margin: 0.5rem 0 0;
      color: var(--wire-color-muted);
      font-size: 0.875rem;
      line-height: 1.6;
    }

    .wire-portal-dashboard__metrics {
      display: grid;
      grid-template-columns: repeat(4, minmax(0, 1fr));
      margin-top: 1rem;
      gap: 1rem;
    }

    .wire-portal-dashboard__metric {
      display: flex;
      min-width: 0;
      padding: 1rem;
      align-items: center;
      gap: 0.85rem;
      border: 1px solid var(--wire-color-border);
      border-radius: 0.85rem;
      background: var(--wire-color-surface);
      box-shadow: var(--wire-shadow-sm);
    }

    .wire-portal-dashboard__metric-icon {
      width: 2.25rem;
      height: 2.25rem;
      flex: none;
      padding: 0.55rem;
      border-radius: 0.7rem;
      color: var(--wire-color-primary);
      background: color-mix(in srgb, var(--wire-color-primary) 12%, transparent);
    }

    .wire-portal-dashboard__metric div {
      display: grid;
      min-width: 0;
    }

    .wire-portal-dashboard__metric small,
    .wire-portal-dashboard__metric span {
      overflow: hidden;
      color: var(--wire-color-muted);
      font-size: 0.7rem;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .wire-portal-dashboard__metric strong {
      margin-block: 0.1rem;
      font-size: 1.3rem;
      font-weight: 650;
    }

    .wire-portal-dashboard__grid {
      display: grid;
      grid-template-columns: minmax(0, 1.2fr) minmax(18rem, 0.8fr);
      margin-top: 1rem;
      gap: 1rem;
    }

    .wire-portal-dashboard__panel {
      min-width: 0;
      padding: 1.1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: 0.9rem;
      background: var(--wire-color-surface);
      box-shadow: var(--wire-shadow-sm);
    }

    .wire-portal-dashboard__panel--actions {
      grid-row: span 2;
    }

    .wire-portal-dashboard__panel-heading {
      display: flex;
      margin-bottom: 0.9rem;
      align-items: center;
      justify-content: space-between;
    }

    .wire-portal-dashboard__panel-heading h2 {
      margin: 0.2rem 0 0;
      font-size: 1rem;
      font-weight: 600;
    }

    .wire-portal-dashboard__actions {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 0.65rem;
    }

    .wire-portal-dashboard__actions a,
    .wire-portal-dashboard__list a {
      display: flex;
      min-width: 0;
      padding: 0.8rem;
      align-items: center;
      gap: 0.7rem;
      border: 1px solid var(--wire-color-border);
      border-radius: 0.7rem;
      color: inherit;
      text-decoration: none;
      transition:
        border-color 150ms ease,
        background 150ms ease,
        transform 150ms ease;
    }

    .wire-portal-dashboard__actions a:hover,
    .wire-portal-dashboard__list a:hover {
      border-color: color-mix(in srgb, var(--wire-color-primary) 45%, var(--wire-color-border));
      background: var(--wire-color-surface-2);
      transform: translateY(-1px);
    }

    .wire-portal-dashboard__actions a > span:first-child {
      width: 1.15rem;
      height: 1.15rem;
      flex: none;
      color: var(--wire-color-primary);
    }

    .wire-portal-dashboard__actions a > span:nth-child(2),
    .wire-portal-dashboard__list a > span:nth-child(2) {
      display: grid;
      min-width: 0;
      flex: 1;
    }

    .wire-portal-dashboard__actions a > span:last-child {
      width: 0.9rem;
      height: 0.9rem;
      color: var(--wire-color-muted);
    }

    .wire-portal-dashboard__actions strong,
    .wire-portal-dashboard__list strong,
    .wire-portal-dashboard__timeline strong {
      font-size: 0.8rem;
      font-weight: 550;
    }

    .wire-portal-dashboard__actions small,
    .wire-portal-dashboard__list small,
    .wire-portal-dashboard__timeline p,
    .wire-portal-dashboard__timeline small {
      margin: 0.15rem 0 0;
      color: var(--wire-color-muted);
      font-size: 0.7rem;
      font-style: normal;
      line-height: 1.45;
    }

    .wire-portal-dashboard__list {
      display: grid;
      gap: 0.5rem;
    }

    .wire-portal-dashboard__list a {
      border: 0;
      border-bottom: 1px solid var(--wire-color-border);
      border-radius: 0;
    }

    .wire-portal-dashboard__status {
      width: 0.55rem;
      height: 0.55rem;
      flex: none;
      border-radius: 999px;
      background: var(--wire-color-muted);
    }

    .wire-portal-dashboard__list em {
      color: var(--wire-color-muted);
      font-size: 0.7rem;
      font-style: normal;
    }

    .wire-portal-dashboard__timeline {
      display: grid;
      gap: 0.8rem;
    }

    .wire-portal-dashboard__timeline article {
      display: flex;
      gap: 0.7rem;
    }

    .wire-portal-dashboard__timeline article > span {
      width: 1rem;
      height: 1rem;
      margin-top: 0.15rem;
      flex: none;
      color: var(--wire-color-primary);
    }

    .wire-portal-dashboard__timeline p {
      margin-block: 0.15rem;
    }

    @media (max-width: 1100px) {
    .wire-portal-dashboard__metrics {
        grid-template-columns: repeat(2, minmax(0, 1fr));
      }
    .wire-portal-dashboard__grid {
        grid-template-columns: 1fr;
      }
    .wire-portal-dashboard__panel--actions {
        grid-row: auto;
      }
    }

    @media (max-width: 640px) {
    .wire-portal-dashboard__hero {
        align-items: flex-start;
        flex-direction: column;
      }
    .wire-portal-dashboard__metrics,
      .wire-portal-dashboard__actions {
        grid-template-columns: 1fr;
      }
    }
  }
}
```

---

## PreferenceSwitcher

Showcase: https://component.wrnexusjs.dev/
Mount: <PreferenceSwitcher /> (legacy: data-component="PreferenceSwitcher")
Category: core
Purpose: Reusable preference switcher component.
Props: size: string = "default", color: string = "primary", themeLabel: string = "Theme", colorLabel: string = "Accent color", languageLabel: string = "Language", languages: string = [, colors: string = [, compact: boolean = true, class: string = ""
Slots: none
Events: theme, color, language

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component PreferenceSwitcher {
  outputs {
    theme(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
    color(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
    language(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
  }

  props {
size: string = "default"
    color: string = "primary"
    themeLabel: string = "Theme"
    colorLabel: string = "Accent color"
    languageLabel: string = "Language"
    languages = [
      {"label": "English", "shortLabel": "EN", "value": "en"},
      {"label": "हिन्दी", "shortLabel": "हि", "value": "hi"},
      {"label": "मराठी", "shortLabel": "म", "value": "mr"}
    ]
    colors = [
      {"label": "Blue", "value": "blue", "hex": "#2563eb"},
      {"label": "Cyan", "value": "cyan", "hex": "#0891b2"},
      {"label": "Emerald", "value": "emerald", "hex": "#059669"},
      {"label": "Violet", "value": "violet", "hex": "#7c3aed"},
      {"label": "Rose", "value": "rose", "hex": "#e11d48"},
      {"label": "Amber", "value": "amber", "hex": "#d97706"}
    ]
    compact: boolean = true
    class: string = ""
  }

  functions {
    client function choose(type, value) {
      output[type]({ value: value })
    }
  }

  view {
    <div class="wire-preferences wire-component--color-{color} wire-component--size-{size} {compact ? 'wire-preferences--compact' : ''} {class}" data-wrn-preferences aria-label="Display and language preferences">
      <details class="wire-preferences__menu" name="wire-preference-menu">
        <summary aria-label="{themeLabel}" title="{themeLabel}">
          <span class="icon-[lucide--sun-moon]" aria-hidden="true"></span>
          {#if !compact}<span>{themeLabel}</span>{/if}
        </summary>
        <div class="wire-preferences__panel">
          <strong>{themeLabel}</strong>
          <div class="wire-preferences__options">
            <button type="button" data-wire-theme-set="light" @click="choose('theme', 'light')"><span class="icon-[lucide--sun]" aria-hidden="true"></span>Light</button>
            <button type="button" data-wire-theme-set="dark" @click="choose('theme', 'dark')"><span class="icon-[lucide--moon]" aria-hidden="true"></span>Dark</button>
          </div>
        </div>
      </details>

      <details class="wire-preferences__menu" name="wire-preference-menu">
        <summary aria-label="{colorLabel}" title="{colorLabel}">
          <span class="icon-[lucide--palette]" aria-hidden="true"></span>
          {#if !compact}<span>{colorLabel}</span>{/if}
        </summary>
        <div class="wire-preferences__panel wire-preferences__panel--colors">
          <strong>{colorLabel}</strong>
          <div class="wire-preferences__swatches">
            {#each colors as item}
              <button type="button" data-wire-accent-set="{item.value}" aria-label="{item.label}" title="{item.label}" style="--wire-preference-swatch: {item.hex}" @click="choose('color', item.value)"></button>
            {/each}
          </div>
        </div>
      </details>

      <details class="wire-preferences__menu wire-preferences__menu--language" name="wire-preference-menu">
        <summary aria-label="{languageLabel}" title="{languageLabel}">
          <span class="icon-[lucide--languages]" aria-hidden="true"></span>
          <span class="wire-preferences__current-language" aria-hidden="true">EN</span>
          {#if !compact}<span>{languageLabel}</span>{/if}
        </summary>
        <div class="wire-preferences__panel wire-preferences__panel--language">
          <strong>{languageLabel}</strong>
          {#each languages as item}
            <button type="button" data-wire-lang-set="{item.value}" lang="{item.value}" @click="choose('language', item.value)">
              <span>{item.shortLabel}</span>{item.label}
            </button>
          {/each}
        </div>
      </details>
    </div>
    <ComponentBaseStyles hidden />
  }

  style {
    /* Portal display, accent, and language preferences */
    .wire-preferences {
      display: inline-flex;
      align-items: center;
      gap: 0.25rem;
    }

    .wire-preferences__menu {
      position: relative;
    }

    .wire-preferences__menu > summary {
      display: inline-flex;
      min-width: 2.25rem;
      min-height: 2.25rem;
      padding: 0.45rem 0.6rem;
      align-items: center;
      justify-content: center;
      gap: 0.4rem;
      border: 1px solid transparent;
      border-radius: 0.6rem;
      color: var(--wire-color-muted);
      cursor: pointer;
      list-style: none;
    }

    .wire-preferences__menu > summary::-webkit-details-marker {
      display: none;
    }

    .wire-preferences__menu > summary:hover,
    .wire-preferences__menu[open] > summary {
      color: var(--wire-color-text);
      border-color: var(--wire-color-border);
      background: var(--wire-color-surface);
    }

    .wire-preferences__menu > summary > span:first-child {
      width: 1rem;
      height: 1rem;
    }

    .wire-preferences__current-language {
      min-width: 1.25rem;
      color: var(--wire-color-text);
      font-size: 0.625rem;
      font-weight: 650;
      letter-spacing: 0.02em;
    }

    .wire-preferences__panel {
      position: absolute;
      z-index: 80;
      top: calc(100% + 0.5rem);
      right: 0;
      display: grid;
      width: max-content;
      min-width: 11rem;
      max-width: min(22rem, calc(100vw - 2rem));
      max-height: min(28rem, calc(100dvh - 2rem));
      overflow-y: auto;
      padding: 0.75rem;
      gap: 0.6rem;
      border: 1px solid var(--wire-color-border);
      border-radius: 0.8rem;
      color: var(--wire-color-text);
      background: var(--wire-color-surface);
      box-shadow: var(--wire-shadow-lg, 0 18px 48px rgb(0 0 0 / 0.18));
      animation: wire-component-enter 160ms ease both;
    }

    .wire-preferences__panel > strong {
      padding-inline: 0.25rem;
      font-size: 0.7rem;
      font-weight: 600;
      letter-spacing: 0.06em;
      text-transform: uppercase;
    }

    .wire-preferences__options {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 0.4rem;
    }

    .wire-preferences__panel button {
      display: inline-flex;
      min-height: 2.25rem;
      padding: 0.5rem 0.65rem;
      align-items: center;
      gap: 0.5rem;
      border-radius: 0.55rem;
      color: var(--wire-color-text);
      background: var(--wire-color-surface-2);
      font-size: 0.75rem;
      font-weight: 500;
    }

    .wire-preferences__panel button:hover {
      outline: 2px solid color-mix(in srgb, var(--wire-color-primary) 40%, transparent);
    }

    .wire-preferences__panel button[aria-pressed="true"] {
      color: var(--wire-color-primary);
      background: color-mix(in srgb, var(--wire-color-primary) 12%, var(--wire-color-surface));
      outline: 1px solid color-mix(in srgb, var(--wire-color-primary) 55%, transparent);
    }

    .wire-preferences__swatches {
      display: grid;
      grid-template-columns: repeat(3, 2rem);
      gap: 0.5rem;
    }

    .wire-preferences__swatches button {
      width: 2rem;
      min-height: 2rem;
      padding: 0;
      border: 3px solid var(--wire-color-surface);
      border-radius: 999px;
      background: var(--wire-preference-swatch);
      box-shadow: 0 0 0 1px var(--wire-color-border);
    }

    .wire-preferences__swatches button[aria-pressed="true"] {
      box-shadow:
        0 0 0 2px var(--wire-color-surface),
        0 0 0 4px var(--wire-color-primary);
    }

    .wire-preferences__panel--language button {
      width: 100%;
      background: transparent;
    }

    .wire-preferences__panel--language button span {
      display: grid;
      width: 1.65rem;
      height: 1.65rem;
      place-items: center;
      border-radius: 0.45rem;
      background: var(--wire-color-surface-2);
      font-size: 0.65rem;
    }

    .wire-preferences__panel--language button[aria-pressed="true"]::after {
      width: 0.9rem;
      height: 0.9rem;
      margin-left: auto;
      content: "";
      background: currentColor;
      mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m5 12 4 4L19 6'/%3E%3C/svg%3E")
        center / contain no-repeat;
    }

    @media (max-width: 640px) {
    .wire-preferences__panel {
        position: fixed;
        top: auto;
        right: 1rem;
        bottom: 1rem;
        left: 1rem;
        width: auto;
      }
    }
  }
}
```

---

## Progress

Showcase: https://component.wrnexusjs.dev/
Mount: <Progress /> (legacy: data-component="Progress")
Category: base
Purpose: Theme-aware, responsive progress component.
Props: size: string = "default", color: string = "primary", label: string = "Progress", value: number = 50, max: number = 100, showValue: boolean = true, class: string = ""
Slots: none
Events: none

### Complete .wrn source contract

```wrn
component Progress {
  props {
    size: string = "default"
    color: string = "primary"
    label: string = "Progress"
    value: number = 50
    max: number = 100
    showValue: boolean = true
    class: string = ""
  }
  view {
    <div class="wire-progress-component {class}" data-color="{color}" data-size="{size}">
      <div class="wire-progress-component__row"><span>{label}</span>{#if showValue}<strong>{value}%</strong>{/if}</div>
      <progress class="wire-progress-component__track" value="{value}" max="{max}" aria-label="{label}"></progress>
    </div>
  }

  style {
    .wire-progress-component {
      --wire-progress-color: var(--wire-color-primary);
      display: grid;
      width: 100%;
      gap: 0.5rem;
      color: var(--wire-color-text);
      font-size: 0.875rem;
    }

    .wire-progress-component[data-color="secondary"] { --wire-progress-color: var(--wire-color-secondary); }
    .wire-progress-component[data-color="success"] { --wire-progress-color: var(--wire-color-success); }
    .wire-progress-component[data-color="warning"] { --wire-progress-color: var(--wire-color-warning); }
    .wire-progress-component[data-color="danger"] { --wire-progress-color: var(--wire-color-danger); }
    .wire-progress-component[data-color="info"] { --wire-progress-color: var(--wire-color-info); }
    .wire-progress-component[data-size="xs"] { font-size: 0.75rem; }
    .wire-progress-component[data-size="lg"],
    .wire-progress-component[data-size="xl"] { font-size: 1rem; }

    .wire-progress-component__row {
      display: flex;
      justify-content: space-between;
      gap: 1rem;
    }

    .wire-progress-component__track {
      width: 100%;
      height: 0.6rem;
      overflow: hidden;
      border: 0;
      border-radius: 999px;
      background: var(--wire-color-surface-2);
      accent-color: var(--wire-progress-color);
    }

    .wire-progress-component__track::-webkit-progress-bar { background: var(--wire-color-surface-2); }
    .wire-progress-component__track::-webkit-progress-value { background: var(--wire-progress-color); }
    .wire-progress-component__track::-moz-progress-bar { background: var(--wire-progress-color); }
  }
}
```

---

## PublicPageShell

Showcase: https://component.wrnexusjs.dev/
Mount: <PublicPageShell /> (legacy: data-component="PublicPageShell")
Category: layout
Purpose: Provide the outer responsive structure, width, background, slots, overflow, and minimum-height behavior for public pages.
Props: maxWidth: string = "full", fullWidth: boolean = true, headerOffset: string = "none", background: string = "default", overflow: string = "clip", minHeight: string = "screen", size: string = "default", color: string = "primary", variant: string = "default", class: string = ""
Slots: before, default, after
Events: none

### Complete .wrn source contract

```wrn
component PublicPageShell {
  props {
    maxWidth: string = "full"
    fullWidth: boolean = true
    headerOffset: string = "none"
    background: string = "default"
    overflow: string = "clip"
    minHeight: string = "screen"
    size: string = "default"
    color: string = "primary"
    variant: string = "default"
    class: string = ""
  }

  view {
    <div
      {...attrs}
      data-ui-component="PublicPageShell"
      class='wire-page-shell {class}'
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      data-background='{background}'
      data-overflow='{overflow}'
      data-min-height='{minHeight}'
      data-header-offset='{headerOffset}'
    >
      <slot name="before"></slot>

      <main
        class="wire-page-shell__main"
        data-full-width='{fullWidth}'
        data-max-width='{maxWidth}'
      >
        <slot></slot>
      </main>

      <slot name="after"></slot>
    </div>
  }

  style {
    .wire-page-shell {
      position: relative;
      isolation: isolate;
      width: 100%;
      min-width: 0;
      color: var(--wire-color-text);
      background: var(--wire-color-background);
    }

    .wire-page-shell[data-background="soft"] {
      background: var(--wire-color-surface-soft);
    }

    .wire-page-shell[data-background="raised"] {
      background: var(--wire-color-surface-raised);
    }

    .wire-page-shell[data-min-height="screen"] {
      min-height: 100vh;
    }

    /*
     * dvh tracks the browser chrome on a phone, so a full-height shell does
     * not leave a gap when the address bar collapses.
     */
    .wire-page-shell[data-min-height="dvh"] {
      min-height: 100dvh;
    }

    .wire-page-shell[data-overflow="clip"] {
      overflow-x: clip;
    }

    .wire-page-shell[data-overflow="hidden"] {
      overflow-x: hidden;
    }

    /*
     * One main element rather than a branch per width. fullWidth is an
     * attribute, so the width cap and the gutters are a couple of rules
     * instead of two copies of the same markup.
     */
    .wire-page-shell__main {
      width: 100%;
      min-width: 0;
    }

    .wire-page-shell__main[data-full-width="false"] {
      margin-inline: auto;
      padding-inline: 1rem;
    }

    .wire-page-shell__main[data-full-width="false"][data-max-width="lg"] {
      max-width: 64rem;
    }

    .wire-page-shell__main[data-full-width="false"][data-max-width="xl"] {
      max-width: 80rem;
    }

    .wire-page-shell__main[data-full-width="false"][data-max-width="2xl"] {
      max-width: 96rem;
    }

    @media (min-width: 640px) {
      .wire-page-shell__main[data-full-width="false"] {
        padding-inline: 1.5rem;
      }
    }

    @media (min-width: 1024px) {
      .wire-page-shell__main[data-full-width="false"] {
        padding-inline: 2rem;
      }
    }

    /* Room for a fixed header to sit over the top of the shell. */
    .wire-page-shell[data-header-offset="sm"] {
      padding-top: 4rem;
    }

    .wire-page-shell[data-header-offset="md"] {
      padding-top: 5rem;
    }

    .wire-page-shell[data-header-offset="lg"] {
      padding-top: 6rem;
    }
  }
}
```

---

## Radio

Showcase: https://component.wrnexusjs.dev/
Mount: <Radio /> (legacy: data-component="Radio")
Category: forms
Purpose: Theme-aware, responsive radio component.
Props: size: string = "default", color: string = "primary", id: string = "", name: string = "", label: string = "Radio", hiddenLabel: boolean = false, placeholder: string = "", variant: string = "normal", icon: string = "", iconPosition: string = "start", value: string = "on", options: unknown[] = [], checked: boolean = false, orientation: string = "vertical", card: boolean = false, rightAligned: boolean = false, list: boolean = false, helperText: string = "", cornerHint: string = "", error: string = "", inline: boolean = false, readonly: boolean = false, disabled: boolean = false, required: boolean = false, class: string = ""
Slots: none
Events: input, change, focus, blur, invalid

### Complete .wrn source contract

```wrn
import FieldStyles from "../styles/FieldStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component Radio {
  outputs {
    input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    invalid(payload: { message?: string; value: string | number | boolean | null | object; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
size: string = "default"
    color: string = "primary"
    id: string = ""
    name: string = ""
    label: string = "Radio"
    hiddenLabel: boolean = false
    placeholder: string = ""
    variant: string = "normal"
    icon: string = ""
    iconPosition: string = "start"
    value: string = "on"
    options: unknown[] = []
    checked: boolean = false
    orientation: string = "vertical"
    card: boolean = false
    rightAligned: boolean = false
    list: boolean = false
    helperText: string = ""
    cornerHint: string = ""
    error: string = ""
    inline: boolean = false
    readonly: boolean = false
    disabled: boolean = false
    required: boolean = false
    class: string = ""
  }
  functions {
    client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ checked: sourceEvent.currentTarget.checked, value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
    client function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
  }
  view {
    <div {...attrs} class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--choice-field wire-next--radio-field {class}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}" data-orientation="{orientation}" data-card="{card}" data-list="{list}" data-right-aligned="{rightAligned}">
      <div class="wire-next__field-heading">{#if options.length || cornerHint}<span class="{hiddenLabel ? 'wire-next__sr-only' : ''}">{label}</span>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}{/if}</div>
      {#if options.length}
        <div class="wire-next__radio-group" role="radiogroup" aria-label="{hiddenLabel ? label : ''}" aria-invalid="{error ? 'true' : 'false'}">
          {#each options as option}
            <label class="wire-next__choice wire-next--radio" for="{id || name}-{option.value}" data-variant="{variant}" data-disabled="{disabled || option.disabled}">
              <input id="{id || name}-{option.value}" type="radio" name="{name}" value="{option.value}" checked="{option.value === value || option.checked}" disabled="{disabled || option.disabled}" required="{required}" readonly="{readonly}" @click="preventReadonly(event)" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @invalid="emitField('invalid', event)" />
              <span class="wire-next__choice-copy"><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
            </label>
          {/each}
        </div>
      {:else}
        <label class="wire-next__choice wire-next--radio" for="{id || name}" data-variant="{variant}" data-icon-position="{iconPosition}"><input id="{id || name}" type="radio" name="{name}" value="{value}" checked="{checked}" disabled="{disabled}" required="{required}" readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" @click="preventReadonly(event)" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @invalid="emitField('invalid', event)" />{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}<span class="{hiddenLabel || cornerHint ? 'wire-next__sr-only' : ''}">{label}</span></label>
      {/if}
      {#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
    </div>
    <FieldStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--radio-field { width: 100%; min-width: 0; }
    .wire-next--radio-field[data-invalid="true"] { --wire-component-color: var(--wire-color-danger); }
  }
}
```

---

## RangeSlider

Showcase: https://component.wrnexusjs.dev/
Mount: <RangeSlider /> (legacy: data-component="RangeSlider")
Category: forms
Purpose: Theme-aware, responsive range slider component.
Props: size: string = "default", color: string = "primary", id: string = "", name: string = "", label: string = "Range", hiddenLabel: boolean = false, placeholder: string = "", variant: string = "normal", icon: string = "", iconPosition: string = "start", value: number = 50, min: number = 0, max: number = 100, step: number = 1, showValue: boolean = true, showBounds: boolean = true, showSteps: boolean = false, marks: unknown[] = [], helperText: string = "", cornerHint: string = "", error: string = "", inline: boolean = false, readonly: boolean = false, disabled: boolean = false, required: boolean = false, class: string = ""
Slots: none
Events: input, change, focus, blur

### Complete .wrn source contract

```wrn
import FieldStyles from "../styles/FieldStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component RangeSlider {
  outputs {
    input(payload: ({ value: number; name: string; sourceEvent: Event }) | ({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]))
    change(payload: ({ value: number; name: string; sourceEvent: Event }) | ({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]))
    focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
    size: string = "default"
    color: string = "primary"
    id: string = ""
    name: string = ""
    label: string = "Range"
    hiddenLabel: boolean = false
    placeholder: string = ""
    variant: string = "normal"
    icon: string = ""
    iconPosition: string = "start"
    value: number = 50
    min: number = 0
    max: number = 100
    step: number = 1
    showValue: boolean = true
    showBounds: boolean = true
    showSteps: boolean = false
    marks: unknown[] = []
    helperText: string = ""
    cornerHint: string = ""
    error: string = ""
    inline: boolean = false
    readonly: boolean = false
    disabled: boolean = false
    required: boolean = false
    class: string = ""
  }
  state currentValue = value
  functions {
    shared function percent() { if (Number(max) === Number(min)) { return 0 } return (Number(currentValue) - Number(min)) / (Number(max) - Number(min)) * 100 }
    client function setValue(nextValue, sourceEvent, commit, detail, root, slider, input, outputElement, progressValue) { if (readonly || disabled) { return } currentValue = Number(nextValue); root = sourceEvent.currentTarget.closest(".wire-next--range-slider"); if (root) { slider = root.querySelector("[role=slider]"); input = root.querySelector("input[name='" + name + "']"); outputElement = root.querySelector("output"); progressValue = (currentValue - Number(min)) / (Number(max) - Number(min)) * 100; if (slider) { slider.setAttribute("aria-valuenow", String(currentValue)); slider.style.setProperty("--wire-range-progress", progressValue + "%") } if (input) { input.value = String(currentValue) } if (outputElement) { outputElement.textContent = String(currentValue) } } detail = { value: currentValue, min: min, max: max, step: step, name: name, sourceEvent: sourceEvent }; output.input(detail); if (commit) { output.change(detail) } }
    client function valueFromPointer(sourceEvent, rect, ratio) { rect = sourceEvent.currentTarget.getBoundingClientRect(); ratio = Math.max(0, Math.min(1, (sourceEvent.clientX - rect.left) / rect.width)); return Number(min) + ratio * (Number(max) - Number(min)) }
    client function beginSlide(sourceEvent, rect, ratio, rawValue) { if (readonly || disabled) { return } sourceEvent.currentTarget.setPointerCapture(sourceEvent.pointerId); rect = sourceEvent.currentTarget.getBoundingClientRect(); ratio = Math.max(0, Math.min(1, (sourceEvent.clientX - rect.left) / rect.width)); rawValue = Number(min) + ratio * (Number(max) - Number(min)); currentValue = Number(min) + Math.round((rawValue - Number(min)) / Number(step)) * Number(step); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
    client function moveSlide(sourceEvent, rect, ratio, rawValue) { if (!sourceEvent.currentTarget.hasPointerCapture(sourceEvent.pointerId)) { return } rect = sourceEvent.currentTarget.getBoundingClientRect(); ratio = Math.max(0, Math.min(1, (sourceEvent.clientX - rect.left) / rect.width)); rawValue = Number(min) + ratio * (Number(max) - Number(min)); currentValue = Number(min) + Math.round((rawValue - Number(min)) / Number(step)) * Number(step); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
    client function endSlide(sourceEvent, rect, ratio, rawValue) { rect = sourceEvent.currentTarget.getBoundingClientRect(); ratio = Math.max(0, Math.min(1, (sourceEvent.clientX - rect.left) / rect.width)); rawValue = Number(min) + ratio * (Number(max) - Number(min)); currentValue = Number(min) + Math.round((rawValue - Number(min)) / Number(step)) * Number(step); if (sourceEvent.currentTarget.hasPointerCapture(sourceEvent.pointerId)) { sourceEvent.currentTarget.releasePointerCapture(sourceEvent.pointerId) } output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
    client function selectMark(sourceEvent, root, slider, input, outputElement, nextValue, progressValue, detail) { if (readonly || disabled) { return } root = sourceEvent.currentTarget.closest(".wire-next--range-slider"); slider = root.querySelector(".wire-next__custom-slider"); input = root.querySelector(".wire-next__range-value"); outputElement = root.querySelector("output"); nextValue = Number(sourceEvent.currentTarget.dataset.value); currentValue = nextValue; progressValue = (nextValue - Number(min)) / (Number(max) - Number(min)) * 100; slider.setAttribute("aria-valuenow", String(nextValue)); slider.style.setProperty("--wire-range-progress", progressValue + "%"); input.setAttribute("value", String(nextValue)); outputElement.replaceChildren(String(nextValue)); detail = { value: nextValue, min: min, max: max, step: step, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
    client function handleKey(sourceEvent) { if (sourceEvent.key === "ArrowRight") { sourceEvent.preventDefault(); currentValue = Math.min(Number(max), Number(currentValue) + Number(step)); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) } if (sourceEvent.key === "ArrowLeft") { sourceEvent.preventDefault(); currentValue = Math.max(Number(min), Number(currentValue) - Number(step)); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) } if (sourceEvent.key === "Home") { sourceEvent.preventDefault(); currentValue = Number(min); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) } if (sourceEvent.key === "End") { sourceEvent.preventDefault(); currentValue = Number(max); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) } }
    client function emitFocus(nameEvent, sourceEvent) { output[nameEvent]({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
    client function decreaseValue(sourceEvent) { currentValue = Math.max(Number(min), Number(currentValue) - Number(step)); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
    client function increaseValue(sourceEvent) { currentValue = Math.min(Number(max), Number(currentValue) + Number(step)); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
  }
  view {
    <div {...attrs} class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--field wire-next--range-slider {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
      <div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
      <div class="wire-next__range-control" data-variant="{variant}" data-icon-position="{iconPosition}" data-show-steps="{showSteps}">
        {#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}
        <button type="button" class="wire-next__range-stepper" aria-label="Decrease {label}" disabled="{disabled || readonly}" @click="decreaseValue(event)">−</button>
        <div class="wire-next__range-track">
          <input id="{id || name}" class="wire-next__sr-only wire-next__range-value" type="number" name="{name}" value="{currentValue}" min="{min}" max="{max}" step="{step}" required="{required}" readonly />
          <div class="wire-next__custom-slider" role="slider" tabindex="{disabled ? '-1' : '0'}" aria-label="{hiddenLabel ? label : ''}" aria-valuemin="{min}" aria-valuemax="{max}" aria-valuenow="{currentValue}" aria-disabled="{disabled}" aria-readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" style="--wire-range-progress: {percent()}%" @pointerdown="beginSlide(event)" @pointermove="moveSlide(event)" @pointerup="endSlide(event)" @pointercancel="endSlide(event)" @keydown="handleKey(event)" @focus="emitFocus('focus', event)" @blur="emitFocus('blur', event)">
            <span class="wire-next__slider-fill"></span><span class="wire-next__slider-thumb"></span>
            {#if marks.length}<span class="wire-next__slider-marks">{#each marks as mark}<button type="button" data-value="{mark.value}" style="left: {(Number(mark.value) - Number(min)) / (Number(max) - Number(min)) * 100}%" aria-label="{mark.label}" @click="selectMark(event)"></button>{/each}</span>{/if}
          </div>
          {#if showBounds}<div class="wire-next__range-bounds"><span>{min}</span><span>Step {step}</span><span>{max}</span></div>{/if}
        </div>
        {#if showValue}<output for="{id || name}">{currentValue}</output>{/if}
        <button type="button" class="wire-next__range-stepper" aria-label="Increase {label}" disabled="{disabled || readonly}" @click="increaseValue(event)">+</button>
      </div>
      {#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
    </div>
    <FieldStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next__range-track {
      display: grid;
      flex: 1;
      min-width: 0;
      gap: 0.35rem;
    }

    .wire-next__range-control input {
      width: 100%;
      min-width: 0;
      margin: 0;
      flex: 1;
      accent-color: var(--wire-field-color);
    }

    .wire-next__custom-slider {
      position: relative;
      width: 100%;
      height: 1.5rem;
      cursor: pointer;
      touch-action: none;
    }

    .wire-next__custom-slider::before,
    .wire-next__slider-fill {
      position: absolute;
      top: 50%;
      height: 0.4rem;
      border-radius: 999px;
      transform: translateY(-50%);
      content: "";
    }

    .wire-next__custom-slider::before {
      right: 0;
      left: 0;
      background: var(--wire-color-surface-2);
      box-shadow: inset 0 0 0 1px var(--wire-color-border);
    }

    .wire-next__slider-fill {
      z-index: 1;
      left: 0;
      width: var(--wire-range-progress);
      background: var(--wire-field-color);
    }

    .wire-next__slider-thumb {
      position: absolute;
      z-index: 3;
      top: 50%;
      left: var(--wire-range-progress);
      width: 1.25rem;
      height: 1.25rem;
      border: 3px solid var(--wire-field-color);
      border-radius: 50%;
      background: var(--wire-color-surface);
      box-shadow: var(--wire-shadow-1);
      transform: translate(-50%, -50%);
    }

    .wire-next__custom-slider:focus-visible {
      outline: 3px solid color-mix(in srgb, var(--wire-field-color) 28%, transparent);
      outline-offset: 3px;
    }

    .wire-next__slider-marks button {
      position: absolute;
      z-index: 2;
      top: 50%;
      width: 0.55rem;
      height: 0.55rem;
      padding: 0;
      border: 2px solid var(--wire-color-surface);
      border-radius: 50%;
      background: var(--wire-color-muted);
      transform: translate(-50%, -50%);
    }

    .wire-next__range-control[data-show-steps="true"] input {
      background-image: repeating-linear-gradient(
        90deg,
        transparent 0,
        transparent calc(10% - 1px),
        var(--wire-color-border) calc(10% - 1px),
        var(--wire-color-border) 10%
      );
    }

    .wire-next__range-bounds {
      display: grid;
      grid-template-columns: 1fr auto 1fr;
      color: var(--wire-color-muted);
      font-size: 0.78em;
      font-variant-numeric: tabular-nums;
    }

    .wire-next__range-bounds span:nth-child(2) {
      text-align: center;
    }

    .wire-next__range-bounds span:last-child {
      text-align: right;
    }

    .wire-next__range-control output {
      min-width: 3ch;
      font-variant-numeric: tabular-nums;
    }

    .wire-next__range-stepper {
      display: inline-grid;
      width: 2.35rem;
      height: 2.35rem;
      flex: 0 0 auto;
      place-items: center;
      border: 1px solid var(--wire-color-border);
      border-radius: 50%;
      background: var(--wire-color-surface);
      color: var(--wire-color-text);
      font: inherit;
      font-size: 1.2rem;
      cursor: pointer;
    }

    .wire-next__range-stepper:hover:not(:disabled) {
      border-color: var(--wire-field-color);
      color: var(--wire-field-color);
    }

    .wire-next__range-stepper:disabled {
      cursor: not-allowed;
      opacity: 0.45;
    }
  }
}
```

---

## Rating

Showcase: https://component.wrnexusjs.dev/
Mount: <Rating /> (legacy: data-component="Rating")
Category: base
Purpose: Theme-aware, responsive rating component.
Props: size: string = "default", color: string = "primary", title: string = "Rating", description: string = "", items: unknown[] = [], variant: string = "default", class: string = ""
Slots: default
Events: input, change

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component Rating {
  outputs {
    input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
  }

  props {
    size: string = "default"
    color: string = "primary"
    title: string = "Rating"
    description: string = ""
    items: unknown[] = []
    variant: string = "default"
    class: string = ""
  }
  view {
    <section class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--rating wire-next--variant-{variant} {class}">
      {#if title}<strong>{title}</strong>{/if}
      {#if description}<p>{description}</p>{/if}
      {#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
      <slot />
    </section>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--rating {
      display: grid;
      gap: 0.75rem;
      padding: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius);
      color: var(--wire-color-text);
      background: var(--wire-color-surface);
    }
    .wire-next--rating > p { margin: 0; color: var(--wire-color-muted); }
    .wire-next--rating > .wire-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
    .wire-next--rating > .wire-next__items > span { padding: 0.35rem 0.6rem; border-radius: var(--wire-radius-sm); background: var(--wire-color-surface-2); }
  }
}
```

---

## Scrollspy

Showcase: https://component.wrnexusjs.dev/
Mount: <Scrollspy /> (legacy: data-component="Scrollspy")
Category: navigation
Purpose: Theme-aware, responsive scrollspy component.
Props: color: string = "primary", size: string = "default", items: unknown[] = [], active: string = "", label: string = "On this page", heading: string = "", class: string = ""
Slots: default
Events: change

### Complete .wrn source contract

```wrn
// Scrollspy -- a table of contents that follows the reader.
//
//   <Scrollspy items='[{"label":"Overview","href":"#overview"}]' />
//
// Each href points at an element on the page. The runtime observes those
// elements and moves aria-current to the link for whichever one is in view.
//
// The runtime writes the marker straight onto the links rather than into
// component state. An IntersectionObserver callback fires long after the
// client function that registered it has returned, and a state write made
// there is dropped -- so the DOM is the only place the answer can live.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Scrollspy {
  outputs {
    change(payload: { href: string; label: string })
  }

  props {
    color: string = "primary"
    size: string = "default"
    items: unknown[] = []
    active: string = ""
    label: string = "On this page"
    heading: string = ""
    class: string = ""
  }

  functions {
    shared function itemList() {
      return Array.isArray(items) ? items : []
    }

    shared function isActive(item) {
      return Boolean(item.href) && item.href === active
    }
  }

  view {
    <nav
      {...attrs}
      data-ui-component="Scrollspy"
      class='wire-scrollspy {class}'
      data-color='{color}'
      data-size='{size}'
      data-wrn-scrollspy="true"
      role="navigation"
      aria-label='{label}'
    >
      <p class="wire-scrollspy__heading" data-show="heading">{heading}</p>

      <ul class="wire-scrollspy__list">
        {#each itemList() as item}
          <li class="wire-scrollspy__item">
            <a
              class="wire-scrollspy__link"
              href='{item.href || "#"}'
              data-active='{isActive(item)}'
              aria-current='{isActive(item) ? "location" : "false"}'
            >
              <span class="wire-scrollspy__label">{item.label}</span>
            </a>
          </li>
        {/each}
      </ul>

      <slot />
    </nav>
  }

  style {
    .wire-scrollspy {
      --scrollspy-accent: var(--wire-color-primary);
      max-width: 100%;
    }

    .wire-scrollspy[data-color="secondary"] {
      --scrollspy-accent: var(--wire-color-secondary);
    }

    .wire-scrollspy[data-color="success"] {
      --scrollspy-accent: var(--wire-color-success);
    }

    .wire-scrollspy[data-color="danger"] {
      --scrollspy-accent: var(--wire-color-danger);
    }

    .wire-scrollspy[data-color="info"] {
      --scrollspy-accent: var(--wire-color-info);
    }

    .wire-scrollspy[data-size="sm"] {
      font-size: 0.82rem;
    }

    .wire-scrollspy[data-size="lg"] {
      font-size: 1rem;
    }

    .wire-scrollspy__heading {
      margin: 0 0 0.5rem;
      color: var(--wire-color-text-muted);
      font-size: 0.72rem;
      font-weight: 700;
      letter-spacing: 0.08em;
      text-transform: uppercase;
    }

    .wire-scrollspy__list {
      display: grid;
      gap: 0.1rem;
      margin: 0;
      padding: 0;
      list-style: none;
      border-left: 1px solid var(--wire-color-border);
    }

    .wire-scrollspy__link {
      display: block;
      padding: 0.3rem 0.75rem;
      margin-left: -1px;
      border-left: 2px solid transparent;
      color: var(--wire-color-text-muted);
      font-size: 0.85rem;
      text-decoration: none;
    }

    .wire-scrollspy__link:hover {
      color: var(--wire-color-text);
    }

    .wire-scrollspy__link:focus-visible {
      outline: 2px solid var(--scrollspy-accent);
      outline-offset: -2px;
    }

    .wire-scrollspy__link[data-active="true"] {
      border-left-color: var(--scrollspy-accent);
      color: var(--scrollspy-accent);
      font-weight: 600;
    }

    /*
     * A table of contents is a sidebar affordance. On a phone it stops being
     * a rail and becomes a horizontal strip that scrolls, so it costs one
     * line rather than a screenful.
     */
    @media (max-width: 767px) {
      .wire-scrollspy__list {
        grid-auto-flow: column;
        grid-auto-columns: max-content;
        overflow-x: auto;
        border-left: 0;
        border-bottom: 1px solid var(--wire-color-border);
      }

      .wire-scrollspy__link {
        margin-left: 0;
        border-left: 0;
        border-bottom: 2px solid transparent;
        white-space: nowrap;
      }

      .wire-scrollspy__link[data-active="true"] {
        border-left-color: transparent;
        border-bottom-color: var(--scrollspy-accent);
      }
    }
  }
}
```

---

## SearchBox

Showcase: https://component.wrnexusjs.dev/
Mount: <SearchBox /> (legacy: data-component="SearchBox")
Category: forms
Purpose: Provide an accessible responsive search field with labels, validation states, sizes, and input or change events.
Props: size: string = "default", color: string = "primary", label: string = "Search Box", name: string = "", value: string = "", placeholder: string = "", type: string = "search", min: string = "", max: string = "", step: string = "", disabled: boolean = false, required: boolean = false, class: string = ""
Slots: none
Events: search, clear

### Complete .wrn source contract

```wrn
component SearchBox {
  outputs {
    search(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    clear(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
    size: string = "default"
    color: string = "primary"
    label: string = "Search Box"
    name: string = ""
    value: string = ""
    placeholder: string = ""
    type: string = "search"
    min: string = ""
    max: string = ""
    step: string = ""
    disabled: boolean = false
    required: boolean = false
    class: string = ""
  }

  state query = value

  view {
    <form
      data-ui-component="SearchBox"
      role="search"
      class='wire-search-box {class}'
      @submit='event.preventDefault(); output.search({ value: query, name: name })'
    >
      <label
        class="wire-search-box__label"
        data-hidden='{label === "" ? "true" : "false"}'
      >
        {label}
      </label>

      <div class="wire-search-box__control">
        <span
          class="icon-[lucide--search] wire-search-box__search-icon"
          aria-hidden="true"
        ></span>

        <input
          name='{name}'
          type='{type}'
          value='{query}'
          placeholder='{placeholder}'
          min='{min}'
          max='{max}'
          step='{step}'
          disabled='{disabled}'
          required='{required}'
          autocomplete="off"
          class="wire-search-box__input"
          data-size='{size}'
          @input='query = event.target.value'
          @change='query = event.target.value'
        />

        <button
          type="button"
          aria-label="Clear search"
          data-show='query.length > 0 && !disabled'
          class="wire-search-box__button wire-search-box__clear"
          @click='query = ""; event.currentTarget.parentElement.querySelector("input")?.focus(); output.clear({ value: "", name: name })'
        >
          <span class="icon-[lucide--x] wire-search-box__button-icon" aria-hidden="true"></span>
        </button>

        <button
          type="submit"
          aria-label='{label || "Search"}'
          disabled='{disabled}'
          class="wire-search-box__button wire-search-box__submit"
        >
          <span class="icon-[lucide--arrow-right] wire-search-box__button-icon" aria-hidden="true"></span>
        </button>
      </div>
    </form>
  }

  style {
    .wire-search-box { width: 100%; }
    .wire-search-box__label {
      display: block;
      margin-bottom: 0.5rem;
      color: var(--wire-color-text);
      font-size: 0.875rem;
      font-weight: 600;
    }
    .wire-search-box__label[data-hidden="true"] {
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
      white-space: nowrap;
      border: 0;
    }
    .wire-search-box__control { position: relative; display: flex; align-items: center; }
    .wire-search-box__search-icon {
      position: absolute;
      left: 1rem;
      width: 1.25rem;
      height: 1.25rem;
      color: var(--wire-color-input-placeholder);
      pointer-events: none;
    }
    .wire-search-box__input {
      width: 100%;
      height: 3rem;
      padding: 0 6rem 0 3rem;
      border: 1px solid var(--wire-color-input-border);
      border-radius: var(--wire-radius);
      outline: none;
      color: var(--wire-color-input-text);
      background: var(--wire-color-input-background);
      transition: border-color var(--wire-motion-base) var(--wire-ease-standard), box-shadow var(--wire-motion-base) var(--wire-ease-standard);
    }
    .wire-search-box__input[data-size="sm"] { height: 2.5rem; }
    .wire-search-box__input[data-size="lg"] { height: 3.5rem; }
    .wire-search-box__input::placeholder { color: var(--wire-color-input-placeholder); }
    .wire-search-box__input:hover { border-color: var(--wire-color-input-border-hover); }
    .wire-search-box__input:focus {
      border-color: var(--wire-color-input-border-focus);
      box-shadow: 0 0 0 2px var(--wire-color-focus);
    }
    .wire-search-box__input:disabled, .wire-search-box__button:disabled { cursor: not-allowed; opacity: 0.6; }
    .wire-search-box__button {
      position: absolute;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      border: 0;
      border-radius: var(--wire-radius-sm);
      transition: color var(--wire-motion-base), background var(--wire-motion-base);
    }
    .wire-search-box__clear { right: 3rem; width: 2rem; height: 2rem; color: var(--wire-color-text-muted); background: transparent; }
    .wire-search-box__clear:hover { color: var(--wire-color-text); background: var(--wire-color-surface-soft); }
    .wire-search-box__submit { right: 0.5rem; width: 2.25rem; height: 2.25rem; color: var(--wire-color-on-primary); background: var(--wire-color-primary); }
    .wire-search-box__submit:hover { background: var(--wire-color-primary-hover); }
    .wire-search-box__button:focus-visible { outline: 2px solid var(--wire-color-focus); outline-offset: 2px; }
    .wire-search-box__button-icon { width: 1rem; height: 1rem; }
  }
}
```

---

## Section

Showcase: https://component.wrnexusjs.dev/
Mount: <Section /> (legacy: data-component="Section")
Category: layout
Purpose: Create a responsive themed page section with controlled spacing, width, borders, and surface treatment.
Props: id: string = "", size: string = "default", color: string = "primary", variant: string = "default", spacing: string = "lg", maxWidth: string = "xl", fullWidth: boolean = false, borderTop: boolean = false, borderBottom: boolean = false, class: string = ""
Slots: default
Events: none

### Complete .wrn source contract

```wrn
import Container from "./Container.wrn"

component Section {
  props {
    id: string = ""
    size: string = "default"
    color: string = "primary"
    variant: string = "default"
    spacing: string = "lg"
    maxWidth: string = "xl"
    fullWidth: boolean = false
    borderTop: boolean = false
    borderBottom: boolean = false
    class: string = ""
  }

  view {
    <section
      {...attrs}
      data-ui-component="Section"
      class='wire-section {class}'
      id='{id}'
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      data-spacing='{spacing}'
      data-border-top='{borderTop}'
      data-border-bottom='{borderBottom}'
    >
      {#if fullWidth}
        <slot></slot>
      {:else}
        <Container
          columns="1"
          gap="md"
          maxWidth='{maxWidth}'
          size='{size}'
          color='{color}'
        >
          <slot></slot>
        </Container>
      {/if}
    </section>
  }

  style {
    .wire-section {
      --section-tint: transparent;
      --section-ink: inherit;
      position: relative;
      isolation: isolate;
      width: 100%;
      min-width: 0;
      padding-block: 3rem;
      background: var(--section-tint);
      color: var(--section-ink);
    }

    .wire-section[data-spacing="sm"] {
      padding-block: 2rem;
    }

    .wire-section[data-spacing="md"] {
      padding-block: 3rem;
    }

    .wire-section[data-spacing="xl"] {
      padding-block: 5rem;
    }

    .wire-section[data-variant="soft"] {
      --section-tint: var(--wire-color-surface-soft);
    }

    .wire-section[data-variant="raised"] {
      --section-tint: var(--wire-color-surface-raised);
    }

    /*
     * tinted and solid pick their fill from the component colour, which is
     * why these are two attributes rather than one class per pairing. The
     * previous version spent a line on every variant and colour combination.
     */
    .wire-section[data-variant="tinted"][data-color="primary"] {
      --section-tint: var(--wire-color-primary-soft);
    }

    .wire-section[data-variant="tinted"][data-color="success"] {
      --section-tint: var(--wire-color-success-soft);
    }

    .wire-section[data-variant="tinted"][data-color="warning"] {
      --section-tint: var(--wire-color-warning-soft);
    }

    .wire-section[data-variant="tinted"][data-color="danger"] {
      --section-tint: var(--wire-color-danger-soft);
    }

    .wire-section[data-variant="tinted"][data-color="info"] {
      --section-tint: var(--wire-color-info-soft);
    }

    .wire-section[data-variant="solid"][data-color="primary"] {
      --section-tint: var(--wire-color-primary);
      --section-ink: var(--wire-color-on-primary);
    }

    .wire-section[data-variant="solid"][data-color="danger"] {
      --section-tint: var(--wire-color-danger);
      --section-ink: var(--wire-color-on-danger);
    }

    .wire-section[data-border-top="true"] {
      border-top: 1px solid var(--wire-color-border);
    }

    .wire-section[data-border-bottom="true"] {
      border-bottom: 1px solid var(--wire-color-border);
    }

    @media (min-width: 640px) {
      .wire-section[data-spacing="lg"] {
        padding-block: 5rem;
      }

      .wire-section[data-spacing="xl"] {
        padding-block: 6rem;
      }
    }
  }
}
```

---

## SectionHeader

Showcase: https://component.wrnexusjs.dev/
Mount: <SectionHeader /> (legacy: data-component="SectionHeader")
Category: layout
Purpose: Introduce a section with an eyebrow, title, description, alignment, and responsive heading hierarchy.
Props: id: string = "", eyebrow: string = "", title: string = "", description: string = "", align: string = "left", size: string = "default", color: string = "primary", headingLevel: number = 2, maxWidth: string = "3xl", class: string = ""
Slots: icon, default, actions
Events: none

### Complete .wrn source contract

```wrn
component SectionHeader {
  props {
    id: string = ""
    eyebrow: string = ""
    title: string = ""
    description: string = ""
    align: string = "left"
    size: string = "default"
    color: string = "primary"
    headingLevel: number = 2
    maxWidth: string = "3xl"
    class: string = ""
  }

  view {
    <header
      {...attrs}
      data-ui-component="SectionHeader"
      class='wire-section-header {class}'
      data-align='{align}'
      data-size='{size}'
      data-color='{color}'
      data-max-width='{maxWidth}'
    >
      <div class="wire-section-header__copy">
        <div class="wire-section-header__eyebrow-row">
          <slot name="icon"></slot>
          <p class="wire-section-header__eyebrow" data-show="eyebrow">{eyebrow}</p>
        </div>

        {#if headingLevel === 1}
          <h1 id='{id}' class="wire-section-header__title">{title}</h1>
        {:else if headingLevel === 3}
          <h3 id='{id}' class="wire-section-header__title">{title}</h3>
        {:else}
          <h2 id='{id}' class="wire-section-header__title">{title}</h2>
        {/if}

        <p class="wire-section-header__description" data-show="description">{description}</p>

        <slot></slot>
      </div>

      <div class="wire-section-header__actions">
        <slot name="actions"></slot>
      </div>
    </header>
  }

  style {
    .wire-section-header {
      --section-header-accent: var(--wire-color-primary);
      --section-header-title: 1.875rem;
      display: flex;
      flex-direction: column;
      gap: 1.25rem;
      min-width: 0;
    }

    .wire-section-header[data-color="secondary"] {
      --section-header-accent: var(--wire-color-secondary);
    }

    .wire-section-header[data-color="success"] {
      --section-header-accent: var(--wire-color-success);
    }

    .wire-section-header[data-color="warning"] {
      --section-header-accent: var(--wire-color-warning-text);
    }

    .wire-section-header[data-color="danger"] {
      --section-header-accent: var(--wire-color-danger);
    }

    .wire-section-header[data-color="info"] {
      --section-header-accent: var(--wire-color-info);
    }

    .wire-section-header[data-align="center"] {
      align-items: center;
      text-align: center;
    }

    .wire-section-header[data-align="right"] {
      align-items: flex-end;
      text-align: right;
    }

    .wire-section-header__copy {
      min-width: 0;
    }

    .wire-section-header[data-align="center"] .wire-section-header__copy {
      margin-inline: auto;
    }

    .wire-section-header[data-max-width="xl"] .wire-section-header__copy {
      max-width: 36rem;
    }

    .wire-section-header[data-max-width="2xl"] .wire-section-header__copy {
      max-width: 42rem;
    }

    .wire-section-header[data-max-width="3xl"] .wire-section-header__copy {
      max-width: 48rem;
    }

    .wire-section-header[data-max-width="4xl"] .wire-section-header__copy {
      max-width: 56rem;
    }

    .wire-section-header__eyebrow-row {
      display: flex;
      align-items: center;
      gap: 0.75rem;
      margin-bottom: 0.75rem;
    }

    .wire-section-header[data-align="center"] .wire-section-header__eyebrow-row {
      justify-content: center;
    }

    .wire-section-header[data-align="right"] .wire-section-header__eyebrow-row {
      justify-content: flex-end;
    }

    .wire-section-header__eyebrow {
      margin: 0;
      color: var(--section-header-accent);
      font-size: 0.75rem;
      font-weight: 700;
      letter-spacing: 0.18em;
      text-transform: uppercase;
    }

    .wire-section-header[data-size="lg"] .wire-section-header__eyebrow {
      font-size: 0.875rem;
    }

    .wire-section-header__title {
      margin: 0;
      color: var(--wire-color-text);
      font-size: var(--section-header-title);
      font-weight: 700;
      line-height: 1.15;
      letter-spacing: -0.02em;
    }

    .wire-section-header__description {
      margin: 1rem 0 0;
      color: var(--wire-color-text-muted);
      font-size: 1rem;
      line-height: 1.7;
    }

    .wire-section-header[data-size="sm"] .wire-section-header__description {
      font-size: 0.875rem;
    }

    .wire-section-header[data-size="lg"] .wire-section-header__description {
      font-size: 1.125rem;
    }

    .wire-section-header__actions {
      display: flex;
      flex-shrink: 0;
      flex-wrap: wrap;
      align-items: center;
      gap: 0.75rem;
    }

    .wire-section-header[data-align="center"] .wire-section-header__actions {
      justify-content: center;
    }

    .wire-section-header[data-align="right"] .wire-section-header__actions,
    .wire-section-header[data-align="split"] .wire-section-header__actions {
      justify-content: flex-end;
    }

    /*
     * Heading sizes scale up with the viewport rather than being fixed, and
     * split only becomes a row once there is width for two columns.
     */
    .wire-section-header[data-size="sm"] {
      --section-header-title: 1.5rem;
    }

    .wire-section-header[data-size="lg"] {
      --section-header-title: 2.25rem;
    }

    @media (min-width: 640px) {
      .wire-section-header {
        --section-header-title: 2.25rem;
      }

      .wire-section-header[data-size="sm"] {
        --section-header-title: 1.75rem;
      }

      .wire-section-header[data-size="lg"] {
        --section-header-title: 3rem;
      }
    }

    @media (min-width: 1024px) {
      .wire-section-header[data-align="split"] {
        flex-direction: row;
        align-items: flex-end;
        justify-content: space-between;
      }
    }
  }
}
```

---

## Select

Showcase: https://component.wrnexusjs.dev/
Mount: <Select /> (legacy: data-component="Select")
Category: forms
Purpose: Theme-aware, responsive select component.
Props: size: string = "default", color: string = "primary", id: string = "", name: string = "", label: string = "Select", hiddenLabel: boolean = false, value: string = "", values: unknown[] = [], options: unknown[] = [], placeholder: string = "Select an option", variant: string = "normal", icon: string = "", iconPosition: string = "start", helperText: string = "", cornerHint: string = "", error: string = "", inline: boolean = false, multiple: boolean = false, readonly: boolean = false, disabled: boolean = false, required: boolean = false, class: string = ""
Slots: none
Events: input, change, focus, blur, open, close, invalid

### Complete .wrn source contract

```wrn
import FieldStyles from "../styles/FieldStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component Select {
  outputs {
    input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    open(payload: { name: string; sourceEvent: Event })
    close(payload: { name: string; sourceEvent: Event })
    invalid(payload: { name: string; message: string; sourceEvent: Event })
  }

  props {
size: string = "default"
    color: string = "primary"
    id: string = ""
    name: string = ""
    label: string = "Select"
    hiddenLabel: boolean = false
    value: string = ""
    values: unknown[] = []
    options: unknown[] = []
    placeholder: string = "Select an option"
    variant: string = "normal"
    icon: string = ""
    iconPosition: string = "start"
    helperText: string = ""
    cornerHint: string = ""
    error: string = ""
    inline: boolean = false
    multiple: boolean = false
    readonly: boolean = false
    disabled: boolean = false
    required: boolean = false
    class: string = ""
  }
  functions {
    shared function selected(option) {
      if (multiple) return values.includes(option.value)
      return option.value === value
    }
    client function emitField(nameEvent, sourceEvent) {
      sourceEvent.stopPropagation()
      output[nameEvent]({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent })
    }
    client function handleFocus(sourceEvent) { emitField("focus", sourceEvent); output.open({ name: name, sourceEvent: sourceEvent }) }
    client function handleBlur(sourceEvent) { emitField("blur", sourceEvent); output.close({ name: name, sourceEvent: sourceEvent }) }
    client function handleInvalid(sourceEvent) { output.invalid({ name: name, message: sourceEvent.currentTarget.validationMessage, sourceEvent: sourceEvent }) }
  }
  view {
    <div {...attrs} class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--field wire-next--select {class}" data-variant="{variant}" data-inline="{inline}" data-floating="{variant === 'floating'}" data-invalid="{error ? 'true' : 'false'}" data-readonly="{readonly}">
      <div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
      <div class="wire-next__field-control" data-icon-position="{iconPosition}">
        {#if icon}<span class="{icon} wire-next__field-icon" aria-hidden="true"></span>{/if}
        <select id="{id || name}" name="{name}" multiple="{multiple}" disabled="{disabled || readonly}" required="{required}" aria-readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" @input="emitField('input', event)" @change="emitField('change', event)" @focus="handleFocus(event)" @blur="handleBlur(event)" @invalid="handleInvalid(event)">
          {#if !multiple}<option value="" selected="{value === ''}" disabled="{required}">{placeholder}</option>{/if}
          {#each options as option}<option value="{option.value}" selected="{selected(option)}" disabled="{option.disabled}">{option.label}</option>{/each}
        </select>
        {#if variant === "floating"}<span class="wire-next__field-floating-label">{label}</span>{/if}
      </div>
      {#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}
      <small class="wire-next__field-error" data-error="{name}">{error}</small>
    </div>
    <FieldStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--select select {
      width: 100%;
      min-height: 2.65rem;
      margin: 0;
      line-height: 1.35;
    }
  }
}
```

---

## Sidebar

Showcase: https://component.wrnexusjs.dev/
Mount: <Sidebar /> (legacy: data-component="Sidebar")
Category: navigation
Purpose: Theme-aware, responsive sidebar component.
Props: color: string = "primary", size: string = "default", label: string = "Sidebar", items: unknown[] = [], active: string = "", mobileLabel: string = "Open navigation", drawerTitle: string = "Navigation", class: string = ""
Slots: default, drawer
Events: toggle, open, close, select

### Complete .wrn source contract

```wrn
// Sidebar -- a vertical navigation rail that becomes a drawer on small screens.
//
//   <Sidebar items='[{"label":"Dashboard","href":"/","value":"dash"}]' active="dash" />
//
// An entry is one of three shapes:
//   { label, href, value }            a single link
//   { heading, items: [...] }         a labelled group
//   { label, items: [...] }           a collapsible branch, nested to 3 levels
//
// The off-canvas presentation is Drawer rather than a second implementation,
// so the focus trap and the body scroll lock come from one place.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
import Drawer from "./Drawer.wrn"

component Sidebar {
  outputs {
    toggle(payload: { open: boolean })
    open(payload: { sourceEvent: Event })
    close(payload: { source: string })
    select(payload: { item: object; value: string; level: number })
  }

  props {
    color: string = "primary"
    size: string = "default"
    label: string = "Sidebar"
    items: unknown[] = []
    active: string = ""
    mobileLabel: string = "Open navigation"
    drawerTitle: string = "Navigation"
    class: string = ""
  }

  state drawerOpen = false

  functions {
    shared function entryList() {
      return Array.isArray(items) ? items : []
    }

    // Accepts children as well as items: children is what shipped in 0.8.5
    // and existing callers should keep working.
    shared function childrenOf(entry) {
      if (!entry) {
        return []
      }
      if (Array.isArray(entry.items)) {
        return entry.items
      }
      return Array.isArray(entry.children) ? entry.children : []
    }

    shared function isGroup(entry) {
      return Boolean(entry.heading)
    }

    shared function isActive(entry) {
      return Boolean(entry.value) && entry.value === active
    }

    client function choose(entry, level) {
      if (entry.disabled) {
        return
      }
      output.select({ item: entry, value: entry.value || "", level: level })
      if (drawerOpen) {
        drawerOpen = false
        output.close({ source: "select" })
      }
    }

    client function openDrawer(sourceEvent) {
      drawerOpen = true
      output.open({ sourceEvent: sourceEvent })
      output.toggle({ open: true })
    }

    client function closeDrawer() {
      drawerOpen = false
      output.close({ source: "drawer" })
      output.toggle({ open: false })
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="Sidebar"
      class='wire-sidebar {class}'
      data-color='{color}'
      data-size='{size}'
    >
      <button
        type="button"
        class="wire-sidebar__launcher"
        aria-label='{mobileLabel}'
        aria-expanded='{drawerOpen}'
        @click='openDrawer(event)'
      >
        <span class="wire-sidebar__launcher-bar" aria-hidden="true"></span>
        <span class="wire-sidebar__launcher-text">{mobileLabel}</span>
      </button>

      <nav class="wire-sidebar__rail" aria-label='{label}'>
        <ul class="wire-sidebar__list" data-wrn-roving="vertical">
          {#each entryList() as entry}
            <li class="wire-sidebar__entry" data-group='{isGroup(entry)}'>
              <p class="wire-sidebar__heading" data-show="entry.heading">{entry.heading}</p>

              <a
                class="wire-sidebar__link"
                data-show="!entry.heading"
                href='{entry.href || "#"}'
                data-wrn-roving-item="true"
                data-active='{isActive(entry)}'
                aria-current='{isActive(entry) ? "page" : "false"}'
                aria-disabled='{entry.disabled ? "true" : "false"}'
                @click='choose(entry, 1)'
              >
                <span
                  class='wire-sidebar__icon {entry.icon}'
                  data-show="entry.icon"
                  aria-hidden="true"
                ></span>
                <span class="wire-sidebar__label">{entry.label}</span>
                <span class="wire-sidebar__badge" data-show="entry.badge">{entry.badge}</span>
              </a>

              <ul class="wire-sidebar__sublist" data-show="childrenOf(entry).length > 0">
                {#each childrenOf(entry) as child}
                  <li class="wire-sidebar__entry">
                    <a
                      class="wire-sidebar__link"
                      href='{child.href || "#"}'
                      data-active='{isActive(child)}'
                      aria-current='{isActive(child) ? "page" : "false"}'
                      aria-disabled='{child.disabled ? "true" : "false"}'
                      @click='choose(child, 2)'
                    >
                      <span
                        class='wire-sidebar__icon {child.icon}'
                        data-show="child.icon"
                        aria-hidden="true"
                      ></span>
                      <span class="wire-sidebar__label">{child.label}</span>
                      <span class="wire-sidebar__badge" data-show="child.badge">{child.badge}</span>
                    </a>

                    <ul
                      class="wire-sidebar__sublist wire-sidebar__sublist--level3"
                      data-show="childrenOf(child).length > 0"
                    >
                      {#each childrenOf(child) as leaf}
                        <li class="wire-sidebar__entry">
                          <a
                            class="wire-sidebar__link"
                            href='{leaf.href || "#"}'
                            data-active='{isActive(leaf)}'
                            aria-current='{isActive(leaf) ? "page" : "false"}'
                            @click='choose(leaf, 3)'
                          >
                            <span class="wire-sidebar__label">{leaf.label}</span>
                          </a>
                        </li>
                      {/each}
                    </ul>
                  </li>
                {/each}
              </ul>
            </li>
          {/each}
        </ul>
        <slot />
      </nav>

      <Drawer
        open={drawerOpen}
        placement="left"
        title={drawerTitle}
        label={label}
        @close="closeDrawer()"
      >
        <slot name="drawer"></slot>
      </Drawer>
    </div>
  }

  style {
    .wire-sidebar {
      --sidebar-accent: var(--wire-color-primary);
      max-width: 100%;
    }

    .wire-sidebar[data-color="secondary"] {
      --sidebar-accent: var(--wire-color-secondary);
    }

    .wire-sidebar[data-color="success"] {
      --sidebar-accent: var(--wire-color-success);
    }

    .wire-sidebar[data-color="danger"] {
      --sidebar-accent: var(--wire-color-danger);
    }

    .wire-sidebar[data-color="info"] {
      --sidebar-accent: var(--wire-color-info);
    }

    .wire-sidebar[data-size="sm"] {
      font-size: 0.82rem;
    }

    .wire-sidebar[data-size="lg"] {
      font-size: 1rem;
    }

    .wire-sidebar__rail {
      width: 100%;
    }

    .wire-sidebar__list,
    .wire-sidebar__sublist {
      display: grid;
      gap: 0.1rem;
      margin: 0;
      padding: 0;
      list-style: none;
    }

    .wire-sidebar__sublist {
      margin-left: 0.85rem;
      padding-left: 0.5rem;
      border-left: 1px solid var(--wire-color-border);
    }

    .wire-sidebar__entry[data-group="true"] + .wire-sidebar__entry {
      margin-top: 0.35rem;
    }

    .wire-sidebar__heading {
      margin: 0.9rem 0 0.35rem;
      color: var(--wire-color-text-muted);
      font-size: 0.7rem;
      font-weight: 700;
      letter-spacing: 0.08em;
      text-transform: uppercase;
    }

    .wire-sidebar__link {
      display: flex;
      align-items: center;
      gap: 0.5rem;
      padding: 0.45rem 0.6rem;
      border-radius: var(--wire-radius-sm);
      color: var(--wire-color-text-muted);
      font-size: 0.88rem;
      font-weight: 600;
      text-decoration: none;
    }

    .wire-sidebar__link:hover {
      background: var(--wire-color-surface-soft);
      color: var(--wire-color-text);
    }

    .wire-sidebar__link:focus-visible {
      outline: 2px solid var(--sidebar-accent);
      outline-offset: 2px;
    }

    .wire-sidebar__link[data-active="true"] {
      background: color-mix(in srgb, var(--sidebar-accent) 16%, transparent);
      color: var(--sidebar-accent);
    }

    .wire-sidebar__link[aria-disabled="true"] {
      opacity: 0.5;
      pointer-events: none;
    }

    .wire-sidebar__label {
      min-width: 0;
    }

    .wire-sidebar__badge {
      margin-left: auto;
      padding: 0.05rem 0.4rem;
      border-radius: 999px;
      background: color-mix(in srgb, var(--sidebar-accent) 16%, transparent);
      color: var(--sidebar-accent);
      font-size: 0.72rem;
    }

    .wire-sidebar__launcher {
      display: none;
      align-items: center;
      gap: 0.5rem;
      padding: 0.45rem 0.7rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-sm);
      background: var(--wire-color-surface);
      color: var(--wire-color-text);
      font: inherit;
      font-size: 0.9rem;
      cursor: pointer;
    }

    .wire-sidebar__launcher-bar {
      width: 1rem;
      height: 2px;
      background: currentColor;
      box-shadow:
        0 -5px 0 currentColor,
        0 5px 0 currentColor;
    }

    /*
     * Below the tablet breakpoint the rail is replaced by the drawer launcher.
     * A permanent rail eats most of a phone screen, and Drawer already handles
     * the focus trap and the scroll lock.
     */
    @media (max-width: 767px) {
      .wire-sidebar__launcher {
        display: inline-flex;
      }

      .wire-sidebar__rail {
        display: none;
      }
    }
  }
}
```

---

## Skeleton

Showcase: https://component.wrnexusjs.dev/
Mount: <Skeleton /> (legacy: data-component="Skeleton")
Category: base
Purpose: Theme-aware, responsive skeleton component.
Props: color: string = "primary", label: string = "Loading", size: string = "md", lines: number = 3, class: string = ""
Slots: none
Events: none

### Complete .wrn source contract

```wrn
component Skeleton {
  props {
    color: string = "primary"
    label: string = "Loading"
    size: string = "md"
    lines: number = 3
    class: string = ""
  }
  view {
    <div class="wire-skeleton-component {class}" data-color="{color}" data-size="{size}" role="status" aria-label="{label}">
      {#each Array(lines).fill(0) as line}<span class="wire-skeleton-component__line"></span>{/each}
    </div>
  }

  style {
    .wire-skeleton-component {
      --wire-skeleton-highlight: var(--wire-color-border);
      display: grid;
      width: min(100%, 24rem);
      gap: 0.55rem;
    }

    .wire-skeleton-component[data-size="xs"] { gap: 0.35rem; }
    .wire-skeleton-component[data-size="lg"],
    .wire-skeleton-component[data-size="xl"] { gap: 0.75rem; }
    .wire-skeleton-component[data-color="secondary"] { --wire-skeleton-highlight: var(--wire-color-secondary-soft); }
    .wire-skeleton-component[data-color="primary"] { --wire-skeleton-highlight: var(--wire-color-primary-soft); }

    .wire-skeleton-component__line {
      height: 0.8rem;
      border-radius: 999px;
      background: linear-gradient(90deg, var(--wire-color-surface-2), var(--wire-skeleton-highlight), var(--wire-color-surface-2));
      background-size: 200% 100%;
      animation: wire-skeleton-component-shimmer 1.4s infinite;
    }

    .wire-skeleton-component__line:last-child { width: 68%; }

    @keyframes wire-skeleton-component-shimmer {
      from { background-position: 200% 0; }
      to { background-position: -200% 0; }
    }

    @media (prefers-reduced-motion: reduce) {
      .wire-skeleton-component__line { animation: none; }
    }
  }
}
```

---

## Spinner

Showcase: https://component.wrnexusjs.dev/
Mount: <Spinner /> (legacy: data-component="Spinner")
Category: base
Purpose: Theme-aware, responsive spinner component.
Props: color: string = "primary", label: string = "Loading", size: string = "md", lines: number = 3, class: string = ""
Slots: none
Events: none

### Complete .wrn source contract

```wrn
component Spinner {
  props {
    color: string = "primary"
    label: string = "Loading"
    size: string = "md"
    lines: number = 3
    class: string = ""
  }
  view {
    <div class="wire-spinner-component {class}" data-color="{color}" data-size="{size}" role="status" aria-label="{label}">
      <i class="wire-spinner-component__indicator" aria-hidden="true"></i>
    </div>
  }

  style {
    .wire-spinner-component {
      --wire-spinner-size: 1.5rem;
      --wire-spinner-color: var(--wire-color-primary);
      display: inline-grid;
      place-items: center;
    }

    .wire-spinner-component[data-size="xs"] { --wire-spinner-size: 0.875rem; }
    .wire-spinner-component[data-size="sm"] { --wire-spinner-size: 1.125rem; }
    .wire-spinner-component[data-size="lg"] { --wire-spinner-size: 2rem; }
    .wire-spinner-component[data-size="xl"] { --wire-spinner-size: 2.5rem; }
    .wire-spinner-component[data-color="secondary"] { --wire-spinner-color: var(--wire-color-secondary); }
    .wire-spinner-component[data-color="success"] { --wire-spinner-color: var(--wire-color-success); }
    .wire-spinner-component[data-color="warning"] { --wire-spinner-color: var(--wire-color-warning); }
    .wire-spinner-component[data-color="danger"] { --wire-spinner-color: var(--wire-color-danger); }
    .wire-spinner-component[data-color="info"] { --wire-spinner-color: var(--wire-color-info); }

    .wire-spinner-component__indicator {
      display: block;
      width: var(--wire-spinner-size);
      height: var(--wire-spinner-size);
      border: 2px solid var(--wire-color-border);
      border-top-color: var(--wire-spinner-color);
      border-radius: 50%;
      animation: wire-spinner-component-spin 700ms linear infinite;
    }

    @keyframes wire-spinner-component-spin { to { transform: rotate(360deg); } }

    @media (prefers-reduced-motion: reduce) {
      .wire-spinner-component__indicator { animation-duration: 1.8s; }
    }
  }
}
```

---

## SplitHero

Showcase: https://component.wrnexusjs.dev/
Mount: <SplitHero /> (legacy: data-component="SplitHero")
Category: marketing
Purpose: Build a responsive two-column introduction balancing descriptive content with a visual, image, or structured data panel.
Props: eyebrow: string = "", eyebrowIcon: string = "icon-[lucide--sparkles]", title: string = "A better digital experience", highlight: string = "", description: string = "", primaryLabel: string = "", primaryHref: string = "", primaryIcon: string = "", secondaryLabel: string = "", secondaryHref: string = "", secondaryIcon: string = "", visualPosition: string = "right", reverse: boolean = false, ratio: string = "balanced", align: string = "left", size: string = "default", color: string = "primary", variant: string = "default", maxWidth: string = "xl", fullBleed: boolean = true, backgroundImage: string = "", visualImage: string = "", visualAlt: string = "", visualIcon: string = "", visualEyebrow: string = "", visualTitle: string = "", visualDescription: string = "", visualItems: unknown[] = [], visualStyle: string = "panel", trustItems: unknown[] = [], class: string = ""
Slots: default, actions, trust, visual
Events: none

### Complete .wrn source contract

```wrn
component SplitHero {
  props {
    eyebrow: string = ""
    eyebrowIcon: string = "icon-[lucide--sparkles]"
    title: string = "A better digital experience"
    highlight: string = ""
    description: string = ""
    primaryLabel: string = ""
    primaryHref: string = ""
    primaryIcon: string = ""
    secondaryLabel: string = ""
    secondaryHref: string = ""
    secondaryIcon: string = ""
    visualPosition: string = "right"
    reverse: boolean = false
    ratio: string = "balanced"
    align: string = "left"
    size: string = "default"
    color: string = "primary"
    variant: string = "default"
    maxWidth: string = "xl"
    fullBleed: boolean = true
    backgroundImage: string = ""
    visualImage: string = ""
    visualAlt: string = ""
    visualIcon: string = ""
    visualEyebrow: string = ""
    visualTitle: string = ""
    visualDescription: string = ""
    visualItems: unknown[] = []
    visualStyle: string = "panel"
    trustItems: unknown[] = []
    class: string = ""
  }

  view {
    <section
      {...attrs}
      data-ui-component="SplitHero"
      data-position='{reverse || visualPosition === "left" ? "left" : "right"}'
      data-ratio='{ratio}'
      data-align='{align}'
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      data-max-width='{maxWidth}'
      data-full-bleed='{fullBleed ? "true" : "false"}'
      data-visual-style='{visualStyle}'
      class='wire-split-hero {class}'
    >
      {#if backgroundImage}
        <img
          src='{backgroundImage}'
          alt=""
          aria-hidden="true"
          class="wire-split-hero__background-image"
        />
      {/if}

      <div class="wire-split-hero__glow wire-split-hero__glow--one" aria-hidden="true"></div>
      <div class="wire-split-hero__glow wire-split-hero__glow--two" aria-hidden="true"></div>

      <div class="wire-split-hero__inner">
        <div class="wire-split-hero__layout">
          <div class="wire-split-hero__content">
            {#if eyebrow}
              <div class="wire-split-hero__eyebrow">
                {#if eyebrowIcon}
                  <span class='wire-split-hero__eyebrow-icon {eyebrowIcon}' aria-hidden="true"></span>
                {/if}
                <span>{eyebrow}</span>
              </div>
            {/if}

            <h1 class="wire-split-hero__title">
              <span>{title}</span>
              {#if highlight}
                <span class="wire-split-hero__highlight">{highlight}</span>
              {/if}
            </h1>

            {#if description}
              <p class="wire-split-hero__description">{description}</p>
            {/if}

            <slot></slot>

            {#if primaryLabel || secondaryLabel}
              <div class="wire-split-hero__actions">
                {#if primaryLabel}
                  <a href='{primaryHref || "#"}' class="wire-split-hero__action wire-split-hero__action--primary">
                    {#if primaryIcon}
                      <span class='wire-split-hero__action-icon {primaryIcon}' aria-hidden="true"></span>
                    {/if}
                    <span>{primaryLabel}</span>
                  </a>
                {/if}

                {#if secondaryLabel}
                  <a href='{secondaryHref || "#"}' class="wire-split-hero__action wire-split-hero__action--secondary">
                    {#if secondaryIcon}
                      <span class='wire-split-hero__action-icon {secondaryIcon}' aria-hidden="true"></span>
                    {/if}
                    <span>{secondaryLabel}</span>
                  </a>
                {/if}

                <slot name="actions"></slot>
              </div>
            {:else}
              <div class="wire-split-hero__actions wire-split-hero__actions--slot">
                <slot name="actions"></slot>
              </div>
            {/if}

            {#if trustItems.length > 0}
              <div class="wire-split-hero__trust">
                {#each trustItems as item}
                  <span class="wire-split-hero__trust-item">
                    <span class='{item.icon || "icon-[lucide--check-circle-2]"}' aria-hidden="true"></span>
                    <span>{item.label || item.title || item}</span>
                  </span>
                {/each}
              </div>
            {/if}

            <slot name="trust"></slot>
          </div>

          <div class="wire-split-hero__visual-column">
            <slot name="visual"></slot>

            {#if visualImage || visualIcon || visualEyebrow || visualTitle || visualDescription || visualItems.length > 0}
              <div class="wire-split-hero__visual">
                {#if visualImage}
                  <div class="wire-split-hero__visual-media">
                    <img src='{visualImage}' alt='{visualAlt}' class="wire-split-hero__visual-image" />
                  </div>
                {/if}

                {#if visualIcon || visualEyebrow || visualTitle || visualDescription}
                  <div class="wire-split-hero__visual-header">
                    {#if visualIcon}
                      <div class="wire-split-hero__visual-icon" aria-hidden="true">
                        <span class='{visualIcon}'></span>
                      </div>
                    {/if}

                    <div class="wire-split-hero__visual-copy">
                      {#if visualEyebrow}
                        <p class="wire-split-hero__visual-eyebrow">{visualEyebrow}</p>
                      {/if}

                      {#if visualTitle}
                        <h2 class="wire-split-hero__visual-title">{visualTitle}</h2>
                      {/if}

                      {#if visualDescription}
                        <p class="wire-split-hero__visual-description">{visualDescription}</p>
                      {/if}
                    </div>
                  </div>
                {/if}

                {#if visualItems.length > 0}
                  <div class="wire-split-hero__visual-list">
                    {#each visualItems as item}
                      <div class="wire-split-hero__visual-item">
                        {#if item.icon}
                          <div class="wire-split-hero__visual-item-icon" aria-hidden="true">
                            <span class='{item.icon}'></span>
                          </div>
                        {/if}

                        <div class="wire-split-hero__visual-item-copy">
                          <div class="wire-split-hero__visual-item-heading">
                            <strong>{item.label || item.title}</strong>
                            {#if item.value}
                              <span>{item.value}</span>
                            {/if}
                          </div>

                          {#if item.description}
                            <p>{item.description}</p>
                          {/if}
                        </div>
                      </div>
                    {/each}
                  </div>
                {/if}
              </div>
            {/if}
          </div>
        </div>
      </div>
    </section>
  }

  style {
    .wire-split-hero {
      --split-accent: var(--wire-color-primary);
      --split-accent-hover: var(--wire-color-primary-hover);
      --split-accent-soft: var(--wire-color-primary-soft);
      --split-accent-muted: var(--wire-color-primary-muted);
      --split-contrast: var(--wire-color-primary-contrast);
      --split-border: color-mix(in srgb, var(--split-accent) 14%, var(--wire-color-border));

      position: relative;
      isolation: isolate;
      width: 100%;
      overflow: hidden;
      color: var(--wire-color-text);
      background:
        radial-gradient(circle at 88% 18%, color-mix(in srgb, var(--split-accent) 11%, transparent), transparent 28%),
        var(--wire-color-background);
    }

    .wire-split-hero[data-color="secondary"] {
      --split-accent: var(--wire-color-secondary);
      --split-accent-hover: var(--wire-color-secondary-hover);
      --split-accent-soft: var(--wire-color-secondary-soft);
      --split-accent-muted: var(--wire-color-secondary-muted);
      --split-contrast: var(--wire-color-secondary-contrast);
    }

    .wire-split-hero[data-color="info"] {
      --split-accent: var(--wire-color-info);
      --split-accent-hover: var(--wire-color-info-hover);
      --split-accent-soft: var(--wire-color-info-soft);
      --split-accent-muted: var(--wire-color-info-muted);
      --split-contrast: var(--wire-color-info-contrast);
    }

    .wire-split-hero[data-color="success"] {
      --split-accent: var(--wire-color-success);
      --split-accent-hover: var(--wire-color-success-hover);
      --split-accent-soft: var(--wire-color-success-soft);
      --split-accent-muted: var(--wire-color-success-muted);
      --split-contrast: var(--wire-color-success-contrast);
    }

    .wire-split-hero[data-color="warning"] {
      --split-accent: var(--wire-color-warning);
      --split-accent-hover: var(--wire-color-warning-hover);
      --split-accent-soft: var(--wire-color-warning-soft);
      --split-accent-muted: var(--wire-color-warning-muted);
      --split-contrast: var(--wire-color-warning-contrast);
    }

    .wire-split-hero[data-color="danger"] {
      --split-accent: var(--wire-color-danger);
      --split-accent-hover: var(--wire-color-danger-hover);
      --split-accent-soft: var(--wire-color-danger-soft);
      --split-accent-muted: var(--wire-color-danger-muted);
      --split-contrast: var(--wire-color-danger-contrast);
    }

    .wire-split-hero[data-variant="soft"] {
      background:
        radial-gradient(circle at 84% 18%, color-mix(in srgb, var(--split-accent) 16%, transparent), transparent 30%),
        linear-gradient(135deg, var(--split-accent-soft), transparent 64%),
        var(--wire-color-background);
    }

    .wire-split-hero[data-variant="raised"] {
      background: var(--wire-color-surface-raised);
      border-block: 1px solid var(--split-border);
      box-shadow: 0 22px 70px color-mix(in srgb, black 14%, transparent);
    }

    .wire-split-hero[data-variant="outline"] {
      background: transparent;
      border-block: 1px solid var(--split-border);
    }

    .wire-split-hero[data-variant="gradient"] {
      background:
        radial-gradient(circle at 82% 16%, color-mix(in srgb, var(--split-accent) 28%, transparent), transparent 32%),
        linear-gradient(135deg, color-mix(in srgb, var(--split-accent) 13%, var(--wire-color-background)), var(--wire-color-background) 58%);
    }

    .wire-split-hero[data-variant="solid"] {
      color: var(--split-contrast);
      background:
        radial-gradient(circle at 82% 16%, color-mix(in srgb, white 14%, transparent), transparent 32%),
        var(--split-accent);
    }

    .wire-split-hero[data-full-bleed="false"] {
      width: min(calc(100% - 2rem), 90rem);
      margin-inline: auto;
      border: 1px solid var(--split-border);
      border-radius: 1.75rem;
    }

    .wire-split-hero__background-image {
      position: absolute;
      inset: 0;
      z-index: -3;
      width: 100%;
      height: 100%;
      object-fit: cover;
      opacity: 0.12;
    }

    .wire-split-hero__glow {
      position: absolute;
      z-index: -2;
      width: 24rem;
      height: 24rem;
      pointer-events: none;
      background: var(--split-accent);
      border-radius: 999px;
      filter: blur(110px);
      opacity: 0.08;
    }

    .wire-split-hero__glow--one {
      top: -11rem;
      right: -6rem;
    }

    .wire-split-hero__glow--two {
      bottom: -14rem;
      left: -7rem;
      opacity: 0.05;
    }

    .wire-split-hero__inner {
      width: min(calc(100% - 2rem), 80rem);
      margin-inline: auto;
      padding-block: 5rem;
    }

    .wire-split-hero[data-max-width="compact"] .wire-split-hero__inner {
      width: min(calc(100% - 2rem), 64rem);
    }

    .wire-split-hero[data-max-width="lg"] .wire-split-hero__inner {
      width: min(calc(100% - 2rem), 72rem);
    }

    .wire-split-hero[data-max-width="wide"] .wire-split-hero__inner,
    .wire-split-hero[data-max-width="2xl"] .wire-split-hero__inner {
      width: min(calc(100% - 2rem), 90rem);
    }

    .wire-split-hero[data-max-width="full"] .wire-split-hero__inner {
      width: 100%;
      max-width: none;
      padding-inline: 2rem;
    }

    .wire-split-hero[data-size="compact"] .wire-split-hero__inner,
    .wire-split-hero[data-size="sm"] .wire-split-hero__inner {
      padding-block: 3rem;
    }

    .wire-split-hero[data-size="lg"] .wire-split-hero__inner {
      padding-block: 6rem;
    }

    .wire-split-hero__layout {
      display: grid;
      grid-template-columns: minmax(0, 1fr);
      align-items: center;
      gap: 3rem;
    }

    .wire-split-hero__content,
    .wire-split-hero__visual-column {
      min-width: 0;
    }

    .wire-split-hero__content {
      max-width: 45rem;
    }

    .wire-split-hero[data-align="center"] .wire-split-hero__content {
      margin-inline: auto;
      text-align: center;
    }

    .wire-split-hero[data-align="right"] .wire-split-hero__content {
      margin-left: auto;
      text-align: right;
    }

    .wire-split-hero__eyebrow {
      display: inline-flex;
      align-items: center;
      gap: 0.5rem;
      min-height: 2.25rem;
      padding: 0.45rem 0.8rem;
      color: var(--split-accent);
      background: var(--split-accent-soft);
      border: 1px solid color-mix(in srgb, var(--split-accent) 22%, transparent);
      border-radius: 999px;
      font-size: 0.76rem;
      font-weight: 600;
      line-height: 1;
    }

    .wire-split-hero[data-variant="solid"] .wire-split-hero__eyebrow {
      color: currentColor;
      background: color-mix(in srgb, white 14%, transparent);
      border-color: color-mix(in srgb, white 22%, transparent);
    }

    .wire-split-hero__eyebrow-icon {
      width: 0.95rem;
      height: 0.95rem;
    }

    .wire-split-hero__title {
      max-width: 15ch;
      margin: 1.45rem 0 0;
      color: var(--wire-color-text);
      font-size: clamp(2.35rem, 6vw, 4.8rem);
      font-weight: 650;
      line-height: 1.02;
      letter-spacing: -0.05em;
    }

    .wire-split-hero[data-size="compact"] .wire-split-hero__title,
    .wire-split-hero[data-size="sm"] .wire-split-hero__title {
      font-size: clamp(2rem, 5vw, 3.4rem);
    }

    .wire-split-hero[data-size="lg"] .wire-split-hero__title {
      font-size: clamp(2.75rem, 7vw, 5.5rem);
    }

    .wire-split-hero[data-align="center"] .wire-split-hero__title,
    .wire-split-hero[data-align="right"] .wire-split-hero__title {
      margin-inline: auto;
    }

    .wire-split-hero[data-align="right"] .wire-split-hero__title {
      margin-right: 0;
    }

    .wire-split-hero[data-variant="solid"] .wire-split-hero__title {
      color: currentColor;
    }

    .wire-split-hero__highlight {
      display: block;
      color: var(--split-accent);
    }

    .wire-split-hero[data-variant="solid"] .wire-split-hero__highlight {
      color: currentColor;
      opacity: 0.82;
    }

    .wire-split-hero__description {
      max-width: 42rem;
      margin: 1.4rem 0 0;
      color: var(--wire-color-text-muted);
      font-size: clamp(0.96rem, 1.8vw, 1.1rem);
      line-height: 1.8;
    }

    .wire-split-hero[data-align="center"] .wire-split-hero__description,
    .wire-split-hero[data-align="right"] .wire-split-hero__description {
      margin-inline: auto;
    }

    .wire-split-hero[data-align="right"] .wire-split-hero__description {
      margin-right: 0;
    }

    .wire-split-hero[data-variant="solid"] .wire-split-hero__description {
      color: color-mix(in srgb, currentColor 82%, transparent);
    }

    .wire-split-hero__actions {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 0.75rem;
      margin-top: 2rem;
    }

    .wire-split-hero__actions--slot:empty {
      display: none;
    }

    .wire-split-hero[data-align="center"] .wire-split-hero__actions {
      justify-content: center;
    }

    .wire-split-hero[data-align="right"] .wire-split-hero__actions {
      justify-content: flex-end;
    }

    .wire-split-hero__action {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 0.55rem;
      min-height: 2.85rem;
      padding: 0.78rem 1.1rem;
      border-radius: 0.75rem;
      font-size: 0.84rem;
      font-weight: 700;
      line-height: 1;
      text-decoration: none;
      transition:
        transform 160ms ease,
        background-color 160ms ease,
        border-color 160ms ease,
        color 160ms ease,
        box-shadow 160ms ease;
    }

    .wire-split-hero__action:hover {
      transform: translateY(-2px);
    }

    .wire-split-hero__action--primary {
      color: var(--split-contrast);
      background: var(--split-accent);
      border: 1px solid transparent;
      box-shadow: 0 10px 26px color-mix(in srgb, var(--split-accent) 22%, transparent);
    }

    .wire-split-hero__action--primary:hover {
      background: var(--split-accent-hover);
    }

    .wire-split-hero__action--secondary {
      color: var(--split-accent);
      background: transparent;
      border: 1px solid color-mix(in srgb, var(--split-accent) 42%, var(--wire-color-border));
    }

    .wire-split-hero__action--secondary:hover {
      background: var(--split-accent-soft);
      border-color: var(--split-accent);
    }

    .wire-split-hero[data-variant="solid"] .wire-split-hero__action--primary {
      color: var(--split-accent);
      background: var(--split-contrast);
      box-shadow: none;
    }

    .wire-split-hero[data-variant="solid"] .wire-split-hero__action--secondary {
      color: currentColor;
      border-color: color-mix(in srgb, white 46%, transparent);
    }

    .wire-split-hero[data-variant="solid"] .wire-split-hero__action--secondary:hover {
      background: color-mix(in srgb, white 12%, transparent);
      border-color: color-mix(in srgb, white 68%, transparent);
    }

    .wire-split-hero__action-icon {
      width: 1rem;
      height: 1rem;
    }

    .wire-split-hero__action:focus-visible {
      outline: 2px solid var(--wire-color-focus);
      outline-offset: 3px;
    }

    .wire-split-hero__trust {
      display: flex;
      flex-wrap: wrap;
      gap: 0.75rem 1.2rem;
      margin-top: 1.7rem;
      color: var(--wire-color-text-muted);
      font-size: 0.78rem;
    }

    .wire-split-hero[data-align="center"] .wire-split-hero__trust {
      justify-content: center;
    }

    .wire-split-hero[data-align="right"] .wire-split-hero__trust {
      justify-content: flex-end;
    }

    .wire-split-hero[data-variant="solid"] .wire-split-hero__trust {
      color: color-mix(in srgb, currentColor 80%, transparent);
    }

    .wire-split-hero__trust-item {
      display: inline-flex;
      align-items: center;
      gap: 0.42rem;
    }

    .wire-split-hero__trust-item > span:first-child {
      width: 0.95rem;
      height: 0.95rem;
      color: var(--split-accent);
    }

    .wire-split-hero[data-variant="solid"] .wire-split-hero__trust-item > span:first-child {
      color: currentColor;
    }

    .wire-split-hero__visual-column {
      position: relative;
    }

    .wire-split-hero__visual-column::before {
      content: "";
      position: absolute;
      inset: 8% -5% -4% 8%;
      z-index: -1;
      background: var(--split-accent-soft);
      border-radius: 2rem;
      filter: blur(2px);
      transform: rotate(2deg);
    }

    .wire-split-hero__visual {
      overflow: hidden;
      background:
        linear-gradient(145deg, color-mix(in srgb, var(--split-accent) 7%, transparent), transparent 58%),
        var(--wire-color-surface-raised);
      border: 1px solid var(--split-border);
      border-radius: 1.5rem;
      box-shadow:
        0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
        0 30px 76px color-mix(in srgb, black 18%, transparent);
    }

    .wire-split-hero[data-visual-style="plain"] .wire-split-hero__visual {
      overflow: visible;
      background: transparent;
      border-color: transparent;
      border-radius: 0;
      box-shadow: none;
    }

    .wire-split-hero[data-visual-style="soft"] .wire-split-hero__visual {
      background:
        linear-gradient(145deg, var(--split-accent-soft), transparent 72%),
        var(--wire-color-surface-raised);
      box-shadow: none;
    }

    .wire-split-hero[data-variant="solid"] .wire-split-hero__visual {
      color: var(--wire-color-text);
      background: var(--wire-color-surface-raised);
      border-color: color-mix(in srgb, white 24%, transparent);
    }

    .wire-split-hero__visual-media {
      position: relative;
      aspect-ratio: 16 / 10;
      overflow: hidden;
      background: var(--wire-color-surface-soft);
    }

    .wire-split-hero__visual-image {
      display: block;
      width: 100%;
      height: 100%;
      object-fit: cover;
    }

    .wire-split-hero__visual-header {
      display: grid;
      grid-template-columns: auto minmax(0, 1fr);
      gap: 1rem;
      padding: 1.5rem;
    }

    .wire-split-hero__visual-icon,
    .wire-split-hero__visual-item-icon {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex: 0 0 auto;
      width: 2.8rem;
      height: 2.8rem;
      color: var(--split-accent);
      background: var(--split-accent-soft);
      border: 1px solid color-mix(in srgb, var(--split-accent) 20%, transparent);
      border-radius: 0.9rem;
      font-size: 1.2rem;
    }

    .wire-split-hero__visual-copy,
    .wire-split-hero__visual-item-copy {
      min-width: 0;
    }

    .wire-split-hero__visual-eyebrow {
      margin: 0 0 0.35rem;
      color: var(--split-accent);
      font-size: 0.69rem;
      font-weight: 700;
      letter-spacing: 0.11em;
      text-transform: uppercase;
    }

    .wire-split-hero__visual-title {
      margin: 0;
      color: var(--wire-color-text);
      font-size: 1.15rem;
      font-weight: 650;
      line-height: 1.35;
    }

    .wire-split-hero__visual-description {
      margin: 0.5rem 0 0;
      color: var(--wire-color-text-muted);
      font-size: 0.82rem;
      line-height: 1.65;
    }

    .wire-split-hero__visual-list {
      display: grid;
      gap: 0;
      border-top: 1px solid var(--wire-color-border);
    }

    .wire-split-hero__visual-item {
      display: grid;
      grid-template-columns: auto minmax(0, 1fr);
      gap: 0.85rem;
      padding: 1.1rem 1.5rem;
    }

    .wire-split-hero__visual-item + .wire-split-hero__visual-item {
      border-top: 1px solid var(--wire-color-border);
    }

    .wire-split-hero__visual-item-icon {
      width: 2.25rem;
      height: 2.25rem;
      border-radius: 0.72rem;
      font-size: 1rem;
    }

    .wire-split-hero__visual-item-heading {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
      color: var(--wire-color-text);
      font-size: 0.82rem;
    }

    .wire-split-hero__visual-item-heading > span {
      color: var(--split-accent);
      font-weight: 700;
    }

    .wire-split-hero__visual-item p {
      margin: 0.35rem 0 0;
      color: var(--wire-color-text-muted);
      font-size: 0.75rem;
      line-height: 1.55;
    }

    @media (min-width: 960px) {
      .wire-split-hero__layout {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        gap: 4.5rem;
      }

      .wire-split-hero[data-ratio="content"] .wire-split-hero__layout {
        grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
      }

      .wire-split-hero[data-ratio="visual"] .wire-split-hero__layout {
        grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
      }

      .wire-split-hero[data-position="left"] .wire-split-hero__content {
        order: 2;
      }

      .wire-split-hero[data-position="left"] .wire-split-hero__visual-column {
        order: 1;
      }
    }

    @media (max-width: 639px) {
      .wire-split-hero__inner {
        padding-block: 3.25rem;
      }

      .wire-split-hero__actions {
        align-items: stretch;
      }

      .wire-split-hero__action {
        width: 100%;
      }

      .wire-split-hero__visual-header,
      .wire-split-hero__visual-item {
        padding-inline: 1.15rem;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .wire-split-hero__action {
        transition: none;
      }

      .wire-split-hero__action:hover {
        transform: none;
      }
    }
  }
}
```

---

## StatsBar

Showcase: https://component.wrnexusjs.dev/
Mount: <StatsBar /> (legacy: data-component="StatsBar")
Category: data
Purpose: Present a compact responsive strip of key facts, counts, performance indicators, or trust signals.
Props: items: unknown[] = [], columns: number = 4, compact: boolean = true, dividers: boolean = true, icons: boolean = true, size: string = "default", color: string = "primary", variant: string = "raised", maxWidth: string = "xl", class: string = ""
Slots: none
Events: none

### Complete .wrn source contract

```wrn
component StatsBar {
  props {
    items: unknown[] = []
    columns: number = 4
    compact: boolean = true
    dividers: boolean = true
    icons: boolean = true
    size: string = "default"
    color: string = "primary"
    variant: string = "raised"
    maxWidth: string = "xl"
    class: string = ""
  }

  view {
    <section
      {...attrs}
      data-ui-component="StatsBar"
      data-columns='{columns}'
      data-compact='{compact ? "true" : "false"}'
      data-dividers='{dividers ? "true" : "false"}'
      data-icons='{icons ? "true" : "false"}'
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      data-max-width='{maxWidth}'
      class='wire-stats-bar {class}'
      aria-label="Key statistics"
    >
      <div class="wire-stats-bar__inner">
        <div class="wire-stats-bar__surface">
          {#each items as item, itemIndex}
            <article
              class="wire-stats-bar__item"
              data-color='{item.color || color}'
              data-index='{itemIndex}'
            >
              <div class="wire-stats-bar__content">
                {#if item.label}
                  <p class="wire-stats-bar__label">{item.label}</p>
                {/if}

                <div class="wire-stats-bar__value-row">
                  <strong class="wire-stats-bar__value">{item.value}</strong>

                  {#if item.suffix}
                    <span class="wire-stats-bar__suffix">{item.suffix}</span>
                  {/if}
                </div>

                {#if item.description}
                  <p class="wire-stats-bar__description">{item.description}</p>
                {/if}

                {#if item.trend || item.trendLabel}
                  <div
                    class="wire-stats-bar__trend"
                    data-direction='{item.trendDirection || "neutral"}'
                  >
                    {#if item.trendDirection === "up"}
                      <span
                        class="icon-[lucide--trending-up] wire-stats-bar__trend-icon"
                        aria-hidden="true"
                      ></span>
                    {:else if item.trendDirection === "down"}
                      <span
                        class="icon-[lucide--trending-down] wire-stats-bar__trend-icon"
                        aria-hidden="true"
                      ></span>
                    {:else}
                      <span
                        class="icon-[lucide--minus] wire-stats-bar__trend-icon"
                        aria-hidden="true"
                      ></span>
                    {/if}

                    {#if item.trend}
                      <span class="wire-stats-bar__trend-value">{item.trend}</span>
                    {/if}

                    {#if item.trendLabel}
                      <span class="wire-stats-bar__trend-label">{item.trendLabel}</span>
                    {/if}
                  </div>
                {/if}
              </div>

              {#if icons && item.icon}
                <div class="wire-stats-bar__icon" aria-hidden="true">
                  <span class='{item.icon}'></span>
                </div>
              {/if}
            </article>
          {/each}
        </div>
      </div>
    </section>
  }

  style {
    .wire-stats-bar {
      --stats-accent: var(--wire-color-primary);
      --stats-contrast: var(--wire-color-primary-contrast);
      --stats-soft: color-mix(in srgb, var(--stats-accent) 12%, transparent);
      --stats-border: color-mix(in srgb, var(--stats-accent) 22%, var(--wire-color-border));

      position: relative;
      width: 100%;
      color: var(--wire-color-text);
    }

    .wire-stats-bar[data-color="secondary"] {
      --stats-accent: var(--wire-color-secondary);
      --stats-contrast: var(--wire-color-secondary-contrast);
    }

    .wire-stats-bar[data-color="info"] {
      --stats-accent: var(--wire-color-info);
      --stats-contrast: var(--wire-color-info-contrast);
    }

    .wire-stats-bar[data-color="success"] {
      --stats-accent: var(--wire-color-success);
      --stats-contrast: var(--wire-color-success-contrast);
    }

    .wire-stats-bar[data-color="warning"] {
      --stats-accent: var(--wire-color-warning);
      --stats-contrast: var(--wire-color-warning-contrast);
    }

    .wire-stats-bar[data-color="danger"] {
      --stats-accent: var(--wire-color-danger);
      --stats-contrast: var(--wire-color-danger-contrast);
    }

    .wire-stats-bar__inner {
      width: min(calc(100% - 2rem), 80rem);
      margin-inline: auto;
    }

    .wire-stats-bar[data-max-width="compact"] .wire-stats-bar__inner {
      width: min(calc(100% - 2rem), 64rem);
    }

    .wire-stats-bar[data-max-width="lg"] .wire-stats-bar__inner {
      width: min(calc(100% - 2rem), 72rem);
    }

    .wire-stats-bar[data-max-width="wide"] .wire-stats-bar__inner,
    .wire-stats-bar[data-max-width="2xl"] .wire-stats-bar__inner {
      width: min(calc(100% - 2rem), 90rem);
    }

    .wire-stats-bar[data-max-width="full"] .wire-stats-bar__inner {
      width: 100%;
      max-width: none;
    }

    .wire-stats-bar__surface {
      display: grid;
      grid-template-columns: minmax(0, 1fr);
      overflow: hidden;
      background:
        linear-gradient(
          135deg,
          color-mix(in srgb, var(--stats-accent) 7%, transparent),
          transparent 42%
        ),
        var(--wire-color-surface-raised);
      border: 1px solid var(--stats-border);
      border-radius: 1.25rem;
      box-shadow:
        0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
        0 18px 48px color-mix(in srgb, black 16%, transparent);
    }

    .wire-stats-bar[data-variant="default"] .wire-stats-bar__surface {
      background: var(--wire-color-surface);
      box-shadow: none;
    }

    .wire-stats-bar[data-variant="soft"] .wire-stats-bar__surface {
      background:
        linear-gradient(135deg, var(--stats-soft), transparent 58%),
        var(--wire-color-surface-raised);
      box-shadow: none;
    }

    .wire-stats-bar[data-variant="outline"] .wire-stats-bar__surface {
      background: transparent;
      box-shadow: none;
    }

    .wire-stats-bar[data-variant="minimal"] .wire-stats-bar__surface {
      background: transparent;
      border-color: transparent;
      border-radius: 0;
      box-shadow: none;
    }

    .wire-stats-bar[data-variant="solid"] .wire-stats-bar__surface {
      color: var(--stats-contrast);
      background:
        linear-gradient(135deg, color-mix(in srgb, white 8%, transparent), transparent 52%),
        var(--stats-accent);
      border-color: color-mix(in srgb, white 24%, transparent);
      box-shadow: 0 20px 56px color-mix(in srgb, var(--stats-accent) 28%, transparent);
    }

    .wire-stats-bar__item {
      --item-accent: var(--stats-accent);

      position: relative;
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      align-items: start;
      gap: 1rem;
      min-width: 0;
      padding: 1.5rem;
      transition:
        background-color 180ms ease,
        transform 180ms ease;
    }

    .wire-stats-bar[data-compact="true"] .wire-stats-bar__item {
      padding: 1.15rem 1.25rem;
    }

    .wire-stats-bar[data-size="sm"] .wire-stats-bar__item {
      padding: 1rem 1.1rem;
    }

    .wire-stats-bar[data-size="lg"] .wire-stats-bar__item {
      padding: 1.8rem;
    }

    .wire-stats-bar__item[data-color="secondary"] {
      --item-accent: var(--wire-color-secondary);
    }

    .wire-stats-bar__item[data-color="info"] {
      --item-accent: var(--wire-color-info);
    }

    .wire-stats-bar__item[data-color="success"] {
      --item-accent: var(--wire-color-success);
    }

    .wire-stats-bar__item[data-color="warning"] {
      --item-accent: var(--wire-color-warning);
    }

    .wire-stats-bar__item[data-color="danger"] {
      --item-accent: var(--wire-color-danger);
    }

    .wire-stats-bar__item:hover {
      background: color-mix(in srgb, var(--item-accent) 6%, transparent);
    }

    .wire-stats-bar[data-variant="solid"] .wire-stats-bar__item:hover {
      background: color-mix(in srgb, white 8%, transparent);
    }

    .wire-stats-bar__content {
      min-width: 0;
    }

    .wire-stats-bar__label {
      margin: 0;
      color: var(--wire-color-text-muted);
      font-size: 0.78rem;
      font-weight: 600;
      line-height: 1.4;
      letter-spacing: 0.01em;
    }

    .wire-stats-bar__value-row {
      display: flex;
      align-items: baseline;
      gap: 0.25rem;
      margin-top: 0.45rem;
    }

    .wire-stats-bar__value {
      margin: 0;
      color: var(--wire-color-text);
      font-size: clamp(1.75rem, 2.4vw, 2.35rem);
      font-weight: 650;
      line-height: 1;
      letter-spacing: -0.035em;
    }

    .wire-stats-bar[data-compact="true"] .wire-stats-bar__value {
      font-size: clamp(1.45rem, 2vw, 1.9rem);
    }

    .wire-stats-bar__suffix {
      color: var(--item-accent);
      font-size: 0.95rem;
      font-weight: 700;
    }

    .wire-stats-bar__description {
      max-width: 30rem;
      margin: 0.7rem 0 0;
      color: var(--wire-color-text-muted);
      font-size: 0.78rem;
      line-height: 1.55;
    }

    .wire-stats-bar[data-compact="true"] .wire-stats-bar__description {
      margin-top: 0.55rem;
      font-size: 0.73rem;
    }

    .wire-stats-bar__icon {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 2.65rem;
      height: 2.65rem;
      color: var(--item-accent);
      background: color-mix(in srgb, var(--item-accent) 11%, transparent);
      border-radius: 0.85rem;
    }

    .wire-stats-bar__icon > span {
      width: 1.2rem;
      height: 1.2rem;
    }

    .wire-stats-bar[data-compact="true"] .wire-stats-bar__icon {
      width: 2.35rem;
      height: 2.35rem;
      border-radius: 0.75rem;
    }

    .wire-stats-bar__trend {
      display: inline-flex;
      align-items: center;
      gap: 0.3rem;
      width: fit-content;
      margin-top: 0.7rem;
      padding: 0.25rem 0.5rem;
      color: var(--wire-color-text-muted);
      font-size: 0.7rem;
      line-height: 1;
      background: color-mix(in srgb, var(--wire-color-text) 5%, transparent);
      border-radius: 999px;
    }

    .wire-stats-bar__trend[data-direction="up"] {
      color: var(--wire-color-success);
    }

    .wire-stats-bar__trend[data-direction="down"] {
      color: var(--wire-color-danger);
    }

    .wire-stats-bar__trend-icon {
      width: 0.8rem;
      height: 0.8rem;
    }

    .wire-stats-bar__trend-value {
      font-weight: 700;
    }

    .wire-stats-bar__trend-label {
      opacity: 0.8;
    }

    .wire-stats-bar[data-variant="solid"] .wire-stats-bar__label,
    .wire-stats-bar[data-variant="solid"] .wire-stats-bar__description,
    .wire-stats-bar[data-variant="solid"] .wire-stats-bar__value,
    .wire-stats-bar[data-variant="solid"] .wire-stats-bar__suffix {
      color: var(--stats-contrast);
    }

    .wire-stats-bar[data-variant="solid"] .wire-stats-bar__label,
    .wire-stats-bar[data-variant="solid"] .wire-stats-bar__description {
      opacity: 0.78;
    }

    .wire-stats-bar[data-variant="solid"] .wire-stats-bar__icon {
      color: var(--stats-contrast);
      background: color-mix(in srgb, white 13%, transparent);
    }

    .wire-stats-bar[data-dividers="true"] .wire-stats-bar__item + .wire-stats-bar__item {
      border-top: 1px solid var(--wire-color-border);
    }

    .wire-stats-bar[data-variant="solid"][data-dividers="true"]
      .wire-stats-bar__item + .wire-stats-bar__item {
      border-color: color-mix(in srgb, white 18%, transparent);
    }

    @media (min-width: 640px) {
      .wire-stats-bar__surface {
        grid-template-columns: repeat(2, minmax(0, 1fr));
      }

      .wire-stats-bar[data-columns="1"] .wire-stats-bar__surface {
        grid-template-columns: minmax(0, 1fr);
      }

      .wire-stats-bar[data-dividers="true"] .wire-stats-bar__item {
        border-top: 0;
      }

      .wire-stats-bar[data-dividers="true"] .wire-stats-bar__item:nth-child(even) {
        border-left: 1px solid var(--wire-color-border);
      }

      .wire-stats-bar[data-dividers="true"] .wire-stats-bar__item:nth-child(n + 3) {
        border-top: 1px solid var(--wire-color-border);
      }

      .wire-stats-bar[data-variant="solid"][data-dividers="true"]
        .wire-stats-bar__item:nth-child(even),
      .wire-stats-bar[data-variant="solid"][data-dividers="true"]
        .wire-stats-bar__item:nth-child(n + 3) {
        border-color: color-mix(in srgb, white 18%, transparent);
      }
    }

    @media (min-width: 1024px) {
      .wire-stats-bar[data-columns="1"] .wire-stats-bar__surface {
        grid-template-columns: minmax(0, 1fr);
      }

      .wire-stats-bar[data-columns="2"] .wire-stats-bar__surface {
        grid-template-columns: repeat(2, minmax(0, 1fr));
      }

      .wire-stats-bar[data-columns="3"] .wire-stats-bar__surface {
        grid-template-columns: repeat(3, minmax(0, 1fr));
      }

      .wire-stats-bar[data-columns="4"] .wire-stats-bar__surface {
        grid-template-columns: repeat(4, minmax(0, 1fr));
      }

      .wire-stats-bar[data-columns="5"] .wire-stats-bar__surface {
        grid-template-columns: repeat(5, minmax(0, 1fr));
      }

      .wire-stats-bar[data-columns="6"] .wire-stats-bar__surface {
        grid-template-columns: repeat(6, minmax(0, 1fr));
      }

      .wire-stats-bar[data-dividers="true"] .wire-stats-bar__item,
      .wire-stats-bar[data-dividers="true"] .wire-stats-bar__item:nth-child(even),
      .wire-stats-bar[data-dividers="true"] .wire-stats-bar__item:nth-child(n + 3) {
        border-top: 0;
        border-left: 1px solid var(--wire-color-border);
      }

      .wire-stats-bar[data-dividers="true"] .wire-stats-bar__item:first-child {
        border-left: 0;
      }

      .wire-stats-bar[data-variant="solid"][data-dividers="true"] .wire-stats-bar__item {
        border-color: color-mix(in srgb, white 18%, transparent);
      }
    }

    @media (max-width: 639px) {
      .wire-stats-bar[data-max-width="full"] .wire-stats-bar__surface {
        border-inline: 0;
        border-radius: 0;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .wire-stats-bar__item {
        transition: none;
      }
    }
  }
}
```

---

## Stepper

Showcase: https://component.wrnexusjs.dev/
Mount: <Stepper /> (legacy: data-component="Stepper")
Category: navigation
Purpose: Theme-aware, responsive stepper component.
Props: color: string = "primary", size: string = "default", steps: unknown[] = [], active: number = 0, orientation: string = "horizontal", clickable: boolean = false, label: string = "Progress", showPanel: boolean = false, controls: boolean = false, allowSkip: boolean = false, nextDisabled: boolean = false, backLabel: string = "Back", nextLabel: string = "Next", skipLabel: string = "Skip", finishLabel: string = "Finish", class: string = ""
Slots: step-{index}, panel-{index}, default
Events: change, back, next, skip, finish

### Complete .wrn source contract

```wrn
// Stepper -- ordered progress through a sequence.
//
//   <Stepper steps='[{"label":"Account"},{"label":"Billing"}]' active={1} />
//
// Each step can be authored by hand instead of using the built-in body, by
// passing a slot named for its index:
//
//   <Stepper steps={steps} active={1}>
//     <div data-slot="step-1">...anything...</div>
//   </Stepper>
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Stepper {
  outputs {
    change(payload: { index: number; step: object })
    back(payload: { index: number; step: object })
    next(payload: { index: number; step: object })
    skip(payload: { index: number; step: object })
    finish(payload: { index: number; step: object })
  }

  props {
    color: string = "primary"
    size: string = "default"
    steps: unknown[] = []
    active: number = 0
    orientation: string = "horizontal"
    clickable: boolean = false
    label: string = "Progress"
    // Render each step body, showing only the active one -- same contract as
    // Tabs, so a wizard does not have to hand-roll the panel switching.
    showPanel: boolean = false
    controls: boolean = false
    allowSkip: boolean = false
    // Lets a form hold the step. The component never validates anything
    // itself: the page owns the form, so it sets this while the step is
    // incomplete and clears it once the step passes.
    nextDisabled: boolean = false
    backLabel: string = "Back"
    nextLabel: string = "Next"
    skipLabel: string = "Skip"
    finishLabel: string = "Finish"
    class: string = ""
  }

  functions {
    shared function stepList() {
      return Array.isArray(steps) ? steps : []
    }

    shared function activeIndex() {
      var count = stepList().length
      if (count < 1) {
        return 0
      }
      var value = Number(active)
      if (!value || value < 0) {
        return 0
      }
      return Math.min(value, count - 1)
    }

    shared function statusFor(index) {
      if (index < activeIndex()) {
        return "complete"
      }
      if (index === activeIndex()) {
        return "current"
      }
      return "upcoming"
    }

    // An empty orientation is how the roving runtime is told to stay out of
    // the way, so a read-only stepper never takes arrow-key focus.
    shared function rovingAxis() {
      if (!clickable) {
        return ""
      }
      return orientation === "vertical" ? "vertical" : "horizontal"
    }

    shared function isActiveStep(index) {
      return index === activeIndex()
    }

    shared function lastIndex() {
      return Math.max(0, stepList().length - 1)
    }

    shared function onLastStep() {
      return activeIndex() >= lastIndex()
    }

    shared function activeStep() {
      var list = stepList()
      return list.length ? list[activeIndex()] : {}
    }

    client function goBack() {
      var target = Math.max(0, activeIndex() - 1)
      output.back({ index: target, step: stepList()[target] || {} })
      output.change({ index: target, step: stepList()[target] || {} })
    }

    client function goNext() {
      if (nextDisabled) {
        return
      }
      if (onLastStep()) {
        output.finish({ index: activeIndex(), step: activeStep() })
        return
      }
      var target = Math.min(lastIndex(), activeIndex() + 1)
      output.next({ index: target, step: stepList()[target] || {} })
      output.change({ index: target, step: stepList()[target] || {} })
    }

    client function goSkip() {
      var target = Math.min(lastIndex(), activeIndex() + 1)
      output.skip({ index: target, step: stepList()[target] || {} })
      output.change({ index: target, step: stepList()[target] || {} })
    }

    client function selectStep(index, step) {
      if (!clickable) {
        return
      }
      output.change({ index: index, step: step })
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="Stepper"
      class='wire-stepper {class}'
      data-orientation='{orientation}'
      data-color='{color}'
      data-size='{size}'
      data-clickable='{clickable}'
    >
    <ol
      class="wire-stepper__list"
      data-wrn-roving='{rovingAxis()}'
      aria-label='{label}'
    >
      {#each stepList() as step, index}
        <li
          class="wire-stepper__step"
          data-status='{statusFor(index)}'
          aria-current='{statusFor(index) === "current" ? "step" : "false"}'
        >
          <button
            type="button"
            class="wire-stepper__button"
            data-wrn-roving-item='{clickable ? "true" : "false"}'
            @click='selectStep(index, step)'
          >
            <span class="wire-stepper__marker" aria-hidden="true">
              <span class='wire-stepper__icon {step.icon}' data-show="step.icon"></span>
              <span class="wire-stepper__number" data-show="!step.icon">{index + 1}</span>
            </span>
            <span class="wire-stepper__body">
              <span class="wire-stepper__label">{step.label}</span>
              <span class="wire-stepper__description" data-show="step.description">
                {step.description}
              </span>
            </span>
          </button>
          <span class="wire-stepper__custom">
            <slot name="step-{index}"></slot>
          </span>
        </li>
      {/each}
    </ol>

    <div class="wire-stepper__panels" data-show="showPanel">
      {#each stepList() as step, index}
        <div
          class="wire-stepper__panel"
          data-show='isActiveStep(index)'
          aria-hidden='{index === activeIndex() ? "false" : "true"}'
        >
          <h4 class="wire-stepper__panel-title" data-show="step.title">{step.title}</h4>
          <p class="wire-stepper__panel-body" data-show="step.content">{step.content}</p>
          <slot name="panel-{index}"></slot>
        </div>
      {/each}
    </div>

    <div class="wire-stepper__controls" data-show="controls">
      <button
        type="button"
        class="wire-stepper__button-control wire-stepper__back"
        disabled='{activeIndex() === 0}'
        @click='goBack()'
      >
        {backLabel}
      </button>
      <span class="wire-stepper__controls-spacer"></span>
      <button
        type="button"
        class="wire-stepper__button-control wire-stepper__skip"
        data-show="allowSkip && !onLastStep()"
        @click='goSkip()'
      >
        {skipLabel}
      </button>
      <button
        type="button"
        class="wire-stepper__button-control wire-stepper__next"
        data-primary="true"
        disabled='{nextDisabled}'
        @click='goNext()'
      >
        {onLastStep() ? finishLabel : nextLabel}
      </button>
    </div>

      <slot />
    </div>
  }

  style {
    .wire-stepper {
      --stepper-accent: var(--wire-color-primary);
      max-width: 100%;
    }

    .wire-stepper__list {
      display: flex;
      gap: 0.5rem;
      margin: 0;
      padding: 0;
      list-style: none;
      max-width: 100%;
    }

    .wire-stepper[data-color="secondary"] {
      --stepper-accent: var(--wire-color-secondary);
    }

    .wire-stepper[data-color="success"] {
      --stepper-accent: var(--wire-color-success);
    }

    .wire-stepper[data-color="danger"] {
      --stepper-accent: var(--wire-color-danger);
    }

    .wire-stepper[data-color="info"] {
      --stepper-accent: var(--wire-color-info);
    }

    .wire-stepper[data-size="sm"] {
      font-size: 0.82rem;
    }

    .wire-stepper[data-size="lg"] {
      font-size: 1rem;
    }

    .wire-stepper[data-orientation="vertical"] .wire-stepper__list {
      flex-direction: column;
    }

    .wire-stepper__step {
      display: flex;
      flex-direction: column;
      flex: 1 1 0;
      min-width: 0;
      gap: 0.35rem;
    }

    .wire-stepper__button {
      appearance: none;
      display: flex;
      align-items: center;
      gap: 0.6rem;
      width: 100%;
      padding: 0.5rem;
      border: 0;
      border-radius: var(--wire-radius-sm);
      background: transparent;
      color: inherit;
      font: inherit;
      text-align: left;
      cursor: default;
    }

    .wire-stepper[data-clickable="true"] .wire-stepper__button {
      cursor: pointer;
    }

    .wire-stepper[data-clickable="true"] .wire-stepper__button:hover {
      background: var(--wire-color-surface-soft);
    }

    .wire-stepper__button:focus-visible {
      outline: 2px solid var(--stepper-accent);
      outline-offset: 2px;
    }

    .wire-stepper__marker {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex: 0 0 auto;
      width: 2rem;
      height: 2rem;
      border: 1px solid var(--wire-color-border);
      border-radius: 999px;
      background: var(--wire-color-surface);
      font-size: 0.85rem;
      font-weight: 700;
    }

    .wire-stepper__step[data-status="complete"] .wire-stepper__marker {
      border-color: var(--stepper-accent);
      background: var(--stepper-accent);
      color: var(--wire-color-primary-contrast);
    }

    .wire-stepper__step[data-status="current"] .wire-stepper__marker {
      border-color: var(--stepper-accent);
      color: var(--stepper-accent);
      box-shadow: 0 0 0 3px color-mix(in srgb, var(--stepper-accent) 22%, transparent);
    }

    .wire-stepper__step[data-status="upcoming"] .wire-stepper__marker {
      color: var(--wire-color-text-muted);
    }

    .wire-stepper__body {
      display: flex;
      flex-direction: column;
      min-width: 0;
    }

    .wire-stepper__label {
      font-size: 0.9rem;
      font-weight: 600;
    }

    .wire-stepper__description {
      color: var(--wire-color-text-muted);
      font-size: 0.78rem;
    }

    .wire-stepper__step[data-status="upcoming"] .wire-stepper__label {
      color: var(--wire-color-text-muted);
    }

    .wire-stepper__panels {
      margin-top: 1rem;
    }

    .wire-stepper__panel {
      padding: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-md);
      background: var(--wire-color-surface);
      animation: wire-stepper-in 200ms ease both;
    }

    .wire-stepper__panel-title {
      margin: 0 0 0.35rem;
      font-size: 0.95rem;
      font-weight: 700;
    }

    .wire-stepper__panel-body {
      margin: 0;
      color: var(--wire-color-text-muted);
      line-height: 1.6;
    }

    .wire-stepper__controls {
      display: flex;
      align-items: center;
      gap: 0.5rem;
      margin-top: 0.85rem;
    }

    .wire-stepper__controls-spacer {
      flex: 1 1 auto;
    }

    .wire-stepper__button-control {
      appearance: none;
      padding: 0.45rem 0.9rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-sm);
      background: var(--wire-color-surface);
      color: var(--wire-color-text);
      font: inherit;
      font-size: 0.86rem;
      font-weight: 600;
      cursor: pointer;
    }

    .wire-stepper__button-control:hover:not(:disabled) {
      background: var(--wire-color-surface-soft);
    }

    .wire-stepper__button-control:focus-visible {
      outline: 2px solid var(--stepper-accent);
      outline-offset: 2px;
    }

    .wire-stepper__button-control[data-primary="true"] {
      border-color: var(--stepper-accent);
      background: var(--stepper-accent);
      color: var(--wire-color-primary-contrast);
    }

    /* A held step has to look held, or the button reads as broken. */
    .wire-stepper__button-control:disabled {
      opacity: 0.45;
      cursor: not-allowed;
    }

    @keyframes wire-stepper-in {
      from {
        opacity: 0;
        transform: translateX(0.75rem);
      }
      to {
        opacity: 1;
        transform: none;
      }
    }

    /* A horizontal stepper cannot stay side by side on a phone. */
    @media (max-width: 639px) {
      .wire-stepper__list {
        flex-direction: column;
      }
    }
  }
}
```

---

## StrongPassword

Showcase: https://component.wrnexusjs.dev/
Mount: <StrongPassword /> (legacy: data-component="StrongPassword")
Category: advanced-forms
Purpose: Theme-aware, responsive strong password component.
Props: size: string = "default", color: string = "primary", label: string = "Password", name: string = "password", value: string = "", placeholder: string = "Create a strong password", autocomplete: string = "new-password", minLength: number = 8, specialCharactersSet: string = "!@#$%^&*()_+-=[]{}|;:,.<>?", requireLowercase: boolean = true, requireUppercase: boolean = true, requireNumber: boolean = true, requireSpecialCharacter: boolean = true, showRequirements: boolean = true, presentation: string = "inline", hintText: string = "Use a unique password you do not use elsewhere.", emptyLabel: string = "Enter a password", weakLabel: string = "Weak", fairLabel: string = "Fair", goodLabel: string = "Good", strongLabel: string = "Strong", disabled: boolean = false, readonly: boolean = false, required: boolean = false, invalid: boolean = false, validationMessage: string = "", class: string = ""
Slots: none
Events: input, change, strength

### Complete .wrn source contract

```wrn
import FieldStyles from "../styles/FieldStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component StrongPassword {
  outputs {
    input(payload: { value: string; score: number; maximumScore: number; percent: number; level: string })
    change(payload: { value: string; score: number; maximumScore: number; percent: number; level: string })
    strength(payload: { value: string; score: number; maximumScore: number; percent: number; level: string })
  }

  props {
    size: string = "default"
    color: string = "primary"
    label: string = "Password"
    name: string = "password"
    value: string = ""
    placeholder: string = "Create a strong password"
    autocomplete: string = "new-password"
    minLength: number = 8
    specialCharactersSet: string = "!@#$%^&*()_+-=[]{}|;:,.<>?"
    requireLowercase: boolean = true
    requireUppercase: boolean = true
    requireNumber: boolean = true
    requireSpecialCharacter: boolean = true
    showRequirements: boolean = true
    presentation: string = "inline"
    hintText: string = "Use a unique password you do not use elsewhere."
    emptyLabel: string = "Enter a password"
    weakLabel: string = "Weak"
    fairLabel: string = "Fair"
    goodLabel: string = "Good"
    strongLabel: string = "Strong"
    disabled: boolean = false
    readonly: boolean = false
    required: boolean = false
    invalid: boolean = false
    validationMessage: string = ""
    class: string = ""
}

  state password = value
  state detailsOpen: boolean = false

  functions {
    shared function hasMinimumLength() {
      return password.length >= Number(minLength)
    }

    shared function hasLowercase() {
      return password !== password.toUpperCase()
    }

    shared function hasUppercase() {
      return password !== password.toLowerCase()
    }

    shared function hasNumber() {
      return containsCharacterAt("0123456789", 0)
    }

    shared function hasSpecialCharacter() {
      return containsCharacterAt(specialCharactersSet, 0)
    }

    shared function containsCharacterAt(characters, index) {
      if (index >= characters.length) {
        return false
      }
      if (password.includes(characters[index])) {
        return true
      }
      return containsCharacterAt(characters, index + 1)
    }

    shared function score() {
      return (hasMinimumLength() ? 1 : 0) + (requireLowercase && hasLowercase() ? 1 : 0) + (requireUppercase && hasUppercase() ? 1 : 0) + (requireNumber && hasNumber() ? 1 : 0) + (requireSpecialCharacter && hasSpecialCharacter() ? 1 : 0)
    }

    shared function maximumScore() {
      return 1 + (requireLowercase ? 1 : 0) + (requireUppercase ? 1 : 0) + (requireNumber ? 1 : 0) + (requireSpecialCharacter ? 1 : 0)
    }

    shared function strengthPercent() {
      return Math.round(score() / maximumScore() * 100)
    }

    shared function strengthLevel() {
      if (!password) {
        return "empty"
      }
      if (strengthPercent() <= 25) {
        return "weak"
      }
      if (strengthPercent() <= 50) {
        return "fair"
      }
      if (strengthPercent() < 100) {
        return "good"
      }
      return "strong"
    }

    shared function strengthLabel() {
      if (strengthLevel() === "weak") {
        return weakLabel
      }
      if (strengthLevel() === "fair") {
        return fairLabel
      }
      if (strengthLevel() === "good") {
        return goodLabel
      }
      if (strengthLevel() === "strong") {
        return strongLabel
      }
      return emptyLabel
    }

  }

  view {
    <div
      class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--strong-password wire-next--strong-password-{presentation} {invalid ? 'wire-next--invalid' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
      data-wrn-strong-password
      data-strength="{password ? strengthLevel() : 'empty'}"
      data-score="{password ? score() : 0}"
    >
      <label for="{name}-strong-password">{label}</label>
      <div class="wire-next__strong-password-anchor">
        <input
          {...attrs}
          id="{name}-strong-password"
          type="password"
          name="{name}"
          value="{password}"
          placeholder="{placeholder}"
          autocomplete="{autocomplete}"
          minlength="{minLength}"
          disabled="{disabled}"
          readonly="{readonly}"
          required="{required}"
          aria-invalid="{invalid ? 'true' : 'false'}"
          aria-describedby="{validationMessage ? name + '-validation' : hintText ? name + '-hint' : ''}"
          @focus="detailsOpen = true"
          @blur="detailsOpen = false"
          @input="password = event.target.value; output.input({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() }); output.strength({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
          @change="output.change({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
        />

        {#if presentation === "popover"}
          <div
            class="wire-next__strong-password-popover"
            data-open="{detailsOpen ? 'true' : 'false'}"
            role="status"
          >
            <div class="wire-next__strong-password-popover-arrow" aria-hidden="true"></div>
            <div class="wire-next__strong-password-details">
              <div class="wire-next__strong-password-summary">
                <span>Password strength</span>
                <strong>{password ? strengthLabel() : emptyLabel}</strong>
              </div>
              <div
                class="wire-next__strong-password-meter"
                role="progressbar"
                aria-label="Password strength"
                aria-valuemin="0"
                aria-valuemax="100"
                aria-valuenow="{password ? strengthPercent() : 0}"
              >
                <span style="width: {password ? strengthPercent() : 0}%"></span>
              </div>
              {#if showRequirements}
                <ul class="wire-next__strong-password-requirements">
                  <li data-met="{password && hasMinimumLength() ? 'true' : 'false'}">
                    <span aria-hidden="true"></span>At least {minLength} characters
                  </li>
                  {#if requireLowercase}<li data-met="{password && hasLowercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One lowercase letter</li>{/if}
                  {#if requireUppercase}<li data-met="{password && hasUppercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One uppercase letter</li>{/if}
                  {#if requireNumber}<li data-met="{password && hasNumber() ? 'true' : 'false'}"><span aria-hidden="true"></span>One number</li>{/if}
                  {#if requireSpecialCharacter}<li data-met="{password && hasSpecialCharacter() ? 'true' : 'false'}"><span aria-hidden="true"></span>One special character: {specialCharactersSet}</li>{/if}
                </ul>
              {/if}
              {#if hintText}<small id="{name}-hint">{hintText}</small>{/if}
            </div>
          </div>
        {/if}
      </div>

      {#if presentation !== "popover"}
        <div class="wire-next__strong-password-details">
          <div class="wire-next__strong-password-summary">
            <span>Password strength</span>
            <strong>{password ? strengthLabel() : emptyLabel}</strong>
          </div>
          <div
            class="wire-next__strong-password-meter"
            role="progressbar"
            aria-label="Password strength"
            aria-valuemin="0"
            aria-valuemax="100"
            aria-valuenow="{password ? strengthPercent() : 0}"
          >
            <span style="width: {password ? strengthPercent() : 0}%"></span>
          </div>
          {#if showRequirements}
            <ul class="wire-next__strong-password-requirements">
              <li data-met="{password && hasMinimumLength() ? 'true' : 'false'}"><span aria-hidden="true"></span>At least {minLength} characters</li>
              {#if requireLowercase}<li data-met="{password && hasLowercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One lowercase letter</li>{/if}
              {#if requireUppercase}<li data-met="{password && hasUppercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One uppercase letter</li>{/if}
              {#if requireNumber}<li data-met="{password && hasNumber() ? 'true' : 'false'}"><span aria-hidden="true"></span>One number</li>{/if}
              {#if requireSpecialCharacter}<li data-met="{password && hasSpecialCharacter() ? 'true' : 'false'}"><span aria-hidden="true"></span>One special character: {specialCharactersSet}</li>{/if}
            </ul>
          {/if}
          {#if hintText}<small id="{name}-hint">{hintText}</small>{/if}
        </div>
      {/if}

      <small id="{name}-validation" class="wire-next__validation" data-error="{name}">{validationMessage}</small>
    </div>
    <FieldStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--strong-password {
      position: relative;
      display: grid;
      width: 100%;
      gap: 0.6rem;
      color: var(--wire-color-text);
    }

    .wire-next--strong-password > label {
      font-size: 0.82em;
      font-weight: 700;
    }

    .wire-next__strong-password-anchor {
      position: relative;
    }

    .wire-next--strong-password input {
      width: 100%;
      min-height: 2.75em;
      padding: 0.68em 0.85em;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-sm);
      outline: 0;
      color: var(--wire-color-text);
      background: var(--wire-color-surface);
      font: inherit;
      transition:
        border-color 160ms ease,
        box-shadow 160ms ease;
    }

    .wire-next--strong-password input:focus-visible {
      border-color: var(--wire-component-color);
      box-shadow: 0 0 0 3px color-mix(in srgb, var(--wire-component-color) 16%, transparent);
    }

    .wire-next__strong-password-details {
      display: grid;
      gap: 0.65rem;
    }

    .wire-next__strong-password-summary {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
      color: var(--wire-color-muted);
      font-size: 0.75em;
    }

    .wire-next__strong-password-summary strong {
      color: var(--wire-color-text);
    }

    .wire-next__strong-password-meter {
      width: 100%;
      height: 0.42rem;
      overflow: hidden;
      border-radius: 999px;
      background: color-mix(in srgb, var(--wire-color-border) 72%, transparent);
    }

    .wire-next__strong-password-meter > span {
      display: block;
      width: 0;
      height: 100%;
      border-radius: inherit;
      background: var(--wire-color-danger);
      transition:
        width 180ms ease,
        background-color 180ms ease;
    }

    .wire-next--strong-password[data-strength="fair"] .wire-next__strong-password-meter > span {
      background: var(--wire-color-warning);
    }

    .wire-next--strong-password[data-strength="good"] .wire-next__strong-password-meter > span {
      background: var(--wire-component-color);
    }

    .wire-next--strong-password[data-strength="strong"] .wire-next__strong-password-meter > span {
      background: var(--wire-color-success);
    }

    .wire-next__strong-password-requirements {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 0.45rem 1rem;
      margin: 0;
      padding: 0;
      list-style: none;
    }

    .wire-next__strong-password-requirements li {
      display: flex;
      min-width: 0;
      align-items: flex-start;
      gap: 0.45rem;
      color: var(--wire-color-muted);
      font-size: 0.72em;
      line-height: 1.45;
    }

    .wire-next__strong-password-requirements li > span {
      position: relative;
      width: 0.9rem;
      height: 0.9rem;
      flex: 0 0 auto;
      margin-top: 0.08rem;
      border: 1px solid currentColor;
      border-radius: 50%;
    }

    .wire-next__strong-password-requirements li > span::after {
      position: absolute;
      top: 0.12rem;
      left: 0.29rem;
      width: 0.22rem;
      height: 0.42rem;
      border: solid var(--wire-color-surface);
      border-width: 0 0.1rem 0.1rem 0;
      content: "";
      opacity: 0;
      transform: rotate(45deg);
    }

    .wire-next__strong-password-requirements li[data-met="true"] {
      color: var(--wire-color-success);
    }

    .wire-next__strong-password-requirements li[data-met="true"] > span {
      border-color: var(--wire-color-success);
      background: var(--wire-color-success);
    }

    .wire-next__strong-password-requirements li[data-met="true"] > span::after {
      opacity: 1;
    }

    .wire-next__strong-password-details > small {
      color: var(--wire-color-muted);
      font-size: 0.7em;
      line-height: 1.5;
    }

    .wire-next__strong-password-popover {
      position: absolute;
      top: calc(100% + 0.7rem);
      left: 0;
      z-index: 30;
      width: min(28rem, calc(100vw - 2rem));
      padding: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius);
      opacity: 0;
      visibility: hidden;
      background: var(--wire-color-surface);
      box-shadow: var(--wire-shadow-2);
      transform: translateY(-0.35rem);
      transition:
        opacity 140ms ease,
        transform 140ms ease,
        visibility 140ms ease;
    }

    .wire-next__strong-password-popover[data-open="true"] {
      opacity: 1;
      visibility: visible;
      transform: translateY(0);
    }

    .wire-next__strong-password-popover-arrow {
      position: absolute;
      top: -0.38rem;
      left: 1.25rem;
      width: 0.7rem;
      height: 0.7rem;
      border-top: 1px solid var(--wire-color-border);
      border-left: 1px solid var(--wire-color-border);
      background: var(--wire-color-surface);
      transform: rotate(45deg);
    }

    @media (max-width: 560px) {
    .wire-next__strong-password-requirements {
        grid-template-columns: 1fr;
      }
    }
  }
}
```

---

## StyledIcon

Showcase: https://component.wrnexusjs.dev/
Mount: <StyledIcon /> (legacy: data-component="StyledIcon")
Category: base
Purpose: Theme-aware, responsive styled icon component.
Props: size: string = "default", color: string = "primary", title: string = "Styled Icon", description: string = "", items: unknown[] = [], variant: string = "default", class: string = ""
Slots: default
Events: none

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component StyledIcon {
  props {
    size: string = "default"
    color: string = "primary"
    title: string = "Styled Icon"
    description: string = ""
    items: unknown[] = []
    variant: string = "default"
    class: string = ""
  }
  view {
    <section class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--styled-icon wire-next--variant-{variant} {class}">
      {#if title}<strong>{title}</strong>{/if}
      {#if description}<p>{description}</p>{/if}
      {#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
      <slot />
    </section>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--styled-icon {
      display: grid;
      gap: 0.75rem;
      padding: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius);
      color: var(--wire-color-text);
      background: var(--wire-color-surface);
    }
    .wire-next--styled-icon > p { margin: 0; color: var(--wire-color-muted); }
    .wire-next--styled-icon > .wire-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
    .wire-next--styled-icon > .wire-next__items > span { padding: 0.35rem 0.6rem; border-radius: var(--wire-radius-sm); background: var(--wire-color-surface-2); }
  }
}
```

---

## Switch

Showcase: https://component.wrnexusjs.dev/
Mount: <Switch /> (legacy: data-component="Switch")
Category: forms
Purpose: Theme-aware, responsive switch component.
Props: size: string = "default", color: string = "primary", id: string = "", name: string = "", label: string = "Switch", hiddenLabel: boolean = false, placeholder: string = "", variant: string = "normal", icon: string = "", iconPosition: string = "start", value: string = "on", checked: boolean = false, helperText: string = "", cornerHint: string = "", error: string = "", inline: boolean = false, readonly: boolean = false, disabled: boolean = false, required: boolean = false, class: string = ""
Slots: none
Events: input, change, focus, blur

### Complete .wrn source contract

```wrn
import FieldStyles from "../styles/FieldStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component Switch {
  outputs {
    input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
    size: string = "default"
    color: string = "primary"
    id: string = ""
    name: string = ""
    label: string = "Switch"
    hiddenLabel: boolean = false
    placeholder: string = ""
    variant: string = "normal"
    icon: string = ""
    iconPosition: string = "start"
    value: string = "on"
    checked: boolean = false
    helperText: string = ""
    cornerHint: string = ""
    error: string = ""
    inline: boolean = false
    readonly: boolean = false
    disabled: boolean = false
    required: boolean = false
    class: string = ""
  }
  functions {
    client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ checked: sourceEvent.currentTarget.checked, value: value, name: name, sourceEvent: sourceEvent }) }
    client function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
  }
  view {
    <div {...attrs} class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--choice-field {class}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
      <div class="wire-next__field-heading">{#if cornerHint}<span class="{hiddenLabel ? 'wire-next__sr-only' : ''}">{label}</span><span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
      <label class="wire-next__choice wire-next--switch" for="{id || name}" data-variant="{variant}" data-icon-position="{iconPosition}"><input id="{id || name}" type="checkbox" role="switch" name="{name}" value="{value}" checked="{checked}" disabled="{disabled}" required="{required}" readonly="{readonly}" aria-checked="{checked ? 'true' : 'false'}" aria-invalid="{error ? 'true' : 'false'}" @click="preventReadonly(event)" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" />{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}<span class="{hiddenLabel || cornerHint ? 'wire-next__sr-only' : ''}">{label}</span></label>
      {#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
    </div>
    <FieldStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--switch input {
      appearance: none;
      position: relative;
      width: 2.5rem;
      height: 1.4rem;
      border: 1px solid var(--wire-color-border);
      border-radius: 999px;
      background: var(--wire-color-surface-2);
      transition: background var(--wire-motion-fast) var(--wire-ease-standard);
    }

    .wire-next--switch {
      min-height: 2.65rem;
      justify-content: flex-start;
    }

    .wire-next--switch > span:last-child {
      line-height: 1.35;
    }

    .wire-next--switch input::after {
      position: absolute;
      top: 0.15rem;
      left: 0.15rem;
      width: 1rem;
      height: 1rem;
      border-radius: 999px;
      background: var(--wire-color-text);
      content: "";
      transition: transform var(--wire-motion-fast) var(--wire-ease-standard);
    }

    .wire-next--switch input:checked {
      border-color: var(--wire-field-color);
      background: var(--wire-field-color);
    }

    .wire-next--switch input:checked::after {
      background: var(--wire-color-primary-contrast, #fff);
      transform: translateX(1.1rem);
    }
  }
}
```

---

## Tabs

Showcase: https://component.wrnexusjs.dev/
Mount: <Tabs /> (legacy: data-component="Tabs")
Category: navigation
Purpose: Switch between related responsive content panels with horizontal or vertical orientation and selection events.
Props: color: string = "primary", size: string = "default", items: unknown[] = [], active: string = "", orientation: string = "horizontal", mode: string = "client", param: string = "tab", label: string = "Tabs", class: string = ""
Slots: panel-{valueOf(item, index)}, default
Events: change, select

### Complete .wrn source contract

```wrn
// Tabs -- a tablist over panels.
//
//   <Tabs items='[{"label":"Overview","value":"overview"}]' active="overview" />
//
// With mode="url" the selection is mirrored into a query parameter using
// history.pushState, so the panel swaps without a page load and the back
// button works. The query parameter is used rather than the hash because it
// survives a reload and does not collide with in-page anchors or Scrollspy.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Tabs {
  outputs {
    change(payload: { value: string; item: object; index: number })
    select(payload: { value: string; item: object; index: number })
  }

  props {
    color: string = "primary"
    size: string = "default"
    items: unknown[] = []
    active: string = ""
    orientation: string = "horizontal"
    mode: string = "client"
    param: string = "tab"
    label: string = "Tabs"
    class: string = ""
  }

  state activeValue = ""
  // Which way the panel should slide in. Set on every change so the animation
  // follows the direction of travel rather than always coming from one side.
  state slideFrom = "forward"

  functions {
    shared function itemList() {
      return Array.isArray(items) ? items : []
    }

    shared function valueOf(item, index) {
      return String(item.value || item.id || index)
    }

    /*
     * In url mode the query parameter is the source of truth, not local state.
     *
     * The client router owns popstate and swaps the whole page on back and
     * forward, which discards component state anyway. Reading the URL means
     * the right tab simply falls out of whatever render happens next, with no
     * listener to lose and nothing to keep in step.
     */
    shared function currentValue() {
      if (mode === "url" && typeof window !== "undefined" && window.location) {
        var fromUrl = new URLSearchParams(window.location.search).get(param || "tab")
        return fromUrl ? fromUrl : defaultValue()
      }
      if (activeValue) {
        return activeValue
      }
      return defaultValue()
    }

    // The selection this instance started with. The runtime falls back to it
    // when the back button lands on a URL that has no tab parameter at all.
    shared function defaultValue() {
      if (active) {
        return active
      }
      var list = itemList()
      return list.length ? valueOf(list[0], 0) : ""
    }

    shared function isSelected(item, index) {
      return valueOf(item, index) === currentValue()
    }

    shared function rovingAxis() {
      return orientation === "vertical" ? "vertical" : "horizontal"
    }

    client function selectTab(item, index, sourceEvent) {
      if (item.disabled) {
        return
      }
      var value = valueOf(item, index)
      var list = itemList()
      var previous = -1
      for (var scan = 0; scan < list.length; scan += 1) {
        if (valueOf(list[scan], scan) === currentValue()) {
          previous = scan
        }
      }
      slideFrom = previous > index ? "back" : "forward"
      activeValue = value

      if (mode === "url" && window.history && window.history.pushState) {
        var url = new URL(window.location.href)
        url.searchParams.set(param || "tab", value)
        window.history.pushState({}, "", url.toString())
      }

      output.change({ value: value, item: item, index: index })
      output.select({ value: value, item: item, index: index })
    }
  }

  view {
    <section
      {...attrs}
      data-ui-component="Tabs"
      class='wire-tabs {class}'
      data-orientation='{orientation}'
      data-color='{color}'
      data-size='{size}'
      data-slide='{slideFrom}'
      data-mode='{mode}'
      data-param='{param}'
    >
      <div
        class="wire-tabs__list"
        role="tablist"
        aria-label='{label}'
        aria-orientation='{orientation}'
        data-wrn-roving='{rovingAxis()}'
      >
        {#each itemList() as item, index}
          <button
            type="button"
            class="wire-tabs__tab"
            role="tab"
            data-value='{valueOf(item, index)}'
            data-wrn-roving-item="true"
            id='wire-tab-{valueOf(item, index)}'
            aria-controls='wire-panel-{valueOf(item, index)}'
            aria-selected='{isSelected(item, index) ? "true" : "false"}'
            aria-disabled='{item.disabled ? "true" : "false"}'
            @click='selectTab(item, index, event)'
          >
            <span class='wire-tabs__icon {item.icon}' data-show="item.icon" aria-hidden="true"></span>
            <span class="wire-tabs__label">{item.label || item.title}</span>
            <span class="wire-tabs__badge" data-show="item.badge">{item.badge}</span>
          </button>
        {/each}
      </div>

      <div class="wire-tabs__panels">
        {#each itemList() as item, index}
          <div
            class="wire-tabs__panel"
            role="tabpanel"
            id='wire-panel-{valueOf(item, index)}'
            aria-labelledby='wire-tab-{valueOf(item, index)}'
            tabindex="0"
            data-show='isSelected(item, index)'
          >
            <h3 class="wire-tabs__title" data-show="item.title && item.title !== item.label">
              {item.title}
            </h3>
            <p class="wire-tabs__description" data-show="item.description">{item.description}</p>
            <div class="wire-tabs__content" data-show="item.content">{item.content}</div>
            <slot name="panel-{valueOf(item, index)}"></slot>
          </div>
        {/each}
        <slot />
      </div>
    </section>
  }

  style {
    .wire-tabs {
      --tabs-accent: var(--wire-color-primary);
      display: flex;
      flex-direction: column;
      gap: 1rem;
      max-width: 100%;
    }

    .wire-tabs[data-color="secondary"] {
      --tabs-accent: var(--wire-color-secondary);
    }

    .wire-tabs[data-color="success"] {
      --tabs-accent: var(--wire-color-success);
    }

    .wire-tabs[data-color="danger"] {
      --tabs-accent: var(--wire-color-danger);
    }

    .wire-tabs[data-color="info"] {
      --tabs-accent: var(--wire-color-info);
    }

    .wire-tabs[data-size="sm"] {
      font-size: 0.82rem;
    }

    .wire-tabs[data-size="lg"] {
      font-size: 1rem;
    }

    .wire-tabs[data-orientation="vertical"] {
      flex-direction: row;
      align-items: flex-start;
    }

    .wire-tabs__list {
      display: flex;
      gap: 0.25rem;
      min-width: 0;
      padding: 0.25rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-md);
      background: var(--wire-color-surface-soft);
      /* A long tablist scrolls rather than wrapping into an unusable stack. */
      overflow-x: auto;
    }

    .wire-tabs[data-orientation="vertical"] .wire-tabs__list {
      flex-direction: column;
      flex: 0 0 auto;
      width: 14rem;
      overflow-x: visible;
    }

    .wire-tabs__tab {
      appearance: none;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 0.4rem;
      flex: 1 0 auto;
      padding: 0.55rem 0.9rem;
      border: 0;
      border-radius: var(--wire-radius-sm);
      background: transparent;
      color: var(--wire-color-text-muted);
      font: inherit;
      font-size: 0.88rem;
      font-weight: 600;
      white-space: nowrap;
      cursor: pointer;
      transition:
        background 160ms ease,
        color 160ms ease;
    }

    .wire-tabs[data-orientation="vertical"] .wire-tabs__tab {
      justify-content: flex-start;
    }

    .wire-tabs__tab:hover {
      color: var(--wire-color-text);
    }

    .wire-tabs__tab:focus-visible {
      outline: 2px solid var(--tabs-accent);
      outline-offset: 2px;
    }

    .wire-tabs__tab[aria-selected="true"] {
      background: var(--wire-color-surface);
      color: var(--tabs-accent);
      box-shadow: var(--wire-shadow-1);
    }

    .wire-tabs__tab[aria-disabled="true"] {
      opacity: 0.5;
      pointer-events: none;
    }

    .wire-tabs__badge {
      padding: 0.05rem 0.4rem;
      border-radius: 999px;
      background: color-mix(in srgb, var(--tabs-accent) 16%, transparent);
      color: var(--tabs-accent);
      font-size: 0.72rem;
    }

    .wire-tabs__panels {
      min-width: 0;
      flex: 1 1 auto;
    }

    .wire-tabs__panel {
      outline: none;
      animation: wire-tabs-slide-forward 220ms cubic-bezier(0.22, 1, 0.36, 1) both;
    }

    .wire-tabs[data-slide="back"] .wire-tabs__panel {
      animation-name: wire-tabs-slide-back;
    }

    /* The panels clip their own slide so it never widens the page. */
    .wire-tabs__panels {
      overflow-x: clip;
    }

    .wire-tabs__panel:focus-visible {
      outline: 2px solid var(--tabs-accent);
      outline-offset: 4px;
      border-radius: var(--wire-radius-sm);
    }

    .wire-tabs__title {
      margin: 0 0 0.35rem;
      font-size: 1rem;
      font-weight: 700;
    }

    .wire-tabs__description {
      margin: 0;
      color: var(--wire-color-text-muted);
      line-height: 1.6;
    }

    .wire-tabs__content {
      margin-top: 0.6rem;
    }

    @keyframes wire-tabs-slide-forward {
      from {
        opacity: 0;
        transform: translateX(1.25rem);
      }
      to {
        opacity: 1;
        transform: none;
      }
    }

    @keyframes wire-tabs-slide-back {
      from {
        opacity: 0;
        transform: translateX(-1.25rem);
      }
      to {
        opacity: 1;
        transform: none;
      }
    }

    /* Respect a reduced-motion preference: swap instantly instead. */
    @media (prefers-reduced-motion: reduce) {
      .wire-tabs__panel {
        animation: none;
      }
    }

    @media (max-width: 639px) {
      .wire-tabs[data-orientation="vertical"] {
        flex-direction: column;
      }

      .wire-tabs[data-orientation="vertical"] .wire-tabs__list {
        width: 100%;
        flex-direction: row;
        overflow-x: auto;
      }
    }
  }
}
```

---

## TextLink

Showcase: https://component.wrnexusjs.dev/
Mount: <TextLink /> (legacy: data-component="TextLink")
Category: marketing
Purpose: Render an accessible text action with optional icon, arrow, underline, external state, and semantic styling.
Props: label: string = "Learn more", href: string = "#", target: string = "", rel: string = "", external: boolean = false, icon: string = "", iconPosition: string = "start", showArrow: boolean = true, underline: boolean = false, size: string = "default", color: string = "primary", variant: string = "default", disabled: boolean = false, class: string = ""
Slots: none
Events: click, focus, blur

### Complete .wrn source contract

```wrn
component TextLink {
  outputs {
    click(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
label: string = "Learn more"
    href: string = "#"
    target: string = ""
    rel: string = ""
    external: boolean = false
    icon: string = ""
    iconPosition: string = "start"
    showArrow: boolean = true
    underline: boolean = false
    size: string = "default"
    color: string = "primary"
    variant: string = "default"
    disabled: boolean = false
    class: string = ""
  }

  view {
    <span
      {...attrs}
      data-ui-component="TextLink"
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      class='wire-text-link {class}'
    >
      {#if disabled}
        <span
          aria-disabled="true"
          class="wire-text-link__control"
          data-size='{size}'
        >
          {#if icon && iconPosition === "start"}
            <span class='{icon + " wire-text-link__icon"}' aria-hidden="true"></span>
          {/if}
          <span>{label}</span>
        </span>
      {:else}
        <a
          href='{href}'
          target='{target}'
          rel='{external ? (rel || "noopener noreferrer") : rel}'
          class="wire-text-link__control"
          data-size='{size}'
          data-color='{color}'
          data-variant='{variant}'
          data-underline='{underline ? "true" : "false"}'
        >
          {#if icon && iconPosition === "start"}
            <span class='{icon + " wire-text-link__icon"}' aria-hidden="true"></span>
          {/if}

          <span>{label}</span>

          {#if icon && iconPosition === "end"}
            <span class='{icon + " wire-text-link__icon"}' aria-hidden="true"></span>
          {/if}

          {#if external}
            <span class="icon-[lucide--external-link] wire-text-link__icon" aria-hidden="true"></span>
          {:else if showArrow}
            <span class="icon-[lucide--arrow-right] wire-text-link__icon wire-text-link__arrow" aria-hidden="true"></span>
          {/if}
        </a>
      {/if}
    </span>
  }

  style {
    .wire-text-link {
      display: inline-flex;
      min-width: 0;
    }

    .wire-text-link__control {
      display: inline-flex;
      align-items: center;
      gap: 0.5rem;
      border-radius: var(--wire-radius-sm);
      color: var(--wire-color-primary);
      font-weight: 600;
      outline: none;
      transition: color var(--wire-motion-base) var(--wire-ease-standard);
    }

    span.wire-text-link__control {
      cursor: not-allowed;
      opacity: 0.5;
    }

    .wire-text-link__control[data-size="sm"],
    .wire-text-link__control[data-size="default"] { font-size: 0.875rem; }
    .wire-text-link__control[data-size="md"] { font-size: 1rem; }
    .wire-text-link__control[data-size="lg"] { font-size: 1.125rem; }
    .wire-text-link__control[data-color="secondary"] { color: var(--wire-color-secondary); }
    .wire-text-link__control[data-color="success"] { color: var(--wire-color-success); }
    .wire-text-link__control[data-color="warning"] { color: var(--wire-color-warning-text); }
    .wire-text-link__control[data-color="danger"] { color: var(--wire-color-danger); }
    .wire-text-link__control[data-color="info"] { color: var(--wire-color-info); }
    .wire-text-link__control[data-color="neutral"] { color: var(--wire-color-text); }
    .wire-text-link__control[data-color="primary"]:hover { color: var(--wire-color-primary-hover); }
    .wire-text-link__control[data-underline="true"] {
      text-decoration: underline;
      text-underline-offset: 0.25rem;
    }
    .wire-text-link__control[data-variant="button"] {
      padding: 0.5rem 0.75rem;
      border-radius: var(--wire-radius);
    }
    .wire-text-link__control[data-variant="button"][data-color="primary"] {
      background: var(--wire-color-primary-soft);
    }
    .wire-text-link__control:focus-visible {
      outline: 2px solid var(--wire-color-focus);
      outline-offset: 2px;
    }
    .wire-text-link__icon { width: 1rem; height: 1rem; flex: none; }
    .wire-text-link__arrow { transition: transform var(--wire-motion-base) var(--wire-ease-standard); }
    .wire-text-link__control:hover .wire-text-link__arrow { transform: translateX(0.25rem); }
  }
}
```

---

## Textarea

Showcase: https://component.wrnexusjs.dev/
Mount: <Textarea /> (legacy: data-component="Textarea")
Category: forms
Purpose: Theme-aware, responsive textarea component.
Props: size: string = "default", color: string = "primary", id: string = "", name: string = "", label: string = "Textarea", hiddenLabel: boolean = false, placeholder: string = "", value: string = "", variant: string = "normal", icon: string = "", iconPosition: string = "start", helperText: string = "", cornerHint: string = "", error: string = "", inline: boolean = false, rows: number = 5, resize: string = "vertical", readonly: boolean = false, disabled: boolean = false, required: boolean = false, minlength: string = "", maxlength: string = "", class: string = ""
Slots: none
Events: input, change, focus, blur, invalid

### Complete .wrn source contract

```wrn
import FieldStyles from "../styles/FieldStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component Textarea {
  outputs {
    input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    invalid(payload: { value: string | number | boolean | null | object; name: string; message: string; sourceEvent: Event })
  }

  props {
size: string = "default"
    color: string = "primary"
    id: string = ""
    name: string = ""
    label: string = "Textarea"
    hiddenLabel: boolean = false
    placeholder: string = ""
    value: string = ""
    variant: string = "normal"
    icon: string = ""
    iconPosition: string = "start"
    helperText: string = ""
    cornerHint: string = ""
    error: string = ""
    inline: boolean = false
    rows: number = 5
    resize: string = "vertical"
    readonly: boolean = false
    disabled: boolean = false
    required: boolean = false
    minlength: string = ""
    maxlength: string = ""
    class: string = ""
  }
  functions {
    client function emitField(nameEvent, sourceEvent) {
      sourceEvent.stopPropagation()
      output[nameEvent]({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent })
    }
    client function handleInvalid(sourceEvent) {
      output.invalid({ value: sourceEvent.currentTarget.value, name: name, message: sourceEvent.currentTarget.validationMessage, sourceEvent: sourceEvent })
    }
  }
  view {
    <div {...attrs} class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--field wire-next--textarea {class}" data-variant="{variant}" data-inline="{inline}" data-floating="{variant === 'floating'}" data-invalid="{error ? 'true' : 'false'}">
      <div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
      <div class="wire-next__field-control" data-icon-position="{iconPosition}">
        {#if icon}<span class="{icon} wire-next__field-icon" aria-hidden="true"></span>{/if}
        <textarea id="{id || name}" name="{name}" placeholder="{variant === 'floating' ? ' ' : placeholder}" rows="{rows}" style="resize: {resize}" readonly="{readonly}" disabled="{disabled}" required="{required}" minlength="{minlength}" maxlength="{maxlength}" aria-invalid="{error ? 'true' : 'false'}" aria-describedby="{error ? (id || name) + '-error' : (helperText ? (id || name) + '-help' : '')}" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @invalid="handleInvalid(event)">{value}</textarea>
        {#if variant === "floating"}<span class="wire-next__field-floating-label">{label}</span>{/if}
      </div>
      {#if helperText}<small id="{(id || name) + '-help'}" class="wire-next__field-help">{helperText}</small>{/if}
      <small id="{(id || name) + '-error'}" class="wire-next__field-error" data-error="{name}">{error}</small>
    </div>
    <FieldStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--textarea { width: 100%; min-width: 0; }
    .wire-next--textarea[data-invalid="true"] { --wire-component-color: var(--wire-color-danger); }
  }
}
```

---

## TimePicker

Showcase: https://component.wrnexusjs.dev/
Mount: <TimePicker /> (legacy: data-component="TimePicker")
Category: forms
Purpose: Theme-aware, responsive time picker component.
Props: size: string = "default", color: string = "primary", id: string = "", name: string = "", label: string = "Time", hiddenLabel: boolean = false, value: string = "", placeholder: string = "", variant: string = "normal", icon: string = "icon-[lucide--clock-3]", iconPosition: string = "end", helperText: string = "", cornerHint: string = "", error: string = "", inline: boolean = false, min: string = "", max: string = "", step: string = "", format: string = "24", minuteStep: number = 5, hours: string = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23"], minutes: string = ["00", "05", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55"], readonly: boolean = false, disabled: boolean = false, required: boolean = false, class: string = ""
Slots: none
Events: input, change, focus, blur, open, close, invalid

### Complete .wrn source contract

```wrn
import FieldStyles from "../styles/FieldStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component TimePicker {
  outputs {
    input(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
    change(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
    focus(payload: { value: string; name: string; sourceEvent: Event })
    blur(payload: { value: string; name: string; sourceEvent: Event })
    open(payload: { value: string; name: string; sourceEvent: Event })
    close(payload: { value: string; name: string; sourceEvent: Event })
    invalid(payload: { name: string; message: string; sourceEvent: Event })
  }

  props {
size: string = "default"
    color: string = "primary"
    id: string = ""
    name: string = ""
    label: string = "Time"
    hiddenLabel: boolean = false
    value: string = ""
    placeholder: string = ""
    variant: string = "normal"
    icon: string = "icon-[lucide--clock-3]"
    iconPosition: string = "end"
    helperText: string = ""
    cornerHint: string = ""
    error: string = ""
    inline: boolean = false
    min: string = ""
    max: string = ""
    step: string = ""
    format: string = "24"
    minuteStep: number = 5
    hours = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23"]
    minutes = ["00", "05", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55"]
    readonly: boolean = false
    disabled: boolean = false
    required: boolean = false
    class: string = ""
  }
  state currentValue = value
  state selectedHour = value ? value.split(":")[0] : "00"
  state selectedMinute = value ? value.split(":")[1] : "00"
  state expanded: boolean = false
  functions {
    client function commit(part, nextValue, sourceEvent, next, detail) { if (part === "hour") { selectedHour = String(nextValue).padStart(2, "0") } if (part === "minute") { selectedMinute = String(nextValue).padStart(2, "0") } next = selectedHour + ":" + selectedMinute; if ((min && next < min) || (max && next > max)) { return } currentValue = next; detail = { value: currentValue, hour: selectedHour, minute: selectedMinute, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
    client function selectTimePart(sourceEvent, root, input, parts, hourValue, minuteValue, next, trigger, detail) { root = sourceEvent.currentTarget.closest(".wire-next--time-picker"); input = root.querySelector(".wire-next__time-value"); parts = String(input.value || value || "00:00").split(":"); hourValue = sourceEvent.currentTarget.dataset.part === "hour" ? sourceEvent.currentTarget.dataset.value : parts[0]; minuteValue = sourceEvent.currentTarget.dataset.part === "minute" ? sourceEvent.currentTarget.dataset.value : parts[1]; next = hourValue + ":" + minuteValue; if ((min && next < min) || (max && next > max)) { return } currentValue = next; input.setAttribute("value", next); trigger = root.querySelector(".wire-next__time-trigger span"); if (trigger) { trigger.replaceChildren(next) } sourceEvent.currentTarget.setAttribute("aria-pressed", "true"); detail = { value: next, hour: hourValue, minute: minuteValue, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
    client function openPicker(sourceEvent) { expanded = true; output.open({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
    client function closePicker(sourceEvent) { expanded = false; output.close({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
    client function handleFocus(sourceEvent) { output.focus({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
    client function handleBlur(sourceEvent) { output.blur({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
    client function handleInvalid(sourceEvent) { output.invalid({ name: name, message: sourceEvent.currentTarget.validationMessage, sourceEvent: sourceEvent }) }
  }
  view {
    <div {...attrs} class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--field wire-next--time-picker {class}" data-variant="{variant}" data-inline="{inline}" data-floating="{variant === 'floating'}" data-invalid="{error ? 'true' : 'false'}" data-expanded="{expanded}">
      <div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
      <div class="wire-next__field-control" data-icon-position="{iconPosition}">
        {#if icon}<span class="{icon} wire-next__field-icon" aria-hidden="true"></span>{/if}
        <input id="{id || name}" class="wire-next__sr-only wire-next__time-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" />
        <button type="button" class="wire-next__time-trigger" disabled="{disabled || readonly}" aria-haspopup="dialog" aria-expanded="{expanded}" @click="openPicker(event)" @focus="handleFocus(event)" @blur="handleBlur(event)"><span>{currentValue || placeholder || "Select time"}</span><i class="{icon}" aria-hidden="true"></i></button>
      </div>
      <div class="wire-next__time-panel" role="dialog" aria-label="{label}"><div><strong>Hour</strong><div class="wire-next__time-options">{#each hours as hour}<button type="button" data-part="hour" data-value="{hour}" aria-pressed="{String(hour) === selectedHour}" @click="selectTimePart(event)">{hour}</button>{/each}</div></div><div><strong>Minute</strong><div class="wire-next__time-options">{#each minutes as minute}<button type="button" data-part="minute" data-value="{minute}" aria-pressed="{String(minute) === selectedMinute}" @click="selectTimePart(event)">{minute}</button>{/each}</div></div><button type="button" class="wire-next__time-done" @click="closePicker(event)">Done</button></div>
      {#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}
      <small class="wire-next__field-error" data-error="{name}">{error}</small>
    </div>
    <FieldStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next__time-panel {
      grid-template-columns: 1fr 1fr;
    }

    .wire-next__time-options {
      display: grid;
      max-height: 12rem;
      margin-top: 0.4rem;
      overflow-y: auto;
      gap: 0.2rem;
    }

    .wire-next__time-done {
      align-self: end;
      background: var(--wire-field-color) !important;
      color: var(--wire-color-primary-contrast, #fff) !important;
    }

    @media (max-width: 640px) {
    .wire-next__time-panel {
        grid-template-columns: 1fr;
      }
    }
  }
}
```

---

## Timeline

Showcase: https://component.wrnexusjs.dev/
Mount: <Timeline /> (legacy: data-component="Timeline")
Category: base
Purpose: Present responsive chronological activity, milestones, or workflow status with rich item metadata.
Props: size: string = "default", color: string = "primary", title: string = "Timeline", description: string = "", items: unknown[] = [], variant: string = "default", class: string = ""
Slots: default
Events: select

### Complete .wrn source contract

```wrn
component Timeline {
  outputs {
    select(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
    size: string = "default"
    color: string = "primary"
    title: string = "Timeline"
    description: string = ""
    items: unknown[] = []
    variant: string = "default"
    class: string = ""
  }

  view {
    <section
      data-ui-component="Timeline"
      aria-label='{title}'
      class='wire-timeline {class}'
    >
      {#if title || description}
        <header class="wire-timeline__header">
          {#if title}
            <h3 class="wire-timeline__title">{title}</h3>
          {/if}
          {#if description}
            <p class="wire-timeline__description">{description}</p>
          {/if}
        </header>
      {/if}

      <ol class="wire-timeline__list">
        {#each items as item, index}
          <li
            class="wire-timeline__item"
            @click='output.select({ item: item, index: index })'
          >
            <span
              class="wire-timeline__marker"
              data-status='{item.status || "default"}'
            >
              <span class='{item.icon || "icon-[lucide--circle]"}' aria-hidden="true"></span>
            </span>

            <article
              class="wire-timeline__card"
              data-variant='{variant}'
            >
              <div class="wire-timeline__card-header">
                <div>
                  <h4 class="wire-timeline__item-title">{item.title || item.label}</h4>
                  {#if item.subtitle}
                    <p class="wire-timeline__subtitle">{item.subtitle}</p>
                  {/if}
                </div>
                {#if item.date || item.meta}
                  <time class="wire-timeline__time">{item.date || item.meta}</time>
                {/if}
              </div>

              {#if item.description}
                <p class="wire-timeline__copy">{item.description}</p>
              {/if}

              {#if item.href}
                <a href='{item.href}' class="wire-timeline__action">
                  <span>{item.actionLabel || "View details"}</span>
                  <span class="icon-[lucide--arrow-right] wire-timeline__action-icon" aria-hidden="true"></span>
                </a>
              {/if}
            </article>
          </li>
        {:empty}
          <li class="wire-timeline__empty">No timeline items available.</li>
        {/each}
      </ol>

      <slot></slot>
    </section>
  }

  style {
    .wire-timeline { width: 100%; }
    .wire-timeline__header { margin-bottom: 1.5rem; }
    .wire-timeline__title { color: var(--wire-color-text); font-size: 1.25rem; font-weight: 700; }
    .wire-timeline__description { max-width: 42rem; margin-top: 0.5rem; color: var(--wire-color-text-muted); line-height: 1.75rem; }
    .wire-timeline__list { position: relative; margin-left: 1rem; border-left: 1px solid var(--wire-color-border); }
    .wire-timeline__item { position: relative; padding: 0 0 2rem 2rem; cursor: pointer; }
    .wire-timeline__item:last-child { padding-bottom: 0; }
    .wire-timeline__marker {
      position: absolute; top: 0; left: -1.05rem; display: flex; width: 2rem; height: 2rem;
      align-items: center; justify-content: center; border: 4px solid var(--wire-color-background);
      border-radius: 50%; color: var(--wire-color-on-primary); background: var(--wire-color-primary); box-shadow: var(--wire-shadow-1);
    }
    .wire-timeline__marker:is([data-status="complete"], [data-status="success"]) { background: var(--wire-color-success); }
    .wire-timeline__marker:is([data-status="warning"], [data-status="pending"]) { background: var(--wire-color-warning); }
    .wire-timeline__marker:is([data-status="danger"], [data-status="failed"]) { background: var(--wire-color-danger); }
    .wire-timeline__card { padding: 1.25rem; border: 1px solid var(--wire-color-border); border-radius: calc(var(--wire-radius) * 1.5); background: var(--wire-color-surface-raised); box-shadow: var(--wire-shadow-1); }
    .wire-timeline__card[data-variant="minimal"] { padding: 0; border-color: transparent; background: transparent; box-shadow: none; }
    .wire-timeline__card-header { display: flex; flex-direction: column; gap: 0.5rem; }
    .wire-timeline__item-title { color: var(--wire-color-text); font-weight: 700; }
    .wire-timeline__subtitle { margin-top: 0.25rem; color: var(--wire-color-primary); font-size: 0.875rem; font-weight: 500; }
    .wire-timeline__time { flex: none; color: var(--wire-color-text-muted); font-size: 0.875rem; }
    .wire-timeline__copy { margin-top: 0.75rem; color: var(--wire-color-text-muted); font-size: 0.875rem; line-height: 1.5rem; }
    .wire-timeline__action { display: inline-flex; align-items: center; gap: 0.5rem; margin-top: 1rem; color: var(--wire-color-primary); font-size: 0.875rem; font-weight: 600; }
    .wire-timeline__action:hover { color: var(--wire-color-primary-hover); }
    .wire-timeline__action-icon { width: 1rem; height: 1rem; }
    .wire-timeline__empty { padding-left: 2rem; color: var(--wire-color-text-muted); font-size: 0.875rem; }
    @media (min-width: 40rem) {
      .wire-timeline__card-header { flex-direction: row; align-items: flex-start; justify-content: space-between; }
    }
  }
}
```

---

## Toaster

Showcase: https://component.wrnexusjs.dev/
Mount: <Toaster /> (legacy: data-component="Toaster")
Category: core
Purpose: Reusable toaster component.
Props: color: string = "info", size: string = "default", position: string = "bottom-right", duration: number = 4500, max: number = 4, pauseOnHover: boolean = true, showIcon: boolean = true, successIcon: string = "", dangerIcon: string = "", warningIcon: string = "", infoIcon: string = "", closable: boolean = true, showProgress: boolean = true, closeLabel: string = "Dismiss notification", class: string = ""
Slots: none
Events: show, dismiss, action

### Complete .wrn source contract

```wrn
//
// Toaster -- the notification host. Mount it once (usually in a layout) and
// raise notifications from anywhere with the runtime global:
//
// toast("Saved")
// toast.success("Invite sent to " + email)
// toast.error("Could not save", { title: "Network error", duration: 8000 })
// toast({ message: "Uploading", tone: "info", duration: 0 })   // 0 = sticky
//
// Each tone ships a built-in icon tinted with that tone colour -- green for
// success, red for danger, amber for warning, blue for info. Swap in your own
// per toast, or per tone on the host:
//
//   toast.success("Shipped", { icon: "icon-[lucide--rocket]" })
//   toast("Quiet one", { icon: false })          // suppress the icon
//   <Toaster successIcon="icon-[lucide--party-popper]" />
//   <Toaster showIcon={false} />                 // never show icons
//
// Actions run your own code. The handler is passed straight through, so it
// closes over whatever the calling function can see:
//
//   toast("Note deleted", {
//     actionLabel: "Undo",
//     onAction: function () { restoreNote(id) }
//   })
//
//   toast.warning("Two versions of this file", {
//     actions: [
//       { label: "Keep mine", onClick: keepMine },
//       { label: "Use theirs", onClick: useTheirs, tone: "danger", dismiss: false }
//     ]
//   })
//
// The toast dismisses itself after an action; pass dismiss: false on the
// action to keep it open (for a step that has to report back). At most two
// actions render -- see normalizeActions. A declarative @action on the tag
// still fires for every click, so a page can log or route centrally.
//
// IMPORTANT -- what a callback may do. It runs long after the function that
// created it returned, and a client function only flushes its state when its
// body ends, so assigning your own component state from inside a callback
// writes to a dead local and is lost. Calling toast(), fetch, navigation and
// anything else that is not a state assignment works normally. To change
// state, dispatch an event and handle it declaratively, which re-enters with
// live state:
//
//   onAction: function () {
//     window.dispatchEvent(new CustomEvent("app:undo-delete"))
//   }
//   ...
//   <div @window:app:undo-delete="restoreItem()">
//
// An icon prop is a CSS class, never markup, so any icon system works
// (iconify, an icon font, your own sprite). NOTE the class has to appear in
// YOUR source for a scanner like Tailwind to emit it -- that is exactly why
// the defaults are inline SVG rather than classes from this package.
//
// The runtime never imports this component: toast() dispatches a
// `wrnexus:toast` window event and this listens for it, so an app can supply
// its own host by listening for the same event.
//
// NOTE: apostrophes are avoided in the style block on purpose -- the .wrn
// block scanner treats a quote as a string delimiter while counting braces,
// so a stray one breaks parsing of the whole component.
component Toaster {
  outputs {
    show(payload: { id: number; message: string; tone: string })
    dismiss(payload: { id: number; reason: string })
    action(payload: { id: number; sourceEvent: Event })
  }

  props {
    // Default tone for toasts raised without one of their own. Every Wire UI
    // component takes color and size; here they set the stack defaults.
    color: string = "info"
    // default | sm | lg -- controls toast width and density.
    size: string = "default"
    // top-left | top-center | top-right | bottom-left | bottom-center | bottom-right
    position: string = "bottom-right"
    // Auto-dismiss delay in ms. 0 keeps a toast until it is dismissed.
    duration: number = 4500
    // Oldest toasts beyond this are retired as new ones arrive.
    max: number = 4
    pauseOnHover: boolean = true
    // Icons: a built-in glyph per tone, tinted with that tone colour.
    // Override any of them with an icon class of your own (iconify, an icon
    // font, whatever your app already uses) -- the built-in SVG is only a
    // fallback so the component needs no icon dependency. Per toast:
    //   toast.success("Saved", { icon: "icon-[lucide--party-popper]" })
    //   toast("Quiet", { icon: false })   // no icon on this one
    showIcon: boolean = true
    successIcon: string = ""
    dangerIcon: string = ""
    warningIcon: string = ""
    infoIcon: string = ""
    closable: boolean = true
    showProgress: boolean = true
    closeLabel: string = "Dismiss notification"
    class: string = ""
  }

  state toasts = []
  state sequence = 0
  // Timer bookkeeping, deliberately outside `toasts`: the view never reads
  // this, so pausing and resuming rebuilds no DOM. See the note in functions.
  state timers = {}

  functions {
    // TIMER DESIGN -- read before changing.
    //
    // 1. No callback here touches state. A client function gets state as a
    //    local snapshot and flushes it back when the body returns, so a write
    //    from a setTimeout callback lands in a dead local and is lost; calling
    //    a peer function from one re-flushes the stale snapshot over live
    //    state. Timers therefore only dispatch a window event, and the
    //    declarative @window handlers re-enter with live state.
    //
    // 2. Timer bookkeeping lives in `timers`, NOT on the toast entries.
    //    The view renders `toasts` through data-for, so touching a toast
    //    object rebuilds every row -- which restarted each progress bar from
    //    zero and made the bar look permanently full. Hover has to be free of
    //    that: pausing must leave the DOM completely alone. `timers` is never
    //    read by the view, so writing it re-renders nothing.
    //
    // 3. Everything that DOES change `toasts` (add, dismiss, remove) returns
    //    untouched items by reference for the rows it is not changing, so the
    //    keyed loop reuses those nodes and their bars keep running.
    client function scheduleEvent(name, id, delay) {
      return setTimeout(function () {
        window.dispatchEvent(new CustomEvent(name, { detail: { id: id } }))
      }, delay)
    }

    // kind is either "dismiss" (the toast lifetime, which hover pauses) or
    // "remove" (the exit-animation cleanup, which hover must NOT touch --
    // see pauseAll).
    client function trackTimer(id, handle, life, kind) {
      var next = {}
      Object.keys(timers).forEach(function (key) { next[key] = timers[key] })
      next[id] = {
        handle: handle,
        expiresAt: Date.now() + life,
        remaining: life,
        kind: kind || "dismiss"
      }
      timers = next
    }

    client function forgetTimer(id) {
      var next = {}
      Object.keys(timers).forEach(function (key) {
        if (String(key) !== String(id)) {
          next[key] = timers[key]
        }
      })
      timers = next
    }

    // An explicit per-toast icon wins; otherwise the tone default prop; and if
    // that is empty the built-in SVG for the tone renders instead. Returns a
    // CSS class name, never markup, so an app can hand us any icon system.
    client function resolveIcon(tone, requested) {
      if (requested) {
        return String(requested)
      }
      if (tone === "success") {
        return successIcon
      }
      if (tone === "danger") {
        return dangerIcon
      }
      if (tone === "warning") {
        return warningIcon
      }
      if (tone === "info") {
        return infoIcon
      }
      return ""
    }

    // Actions arrive either as a single actionLabel/onAction pair or as an
    // actions array. At most two are rendered: the view has two fixed slots
    // because a data-for inside a data-for is not expanded by the runtime, so
    // an arbitrary list cannot be rendered per row. Two covers the real cases
    // (Undo, Retry, View / Dismiss); anything beyond that is dropped loudly
    // rather than silently.
    client function normalizeActions(detail) {
      var list = []
      if (Array.isArray(detail.actions)) {
        list = detail.actions.filter(function (action) { return action && action.label })
      } else if (detail.actionLabel) {
        list = [{ label: detail.actionLabel, onClick: detail.onAction, tone: detail.actionTone }]
      }
      if (list.length > 2) {
        console.warn(
          "[wrnexus] Toaster renders at most 2 actions per toast; ignoring " +
            (list.length - 2) + " extra."
        )
      }
      return list.slice(0, 2)
    }

    // Is the pointer resting on the stack right now?
    //
    // mouseenter only fires when the pointer MOVES. A toast raised while the
    // cursor is already parked over the stack -- which is exactly what an
    // action handler does -- therefore gets no enter event, is never paused,
    // and counts down and disappears while the user is still reaching for its
    // button. Its progress bar meanwhile IS paused, because CSS :hover does
    // apply, so the bar sat still while the toast quietly expired. Asking the
    // DOM for the live :hover state closes that gap.
    client function stackHovered() {
      var list = refs.list
      return !!(pauseOnHover && list && list.matches(":hover"))
    }

    client function receiveToast(sourceEvent) {
      var detail = sourceEvent.detail || {}

      // One toast per raise, however many hosts are mounted.
      //
      // toast() dispatches a window event, so EVERY mounted Toaster hears it
      // and a page with two hosts showed the message twice (the component
      // showcase mounts five and showed five). Claiming the event on the
      // detail object lets the first host win and the rest stand down, so an
      // accidental second host is harmless instead of multiplying every
      // notification.
      if (detail.__wrnClaimed) {
        return
      }
      detail.__wrnClaimed = true
      sequence = sequence + 1

      var id = sequence
      var life = detail.duration === 0 ? 0 : (detail.duration || duration)
      var tone = detail.tone || color

      var entry = {
        id: id,
        title: detail.title || "",
        message: detail.message === undefined ? "" : String(detail.message),
        tone: tone,
        icon: resolveIcon(tone, detail.icon),
        showIcon: showIcon && detail.icon !== false,
        actions: normalizeActions(detail),
        duration: life,
        leaving: false
      }

      // Retire the oldest live toasts in the same pass that appends the new
      // one, so a burst can never leave the stack over max. Untouched rows are
      // returned by reference so their nodes (and bars) survive.
      var live = toasts.filter(function (item) { return !item.leaving })
      var retire = live.length + 1 > max ? live.slice(0, live.length + 1 - max) : []

      toasts = toasts
        .map(function (item) {
          var doomed = retire.some(function (old) { return old.id === item.id })
          return doomed ? Object.assign({}, item, { leaving: true }) : item
        })
        .concat([entry])

      retire.forEach(function (item) {
        clearTimeout((timers[item.id] || {}).handle)
        trackTimer(item.id, scheduleEvent("wrnexus:toast:remove", item.id, 240), 240, "remove")
      })

      if (life) {
        // Born paused when the stack is already hovered: handle 0 parks it,
        // and resumeAll starts the clock when the pointer finally leaves.
        if (stackHovered()) {
          trackTimer(id, 0, life, "dismiss")
        } else {
          trackTimer(id, scheduleEvent("wrnexus:toast:dismiss", id, life), life, "dismiss")
        }
      }
      output.show({ id: id, message: entry.message, tone: entry.tone })
    }

    client function dismissToast(id, reason) {
      var found = false
      toasts = toasts.map(function (item) {
        if (item.id !== id || item.leaving) {
          return item
        }
        found = true
        return Object.assign({}, item, { leaving: true })
      })
      if (!found) {
        return
      }
      clearTimeout((timers[id] || {}).handle)
      // Give the exit animation time to play, then drop the entry.
      trackTimer(id, scheduleEvent("wrnexus:toast:remove", id, 240), 240, "remove")
      output.dismiss({ id: id, reason: reason || "auto" })
    }

    client function removeToast(id) {
      clearTimeout((timers[id] || {}).handle)
      forgetTimer(id)
      toasts = toasts.filter(function (item) { return item.id !== id })
    }

    client function dismissById(sourceEvent) {
      var detail = sourceEvent.detail || {}
      dismissToast(detail.id, detail.reason || "timeout")
    }

    client function removeById(sourceEvent) {
      var detail = sourceEvent.detail || {}
      removeToast(detail.id)
    }

    client function clearAll() {
      toasts.slice().forEach(function (item) {
        if (!item.leaving) {
          dismissToast(item.id, "clear")
        }
      })
    }

    // Hovering must not run the clock down while a toast is being read, so
    // the remaining time is banked and the timers restart on the way out.
    // Only `timers` is written, so not a single DOM node is rebuilt -- the
    // progress bars simply stop where they are (CSS pauses them on :hover)
    // and carry on from there.
    client function pauseAll() {
      if (!pauseOnHover) {
        return
      }
      var now = Date.now()
      var next = {}
      Object.keys(timers).forEach(function (key) {
        var entry = timers[key]
        // A "remove" timer finishes an exit animation -- pausing it strands
        // the toast: invisible, still taking up space in the stack, and still
        // able to swallow clicks, forever. Only lifetimes pause.
        if (!entry || !entry.handle || entry.kind !== "dismiss") {
          next[key] = entry
          return
        }
        clearTimeout(entry.handle)
        var left = entry.expiresAt - now
        next[key] = {
          handle: 0,
          expiresAt: entry.expiresAt,
          remaining: left > 0 ? left : 1,
          kind: "dismiss"
        }
      })
      timers = next
    }

    client function resumeAll() {
      if (!pauseOnHover) {
        return
      }
      var now = Date.now()
      var next = {}
      Object.keys(timers).forEach(function (key) {
        var entry = timers[key]
        if (!entry || entry.handle || entry.kind !== "dismiss") {
          next[key] = entry
          return
        }
        var left = entry.remaining > 0 ? entry.remaining : 1
        // Number(key): object keys come back as strings, and the dismiss
        // handler matches ids with !==, so a string id silently matches no
        // toast and the resumed timer would fire into the void.
        next[key] = {
          handle: scheduleEvent("wrnexus:toast:dismiss", Number(key), left),
          expiresAt: now + left,
          remaining: left,
          kind: "dismiss"
        }
      })
      timers = next
    }

    // The click handler for an action. Runs as a fresh invocation from the
    // DOM, so state here is live and calling peer functions is safe.
    client function runAction(id, index, sourceEvent) {
      var toast = null
      toasts.forEach(function (item) {
        if (item.id === id) {
          toast = item
        }
      })
      if (!toast) {
        return
      }

      var action = toast.actions[index]
      if (!action) {
        return
      }

      // Declarative listeners on the <Toaster> tag see every action too.
      output.action({
        id: id,
        index: index,
        label: action.label,
        sourceEvent: sourceEvent
      })

      // The callback is application code: a throw here must not take the
      // toaster down with it, or the toast would be stuck on screen forever.
      if (typeof action.onClick === "function") {
        try {
          action.onClick(sourceEvent)
        } catch (error) {
          console.error("[wrnexus] toast action handler failed", error)
        }
      }

      // NOTHING may call a peer function past this point.
      //
      // The callback is application code and is re-entrant: a handler that
      // raises its own toast runs receiveToast in a fresh invocation, which
      // appends to live state. Calling a peer from here would first flush the
      // snapshot this function captured on entry -- taken BEFORE the callback
      // ran -- straight over that live state, silently erasing the toast the
      // handler just raised. So the dismissal is dispatched inline instead,
      // and handled on the next tick with state that is actually current.
      // This function never assigns to state itself, so it flushes nothing.
      if (action.dismiss !== false) {
        setTimeout(function () {
          window.dispatchEvent(
            new CustomEvent("wrnexus:toast:dismiss", {
              detail: { id: id, reason: "action" }
            })
          )
        }, 0)
      }
    }
  }

  view {
    <div
      {...attrs}
      data-ui-component="Toaster"
      data-toaster="true"
      data-position='{position}'
      data-size='{size}'
      class='wire-toaster {class}'
      role="region"
      aria-label="Notifications"
      @window:wrnexus:toast='receiveToast(event)'
      @window:wrnexus:toast:dismiss='dismissById(event)'
      @window:wrnexus:toast:clear='clearAll()'
      @window:wrnexus:toast:remove='removeById(event)'
    >
      <!-- Hover is bound to the list, not to each toast, and pauses the whole
           stack. A row is a data-for clone that is recreated whenever state
           changes, so a per-row mouseleave would be bound to a node the
           browser has already discarded: the pointer would never "leave", the
           toast would stay paused, and it would hang on screen forever.
           The list element is stable for the lifetime of the page. Pausing
           the whole stack is also what people expect -- reading one toast
           should not let its neighbours expire underneath it. -->
      <ol
        class="wire-toaster__list"
        data-ref="list"
        aria-live="polite"
        aria-relevant="additions text"
        @mouseenter='pauseAll()'
        @mouseleave='resumeAll()'
        @focusin='pauseAll()'
        @focusout='resumeAll()'
      >
        <li
          class="wire-toast"
          data-for="item in toasts"
          data-key="item.id"
          data-tone='{item.tone}'
          data-leaving='{item.leaving}'
        >
          <span
            class="wire-toast__indicator"
            aria-hidden="true"
          >
          </span>

          <!-- An app-supplied icon class (iconify or otherwise). -->
          <span
            class='wire-toast__icon {item.icon}'
            data-show="item.showIcon && item.icon"
            aria-hidden="true"
          >
          </span>

          <!-- Built-in fallback. Inline SVG on purpose: the package cannot
               assume the host app has an icon set installed, and a class from
               here would not be in the app Tailwind content globs anyway, so
               it would generate no CSS and render nothing. Each tone shows its
               own glyph via data-show; all of them inherit --toast-accent, so
               the icon is green / red / amber / blue with the tone. -->
          <svg
            class="wire-toast__icon wire-toast__icon--default"
            data-show="item.showIcon && !item.icon"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
            aria-hidden="true"
          >
            <g data-show="item.tone === 'success'">
              <circle cx="12" cy="12" r="10" />
              <path d="m9 12 2 2 4-4" />
            </g>
            <g data-show="item.tone === 'danger'">
              <circle cx="12" cy="12" r="10" />
              <path d="M12 8v4" />
              <path d="M12 16h.01" />
            </g>
            <g data-show="item.tone === 'warning'">
              <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
              <path d="M12 9v4" />
              <path d="M12 17h.01" />
            </g>
            <g data-show="item.tone !== 'success' && item.tone !== 'danger' && item.tone !== 'warning'">
              <circle cx="12" cy="12" r="10" />
              <path d="M12 16v-4" />
              <path d="M12 8h.01" />
            </g>
          </svg>

          <div
            class="wire-toast__body"
          >
            <p
              class="wire-toast__title"
              data-show="item.title"
            >
              {item.title}
            </p>
            <p
              class="wire-toast__message"
            >
              {item.message}
            </p>
          </div>

          <!-- Two fixed action slots. A data-for inside a data-for is not
               expanded by the runtime, so an arbitrary list cannot be looped
               per row; normalizeActions caps the array at two and warns. -->
          <div
            class="wire-toast__actions"
            data-show="item.actions.length"
          >
            <button
              type="button"
              class="wire-toast__action"
              data-show="item.actions.length > 0"
              data-tone='{item.actions[0].tone}'
              @click='runAction(item.id, 0, event)'
            >
              {item.actions[0].label}
            </button>

            <button
              type="button"
              class="wire-toast__action"
              data-show="item.actions.length > 1"
              data-tone='{item.actions[1].tone}'
              @click='runAction(item.id, 1, event)'
            >
              {item.actions[1].label}
            </button>
          </div>

          <button
            type="button"
            class="wire-toast__close"
            data-show="closable"
            aria-label='{closeLabel}'
            @click='dismissToast(item.id, "close-button")'
          >
            <svg
              viewBox="0 0 24 24"
              width="14"
              height="14"
              fill="none"
              stroke="currentColor"
              stroke-width="2.2"
              stroke-linecap="round"
              aria-hidden="true"
            >
              <path d="M18 6 6 18" />
              <path d="M6 6 18 18" />
            </svg>
          </button>

          <!-- Decoration only: the timer above owns dismissal. Shown when the
               toast has a lifetime, and paused in step with it on hover. -->
          <span
            class="wire-toast__progress"
            data-show="showProgress && item.duration"
            style="--toast-duration: {item.duration}ms"
            aria-hidden="true"
          >
          </span>
        </li>
      </ol>
    </div>
  }

  style {
    .wire-toaster {
      position: fixed;
      inset: 0;
      z-index: 1400;
      display: flex;
      padding: clamp(0.75rem, 2vw, 1.25rem);
      /* The host covers the viewport so it can align the stack in any corner;
         it must never swallow clicks meant for the page underneath. */
      pointer-events: none;
    }

    /*
     * pointer-events MUST be re-enabled here, not only on .wire-toast.
     *
     * The host is pointer-events: none so the page underneath stays clickable
     * through the empty overlay, and the list inherits that. An element with
     * pointer-events: none is never a hit-test target, so it never matches
     * :hover AND never receives mouseenter/mouseleave -- and those two do not
     * bubble up from the rows either. The result was that hover-to-pause did
     * nothing at all for a real user: toasts kept counting down and vanished
     * from under the cursor as they reached for the action button. (Synthetic
     * dispatchEvent bypasses hit-testing, so it hid this in testing.)
     *
     * The list box wraps the stack exactly -- its height is the toasts plus
     * their gaps -- so making it interactive costs the page nothing.
     */
    .wire-toaster__list {
      display: flex;
      flex-direction: column;
      gap: 0.6rem;
      width: min(23rem, 100%);
      margin: 0;
      padding: 0;
      list-style: none;
      pointer-events: auto;
    }

    /* Nothing to hover when the stack is empty. */
    .wire-toaster__list:empty {
      pointer-events: none;
    }

    .wire-toaster[data-size="sm"] .wire-toaster__list {
      width: min(18rem, 100%);
    }

    .wire-toaster[data-size="lg"] .wire-toaster__list {
      width: min(28rem, 100%);
    }

    .wire-toaster[data-size="sm"] .wire-toast {
      padding: 0.6rem 0.7rem;
      font-size: 0.78rem;
    }

    .wire-toaster[data-size="lg"] .wire-toast {
      padding: 1.05rem 1.1rem;
    }

    .wire-toaster[data-position^="top"] {
      align-items: flex-start;
    }

    .wire-toaster[data-position^="bottom"] {
      align-items: flex-end;
    }

    /* Newest nearest the screen edge: at the bottom that means visually last,
       so the column is reversed rather than the state array. */
    .wire-toaster[data-position^="bottom"] .wire-toaster__list {
      flex-direction: column-reverse;
    }

    .wire-toaster[data-position$="left"] {
      justify-content: flex-start;
    }

    .wire-toaster[data-position$="center"] {
      justify-content: center;
    }

    .wire-toaster[data-position$="right"] {
      justify-content: flex-end;
    }

    .wire-toast {
      position: relative;
      display: flex;
      align-items: flex-start;
      gap: 0.7rem;
      overflow: hidden;
      padding: 0.85rem 0.9rem;
      color: var(--wire-color-text);
      background: var(--wire-color-surface-raised);
      border: 1px solid var(--wire-color-border);
      border-radius: 0.9rem;
      box-shadow: 0 18px 40px color-mix(in srgb, black 28%, transparent);
      pointer-events: auto;
      animation: wire-toast-in 220ms cubic-bezier(0.16, 1, 0.3, 1) both;
    }

    .wire-toaster[data-position$="left"] .wire-toast {
      animation-name: wire-toast-in-left;
    }

    /*
     * Belt and braces: a toast on its way out is faded to nothing but still
     * occupies its box until it is dropped from the list, so without this it
     * can swallow clicks aimed at whatever is under it.
     */
    .wire-toast[data-leaving="true"] {
      animation: wire-toast-out 200ms ease forwards;
      pointer-events: none;
    }

    .wire-toast__indicator {
      flex: 0 0 auto;
      width: 0.4rem;
      align-self: stretch;
      border-radius: 999px;
      background: var(--toast-accent, var(--wire-color-primary));
    }

    .wire-toast[data-tone="success"] {
      --toast-accent: var(--wire-color-success);
    }

    .wire-toast[data-tone="danger"] {
      --toast-accent: var(--wire-color-danger);
    }

    .wire-toast[data-tone="warning"] {
      --toast-accent: var(--wire-color-warning);
    }

    .wire-toast[data-tone="info"] {
      --toast-accent: var(--wire-color-info);
    }

    /*
     * Centred like the trailing controls. The row is top-aligned so long text
     * starts at the top, but on a one-line toast the 1.75rem close button is
     * taller than the text, so a top-aligned body left the text sitting a few
     * pixels above the button it is supposed to line up with.
     */
    /*
     * Tinted with the tone accent, so the glyph reads as the status at a
     * glance -- green for success, red for danger, amber for warning, and the
     * info colour otherwise. The built-in SVG strokes with currentColor, and
     * an app icon class that uses currentColor (iconify does) picks up the
     * same value for free.
     */
    .wire-toast__icon {
      flex: 0 0 auto;
      align-self: center;
      width: 1.15rem;
      height: 1.15rem;
      color: var(--toast-accent, var(--wire-color-primary));
    }

    .wire-toast__body {
      flex: 1 1 auto;
      min-width: 0;
      align-self: center;
      display: grid;
      gap: 0.15rem;
    }

    .wire-toast__title,
    .wire-toast__message {
      margin: 0;
      overflow-wrap: anywhere;
    }

    .wire-toast__title {
      color: var(--wire-color-text);
      font-size: 0.85rem;
      font-weight: 650;
      line-height: 1.35;
    }

    .wire-toast__message {
      color: var(--wire-color-text-muted);
      font-size: 0.82rem;
      line-height: 1.5;
    }

    /*
     * The trailing controls share one alignment. The close button used to be
     * pinned to the top with a negative margin while the action sat centred,
     * so the two sat on different lines and read as misaligned. Both are
     * centred against the toast body now, and both are the same height, so
     * their centres line up whether the toast is one line or three.
     */
    .wire-toast__actions {
      display: flex;
      align-items: center;
      flex: 0 0 auto;
      align-self: center;
      gap: 0.35rem;
    }

    /* A destructive action reads in the danger colour regardless of tone. */
    .wire-toast__action[data-tone="danger"] {
      color: var(--wire-color-danger);
      border-color: color-mix(in srgb, var(--wire-color-danger) 45%, transparent);
    }

    .wire-toast__action {
      appearance: none;
      flex: 0 0 auto;
      min-height: 1.75rem;
      display: inline-flex;
      align-items: center;
      padding: 0 0.6rem;
      color: var(--toast-accent, var(--wire-color-primary));
      background: transparent;
      border: 1px solid color-mix(in srgb, var(--toast-accent, var(--wire-color-primary)) 40%, transparent);
      border-radius: 0.55rem;
      font: inherit;
      font-size: 0.78rem;
      font-weight: 650;
      cursor: pointer;
    }

    /*
     * padding is reset explicitly: an app-level button padding rule beats the
     * browser default and collapses the icon to a sliver inside this
     * fixed-size button.
     */
    .wire-toast__close {
      appearance: none;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex: 0 0 auto;
      align-self: center;
      padding: 0;
      width: 1.75rem;
      height: 1.75rem;
      margin: 0;
      color: var(--wire-color-text-muted);
      background: transparent;
      border: 0;
      border-radius: 999px;
      cursor: pointer;
      transition: color 150ms ease, background 150ms ease;
    }

    .wire-toast__close:hover {
      color: var(--wire-color-text);
      background: var(--wire-color-surface-soft);
    }

    .wire-toast__close svg {
      flex: 0 0 auto;
      width: 0.875rem;
      height: 0.875rem;
    }

    /*
     * Sweeps left to right as the toast lives out its delay, reaching full
     * width as it is dismissed, so the remaining time is readable at a glance.
     * (Flip the keyframes below to run 1 -> 0 if you would rather it drain.)
     */
    .wire-toast__progress {
      position: absolute;
      left: 0;
      bottom: 0;
      height: 2px;
      width: 100%;
      transform-origin: left center;
      background: var(--toast-accent, var(--wire-color-primary));
      animation: wire-toast-progress var(--toast-duration, 4500ms) linear forwards;
    }

    /* Hover pauses the clock, so the bar must pause with it or it would lie
       about how much time is left. */
    /*
     * Paused straight from :hover rather than from a state flag. A flag would
     * mean writing state on every hover, and that rebuilds the rows -- which
     * restarts the bars from zero and is exactly what made them look stuck.
     * CSS pauses the animation in place and touches no DOM, and the JS timer
     * is banked on the same mouseenter, so the two stay in step.
     */
    .wire-toaster__list:hover .wire-toast__progress,
    .wire-toaster__list:focus-within .wire-toast__progress {
      animation-play-state: paused;
    }

    @keyframes wire-toast-in {
      from {
        opacity: 0;
        transform: translateX(18px) scale(0.98);
      }
      to {
        opacity: 1;
        transform: none;
      }
    }

    @keyframes wire-toast-in-left {
      from {
        opacity: 0;
        transform: translateX(-18px) scale(0.98);
      }
      to {
        opacity: 1;
        transform: none;
      }
    }

    @keyframes wire-toast-out {
      to {
        opacity: 0;
        transform: translateX(12px) scale(0.97);
      }
    }

    @keyframes wire-toast-progress {
      from {
        transform: scaleX(0);
      }
      to {
        transform: scaleX(1);
      }
    }

    @media (max-width: 639px) {
      .wire-toaster {
        justify-content: stretch;
      }

      .wire-toaster__list {
        width: 100%;
      }
    }

    /*
     * Reduced motion drops the entry/exit movement but KEEPS the progress
     * sweep: it is a clock, not decoration, and a frozen bar would both
     * misreport the time left and look like the bug it used to be. A linear
     * 2px bar carries no vestibular risk.
     */
    @media (prefers-reduced-motion: reduce) {
      .wire-toast,
      .wire-toast[data-leaving="true"] {
        animation: none;
      }
    }
  }
}
```

---

## ToggleCount

Showcase: https://component.wrnexusjs.dev/
Mount: <ToggleCount /> (legacy: data-component="ToggleCount")
Category: advanced-forms
Purpose: Theme-aware, responsive toggle count component.
Props: size: string = "default", color: string = "primary", variant: string = "segmented", class: string = "", name: string = "billing-cycle", value: string = "monthly", firstValue: string = "monthly", firstLabel: string = "Monthly", secondValue: string = "annual", secondLabel: string = "Annual", ariaLabel: string = "Billing frequency", items: unknown[] = [], currency: string = "$", suffix: string = "", firstValueKey: string = "monthly", secondValueKey: string = "annual", emptyValue: string = "—", align: string = "end", fullWidth: boolean = true, disabled: boolean = false, animate: boolean = true, animationDuration: number = 450, animationSteps: number = 18
Slots: none
Events: change, toggle

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component ToggleCount {
  outputs {
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    toggle(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
    size: string = "default"
    color: string = "primary"
    variant: string = "segmented"
    class: string = ""

    name: string = "billing-cycle"
    value: string = "monthly"
    firstValue: string = "monthly"
    firstLabel: string = "Monthly"
    secondValue: string = "annual"
    secondLabel: string = "Annual"
    ariaLabel: string = "Billing frequency"

    items: unknown[] = []
    currency: string = "$"
    suffix: string = ""
    firstValueKey: string = "monthly"
    secondValueKey: string = "annual"
    emptyValue: string = "—"

    align: string = "end"
    fullWidth: boolean = true
    disabled: boolean = false
    animate: boolean = true
    animationDuration: number = 450
    animationSteps: number = 18

  }

  state selectedValue = value

  functions {
    shared function isFirstSelected() {
      return selectedValue === firstValue
    }

    shared function isSecondSelected() {
      return selectedValue === secondValue
    }

    shared function displayValue(item) {
      if (isSecondSelected()) {
        return item[secondValueKey] !== undefined
          ? item[secondValueKey]
          : emptyValue
      }

      return item[firstValueKey] !== undefined
        ? item[firstValueKey]
        : emptyValue
    }

    client function dispatchToggleEvent(sourceEvent, previousValue, payload) {
      payload = {
        component: "ToggleCount",
        name: name,
        value: selectedValue,
        previousValue: previousValue,
        firstValue: firstValue,
        secondValue: secondValue
      }
      output.change(payload)
      output.toggle(payload)
    }

    client function selectValue(sourceEvent, nextValue, previousValue) {
      if (disabled || selectedValue === nextValue) {
        return
      }

      previousValue = selectedValue
      selectedValue = nextValue

      if (
        animate &&
        !window.matchMedia("(prefers-reduced-motion: reduce)").matches
      ) {
        animateDisplayedValues(sourceEvent, previousValue)
      }

      dispatchToggleEvent(sourceEvent, previousValue)
    }

    client function animateDisplayedValues(sourceEvent, previousValue, root, nodes, animationToken) {
      root = sourceEvent.currentTarget.closest("[data-wrn-toggle-count]")

      if (!root) {
        return
      }

      animationToken = String(Date.now()) + ":" + String(selectedValue)
      root.setAttribute("data-animation-token", animationToken)
      nodes = root.querySelectorAll("[data-toggle-count-value]")
      animateValueAt(nodes, 0, previousValue, root, animationToken)
    }

    client function animateValueAt(
      nodes,
      index,
      previousValue,
      root,
      animationToken,
      node,
      fromValue,
      toValue
    ) {
      if (index >= nodes.length) {
        return
      }

      node = nodes[index]
      fromValue = Number(
        previousValue === secondValue
          ? node.getAttribute("data-second-value")
          : node.getAttribute("data-first-value")
      )
      toValue = Number(
        selectedValue === secondValue
          ? node.getAttribute("data-second-value")
          : node.getAttribute("data-first-value")
      )

      if (!Number.isNaN(fromValue) && !Number.isNaN(toValue)) {
        node.replaceChildren(String(fromValue))
        scheduleValueFrame(
          node,
          fromValue,
          toValue,
          1,
          Math.max(1, Number(animationSteps) || 1),
          Math.max(1, Number(animationDuration) || 1),
          root,
          animationToken
        )
      }

      animateValueAt(
        nodes,
        index + 1,
        previousValue,
        root,
        animationToken
      )
    }

    client function scheduleValueFrame(
      node,
      fromValue,
      toValue,
      frame,
      totalFrames,
      duration,
      root,
      animationToken,
      nextValue
    ) {
      if (frame > totalFrames) {
        return
      }

      nextValue = Math.round(
        fromValue +
        (toValue - fromValue) * (frame / totalFrames)
      )

      setTimeout(
        applyAnimatedValue,
        Math.max(1, duration * frame / totalFrames),
        node,
        nextValue,
        root,
        animationToken
      )

      scheduleValueFrame(
        node,
        fromValue,
        toValue,
        frame + 1,
        totalFrames,
        duration,
        root,
        animationToken
      )
    }

    client function applyAnimatedValue(
      node,
      nextValue,
      root,
      animationToken
    ) {
      if (
        root &&
        root.getAttribute("data-animation-token") !== animationToken
      ) {
        return
      }

      node.replaceChildren(String(nextValue))
    }

    client function toggleValue(sourceEvent) {
      selectValue(
        sourceEvent,
        isFirstSelected() ? secondValue : firstValue
      )
    }
  }

  view {
    <section
      {...attrs}
      data-wrn-toggle-count
      data-variant="{variant}"
      data-value="{selectedValue}"
      data-disabled="{disabled ? 'true' : 'false'}"
      class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--toggle-count wire-next--toggle-count-{variant} {fullWidth ? 'wire-next--toggle-count-full' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
    >
      <input type="hidden" name="{name}" value="{selectedValue}" />

      <div class="wire-next__toggle-count-control wire-next__toggle-count-control--{align}">
        {#if variant === "switch"}
          <span data-selected="{isFirstSelected() ? 'true' : 'false'}">{firstLabel}</span>
          <button
            type="button"
            role="switch"
            aria-label="{ariaLabel}"
            aria-checked="{isSecondSelected() ? 'true' : 'false'}"
            disabled="{disabled}"
            @click="toggleValue(event)"
            class="wire-next__toggle-count-switch"
          >
            <span aria-hidden="true"></span>
          </button>
          <span data-selected="{isSecondSelected() ? 'true' : 'false'}">{secondLabel}</span>
        {:else}
          <div class="wire-next__toggle-count-segmented" role="group" aria-label="{ariaLabel}">
            <button
              type="button"
              aria-pressed="{isFirstSelected() ? 'true' : 'false'}"
              disabled="{disabled}"
              @click="selectValue(event, firstValue)"
            >{firstLabel}</button>
            <button
              type="button"
              aria-pressed="{isSecondSelected() ? 'true' : 'false'}"
              disabled="{disabled}"
              @click="selectValue(event, secondValue)"
            >{secondLabel}</button>
          </div>
        {/if}
      </div>

      {#if items.length > 0}
        <div class="wire-next__toggle-count-items">
          {#each items as item}
            <article>
              <span>{item.label || item.name}</span>
              <strong>
                {#if currency}<small>{currency}</small>{/if}
                <span
                  data-toggle-count-value
                  data-first-value="{item[firstValueKey]}"
                  data-second-value="{item[secondValueKey]}"
                >{displayValue(item)}</span>
                {#if suffix}<small>{suffix}</small>{/if}
              </strong>
              {#if item.description}<small>{item.description}</small>{/if}
            </article>
          {/each}
        </div>
      {/if}
    </section>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next__toggle-count-control--start {
      justify-content: flex-start;
    }
    .wire-next__toggle-count-control--center {
      justify-content: center;
    }
    .wire-next__toggle-count-control--end {
      justify-content: flex-end;
    }

    .wire-next--toggle-count {
      --wire-toggle-count-accent: var(--wire-component-color);
      display: grid;
      width: fit-content;
      gap: 0.75rem;
      color: var(--wire-color-text);
    }

    .wire-next--toggle-count-full {
      width: 100%;
    }

    .wire-next__toggle-count-control {
      display: flex;
    }

    .wire-next__toggle-count-segmented {
      display: inline-flex;
      gap: 0.2rem;
      padding: 0.2rem;
      border-radius: calc(var(--wire-radius-sm) + 0.15rem);
      background: var(--wire-color-surface-2);
      box-shadow: inset 0 0 0 1px var(--wire-color-border);
    }

    .wire-next__toggle-count-segmented button {
      min-height: 2.35rem;
      padding: 0.45rem 0.8rem;
      border: 0;
      border-radius: var(--wire-radius-sm);
      background: transparent;
      color: var(--wire-color-muted);
      font: inherit;
      font-size: 0.78rem;
      font-weight: 700;
      cursor: pointer;
    }

    .wire-next__toggle-count-segmented button[aria-pressed="true"] {
      background: var(--wire-color-surface);
      color: var(--wire-color-text);
      box-shadow:
        var(--wire-shadow-1),
        inset 0 -2px var(--wire-toggle-count-accent);
    }

    .wire-next__toggle-count-segmented button:focus-visible,
    .wire-next__toggle-count-switch:focus-visible {
      outline: 2px solid var(--wire-toggle-count-accent);
      outline-offset: 2px;
    }

    .wire-next__toggle-count-segmented button:disabled,
    .wire-next__toggle-count-switch:disabled {
      cursor: not-allowed;
    }

    .wire-next__toggle-count-control:has(.wire-next__toggle-count-switch) {
      align-items: center;
      gap: 0.7rem;
      font-size: 0.78rem;
      font-weight: 700;
    }

    .wire-next__toggle-count-control > span {
      color: var(--wire-color-muted);
    }

    .wire-next__toggle-count-control > span[data-selected="true"] {
      color: var(--wire-color-text);
    }

    .wire-next__toggle-count-switch {
      position: relative;
      width: 2.65rem;
      height: 1.45rem;
      padding: 0.15rem;
      border: 1px solid var(--wire-color-border);
      border-radius: 999px;
      background: var(--wire-color-surface-2);
      cursor: pointer;
      transition: background-color var(--wire-motion-fast) var(--wire-ease-standard);
    }

    .wire-next__toggle-count-switch > span {
      display: block;
      width: 1rem;
      height: 1rem;
      border-radius: 50%;
      background: var(--wire-color-text);
      box-shadow: var(--wire-shadow-1);
      transition: transform var(--wire-motion-fast) var(--wire-ease-standard);
    }

    .wire-next__toggle-count-switch[aria-checked="true"] {
      border-color: var(--wire-toggle-count-accent);
      background: var(--wire-toggle-count-accent);
    }

    .wire-next__toggle-count-switch[aria-checked="true"] > span {
      background: white;
      transform: translateX(1.15rem);
    }

    .wire-next__toggle-count-items {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
      overflow: hidden;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-md);
      background: var(--wire-color-surface);
    }

    .wire-next__toggle-count-items article {
      display: grid;
      align-content: start;
      gap: 0.35rem;
      min-width: 0;
      padding: 1rem;
    }

    .wire-next__toggle-count-items article + article {
      border-left: 1px solid var(--wire-color-border);
    }

    .wire-next__toggle-count-items article > span {
      color: var(--wire-color-muted);
      font-size: 0.75rem;
      font-weight: 600;
    }

    .wire-next__toggle-count-items strong {
      display: flex;
      align-items: baseline;
      gap: 0.2rem;
      font-size: 1.5rem;
      line-height: 1;
    }

    .wire-next__toggle-count-items [data-toggle-count-value] {
      min-width: 2ch;
      font-variant-numeric: tabular-nums;
      transition:
        color var(--wire-motion-fast) var(--wire-ease-standard),
        transform var(--wire-motion-fast) var(--wire-ease-standard);
    }

    .wire-next__toggle-count-items strong small {
      color: inherit;
      font-size: 0.65em;
    }

    .wire-next__toggle-count-items article > small {
      color: var(--wire-color-muted);
      font-size: 0.68rem;
    }

    @media (max-width: 36rem) {
    .wire-next__toggle-count-control {
        justify-content: center;
      }
    .wire-next__toggle-count-items {
        grid-template-columns: 1fr;
      }
    .wire-next__toggle-count-items article + article {
        border-top: 1px solid var(--wire-color-border);
        border-left: 0;
      }
    }

    @media (prefers-reduced-motion: reduce) {
    .wire-next__toggle-count-items [data-toggle-count-value] {
        transition: none;
      }
    }
  }
}
```

---

## TogglePassword

Showcase: https://component.wrnexusjs.dev/
Mount: <TogglePassword /> (legacy: data-component="TogglePassword")
Category: advanced-forms
Purpose: Accessible password field with optional show and hide controls.
Props: size: string = "default", color: string = "primary", label: string = "Password", name: string = "password", value: string = "", placeholder: string = "Enter your password", autocomplete: string = "current-password", minlength: string = "", maxlength: string = "", pattern: string = "(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).{8,}", fields: unknown[] = [], visible: boolean = false, toggleable: boolean = true, toggleMode: string = "button", checkboxLabel: string = "Show password", showLabel: string = "Show password", hideLabel: string = "Hide password", disabled: boolean = false, readonly: boolean = false, required: boolean = false, invalid: boolean = false, helpText: string = "", validationMessage: string = "", class: string = ""
Slots: none
Events: input, change, toggle

### Complete .wrn source contract

```wrn
import FieldStyles from "../styles/FieldStyles.wrn"

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component TogglePassword {
  outputs {
    input(payload: { name?: string; value: string | number | boolean | null | object; visible: boolean })
    change(payload: { name?: string; value: string | number | boolean | null | object; visible: boolean })
    toggle(payload: { visible: boolean })
  }

  props {
    size: string = "default"
    color: string = "primary"
    label: string = "Password"
    name: string = "password"
    value: string = ""
    placeholder: string = "Enter your password"
    autocomplete: string = "current-password"
    minlength: string = ""
    maxlength: string = ""
    pattern: string = "(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).{8,}"
    fields: unknown[] = []
    visible: boolean = false
    toggleable: boolean = true
    toggleMode: string = "button"
    checkboxLabel: string = "Show password"
    showLabel: string = "Show password"
    hideLabel: string = "Hide password"
    disabled: boolean = false
    readonly: boolean = false
    required: boolean = false
    invalid: boolean = false
    helpText: string = ""
    validationMessage: string = ""
    class: string = ""
}

  state revealed = visible

  functions {
    shared function toggleVisibility() {
      if (disabled || readonly || !toggleable) {
        return
      }
      revealed = !revealed
    }

    shared function fieldId(field, index) {
      return field.id || field.name || name + "-" + index
    }

    shared function fieldName(field, index) {
      return field.name || name + "-" + index
    }
  }

  view {
    <fieldset
      class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--toggle-password {invalid ? 'wire-next--invalid' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
      data-wrn-toggle-password
      data-visible="{revealed ? 'true' : 'false'}"
    >
      {#if fields.length}
        <div class="wire-next__password-fields">
          {#each fields as field, index}
            <div class="wire-next__password-field">
              <label for="{fieldId(field, index)}">{field.label || label}</label>
              <div class="wire-next__password-control">
                <input
                  id="{fieldId(field, index)}"
                  type="{revealed ? 'text' : 'password'}"
                  name="{fieldName(field, index)}"
                  value="{field.value || ''}"
                  placeholder="{field.placeholder || placeholder}"
                  autocomplete="{field.autocomplete || autocomplete}"
                  minlength="{field.minlength || minlength}"
                  maxlength="{field.maxlength || maxlength}"
                  pattern="{field.pattern || pattern}"
                  disabled="{disabled || field.disabled}"
                  readonly="{readonly || field.readonly}"
                  required="{required || field.required}"
                  aria-invalid="{invalid ? 'true' : 'false'}"
                  @input="output.input({ name: event.target.name, value: event.target.value, visible: revealed })"
                  @change="output.change({ name: event.target.name, value: event.target.value, visible: revealed })"
                />
                {#if toggleable && toggleMode === "button"}
                  <button
                    type="button"
                    class="wire-next__password-toggle"
                    aria-label="{revealed ? hideLabel : showLabel}"
                    title="{revealed ? hideLabel : showLabel}"
                    aria-pressed="{revealed ? 'true' : 'false'}"
                    disabled="{disabled || readonly}"
                    @click="toggleVisibility(); output.toggle({ visible: revealed })"
                  >
                    <span class="wire-next__password-icon wire-next__password-icon--show" aria-hidden="true"></span>
                  </button>
                {/if}
              </div>
            </div>
          {/each}
        </div>
      {:else}
        <label for="{name}-password">{label}</label>
        <div class="wire-next__password-control">
          <input
            {...attrs}
            id="{name}-password"
            type="{revealed ? 'text' : 'password'}"
            name="{name}"
            value="{value}"
            placeholder="{placeholder}"
            autocomplete="{autocomplete}"
            minlength="{minlength}"
            maxlength="{maxlength}"
            pattern="{pattern}"
            disabled="{disabled}"
            readonly="{readonly}"
            required="{required}"
            aria-invalid="{invalid ? 'true' : 'false'}"
            aria-describedby="{validationMessage ? name + '-validation' : helpText ? name + '-help' : ''}"
            @input="output.input({ value: event.target.value, visible: revealed })"
            @change="output.change({ value: event.target.value, visible: revealed })"
          />
          {#if toggleable && toggleMode === "button"}
            <button
              type="button"
              class="wire-next__password-toggle"
              aria-label="{revealed ? hideLabel : showLabel}"
              title="{revealed ? hideLabel : showLabel}"
              aria-pressed="{revealed ? 'true' : 'false'}"
              disabled="{disabled || readonly}"
              @click="toggleVisibility(); output.toggle({ visible: revealed })"
            >
              <span class="wire-next__password-icon wire-next__password-icon--show" aria-hidden="true"></span>
            </button>
          {/if}
        </div>
      {/if}
      {#if toggleable && toggleMode === "checkbox"}
        <label class="wire-next__password-checkbox">
          <input
            type="checkbox"
            checked="{revealed}"
            disabled="{disabled || readonly}"
            @change="revealed = event.target.checked; output.toggle({ visible: revealed })"
          />
          <span>{checkboxLabel}</span>
        </label>
      {/if}
      {#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
      <small
        id="{name}-validation"
        class="wire-next__validation"
        data-error="{name}"
      >{validationMessage}</small>
    </fieldset>
    <FieldStyles hidden />
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--toggle-password {
      display: grid;
      width: 100%;
      min-width: 0;
      max-width: none;
      box-sizing: border-box;
      gap: 0.45rem;
      padding: 0;
      border: 0;
    }

    .wire-next--toggle-password > label {
      color: var(--wire-color-text);
      font-size: 0.75rem;
      font-weight: 600;
    }

    .wire-next__password-fields {
      display: grid;
      gap: 1rem;
    }

    .wire-next__password-field {
      display: grid;
      gap: 0.45rem;
    }

    .wire-next__password-field > label {
      color: var(--wire-color-text);
      font-size: 0.82em;
      font-weight: 750;
    }

    .wire-next__password-control {
      position: relative;
      display: flex;
      min-width: 0;
      align-items: center;
      width: 100%;
    }

    .wire-next__password-control > input {
      width: 100%;
      min-width: 0;
      min-height: 2.75em;
      padding: 0.65em 3em 0.65em 0.8em;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius-sm);
      outline: 0;
      color: var(--wire-color-text);
      background: var(--wire-color-bg);
      font: inherit;
      font-size: 0.8125rem;
      transition:
        border-color var(--wire-motion-base) var(--wire-ease-standard),
        box-shadow var(--wire-motion-base) var(--wire-ease-standard);
    }

    .wire-next__password-control > input:focus-visible {
      border-color: var(--wire-component-color);
      box-shadow: 0 0 0 3px color-mix(in srgb, var(--wire-component-color) 18%, transparent);
    }

    .wire-next__password-toggle {
      position: absolute;
      top: 50%;
      right: 0.45em;
      display: grid;
      width: 2em;
      height: 2em;
      padding: 0;
      place-items: center;
      border: 0;
      border-radius: calc(var(--wire-radius-sm) * 0.72);
      color: var(--wire-color-muted);
      background: transparent;
      cursor: pointer;
      transform: translateY(-50%);
    }

    .wire-next__password-toggle:hover,
    .wire-next__password-toggle:focus-visible {
      color: var(--wire-component-color);
      background: color-mix(in srgb, var(--wire-component-color) 10%, transparent);
      outline: 0;
    }

    .wire-next__password-icon {
      display: block;
      width: 1.05em;
      height: 1.05em;
      background: currentColor;
      mask-position: center;
      mask-repeat: no-repeat;
      mask-size: contain;
    }

    .wire-next__password-icon--show {
      mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.06 12.35a1 1 0 0 1 0-.7C3.7 7.6 7.64 5 12 5c4.36 0 8.3 2.6 9.94 6.65a1 1 0 0 1 0 .7C20.3 16.4 16.36 19 12 19c-4.36 0-8.3-2.6-9.94-6.65Z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E");
    }

    .wire-next__password-toggle[aria-pressed="true"]::after {
      position: absolute;
      width: 1.25em;
      height: 0.12em;
      border-radius: 999px;
      background: currentColor;
      content: "";
      transform: rotate(45deg);
      box-shadow: 0 0 0 1px var(--wire-color-bg);
    }

    .wire-next__password-checkbox {
      display: inline-flex;
      width: fit-content;
      align-items: center;
      gap: 0.55rem;
      color: var(--wire-color-muted);
      font-size: 0.78em;
      cursor: pointer;
    }

    .wire-next__password-checkbox > input {
      width: 1rem;
      height: 1rem;
      margin: 0;
      accent-color: var(--wire-component-color);
    }

    .wire-next--toggle-password > small {
      color: var(--wire-color-muted);
      font-size: 0.72em;
    }
  }
}
```

---

## Tooltip

Showcase: https://component.wrnexusjs.dev/
Mount: <Tooltip /> (legacy: data-component="Tooltip")
Category: overlays
Purpose: Show concise accessible contextual help on hover, focus, click, or controlled open state.
Props: id: string = "", open: boolean = false, defaultOpen: boolean = false, title: string = "", description: string = "", content: string = "Tooltip", trigger: string = "hover", placement: string = "top", size: string = "default", color: string = "primary", variant: string = "dark", maxWidth: string = "18rem", offset: number = 10, showArrow: boolean = true, interactive: boolean = false, disabled: boolean = false, class: string = ""
Slots: trigger, default
Events: toggle, open, close

### Complete .wrn source contract

```wrn
component Tooltip {
  outputs {
    toggle(payload: { open: boolean; reason: string; sourceEvent: Event })
    open(payload: { reason: string; sourceEvent: Event })
    close(payload: { reason: string; sourceEvent: Event })
  }

  props {
id: string = ""
    open: boolean = false
    defaultOpen: boolean = false
    title: string = ""
    description: string = ""
    content: string = "Tooltip"
    trigger: string = "hover"
    placement: string = "top"
    size: string = "default"
    color: string = "primary"
    variant: string = "dark"
    maxWidth: string = "18rem"
    offset: number = 10
    showArrow: boolean = true
    interactive: boolean = false
    disabled: boolean = false
    class: string = ""
  }

  state visible = defaultOpen

  functions {
    shared function isOpen() {
      return open || visible
    }

    client function showTooltip(reason, sourceEvent) {
      if (disabled || trigger === "manual") {
        return
      }
      visible = true
      output.open({ reason: reason, sourceEvent: sourceEvent })
      output.toggle({ open: true, reason: reason, sourceEvent: sourceEvent })
    }

    client function hideTooltip(reason, sourceEvent) {
      if (trigger === "manual") {
        return
      }
      visible = false
      output.close({ reason: reason, sourceEvent: sourceEvent })
      output.toggle({ open: false, reason: reason, sourceEvent: sourceEvent })
    }

    client function toggleTooltip(sourceEvent) {
      if (isOpen()) {
        hideTooltip("click", sourceEvent)
      } else {
        showTooltip("click", sourceEvent)
      }
    }

    client function handleKeydown(sourceEvent) {
      if (sourceEvent.key === "Escape") {
        sourceEvent.preventDefault()
        hideTooltip("escape", sourceEvent)
      }
    }
  }

  view {
    <span
      {...attrs}
      data-ui-component="Tooltip"
      data-open='{open || visible ? "true" : "false"}'
      data-trigger='{trigger}'
      data-placement='{placement}'
      data-size='{size}'
      data-color='{color}'
      data-variant='{variant}'
      data-interactive='{interactive ? "true" : "false"}'
      class='wire-tooltip {class}'
      style='--wire-tooltip-max-width:{maxWidth};--wire-tooltip-offset:{offset}px;'
      @mouseenter='if (trigger === "hover" || trigger === "both") { showTooltip("hover", event) }'
      @mouseleave='if (trigger === "hover" || trigger === "both") { hideTooltip("hover", event) }'
      @focusin='if (trigger === "focus" || trigger === "hover" || trigger === "both") { showTooltip("focus", event) }'
      @focusout='if (trigger === "focus" || trigger === "hover" || trigger === "both") { hideTooltip("focus", event) }'
      @keydown='handleKeydown(event)'
    >
      <span
        class="wire-tooltip__trigger"
        tabindex='{disabled ? "-1" : "0"}'
        aria-describedby='{(open || visible) && id ? id : ""}'
        @click='if (trigger === "click" || trigger === "both") { toggleTooltip(event) }'
      >
        <slot name="trigger"></slot>
      </span>

      <span
        id='{id}'
        class="wire-tooltip__content"
        data-wrn-anchored="true"
        data-show='{open || visible}'
        role="tooltip"
      >
        {#if showArrow}
          <span class="wire-tooltip__arrow" data-wrn-anchor-arrow="true" aria-hidden="true"></span>
        {/if}

        {#if title}
          <strong>{title}</strong>
        {/if}

        {#if description}
          <span class="wire-tooltip__description">{description}</span>
        {:else if content}
          <span class="wire-tooltip__description">{content}</span>
        {/if}

        <slot></slot>
      </span>
    </span>
  }

  style {
    .wire-tooltip {
      --tooltip-accent: var(--wire-color-primary);
      --tooltip-soft: var(--wire-color-primary-soft);
      position: relative;
      display: inline-flex;
      max-width: 100%;
    }

    .wire-tooltip[data-color="secondary"] {
      --tooltip-accent: var(--wire-color-secondary);
      --tooltip-soft: var(--wire-color-secondary-soft);
    }

    .wire-tooltip[data-color="info"] {
      --tooltip-accent: var(--wire-color-info);
      --tooltip-soft: var(--wire-color-info-soft);
    }

    .wire-tooltip[data-color="success"] {
      --tooltip-accent: var(--wire-color-success);
      --tooltip-soft: var(--wire-color-success-soft);
    }

    .wire-tooltip[data-color="warning"] {
      --tooltip-accent: var(--wire-color-warning);
      --tooltip-soft: var(--wire-color-warning-soft);
    }

    .wire-tooltip[data-color="danger"] {
      --tooltip-accent: var(--wire-color-danger);
      --tooltip-soft: var(--wire-color-danger-soft);
    }

    .wire-tooltip__trigger {
      display: inline-flex;
      max-width: 100%;
      outline: none;
    }

    .wire-tooltip__trigger:focus-visible {
      border-radius: 0.4rem;
      outline: 2px solid var(--wire-color-focus);
      outline-offset: 3px;
    }

    .wire-tooltip__content {
      position: absolute;
      z-index: 1300;
      left: 50%;
      bottom: calc(100% + var(--wire-tooltip-offset));
      display: grid;
      gap: 0.22rem;
      width: max-content;
      max-width: min(var(--wire-tooltip-max-width), calc(100vw - 1rem));
      padding: 0.58rem 0.72rem;
      color: white;
      background: color-mix(in srgb, black 88%, var(--tooltip-accent) 12%);
      border: 1px solid color-mix(in srgb, white 12%, transparent);
      border-radius: 0.65rem;
      box-shadow: 0 14px 38px color-mix(in srgb, black 28%, transparent);
      font-size: 0.73rem;
      line-height: 1.45;
      text-align: left;
      transform: translateX(-50%);
      pointer-events: none;
    }

    .wire-tooltip[data-interactive="true"] .wire-tooltip__content {
      pointer-events: auto;
    }

    .wire-tooltip[data-variant="soft"] .wire-tooltip__content {
      color: var(--wire-color-text);
      background: var(--tooltip-soft);
      border-color: color-mix(in srgb, var(--tooltip-accent) 24%, var(--wire-color-border));
    }

    .wire-tooltip[data-variant="light"] .wire-tooltip__content {
      color: var(--wire-color-text);
      background: var(--wire-color-surface-raised);
      border-color: var(--wire-color-border);
    }

    .wire-tooltip[data-variant="solid"] .wire-tooltip__content {
      color: var(--wire-color-primary-contrast);
      background: var(--tooltip-accent);
      border-color: color-mix(in srgb, white 18%, transparent);
    }

    .wire-tooltip__content strong {
      font-size: 0.75rem;
      font-weight: 650;
    }

    .wire-tooltip__description {
      color: color-mix(in srgb, currentColor 82%, transparent);
    }

    .wire-tooltip__arrow {
      position: absolute;
      left: 50%;
      bottom: -0.32rem;
      width: 0.62rem;
      height: 0.62rem;
      background: inherit;
      border-right: 1px solid color-mix(in srgb, white 10%, transparent);
      border-bottom: 1px solid color-mix(in srgb, white 10%, transparent);
      transform: translateX(-50%) rotate(45deg);
    }

    .wire-tooltip[data-placement="bottom"] .wire-tooltip__content {
      top: calc(100% + var(--wire-tooltip-offset));
      bottom: auto;
    }

    .wire-tooltip[data-placement="bottom"] .wire-tooltip__arrow {
      top: -0.32rem;
      bottom: auto;
      transform: translateX(-50%) rotate(225deg);
    }

    .wire-tooltip[data-placement="left"] .wire-tooltip__content {
      top: 50%;
      right: calc(100% + var(--wire-tooltip-offset));
      bottom: auto;
      left: auto;
      transform: translateY(-50%);
    }

    .wire-tooltip[data-placement="left"] .wire-tooltip__arrow {
      top: 50%;
      right: -0.32rem;
      bottom: auto;
      left: auto;
      transform: translateY(-50%) rotate(-45deg);
    }

    .wire-tooltip[data-placement="right"] .wire-tooltip__content {
      top: 50%;
      bottom: auto;
      left: calc(100% + var(--wire-tooltip-offset));
      transform: translateY(-50%);
    }

    .wire-tooltip[data-placement="right"] .wire-tooltip__arrow {
      top: 50%;
      bottom: auto;
      left: -0.32rem;
      transform: translateY(-50%) rotate(135deg);
    }

    .wire-tooltip[data-placement="top-start"] .wire-tooltip__content,
    .wire-tooltip[data-placement="bottom-start"] .wire-tooltip__content {
      left: 0;
      transform: none;
    }

    .wire-tooltip[data-placement="top-start"] .wire-tooltip__arrow,
    .wire-tooltip[data-placement="bottom-start"] .wire-tooltip__arrow {
      left: 1rem;
      transform: rotate(45deg);
    }

    .wire-tooltip[data-placement="bottom-start"] .wire-tooltip__arrow {
      transform: rotate(225deg);
    }

    .wire-tooltip[data-placement="top-end"] .wire-tooltip__content,
    .wire-tooltip[data-placement="bottom-end"] .wire-tooltip__content {
      right: 0;
      left: auto;
      transform: none;
    }

    .wire-tooltip[data-placement="top-end"] .wire-tooltip__arrow,
    .wire-tooltip[data-placement="bottom-end"] .wire-tooltip__arrow {
      right: 1rem;
      left: auto;
      transform: rotate(45deg);
    }

    .wire-tooltip[data-placement="bottom-end"] .wire-tooltip__arrow {
      transform: rotate(225deg);
    }

    .wire-tooltip[data-size="sm"] .wire-tooltip__content {
      padding: 0.45rem 0.58rem;
      font-size: 0.68rem;
    }

    .wire-tooltip[data-size="lg"] .wire-tooltip__content {
      padding: 0.72rem 0.85rem;
      font-size: 0.8rem;
    }

    @media (max-width: 639px) {
      .wire-tooltip__content {
        position: fixed;
        right: 0.75rem;
        bottom: 0.75rem;
        left: 0.75rem;
        top: auto;
        width: auto;
        max-width: none;
        transform: none;
      }

      .wire-tooltip__arrow {
        display: none;
      }
    }
  }
}
```

---

## TreeView

Showcase: https://component.wrnexusjs.dev/
Mount: <TreeView /> (legacy: data-component="TreeView")
Category: base
Purpose: Theme-aware, responsive tree view component.
Props: title: string = "Tree View", description: string = "", items: unknown[] = [], valueKey: string = "value", labelKey: string = "label", defaultExpanded: unknown[] = [], selected: string = "", size: string = "default", color: string = "primary", class: string = ""
Slots: default
Events: select, toggle, expand, collapse

### Complete .wrn source contract

```wrn
component TreeView {
  outputs {
    select(payload: { item: object; value: string; sourceEvent?: Event })
    toggle(payload: { item: object; value: string; expanded: boolean; sourceEvent?: Event })
    expand(payload: { item: object; value: string; sourceEvent?: Event })
    collapse(payload: { item: object; value: string; sourceEvent?: Event })
  }

  props {
    title: string = "Tree View"
    description: string = ""
    items: unknown[] = []
    valueKey: string = "value"
    labelKey: string = "label"
    defaultExpanded: unknown[] = []
    selected: string = ""
    size: string = "default"
    color: string = "primary"
    class: string = ""
  }

  state expandedValues = defaultExpanded
  state selectedValue = selected

  functions {
    shared function nodeValue(item, index, parent) { return String(item[valueKey] || item.id || parent + index) }
    shared function isExpanded(value) { return expandedValues.includes(value) }

    client function toggleNode(item, value, sourceEvent, next, expanded) {
      next = expandedValues.slice()
      expanded = !next.includes(value)
      if (expanded) {
        next.push(value)
      } else {
        next = next.filter(function (entry) { return entry !== value })
      }
      expandedValues = next
      output.toggle({ item: item, value: value, expanded: expanded, sourceEvent: sourceEvent })
      if (expanded) {
        output.expand({ item: item, value: value, sourceEvent: sourceEvent })
      } else {
        output.collapse({ item: item, value: value, sourceEvent: sourceEvent })
      }
    }

    client function selectNode(item, value, sourceEvent) {
      selectedValue = value
      output.select({ item: item, value: value, sourceEvent: sourceEvent })
    }
  }

  view {
    <section {...attrs} data-ui-component="TreeView" class='wire-tree-view {class}'>
      {#if title}<strong class="wire-tree-view__title">{title}</strong>{/if}
      {#if description}<p class="wire-tree-view__description">{description}</p>{/if}
      <div class="wire-tree-view__tree" role="tree" aria-label='{title}' data-wrn-roving="vertical">
        {#each items as item, index}
          <div class="wire-tree-view__branch" role="none">
            <div class="wire-tree-view__row" role="treeitem" aria-level="1" aria-expanded='{item.children && item.children.length ? isExpanded(nodeValue(item, index, "root-")) : ""}' aria-selected='{selectedValue === nodeValue(item, index, "root-") ? "true" : "false"}'>
              {#if item.children && item.children.length}
                <button type="button" class="wire-tree-view__toggle" aria-label='{isExpanded(nodeValue(item, index, "root-")) ? "Collapse " + item[labelKey] : "Expand " + item[labelKey]}' @click='toggleNode(item, nodeValue(item, index, "root-"), event)'><span class="icon-[lucide--chevron-right]" aria-hidden="true"></span></button>
              {:else}<span class="wire-tree-view__toggle" aria-hidden="true"></span>{/if}
              <button type="button" class="wire-tree-view__node" data-wrn-roving-item @click='selectNode(item, nodeValue(item, index, "root-"), event)'>
                {#if item.icon}<span class='{item.icon}' aria-hidden="true"></span>{/if}<span>{item[labelKey]}</span>
              </button>
            </div>
            {#if item.children && item.children.length}
              <div class="wire-tree-view__group" role="group" data-show='isExpanded(nodeValue(item, index, "root-"))'>
                {#each item.children as child, childIndex}
                  <div class="wire-tree-view__row" role="treeitem" aria-level="2" aria-selected='{selectedValue === nodeValue(child, childIndex, nodeValue(item, index, "root-") + "-") ? "true" : "false"}'>
                    <span class="wire-tree-view__toggle" aria-hidden="true"></span>
                    <button type="button" class="wire-tree-view__node" data-wrn-roving-item @click='selectNode(child, nodeValue(child, childIndex, nodeValue(item, index, "root-") + "-"), event)'>
                      {#if child.icon}<span class='{child.icon}' aria-hidden="true"></span>{/if}<span>{child[labelKey]}</span>
                    </button>
                  </div>
                {/each}
              </div>
            {/if}
          </div>
        {:empty}<p class="wire-tree-view__empty">No tree items available.</p>{/each}
      </div>
      <slot />
    </section>
  }

  style {
    .wire-tree-view { display: grid; gap: 0.5rem; color: var(--wire-color-text); }
    .wire-tree-view__title { font-size: 0.95rem; }
    .wire-tree-view__description, .wire-tree-view__empty { margin: 0; color: var(--wire-color-muted); font-size: 0.8rem; }
    .wire-tree-view__tree { display: grid; gap: 0.15rem; padding: 0.4rem; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius); background: var(--wire-color-surface); }
    .wire-tree-view__row { display: flex; min-width: 0; align-items: center; gap: 0.2rem; border-radius: var(--wire-radius-sm); }
    .wire-tree-view__row[aria-selected="true"] { background: var(--wire-color-primary-soft); }
    .wire-tree-view__toggle { display: inline-grid; width: 1.8rem; height: 1.8rem; flex: none; padding: 0; place-items: center; border: 0; border-radius: 0.35rem; color: var(--wire-color-muted); background: transparent; }
    button.wire-tree-view__toggle { cursor: pointer; }
    .wire-tree-view__toggle > span { width: 0.9rem; height: 0.9rem; transition: transform var(--wire-motion-fast); }
    .wire-tree-view__row[aria-expanded="true"] > .wire-tree-view__toggle > span { transform: rotate(90deg); }
    .wire-tree-view__node { display: flex; min-width: 0; flex: 1; align-items: center; gap: 0.45rem; padding: 0.45rem 0.5rem; border: 0; border-radius: var(--wire-radius-sm); color: inherit; background: transparent; font: inherit; text-align: left; cursor: pointer; }
    .wire-tree-view__node:hover, button.wire-tree-view__toggle:hover { background: var(--wire-color-surface-2); }
    .wire-tree-view__node:focus-visible, button.wire-tree-view__toggle:focus-visible { outline: 2px solid var(--wire-color-focus); outline-offset: -2px; }
    .wire-tree-view__node > [class*="icon-"] { width: 1rem; height: 1rem; flex: none; color: var(--wire-color-primary); }
    .wire-tree-view__group { display: grid; margin-left: 1.8rem; padding-left: 0.45rem; border-left: 1px solid var(--wire-color-border); }
  }
}
```

---

## Typography

Showcase: https://component.wrnexusjs.dev/
Mount: <Typography /> (legacy: data-component="Typography")
Category: layout
Purpose: Apply consistent readable responsive typography, widths, columns, spacing, and editorial hierarchy.
Props: size: string = "default", color: string = "primary", columns: number = 2, gap: string = "md", maxWidth: string = "xl", class: string = ""
Slots: default
Events: none

### Complete .wrn source contract

```wrn
component Typography {
  props {
    size: string = "default"
    color: string = "primary"
    columns: number = 2
    gap: string = "md"
    maxWidth: string = "xl"
    class: string = ""
  }

  view {
    <div
      data-ui-component="Typography"
      class='wire-typography {class}'
      data-size='{size}'
      data-columns='{columns}'
      data-gap='{gap}'
      data-max-width='{maxWidth}'
    >
      <slot></slot>
    </div>
  }

  style {
    .wire-typography {
      color: var(--wire-color-text);
      font-size: 1rem;
    }

    .wire-typography[data-size="sm"] {
      font-size: 0.875rem;
    }

    .wire-typography[data-size="lg"] {
      font-size: 1.125rem;
    }

    .wire-typography[data-max-width="md"] {
      max-width: 48rem;
    }

    .wire-typography[data-max-width="lg"] {
      max-width: 64rem;
    }

    .wire-typography[data-max-width="xl"] {
      max-width: 80rem;
    }

    .wire-typography[data-max-width="full"] {
      max-width: none;
    }

    /*
     * Multi-column prose only below a comfortable reading width; a narrow
     * screen split into columns is unreadable.
     */
    .wire-typography[data-gap="sm"] {
      column-gap: 1rem;
    }

    .wire-typography[data-gap="md"] {
      column-gap: 2rem;
    }

    .wire-typography[data-gap="lg"] {
      column-gap: 3rem;
    }

    @media (min-width: 768px) {
      .wire-typography[data-columns="2"] {
        column-count: 2;
      }
    }

    @media (min-width: 1024px) {
      .wire-typography[data-columns="3"] {
        column-count: 3;
      }
    }

    .wire-typography :where(h1, h2, h3, h4) {
      color: var(--wire-color-text);
      font-weight: 700;
      letter-spacing: -0.02em;
      line-height: 1.2;
      break-after: avoid;
    }

    .wire-typography :where(h1) { font-size: clamp(2rem, 4vw, 3.5rem); margin: 0 0 1.5rem; }
    .wire-typography :where(h2) { font-size: clamp(1.5rem, 3vw, 2.25rem); margin: 2.5rem 0 1rem; }
    .wire-typography :where(h3) { font-size: 1.35rem; margin: 2rem 0 0.75rem; }
    .wire-typography :where(p, ul, ol, blockquote, pre, table) { margin: 1rem 0; }
    .wire-typography :where(p, li) { color: var(--wire-color-text-muted); line-height: 1.8; }
    .wire-typography :where(a) { color: var(--wire-color-primary); font-weight: 600; text-underline-offset: 0.2em; }
    .wire-typography :where(a:hover) { color: var(--wire-color-primary-hover); text-decoration: underline; }
    .wire-typography :where(ul, ol) { padding-left: 1.4rem; }
    .wire-typography :where(ul) { list-style: disc; }
    .wire-typography :where(ol) { list-style: decimal; }
    .wire-typography :where(blockquote) {
      border-left: 4px solid var(--wire-color-primary);
      background: var(--wire-color-primary-soft);
      border-radius: 0 0.75rem 0.75rem 0;
      padding: 1rem 1.25rem;
      color: var(--wire-color-text);
    }
    .wire-typography :where(code) {
      border-radius: 0.35rem;
      background: var(--wire-color-surface-soft);
      padding: 0.15rem 0.35rem;
      font-size: 0.9em;
    }
    .wire-typography :where(pre) {
      overflow-x: auto;
      border: 1px solid var(--wire-color-border);
      border-radius: 1rem;
      background: var(--wire-color-surface-raised);
      padding: 1rem;
    }
    .wire-typography :where(img) { border-radius: 1rem; }
    .wire-typography :where(hr) { border-color: var(--wire-color-border); margin: 2rem 0; }
  }
}
```

---

## WysiwygEditor

Showcase: https://component.wrnexusjs.dev/
Mount: <WysiwygEditor /> (legacy: data-component="WysiwygEditor")
Category: integrations
Purpose: Theme-aware, responsive wysiwyg editor component.
Props: size: string = "default", color: string = "primary", title: string = "Wysiwyg Editor", description: string = "", items: unknown[] = [], variant: string = "default", class: string = ""
Slots: default
Events: input, change, focus, blur

### Complete .wrn source contract

```wrn
import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"

component WysiwygEditor {
  outputs {
    input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
    focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
  }

  props {
    size: string = "default"
    color: string = "primary"
    title: string = "Wysiwyg Editor"
    description: string = ""
    items: unknown[] = []
    variant: string = "default"
    class: string = ""
  }
  view {
    <section class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--wysiwyg-editor wire-next--variant-{variant} {class}">
      {#if title}<strong>{title}</strong>{/if}
      {#if description}<p>{description}</p>{/if}
      {#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
      <slot />
    </section>
    <ComponentBaseStyles hidden />
  }

  style {
    .wire-next--wysiwyg-editor {
      display: grid;
      gap: 0.75rem;
      padding: 1rem;
      border: 1px solid var(--wire-color-border);
      border-radius: var(--wire-radius);
      color: var(--wire-color-text);
      background: var(--wire-color-surface);
    }
    .wire-next--wysiwyg-editor > p { margin: 0; color: var(--wire-color-muted); }
    .wire-next--wysiwyg-editor > .wire-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
    .wire-next--wysiwyg-editor > .wire-next__items > span { padding: 0.35rem 0.6rem; border-radius: var(--wire-radius-sm); background: var(--wire-color-surface-2); }
  }
}
```
