Files
WRNexusJS/docs/plans/2026-07-02-wire-ui-foundation-design.md
T
2026-07-12 15:55:18 +05:30

5.0 KiB

WrNexus batteries-included: design & roadmap

Goal: let users start faster with a built-in UI library, theming, i18n, and validation. Built foundation-first so each subsystem lands solid and tested.

Order (one subsystem per turn):

  1. Theme system done — design tokens, SSR + client, no-flash, overridable.
  2. Wire UI (@wrnexus/ui) done — 17 components + layout primitives as .wrn, consuming theme tokens, overridable 4 ways; auto-discovered + wrnexus eject.
  3. Layout system done (folded into turn 2) — layout primitives (Container, Stack, HStack, Grid, Divider, Spacer) + page layouts (app/layout.wrn + <slot>).
  4. Validation done — one schema for both form (client) and API (server).
  5. i18n done — translations for pages and API responses.

4. i18n (done)

  • @wrnexus/i18n: locales in app/locales/<lang>.json (nested/flat keys, {param} interpolation). Per request the lang resolves from the wire-lang cookie → Accept-Language → config default (wrnexus.config.ts i18n.default).
  • Server: ctx.lang + ctx.t(key, params) on every request (pages + API).
  • Views: {t:key} text sugar compiles to a <span data-t="key"> marker; t:<attr>="key" translates an attribute. translateHtml resolves both on the final HTML (after components + layout), and <html lang> is set (no flash).
  • Switch: [data-wire-lang-set="es"] (or <select data-wire-lang>) → the tiny /__wrnexus/i18n.js sets the cookie + reloads; injected only when present.
  • Fallback chain: current lang → default lang → the key itself.

All four subsystems + the layout system are complete.

3. Validation (done)

  • @wrnexus/validation: fluent v builder (v.object({ email: v.string().email(), password: v.string().min(8) })). .parse(data) runs server-side (coerce + errors); .describe() emits a JSON descriptor. Shared checkField/applyRule back both.
  • Define once in app/schemas/<name>.ts. API: import login from "../schemas/login"; parseBody(login, ctx.req) → 400 {errors} or {value}. Form: <form data-schema="login"> with [data-error="field"] spans.
  • The framework bakes all descriptors into /__wrnexus/schemas.js (window.__wireSchemas) and ships an eval-free /__wrnexus/validate.js that validates form[data-schema] on submit/blur — injected only when a page has data-schema.

2 + 5. Wire UI + layout (done)

  • Distribution: @wrnexus/ui ships .wrn components under components/; the framework auto-discovers them (router componentDirs), app/components shadow by name, and wrnexus eject <name> copies one into the app to own it.
  • Codegen: components now bake prop-driven text/attributes server-side (__wireHtml/__wireAttr) so static components ship zero JS; state-referencing text stays a reactive mustache. Reserved-word props (class) get __p_ refs; attribute {expr} enables variant/size/class composition.
  • Slots: renderComponents is nesting-aware and fills <slot> with mount children — this also powers page layouts (app/layout.wrn wraps every page).
  • Styles: one themed stylesheet /__wrnexus/ui.css (all .wire-* classes use var(--wire-*)), linked theme → ui → app so app CSS overrides win.

1. Theme system (this turn)

Tokens. Kebab-case token keys become --wire-<key> CSS custom properties. Framework ships default light + dark token sets (colors, radius, etc.); user config deep-merges over them and may add new themes.

Config (wrnexus.config.ts):

theme: {
  default: "dark",
  themes: {
    light: { "color-primary": "#2563eb", "radius": "8px", ... },
    dark:  { "color-primary": "#6c8cff", ... },
  },
}

Generated CSS (/__wrnexus/theme.css), linked first in <head> so global.css and component styles can read/override it:

:root{ /* default theme tokens */ }
[data-theme="light"]{ --wire-color-primary:#2563eb; color-scheme:light; ... }
[data-theme="dark"]{ --wire-color-primary:#6c8cff; color-scheme:dark; ... }

SSR (no flash). Server reads the wire-theme cookie (falls back to config default) and renders <html data-theme="…">, so the correct theme paints on the first byte. The theme name is validated against configured names.

Client (/__wrnexus/theme.js, injected only when a page has a toggle): window.wireTheme.{get,set,toggle} — sets data-theme on <html>, persists the wire-theme cookie, and binds [data-wire-theme-toggle] / [data-wire-theme-set="name"] elements so a .wrn component can switch themes without writing JS.

Override. Users change tokens in config, or redefine any --wire-* variable in their own CSS (loaded after theme.css → wins). Wire UI components consume var(--wire-*), so overriding a token restyles every component at once.

Integration points: @wrnexus/styles (theme module), @wrnexus/ssr (renderDocument html attrs), dev runtime/assets, prod server, wrnexus build (emits dist/theme.css + dist/theme.js, hashed into the asset version).