geoffjay/nemo · Archived

nemo-xml-reference

Complete configuration reference for Nemo applications including all component types, properties, data sources, expressions, bindings, and templates. Use when writing or debugging Nemo config files (app.nemo SFC entries and imported .nemo components).

First seen Jul 15, 2026

Installation

$ npx skills add geoffjay/nemo --skill nemo-xml-reference

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from geoffjay/nemo.

npx skills add geoffjay/nemo

Browse all from geoffjay/nemo

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 1
License LICENSE-APACHE
Default branch main
Open issues 59
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 32,225 B
  • docs SUMMARY.md 277 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 5 installs

SKILL.md

Nemo Configuration Reference

Use this skill when writing, modifying, or debugging Nemo configuration files. The application entry is an app.nemo SFC. Legacy app.xml entries are no longer supported — the loader rejects them. In app.nemo the layout tree lives in a <template name="app"> body and app-level blocks (<app>, <data>, <imports>, <variable>, <templates>, <include>) appear at the top level (no <nemo> wrapper). XML remains valid only inside <include> fragments.

Document Structure

<nemo>
  <themes>                                   <!-- optional: register project theme files -->
    <theme-set src="themes/aurora.json" />
  </themes>

  <app title="App Title">
    <window title="Window Title" width="1200" height="800"
            min-width="400" min-height="300">
      <header-bar github-url="https://..." theme-toggle="true" />
    </window>
    <theme name="kanagawa" mode="dark" />
  </app>

  <variable name="key" type="string" default="value" />
  <script src="./scripts" on-load="init_handler" />
  <template name="card"> ... </template>

  <imports>                                  <!-- optional: single-file components (.nemo) -->
    <import src="./components/card.nemo" />
  </imports>
  <components dir="./components" />           <!-- or auto-discover every *.nemo in a dir -->

  <data>
    <source name="..." type="..." ... />
    <sink name="..." type="..." ... />
  </data>

  <plugin name="..." path="./plugins/..." />

  <layout type="stack">
    <!-- Component tree -->
  </layout>
</nemo>

Top-level blocks are order-tolerant. <themes>, <imports>/<components>, and routing primitives are documented in their own sections below.

Themes

<theme name="…" mode="…"> under <app> selects a theme set. The seven built-in sets (match on the set name, case-insensitive):

name Description
kanagawa Warm, muted palette inspired by Japanese art
kanagawa-dragon Darker Kanagawa variant
tokyo-night Cool, modern dark theme
nord Arctic-inspired pastel scheme
catppuccin Soft pastel theme
catppuccin-macchiato Warmer Catppuccin variant
gruvbox Retro warm palette with high contrast

Modes: dark, light (each set supplies the variants it defines).

Project-defined themes

Ship your own theme sets and register them with a top-level <themes> block. Each <theme-set src="…"> points at a JSON file using the same schema as the built-ins (copy a shipped crates/nemo/src/theme/*.json and edit its colors). Registered sets are then selectable by name and appear in the settings picker:

<themes>
  <theme-set src="themes/aurora.json" />
</themes>
<app>
  <theme name="aurora" mode="dark" />
</app>

Color overrides (<extend>)

Override individual colors on top of the selected theme (overrides win over the base). Keys are dotted color roles; values are hex or a theme.* reference:

<theme name="aurora" mode="dark">
  <extend>
    <color key="primary.background" value="#ff7a45" />
    <color key="primary.hover.background" value="#ff9466" />
  </extend>
</theme>

Expression Syntax

Expressions use ${...} in attribute values:

Pattern Example Description
${var.name} ${var.api_url} Variable reference
${env.KEY} ${env.API_TOKEN} Environment variable
${upper(expr)} ${upper(var.name)} Uppercase
${lower(expr)} ${lower(var.name)} Lowercase
${trim(expr)} ${trim(var.input)} Trim whitespace
${length(expr)} ${length(var.list)} Length
${coalesce(a, b)} ${coalesce(var.custom, var.default)} First non-null
${env(KEY)} ${env("HOME")} Env var function
String interpolation Hello, ${var.user}! Inline in strings

Data Sources

<data>
  <source name="ticker" type="timer" interval="1" />                 <!-- tick every 1 second -->
  <source name="api" type="http" url="https://api.example.com" interval="30" />  <!-- poll every 30 seconds -->
  <source name="live" type="websocket" url="ws://localhost:8080" />
  <source name="events" type="mqtt" url="mqtt://localhost:1883" topic="sensors/#" />
  <source name="cache" type="redis" url="redis://localhost:6379" channel="updates" />
  <source name="msgs" type="nats" url="nats://localhost:4222" subject="data.>" />
  <source name="conf" type="file" path="./data.json" watch="true" />
</data>

interval is in SECONDS, not milliseconds. create_source reads it via
Duration::from_secs(...) (crates/nemo-data/src/sources/mod.rs), so
interval="1000" means poll every ~17 minutes — data will appear to never
load. Use interval="1" for a 1-second timer, interval="30" for a 30-second
poll. Only interval is honored — the refresh attribute seen in some older
examples is not wired to any source and is silently ignored.

Data Bindings

Connect data source paths to component properties. A source object is delivered whole to the binding; transform (optional) reshapes it before it reaches the target property.

<!-- Whole array / nested subtree -> table/chart/list: target="data", NO transform.
     A dot-path in `source` selects the subtree. -->
<table id="agents">
  <binding source="data.agents" target="data" />
</table>
<table id="nodes">
  <binding source="data.cluster.nodes" target="data" />   <!-- nested source path is fine -->
</table>

<!-- Scalar field of a source object -> label/text: name the field in `transform`. -->
<label id="mode">
  <binding source="data.node" target="text" transform="mode" />        <!-- data.node.mode -->
</label>
<label id="temp">
  <binding source="data.sensors" target="text" transform="payload.temperature" />  <!-- nested field -->
</label>

What transform actually does

The transform string is applied by apply_transform (crates/nemo-layout/src/binding.rs) and supports exactly two forms:

  1. Field extraction — a dot-path with no spaces and no literal value

(e.g. transform="origin", transform="payload.temp") walks into the incoming Object and returns that nested field. A missing field passes the original value through unchanged.

  1. String templating — any transform containing the word value

(e.g. transform="Temperature: value°C") stringifies the incoming data and substitutes it for value, yielding a formatted string.

There is no select:..., filter:..., or other prefixed transform syntax on bindings, and the transform attribute does not call Rhai functions — only the two forms above work. (Rust-level pipeline transforms like select exist in nemo-data but are not reachable from the XML transform attribute.)

bind-<prop> shorthand

Instead of a <binding> child, any bind-<property> attribute creates a one-way binding to that property (crates/nemo/src/runtime.rs):

<text id="raw" content="waiting…" bind-content="data.api" />
<label id="t" bind-text="data.sensors.payload.temperature" />

bind-content, bind-text, bind-value, etc. all follow the bind-<prop> pattern; the attribute value is the source path (no transform).

Event Handlers

Reference Rhai function names:

<button id="save" label="Save" on-click="handle_save" />
<input id="search" on-change="handle_search_change" />

The <script> element accepts an on-load attribute naming a Rhai function run once at startup:

<script src="./scripts" on-load="init_handler" />

on-load is the app-level lifecycle hook; <route> elements additionally expose per-route on-enter / on-leave hooks (see Routing below). All are invoked with the same (componentid, eventdata) signature.

Handler signature: every handler takes (componentid, eventdata)

Nemo invokes every XML-referenced handler — on-click, on-change, on-load, … — with exactly two string arguments, (componentid, eventdata). Rhai resolves functions by name and arity, so a zero-parameter handler fails at runtime with Function not found: <name>. Always write:

fn init_handler(component_id, event_data) { ... }

on-load is no exception: it is dispatched as callhandler(handler, "app", "load") (crates/nemo/src/app.rs), so componentid is "app" and event_data is "load".

Single-File Components (.nemo SFCs)

Package a reusable piece of UI into one .nemo file (template + optional scoped script/style), then import it and use it as a custom tag — Vue-like. An SFC is expanded onto the template machinery, so by render time it is ordinary built-in components.

Importing

<imports>
  <import src="./components/card.nemo" />            <!-- tag from <template name> or filename -->
  <import src="./components/button.nemo" as="my-btn" />  <!-- as= overrides the tag -->
  <import src="github.com/user/nemo-widgets" as="nw" />  <!-- remote library (see Build below) -->
</imports>

<components dir="./components" />                     <!-- OR auto-discover every *.nemo in a dir -->

The tag is resolved as= > <template name> > filename stem, then kebab→snake normalized (write <labeled-button>; it matches internally as labeled_button). A remote src (a github.com/… module path) resolves against the fetched package cache and brings in all the package's exported tags; as= becomes a tag-name prefix.

Authoring a .nemo file

A .nemo file is not wrapped in <nemo>. Its top-level children:

  • <props> — optional. Declares typed props with defaults; instance attributes

override them.

  • <template> — required, exactly one root element. Use ${prop}

placeholders for interpolation and <slot /> for consumer content.

  • <style> — optional scoped CSS subset (folded onto template nodes at compile

time; there is no runtime cascade).

  • <script> — optional Rhai, loaded under the id sfc:<tag>.
<!-- components/labeled-button.nemo -->
<props>
  <prop name="label" type="string" default="Button" />
  <prop name="variant" type="string" default="primary" />
</props>
<template name="labeled-button">
  <button label="${label}" variant="${variant}" on-click="handleClick" />
</template>
<script><![CDATA[
fn handleClick(component_id, event_data) {
    set_component_property(component_id, "label", "Clicked!");
}
]]></script>

Used in app.nemo:

<labeled-button label="Save" />        <!-- ${label} → "Save"; variant → default "primary" -->

Slots

The default <slot /> receives consumer children; a named <slot name="header" required="true" multiple="false" /> receives children tagged slot="header". Slots inject component children, not raw text — provide text via a child component (<label text="…"/>) or a ${prop}.

<!-- card.nemo template -->
<template name="card">
  <panel>
    <stack id="head"><slot name="header" required="true" /></stack>
    <stack id="body"><slot /></stack>
  </panel>
</template>
<!-- usage -->
<card>
  <label slot="header" text="Title" />   <!-- → header slot -->
  <text content="Body" />                 <!-- → default slot -->
</card>

Scoped <style>

A CSS subset folded onto matching template nodes at compile time, scoped to the SFC's own subtree. v1 selectors: type (panel { … } matches nodes with that type) and id (#head { … }). No class/combinator/pseudo/media. Declarations are limited to the universal style attributes below; CSS names normalize to nemo's (border-radius→rounded, background-color→background), sizes drop px (20px→20), colors stay strings (incl. theme.*).

<style><![CDATA[
  panel { padding: 20px; border-radius: lg; }
  #head { padding-bottom: 8px; }
]]></style>

Rules that bite

  • CDATA for </&. .nemo is parsed as XML, which has no raw-text

elements, so a <script>/<style> body containing < or & (Rhai &&, generics, a CSS > combinator) must be wrapped in <![CDATA[ … ]]>. The body must also be one contiguous block.

  • Handler scoping. A template-authored bare on-click="fn" routes to the

SFC's own <script> (rewritten to sfc:<tag>::fn). An instance handler (<labeled-button on-click="globalFn"/>) stays bare and routes to the global handlers script. One SFC script serves every instance; the handler receives the per-instance scoped component_id, so it mutates only the instance that fired.

  • Id scoping is automatic. Template-owned child ids are prefixed with the

instance id (body → <instance>_body), so multiple instances never collide. Slot-injected children keep their own ids.

  • Two ${} systems. SFC props use bare ${label} (runtime interpolation,

string-only), distinct from load-time ${var.x}/${env.x}. Data still flows through bind-*/<binding>.

Control-Flow Directives (n:for / n:if / n:key)

Vue-style namespaced attributes for conditionals and iteration on any template element (layout, <template>, or a .nemo SFC template). Resolved by a compile pass before layout building.

<!-- n:if — conditionally show a component -->
<panel n:if="data.api.status == 'error'">
  <label text="Something went wrong" />
</panel>

<!-- n:for over a static literal array (compile-time) -->
<tab-item n:for="tab in ['home', 'settings', 'about']" n:key="tab"
          label="${tab}" />

<!-- n:for over a live data source (runtime) -->
<card n:for="user in data.api.users" n:key="user.id">
  <label slot="header" text="${user.name}" />
  <text content="${user.email}" />
</card>
Directive When Compiles to
n:if="path" compile-time bind-visible on path's truthiness
n:if="path == 'x'" (or !=) compile-time a visible binding with a comparison transform
n:for="i in ['a','b']" (literal array) compile-time N sibling nodes, ${i}/${i.field} substituted
n:for="i in data.x.y" (data.* path) runtime a list container the runtime grows/shrinks as the array changes
  • n:key gives loop items stable identity — keyed instances keep their

state across reorders; unkeyed items match by index (state lost on reorder). Static-expansion ids are suffixed <index> or <key>.

  • ${item} / ${item.field} inside the loop body reference the current

item; for live-data n:for these become per-instance data bindings.

  • n:for + n:if on one element: n:for wins; the n:if is evaluated per

instance.

  • Condition syntax is a source path or an ==/!= comparison against a

string/number/bool literal — not a full expression.

Routing (<router> / <route> / <nav-link>)

A chrome-free page-routing primitive. A <router> mounts only the matching route's body; navigation is either declarative (<nav-link>) or scripted (navigate() in Rhai).

<!-- Declarative nav bar: no handler needed. `router` is optional if the target
     router has primary="true". A link highlights when its route is active. -->
<stack direction="horizontal">
  <nav-link router="main" route="/home" label="Home" />
  <nav-link router="main" route="/users/42" label="User 42" />
  <nav-link router="main" route="/settings" label="Settings" />
</stack>

<router id="main" default="/home" primary="true" flex="1">
  <route path="/home">
    <label text="Home" size="xl" />
  </route>

  <!-- :param capture → data.route.<router-id>.params.<name> -->
  <route id="user_route" path="/users/:id" on-enter="on_user_enter" on-leave="on_user_leave">
    <label bind-text="data.route.main.params.id" />
  </route>

  <!-- A route may host its own nested <router>. -->
  <route path="/settings"> … </route>

  <!-- Trailing "*" is the not-found fallback (routes match in document order). -->
  <route path="*"><label text="Not found" /></route>
</router>
Element Key attributes
router id, default (initial path), primary (target of router-less nav-links / bare navigate()), plus layout props (flex, height, scroll)
route path (/x, /x/:param, or *), optional id, on-enter, on-leave
nav-link route (required), router (optional if a primary router exists), label
  • Params are projected to data.route.<router-id>.path and

data.route.<router-id>.params.<name> — read them with bind-text / <binding> (e.g. bind-text="data.route.main.params.id").

  • Rhai API: navigate("/path") (primary router) / `navigate("router-id",

"/path") (explicit); back() / forward() (optionally back("router-id")`).

  • Launch flag: --route <path> (or --route <router-id>=<path>) starts a

router at a specific route instead of its default, for this launch only.

  • Sizing: the router renders content-sized (not size_full). For a tall,

scrolling page, wrap the router in a scroll stack and leave the router itself content-sized.

Gotcha — router inside an SFC. A <router> nested inside an SFC
<template> has its id scoped per instance (main → <instance>_main), but
<nav-link router="main"> / navigate("main", …) targets are not yet
rewritten to the scoped id — navigation to such a router silently fails. Until
that follow-up lands, keep routers at the top level, not inside a reused SFC.

Components — Layout

stack

<stack direction="vertical" spacing="8" padding="16" scroll="false">
  <!-- children -->
</stack>
Property Type Default Description
direction string "vertical" "vertical" or "horizontal"
spacing integer 0 Gap between children (px)
padding integer — Inner padding (px)
scroll boolean false Enable scrolling
width/height integer — Fixed dimensions
max-width/max-height integer — Maximum dimensions
flex float — Flex grow factor
align string cross-axis start/center/end/stretch (default: center for horizontal, stretch for vertical)
justify string main-axis start/center/end/between/around

dock

<dock position="center">
  <!-- dockable panels -->
</dock>

Layout container with dockable panels. position defaults to "center".

panel

<panel title="Section" padding="16" border="1" shadow="md" rounded="lg">
  <!-- children -->
</panel>

tabs

<tabs id="my_tabs" active-tab="0" variant="underline">
  <tab-item id="t1" label="Tab 1"> ...content... </tab-item>
  <tab-item id="t2" label="Tab 2"> ...content... </tab-item>
</tabs>
  • Tab pages MUST be <tab-item id="…" label="…">. A <panel> (or any other

element) is valid XML anywhere, so a config with <panel> tab pages passes nemo validate but the tab bar gets zero pages and the whole component renders invisibly. The render dispatch only collects children whose type is tab_item (crates/nemo/src/app.rs).

  • active-tab — 0-based index of the initially selected tab.
  • variant — tab style, one of underline (default), pill, segmented,

outline, tab (crates/nemo/src/components/tabs.rs). Any other value falls back to underline.

Child-only elements. Several elements are valid only inside a specific
parent and render nothing (with no validation error) if placed elsewhere or if
the parent's direct children are the wrong type:
tab-item (in tabs), menu-item (in dropdown-button), option (in
select), list-item (in list), accordion-item (in accordion),
sidenav-bar-item (in sidenav-bar), slot (in template). When a
container renders blank, check that its direct children are the right type.

Components — Display

label

<label id="title" text="Hello World" size="lg" />

Required: text. Sizes: sm, md, lg

text

<text id="body" content="Paragraph text here" />

Required: content

icon

<icon id="info" name="info" size="24" />

Required: name. Uses Lucide icon names.

image

<image id="logo" src="https://example.com/image.png" alt="Logo" />

svg

Renders SVG vector graphics either from a file/URL or from embedded standard SVG markup written inline. src (a filesystem path or http(s) URL) takes precedence; otherwise the inline <svg> body is captured verbatim and rasterized. Optional width/height set the render size (px); otherwise the SVG's intrinsic size is used.

<!-- From a file (relative paths resolve against the config file's directory) -->
<svg id="logo" src="assets/logo.svg" width="120" height="120" />

<!-- Embedded standard SVG markup (nested elements, gradients, text, and
     hyphenated attributes are all preserved) -->
<svg id="badge" width="64" height="64" viewBox="0 0 100 100">
  <circle cx="50" cy="50" r="40" fill="#4c566a" stroke="#eceff4" stroke-width="4" />
</svg>

on-click and on-hover make the SVG interactive. Because the graphic rasterizes to a single image, its child elements (<path>, <circle>, ...) are not individually hit-testable — the handler fires for the SVG as a whole. To recolor, scale, or rotate, rewrite the whole markup from the handler: setcomponentproperty(id, "content", "<svg…>") (or swap src); it re-rasterizes on the next render.

Event event_data
on-click "click"
on-hover "hover" (enter), "hover_end" (leave)
<svg id="badge" width="64" height="64" viewBox="0 0 100 100"
     on-click="recolor" on-hover="highlight">
  <circle cx="50" cy="50" r="40" fill="#4c566a" />
</svg>

progress

<progress id="loading" value="75" max="100" />

avatar

<avatar id="user" name="John Doe" />

badge

<badge id="count" count="5" />
<badge id="status" dot="true" />

tag

<tag id="status" label="Active" variant="success" outline="true" />

Variants: primary, secondary, danger, warning, success, info

spinner

<spinner id="loading" size="lg" />

Sizes: sm, md, lg

alert

<alert id="warning" message="Disk space low" title="Warning" variant="warning" />

Required: message. Variants: info, warning, danger, success

accordion

<accordion id="faq" items='[{"title":"Q1","content":"A1"}]' multiple="false" bordered="true" />

collapsible

<collapsible id="details" title="More Info" open="false">
  <!-- children shown when expanded -->
</collapsible>

dropdown-button

<dropdown-button id="actions" label="Action" variant="primary">
  <menu-item label="Edit" on-click="handle_edit" />
  <menu-item label="Delete" on-click="handle_delete" />
</dropdown-button>

A button that opens a dropdown menu built from menu-item children. label defaults to "Action".

Components — Input

button

<button id="submit" label="Save" variant="primary" size="md" on-click="handle_save" disabled="false" />

Required: label. Variants: primary, secondary, danger, ghost, warning, success, info, outline, link

input

<input id="name" placeholder="Enter name" value="" on-change="handle_change" />

textarea

<textarea id="notes" placeholder="Enter notes" rows="4" auto-grow-min="2" auto-grow-max="10" />

code-editor

<code-editor id="code" language="rust" line-number="true" searchable="true" tab-size="4" rows="10" />

text-editor

<text-editor id="content" placeholder="Write here..." rows="6" />

checkbox

<checkbox id="agree" label="I agree" checked="false" on-change="handle_toggle" />

select

<select id="color" options='["Red","Green","Blue"]' value="Red" on-change="handle_select" />

Options may also be supplied declaratively as option children (each with value and label), instead of the options JSON attribute:

<select id="color" value="red" on-change="handle_select">
  <option value="red" label="Red" />
  <option value="green" label="Green" />
</select>

radio

<radio id="size" options='["Small","Medium","Large"]' value="Medium" direction="horizontal" />

slider

<slider id="volume" min="0" max="100" step="1" value="50" />

switch / toggle

<switch id="dark_mode" label="Dark Mode" checked="false" />
<toggle id="bold" label="Bold" icon="bold" />

Components — Data

table

<table id="users" height="400" stripe="true" bordered="true" on-click="handle_row_click">
  <binding source="data.api" target="data" />
</table>

IMPORTANT: Must have a parent with definite height (the height property sets this).

tree

<tree id="files" height="300">
  <binding source="data.filesystem" target="items" />
</tree>

list

<list id="items">
  <binding source="data.items" target="items" />
</list>

A list may also be built from list-item children, whose own children are the row content:

<list id="items">
  <list-item><label text="Row one" /></list-item>
  <list-item><label text="Row two" /></list-item>
</list>

Components — Charts

All charts accept height (default 300), width, and data (via binding or inline).

line-chart

<line-chart id="trend" x-field="date" y-field="value" dot="true" linear="false" height="300">
  <binding source="data.timeseries" target="data" />
</line-chart>

Required: x-field, y-field

realtime-chart

<realtime-chart id="live" x-field="timestamp" y-fields='["temp","humidity"]' height="400">
  <binding source="data.stream" target="data" />
</realtime-chart>

Required: x-field

bar-chart / column-chart

<bar-chart id="sales" x-field="month" y-field="revenue" show-label="true" />

Required: x-field, y-field

area-chart

<area-chart id="stacked" x-field="date" y-fields='["a","b","c"]' fill-opacity="0.3" />

Required: x-field, y-fields

pie-chart

<pie-chart id="dist" value-field="count" inner-radius="40" outer-radius="100" />

Required: value-field

scatter-chart / bubble-chart

<scatter-chart id="plot" x-field="x" y-field="y" dot-size="4" />
<bubble-chart id="bubbles" x-field="x" y-field="y" size-field="pop" min-radius="3" max-radius="30" />

heatmap-chart

<heatmap-chart id="heat" x-field="hour" y-field="day" value-field="count" />

Required: x-field, y-field, value-field

radar-chart

<radar-chart id="skills" categories='["A","B","C","D","E"]' y-fields='["score1","score2"]' max-value="100" />

Required: categories, y-fields

candlestick-chart

<candlestick-chart id="stock" x-field="date" open-field="open" high-field="high" low-field="low" close-field="close" />

Required: x-field, open-field, high-field, low-field, close-field

stacked/clustered variants

<stacked-column-chart x-field="month" y-fields='["a","b"]' />
<clustered-column-chart x-field="month" y-fields='["a","b"]' />
<stacked-bar-chart y-field="category" x-fields='["q1","q2"]' />
<clustered-bar-chart y-field="category" x-fields='["q1","q2"]' />

pyramid-chart / funnel-chart

<pyramid-chart label-field="stage" value-field="count" />
<funnel-chart label-field="step" value-field="users" />

Required: label-field, value-field

Components — Navigation

sidenav-bar

<sidenav-bar id="nav" collapsed="false" width="200">
  <sidenav-bar-item icon="home" label="Home" on-click="nav_home" />
  <sidenav-bar-item icon="settings" label="Settings" on-click="nav_settings" />
</sidenav-bar>

Components — Feedback

modal

<modal id="confirm" title="Confirm Action" open="false">
  <label text="Are you sure?" />
  <button label="Yes" on-click="confirm_action" />
</modal>

notification

<notification id="toast" message="Saved successfully" kind="success" />

Required: message. Kinds: info, warning, danger, success

tooltip

<tooltip id="help" content="Click to save">
  <button label="Save" />
</tooltip>

Required: content

Common Layout Properties

These work on most components via applylayoutstyles():

Property Type Description
width / height integer Fixed size in px
min-width / min-height integer Minimum size
flex float Flex grow factor
padding integer All-side padding
padding-x / padding-y integer Horizontal/vertical padding
margin integer All-side margin
margin-x / margin-y integer Horizontal/vertical margin
border integer Border width
border-color string Border color (theme ref or hex)
background / background-color string Background color
shadow string Shadow preset: sm, md, lg, xl, 2xl
rounded string Corner radius: sm, md, lg, xl, full
visible boolean Show/hide the component

Definite-height gotcha (silent 0px collapse)

table, tree, and list render their bodies with a uniform_list, which collapses to 0px unless an ancestor has a definite height — the rows silently disappear (a table's header may still show, which is misleading). This also bites content inside a tabs region.

Give the scrollable region a real height instead of relying on flex to size it from content:

<!-- Preferred: a scrolling stack that grows to fill its parent -->
<stack scroll="true" flex="1">
  <table id="rows"> <binding source="data.rows" target="data" /> </table>
</stack>

<!-- Or set an explicit height on the widget -->
<table id="rows" height="400"> <binding source="data.rows" target="data" /> </table>

Project Manifest & Build (nemo.toml, nemo build, nemo get)

A nemo project is a directory with a nemo.toml manifest at its root. Any nemo/nemo dev/nemo build invocation walks up from the target to find that root, so relative paths (entry, <import src>, dir=, theme src) resolve against it consistently.

# nemo.toml — an application project
name = "my-app"
entry = "app.nemo"         # the SFC entry the app launches

[build]
out = "dist"               # build output directory
load = "source"            # "source" (default) parses .nemo at launch;
                           # "dist" loads pre-compiled artifacts from out/
# nemo.toml — a reusable component library (published for others to import)
name = "nemo-widgets"

[package]
exports = ["components/*.nemo"]   # which .nemo files this package exposes

nemo build compiles .nemo components to JSON artifacts:

  • nemo build path/to/card.nemo — compile one component.
  • nemo build in a [package] project — compile the whole exported library.
  • nemo build in an app project — build the app to a loadable out/ (dist/).

Launch it with nemo --dist (or set load = "dist" in the manifest) to load the pre-compiled artifacts instead of re-parsing .nemo at startup.

Remote component libraries (Go-style): import a library by module path and fetch it with nemo get.

<imports>
  <import src="github.com/user/nemo-widgets" as="nw" />
</imports>
nemo get                    # fetch every module <import> into .nemo/packages/,
                            # pinning resolved versions in nemo.lock

nemo get clones each github.com/… module (git) into .nemo/packages/, records exact versions in nemo.lock, and module <import>s then resolve against that cache. Commit nemo.lock; the .nemo/packages/ cache is generated (gitignore it).