Skip to content

UI Engine

Claude edited this page Sep 7, 2026 · 3 revisions

The UI Engine

Nirdosha ships a declarative UI DSL plus a UI generator that derives a full CRUD + dashboard web application from a program's struct declarations and its function-naming conventions — no UI syntax needed for the common case.

Zero-syntax inference

compiler/src/ui_gen.rs looks for list_<struct>, create_<struct>, update_<struct>, delete_<struct>, get_<struct>, and stat_<name>/chart_<name> functions and generates a complete HTML/JS CRUD app + dashboard from them alone.

Optional screen / dashboard blocks

For what a naming convention can't express (a friendlier title, a relabeled field, a custom action), there is an additive DSL:

struct Product {
    id: i64,
    name: str,
    price_cents: i64,
    stock: i64,
}

fn list_product() -> Result(json, str) { ... }
fn create_product(p: Product) -> Result(i64, str) requires(role: "admin") { ... }
fn restock_product(id: i64) -> Result(i64, str) requires(role: "admin") { ... }

screen Product {
    title: "Catalog"
    field name { label: "Product Name" pattern: "^[A-Za-z0-9 ]+$" }
    field stock { min: 0 }
    action "Restock +10" -> restock_product {
        style: "outlined"
        confirm: "Restock this product by 10 units?"
    }
}

dashboard {
    tile "Products" -> stat_product_count
    chart "By Price" -> chart_products_by_price
}
  • screen/dashboard are real reserved keywords (top-level items like struct/fn); field/action/tile/chart are contextual keywords.
  • Typechecked: screen <Name> must name a real struct; field/action targets must resolve; view/edit must be role(...)/claim(...); pattern must compile as a regex and only apply to a str field; format must be one of "email"/"phone"/"date"/"url"/"uuid"; min/max must only apply to a numeric field.
  • Inert to native codegennirdosha build compiles a program containing screen/dashboard cleanly (codegen never inspects them). They're consumed only by nirdosha emit-ui/nirdosha serve.
  • view/edit (role/claim visibility) and pattern/format/min/max (format validation) are enforced for real, both client- and server-side — serve.rs is the actual security/validation boundary (redact_gated_fields, check_edit_gates, check_field_validations); the client-side version is cosmetic convenience only.
  • Tracked-but-not-wired (see compiler/UI_DSL_TODO.md): paginate, searchable/sortable as DSL keysnirdosha serve --db already provides real sorting/search/pagination unconditionally per struct via its own table route, independent of these two keys.
  • Deliberately closed, not a fixed ceiling: four built-in chart shapes (inline-SVG bar_chart/graph/heatmap/timeline, no Recharts/D3/ Victory dependency) are joined by render: "chart" — a bounded grammar-of-graphics config, not a fifth hardcoded shape — and, if that still isn't enough, a Rust crate can contribute an entirely new layout widget kind. See "Extending the catalog" below. Four fixed built-in animations (fade-in/slide-up/scale-in/pop, no custom transitions or Framer-Motion-style gesture/physics motion) and a fixed seven-kind form-control set (text/number/checkbox/select/struct/ readonly/date — no rich text editor, color picker, drag-drop upload preview, autocomplete, calendar/scheduler, or signature pad) remain genuinely closed, not yet extensible the same way. See crates/compiler/UI_DSL_TODO.md's "Deliberate non-goals" section for the full rationale.

Extending the catalog

Two ways to grow the chart/component vocabulary above, both still fully closed and typechecked — no arbitrary markup, no runtime registration, nothing an agent or a served request can add on its own.

render: "chart" — a grammar-of-graphics config (mark × encode <channel>) for chart shapes the four fixed ones don't cover, additive alongside them (also wired into workspace panel { ... }, same grammar):

dashboard {
    visual "Revenue by month" -> chart_revenue_by_month {
        render: "chart"
        mark: "bar"
        encode x { field: "month" type: "temporal" }
        encode y { field: "amount" type: "quantitative" aggregate: "sum" }
    }
}

A Rust crate contributing a new layout widget kind — for something no chart shape covers at all. nirdosha emit-ui --manifest-path <Cargo.toml> (or an auto-detected Cargo.toml sitting next to the .nir file) discovers any dependency tagged [package.metadata.nirdosha] kind = "nir-ui-component" in the app's own Cargo.toml and links its JS automatically — no hand-written Rust glue needed for the common case:

layout {
    sparkline { source: recent_sales_totals field: "amount" }
}

Both resolve entirely at nirdosha build/emit-ui time — before the compiled artifact exists, let alone before an agent ever talks to it. There's no admin API, no hot-reload, no runtime negotiation that adds or changes a component after that point: growing the catalog is a human adding a reviewed Cargo dependency once, never something reachable from a served request or a chat turn. See rfcs/0009 for the full design, the real reference plugin crate (crates/ui-plugin-example-sparkline), and exactly what's shipped vs. still open — widening a component past a single layout widget kind to a full typed catalog entry, and Cargo-driven discovery for native builtins (a different, harder problem), both remain future work.

Design tokens: --theme

nirdosha emit-ui/serve --theme theme.json layers a full design system on the baked-in Material Design 3 defaults — brand/neutral color ramps, fonts, radius, shadow, density, real entrance/hover/press animations, three dark-mode strategies, and CSS-only layout shell variants (LANGUAGE.md §11b). Every section is optional; a program with no --theme renders exactly as before this existed. nirdosha serve re-reads the file on a TTL, so a redeployed theme takes effect without restarting the server.

Serving

nirdosha serve <file.nir> runs a tiny_http server exposing the inferred functions as a JSON API (POST /api/<fn>), with optional OIDC JWKS/issuer/audience gating — the same identity primitives as Language Features, applied to HTTP.

Why this matters for an agent

The zero-syntax path means an LLM asked for "a CRUD app over Product" doesn't need to also generate any UI code, HTML, or client-side validation logic — it writes struct Product and five naming-convention functions, and a working, role-gated, themeable web app falls out. The demo referenced in the README (examples/vendor_ops.nir, 334 lines, zero UI code) is the concrete proof: a themed dashboard, a sortable/searchable table, and a role-gated approval action, all derived, all enforced server-side — not generated client JS that a curious user could bypass by reading the page source.

Clone this wiki locally