Object props never worked in generated demos, and my two earlier attempts each
traded one failure for another:
- a bare {...} attribute is read by the compiler as an interpolation, so it
parsed JSON as JavaScript and the page 500ed
- parenthesising it compiled, but prop coercion runs JSON.parse on the raw
attribute, so ({...}) threw and every demo rendered empty and silent
- entity-escaping the braces did not help either: the compiler hands the
attribute over without decoding, so JSON.parse still failed
They are now hoisted into page state and bound, which is what the playground
has always done. The state initialiser uses JSON.parse rather than an object
literal because the parser reads a leading brace as the start of a block.
Navbar gains a real profile: a brand, links, a two-column dropdown panel and
calls to action, instead of the generic scaffold samples that made every demo
look identical and showed no dropdown at all.
MegaMenu closed while the pointer travelled to it. The panel sits below the
trigger and that offset belongs to neither element, so crossing it fired
mouseleave on the root. A descendant now covers the gap.
Scrollspy could not be exercised at all: its links pointed at ids that did not
exist on the page. The demo now ships real sections, in page flow because the
runtime observes against the viewport.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wrnexus/ui
First-party Wire UI component library — a set of themeable
.wrncomponents 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
bun add @wrnexus/ui
Private package — the machine must be authenticated to the
wrnexusnpm 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).
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. 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:
"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.
Tailwind and motion
Components use static Tailwind utility classes alongside the shared .wire-*
layer. If the package is consumed by a separate Tailwind build, include its
component sources so every utility is generated:
@import "tailwindcss";
@source "../node_modules/@wrnexus/ui/components/*.wrn";
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:
.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:
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:
<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:
- Theme tokens — override CSS custom properties such as
--wire-color-primary,--wire-color-surface,--wire-radius-sm, etc. Every component style resolves throughvar(--wire-*), so changing a token restyles everything instantly (including across theme switches). - App CSS — redefine a
.wire-*class in your own stylesheet, which is loaded afterui.cssand therefore wins. classprop — pass aclassprop to a component; it is appended to the component's root element, letting you add per-instance classes without touching the base styles.wrnexus eject <name>— copy the component's.wrnsource into yourapp/components, where (because app components shadow library ones) you fully own and can edit it. UseuiComponentNames()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(viacomponentDirs) and served by@wrnexus/dev-server/ built by@wrnexus/cli. - Depends on
@wrnexus/core(dependencies). theme-togglerelies on the framework's theme runtime, which binds thedata-wire-theme-toggleattribute — no per-component JS is required.