Skip to content

Repository files navigation

Mado UI

Open-code UI for @madojs/mado.

Mado UI is not a component runtime. Its CLI copies reviewed Mado components, styles, blocks and page recipes into an application. The application owns the resulting source and ships no @madojs/ui code to the browser.

@madojs/mado  = browser runtime
@madojs/ui    = development CLI + versioned source registry
your project  = final owner of every installed file

Status: pre-1.0. The CLI contract, native-first primitives, composable blocks and six application templates are implemented; the live catalog documents every registry item and the stable CLI workflow.

Quick start

From a Mado application:

npx @madojs/ui@latest init
npx @madojs/ui@latest list
npx @madojs/ui@latest view panel
npx @madojs/ui@latest add \
  alert avatar badge breadcrumbs button code combobox command-palette \
  content-state description-list dialog disclosure field filter-bar \
  form-controls form-section layout menu meter metric-card navigation-list \
  navigation-popover notification-region page-header pagination panel popover progress prose separator \
  settings-row skeleton spinner table tabs toolbar tooltip \
  application-shell authentication-shell detail-page edit-form-page \
  list-data-page settings-page

During local development from sibling repositories:

cd ../ui
npm install
npm link

cd ../site
mado-ui init
mado-ui add \
  alert avatar badge breadcrumbs button code combobox command-palette \
  content-state description-list dialog disclosure field filter-bar \
  form-controls form-section layout menu meter metric-card navigation-list \
  navigation-popover notification-region page-header pagination panel popover progress prose separator \
  settings-row skeleton spinner table tabs toolbar tooltip \
  application-shell authentication-shell detail-page edit-form-page \
  list-data-page settings-page

init detects the official Mado project shapes:

Project Components Styles Templates
Default starter src/components src/styles src/templates
Modular starter src/shared/ui src/shared/styles src/templates

Override any path explicitly when initializing:

mado-ui init \
  --components src/design-system/components \
  --styles src/design-system/styles \
  --templates src/design-system/templates

What gets installed

Adding avatar and badge resolves their shared theme dependency and writes ordinary project files:

src/shared/
├── styles/
│   └── mado-ui-theme.css
└── ui/
    ├── mado-ui-avatar.component.ts
    └── mado-ui-badge.component.ts

Import the theme once from src/main.ts:

import "./shared/styles/mado-ui-theme.css";

Import a component where it is used:

import "../../shared/ui/mado-ui-avatar.component";
import "../../shared/ui/mado-ui-badge.component";
import { html } from "@madojs/mado";

html`
  <span class="mado-ui-cluster">
    <mado-ui-avatar src="/people/ada.webp" alt="Ada Lovelace">
      <span slot="fallback">AL</span>
    </mado-ui-avatar>
    <mado-ui-badge tone="success">Ready</mado-ui-badge>
  </span>
`;

There is deliberately no runtime import such as import "@madojs/ui/badge.js".

Native CSS recipes

Buttons, links, code, dialogs, disclosures, form controls, layout, popovers, thematic separators and feedback indicators stay native HTML or CSS-only. Install their opt-in styles:

mado-ui add \
  button code dialog disclosure form-controls layout meter popover progress \
  separator skeleton spinner

The CLI copies these files next to the theme:

src/shared/styles/
├── mado-ui-theme.css
├── mado-ui-button.css
├── mado-ui-code.css
├── mado-ui-dialog.css
├── mado-ui-disclosure.css
├── mado-ui-form-controls.css
├── mado-ui-layout.css
├── mado-ui-meter.css
├── mado-ui-popover.css
├── mado-ui-progress.css
├── mado-ui-separator.css
├── mado-ui-skeleton.css
└── mado-ui-spinner.css

Import each installed stylesheet once from src/main.ts:

import "./shared/styles/mado-ui-theme.css";
import "./shared/styles/mado-ui-button.css";
import "./shared/styles/mado-ui-code.css";
import "./shared/styles/mado-ui-dialog.css";
import "./shared/styles/mado-ui-disclosure.css";
import "./shared/styles/mado-ui-form-controls.css";
import "./shared/styles/mado-ui-layout.css";
import "./shared/styles/mado-ui-meter.css";
import "./shared/styles/mado-ui-popover.css";
import "./shared/styles/mado-ui-progress.css";
import "./shared/styles/mado-ui-separator.css";
import "./shared/styles/mado-ui-skeleton.css";
import "./shared/styles/mado-ui-spinner.css";

Then compose ordinary platform elements:

<form class="mado-ui-stack">
  <label for="email">Email</label>
  <input
    id="email"
    class="mado-ui-control"
    name="email"
    type="email"
    required
  />

  <button class="mado-ui-button" type="submit">Save</button>
</form>

Native status elements keep their own values and accessibility semantics:

<label for="build-progress">Build progress</label>
<progress
  id="build-progress"
  class="mado-ui-progress"
  value="68"
  max="100"
>
  68%
</progress>

<div role="status" class="mado-ui-cluster">
  <span class="mado-ui-spinner" aria-hidden="true"></span>
  <span>Loading preview…</span>
</div>

Native disclosures likewise own their expanded state and keyboard behavior:

<details class="mado-ui-disclosure">
  <summary class="mado-ui-disclosure-summary">
    Can I customize the installed source?
  </summary>
  <div class="mado-ui-disclosure-content">
    <p>Yes. The copied file belongs to your application.</p>
  </div>
</details>

Give several details the same non-empty name only when they should form an exclusive group; the .mado-ui-disclosure-group wrapper is presentational. Install the complete standalone and grouped recipe with mado-ui add disclosure.

Recipes live in the low-priority mado-ui cascade layer, so normal unlayered application CSS can override them. A disabled action must remain a native <button disabled>; CSS cannot make an anchor behaviorally disabled. The bundled theme uses OKLCH colors and switches to system color keywords in forced-colors mode.

See the primitive contracts for variants, form semantics and layout properties.

Editable combobox

combobox copies a root-ref Mado helper and its opt-in style recipe, resolving the native form-controls and theme dependencies:

mado-ui add combobox

Author a real text input backed by datalist, plus hidden enhancement controls. Without Popover support the browser keeps the complete native fallback. With support, the helper replaces only the suggestion popup:

import "./shared/styles/mado-ui-theme.css";
import "./shared/styles/mado-ui-form-controls.css";
import "./shared/styles/mado-ui-combobox.css";
import { html, page } from "@madojs/mado";
import { madoUiCombobox } from "../../shared/ui/mado-ui-combobox.js";

export default page({
  title: "Assign reviewer",
  view: () => {
    const reviewer = madoUiCombobox();

    return html`
      <label for="reviewer">Reviewer</label>
      <div class="mado-ui-combobox" ref=${reviewer.root}>
        <input
          id="reviewer"
          class="mado-ui-control mado-ui-combobox-input"
          name="reviewer"
          list="reviewer-options"
        />
        <button
          class="mado-ui-combobox-toggle"
          type="button"
          tabindex="-1"
          popovertarget="reviewer-listbox"
          aria-label="Show reviewer suggestions"
          hidden
        ></button>
        <datalist id="reviewer-options">
          <option value="Ada Lovelace" label="Platform engineering"></option>
          <option value="Grace Hopper" label="Compiler systems"></option>
        </datalist>
        <ul
          id="reviewer-listbox"
          class="mado-ui-combobox-listbox"
          popover="auto"
          aria-label="Reviewer suggestions"
          hidden
        ></ul>
      </div>
    `;
  },
});

Focus remains on the input while aria-activedescendant exposes the active suggestion. Arrow keys browse, Enter accepts the active suggestion, Escape closes, and Tab continues the normal page sequence. The input still submits, validates and resets as a native form control, and arbitrary text remains valid. Optional onSelect and onResultsChange callbacks let composed blocks reuse the same filtering contract without inspecting the generated popup. Use a native select when the value must come from a closed set.

Tooltip

tooltip installs a small Mado helper and an opt-in style recipe. Its authored HTML remains the accessible baseline: an enabled native button or link references one plain description, while native Interest Invokers own the visual lifecycle in supporting browsers:

mado-ui add button tooltip

Import the installed style once from src/main.ts and the copied helper where the trigger and tooltip are rendered:

import "./shared/styles/mado-ui-theme.css";
import "./shared/styles/mado-ui-tooltip.css";
import { html, page } from "@madojs/mado";
import { madoUiTooltip } from "../../shared/ui/mado-ui-tooltip.js";

export default page({
  title: "Copy command",
  view: () => {
    const copyHelp = madoUiTooltip();

    return html`
      <button
        class="mado-ui-button mado-ui-tooltip-trigger"
        type="button"
        interestfor="copy-command-help"
        aria-describedby="copy-command-help"
        ref=${copyHelp.trigger}
      >
        Copy command
      </button>
      <span
        id="copy-command-help"
        class="mado-ui-tooltip"
        role="tooltip"
        popover="hint"
        ref=${copyHelp.tooltip}
      >
        Copies the install command without leaving the current page.
      </span>
    `;
  },
});

The paired refs validate that relationship and, on a Popover and Anchor Positioning baseline, provide the same delayed hover, focus and Escape behavior where Interest Invokers are not yet available. Focus remains on the trigger, and touch does not gain a synthetic click or long-press interaction. Tooltip content must be short, descriptive and non-interactive; essential information belongs in visible content.

Dropdown menu

menu is the first progressively enhanced primitive that installs both a small Mado helper and opt-in CSS. It also resolves the popover and theme dependencies:

mado-ui add menu

Import the installed styles once from src/main.ts:

import "./shared/styles/mado-ui-theme.css";
import "./shared/styles/mado-ui-popover.css";
import "./shared/styles/mado-ui-menu.css";

Import the copied helper inside the light-DOM page or layout that owns the menu. Its two refs connect the trigger and menu without adding an @madojs/ui runtime import:

import { html, page } from "@madojs/mado";
import { madoUiMenu } from "../../shared/ui/mado-ui-menu.js";

export default page({
  title: "Invoice",
  view: () => {
    const invoiceActions = madoUiMenu();

    return html`
      <button
        id="invoice-actions-trigger"
        class="mado-ui-popover-invoker"
        type="button"
        popovertarget="invoice-actions"
        ref=${invoiceActions.trigger}
      >
        Actions
      </button>
      <div
        id="invoice-actions"
        class="mado-ui-popover mado-ui-menu"
        popover="auto"
        aria-labelledby="invoice-actions-trigger"
        ref=${invoiceActions.menu}
      >
        <a class="mado-ui-menu-item" href="/invoices/42">Edit invoice</a>
        <button class="mado-ui-menu-item" type="button">
          Copy number
        </button>
        <hr class="mado-ui-menu-separator" />
        <button class="mado-ui-menu-item" type="button">
          Mark as paid
        </button>
      </div>
    `;
  },
});

The helper adds composite menu roles, arrow-key navigation, Home/End, typeahead and focus return when the Popover API is available. The authored links and buttons remain usable in the native fallback.

Tabs

tabs installs a small light-DOM helper and an opt-in style recipe:

mado-ui add tabs

Import the stylesheet once from src/main.ts and the copied helper where the tab content is rendered:

import "./shared/styles/mado-ui-theme.css";
import "./shared/styles/mado-ui-tabs.css";
import { html, page } from "@madojs/mado";
import { madoUiTabs } from "../../shared/ui/mado-ui-tabs.js";

export default page({
  title: "Product settings",
  view: () => {
    const settingsTabs = madoUiTabs();

    return html`
      <div class="mado-ui-tabs" ref=${settingsTabs.root}>
        <div class="mado-ui-tabs-list" aria-label="Product settings">
          <a
            id="account-tab"
            class="mado-ui-tabs-tab"
            href="#account-panel"
          >
            Account
          </a>
          <a
            id="billing-tab"
            class="mado-ui-tabs-tab"
            href="#billing-panel"
          >
            Billing
          </a>
        </div>

        <div class="mado-ui-tabs-panels">
          <section id="account-panel" class="mado-ui-tabs-panel">
            <h2>Account profile</h2>
            <p>Update the public name and contact address.</p>
          </section>
          <section id="billing-panel" class="mado-ui-tabs-panel">
            <h2>Billing details</h2>
            <p>Review the current plan and next renewal date.</p>
          </section>
        </div>
      </div>
    `;
  },
});

Before enhancement, the anchors remain useful fragment links and every section is visible. The helper validates the one-to-one relationships and document-unique tab and panel IDs, adds tab semantics, keeps one tab in the Tab order and activates adjacent horizontal tabs with Left/Right. Put data-orientation="vertical" on the root to use Up/Down instead. Home and End select the edges, and Tab moves into the active panel without being trapped. Inactive panels use a namespaced helper state and CSS display: none, not hidden, so the authored fragment fallback remains honest. Valid paired Mado updates are reconciled; an incomplete structure fails open to visible sections and automatically enhances again after repair. This first contract intentionally has no disabled or manual-activation mode and does not synchronize the URL hash after reading a matching initial fragment.

Blocks

Blocks compose primitives and native HTML into larger source recipes. Most are CSS-only; navigation-popover adds a small Mado helper for its repeated responsive close lifecycle. Install the navigation, form, data, surface, feedback and page-action blocks:

mado-ui add \
  alert breadcrumbs command-palette content-state description-list field \
  filter-bar form-section metric-card navigation-list navigation-popover \
  notification-region page-header pagination panel prose settings-row table toolbar

The CLI copies their styles next to the theme. The navigation-popover helper is also copied to the configured components root as mado-ui-navigation-popover.ts:

src/shared/styles/
├── mado-ui-theme.css
├── mado-ui-alert.css
├── mado-ui-breadcrumbs.css
├── mado-ui-command-palette.css
├── mado-ui-content-state.css
├── mado-ui-description-list.css
├── mado-ui-field.css
├── mado-ui-filter-bar.css
├── mado-ui-form-section.css
├── mado-ui-metric-card.css
├── mado-ui-navigation-list.css
├── mado-ui-navigation-popover.css
├── mado-ui-notification-region.css
├── mado-ui-page-header.css
├── mado-ui-pagination.css
├── mado-ui-panel.css
├── mado-ui-prose.css
├── mado-ui-settings-row.css
├── mado-ui-table.css
└── mado-ui-toolbar.css

Import each installed stylesheet once from src/main.ts:

import "./shared/styles/mado-ui-theme.css";
import "./shared/styles/mado-ui-alert.css";
import "./shared/styles/mado-ui-breadcrumbs.css";
import "./shared/styles/mado-ui-command-palette.css";
import "./shared/styles/mado-ui-content-state.css";
import "./shared/styles/mado-ui-description-list.css";
import "./shared/styles/mado-ui-field.css";
import "./shared/styles/mado-ui-filter-bar.css";
import "./shared/styles/mado-ui-form-section.css";
import "./shared/styles/mado-ui-metric-card.css";
import "./shared/styles/mado-ui-navigation-list.css";
import "./shared/styles/mado-ui-navigation-popover.css";
import "./shared/styles/mado-ui-notification-region.css";
import "./shared/styles/mado-ui-page-header.css";
import "./shared/styles/mado-ui-pagination.css";
import "./shared/styles/mado-ui-panel.css";
import "./shared/styles/mado-ui-prose.css";
import "./shared/styles/mado-ui-settings-row.css";
import "./shared/styles/mado-ui-table.css";
import "./shared/styles/mado-ui-toolbar.css";

Blocks do not invent accessibility semantics. Choose native elements and ARIA for the content's actual lifecycle; a data-tone value only changes presentation. The navigation-popover helper validates paired Mado refs, delegates close behavior for native links (including dynamically rendered items), and closes at an optional responsive media-query transition. Native Popover behavior still owns opening, light dismissal, Escape and focus return; the content alone scrolls while the header and account/action footer remain pinned. content-state supports the same neutral, info, success, warning and danger tone vocabulary as alert; none of those values assigns a live-region role. prose provides an explicit, width-constrained document flow for guides and policies without leaking typography into nested blocks. Most blocks depend directly on theme. notification-region composes the existing alert surface. command-palette composes the native dialog and editable combobox contracts, so its install graph also includes form-controls. navigation-popover composes the popover and navigation-list contracts. Actions are optional: install the independent button recipe when block examples use .mado-ui-button or .mado-ui-link. Likewise, install form-controls alongside field, filter-bar or settings-row when their compositions use .mado-ui-control or .mado-ui-checkbox. The filter-bar example also composes field; neither optional item becomes its registry dependency.

The command palette is a CSS-only composition, not another JavaScript component. A native dialog owns modality, focus and Escape; the installed madoUiCombobox() helper owns only its editable suggestions. The application keeps one command collection, installs and cleans up any Ctrl/⌘K listener, executes enabled command IDs and owns both result and action announcements. Use declarative commandfor/command="show-modal" with a feature-detected showModal() fallback, and preserve ordinary links and buttons because the palette is an accelerator rather than the only route to an action.

The notification region is an ordinary in-flow stack. Application markup keeps a separate .mado-ui-notification-announcer mounted with role="status" and aria-atomic="true", updates its concise text, and independently renders persistent alerts in an ol.mado-ui-notification-list. The visual list and its items have no live role. This block creates no global queue, timer or focus behavior; urgent interruptions use a separate application-owned role="alert" or modal response.

See the block contracts for exact markup, customization properties, validation lifecycle, heading and landmark ownership, alert lifecycle rules, and the important distinction between a visual action group and an ARIA toolbar.

Templates

Templates are ordinary Mado page() and layout() recipes copied into the neutral templates target:

mado-ui add \
  application-shell authentication-shell detail-page edit-form-page \
  list-data-page settings-page
src/templates/
├── layouts/
│   ├── application-shell.layout.ts
│   └── authentication-shell.layout.ts
└── pages/
    ├── detail.page.ts
    ├── edit-form.page.ts
    ├── list-data.page.ts
    └── settings.page.ts

The application shell composes a skip link, native navigation and exactly one route-content <main>. The list page composes a native GET filter form, responsive table, pagination and empty state without choosing an API or data layer. The focused authentication shell and settings page keep access policy and persistence application-owned. Mount both shells explicitly in src/app.routes.ts:

import { layout, routes } from "@madojs/mado";
import { guestOnly, requireAuth } from "./modules/auth/auth.guard";

export const manifest = {
  "/auth": layout({
    layout: () =>
      import("./templates/layouts/authentication-shell.layout"),
    guard: guestOnly,
    routes: {
      "/sign-in": () => import("./pages/sign-in.page"),
    },
  }),
  "": layout({
    layout: () =>
      import("./templates/layouts/application-shell.layout"),
    guard: requireAuth,
    routes: {
      "/": () => import("./pages/overview.page"),
      "/records": () =>
        import("./templates/pages/list-data.page"),
      "/records/:id": () =>
        import("./templates/pages/detail.page"),
      "/records/:id/edit": () =>
        import("./templates/pages/edit-form.page"),
      "/settings": () =>
        import("./templates/pages/settings.page"),
    },
  }),
  "*": () => import("./pages/not-found.page"),
};

export default routes(manifest);

The explicit /auth group stays outside the guarded empty-prefix application group, while the copied record paths remain /records. If the application shell is mounted under /app instead, update every copied navigation and record-path constant to the same prefix.

The CLI installs source and dependencies but deliberately does not edit the route map. See the template contracts for the complete CSS import list, route-prefix adaptation and copy-time decisions.

Catalog

The live catalog is a Mado application built from this repository. It is the visual overview on /, includes installation, theming and CLI guides, and provides a direct route for every registry item:

/
/docs/installation
/docs/theming
/docs/cli
/foundations/theme
/components/avatar
/components/badge
/components/button
/components/code
/components/combobox
/components/dialog
/components/disclosure
/components/form-controls
/components/layout
/components/menu
/components/meter
/components/popover
/components/progress
/components/separator
/components/skeleton
/components/spinner
/components/tabs
/components/tooltip
/blocks/alert
/blocks/breadcrumbs
/blocks/command-palette
/blocks/content-state
/blocks/description-list
/blocks/field
/blocks/filter-bar
/blocks/form-section
/blocks/metric-card
/blocks/navigation-list
/blocks/navigation-popover
/blocks/notification-region
/blocks/page-header
/blocks/pagination
/blocks/panel
/blocks/prose
/blocks/settings-row
/blocks/table
/blocks/toolbar
/templates/application-shell
/templates/authentication-shell
/templates/detail-page
/templates/edit-form-page
/templates/list-data-page
/templates/settings-page

Run it locally with:

npm run catalog:dev

Registry metadata remains the source of truth. A typed catalog manifest adds routes, lazy showcases, family-split reference content and resolved install graphs; startup validation rejects missing, unknown or duplicate coverage. Each item page documents its composition, public API, accessibility, focus, fallback and copied source. Installation, theming and CLI guides use one Node-readable route manifest. The live catalog always invokes the current stable CLI and registry through @latest; its package version remains build metadata, not the install selector. Future immutable versioned pages use their exact @x.y.z instead. The catalog deploys independently to ui.madojs.dev; the main Mado site only links to it.

See the library scope for the path to v1 and the catalog architecture for the route and deployment contract.

Commands

mado-ui init                         create mado-ui.json
mado-ui list [--json]                list registry items and local state
mado-ui view <item> [--json]         inspect source before installing
mado-ui add <item...>                copy items and dependencies
mado-ui add <item...> --dry-run      preview writes
mado-ui add <item...> --overwrite    explicitly reset selected items
mado-ui update [item...]              update selected or all locked items
mado-ui update [item...] --dry-run    preview safe replacements
mado-ui update <item...> --overwrite replace customized selected files
mado-ui remove <item...>              remove explicit items and new orphans
mado-ui remove <item...> --dry-run    preview exact file deletions
mado-ui migrate <explicit-item...>    declare ownership for a v1 lock
mado-ui diff [item...]               compare local and current registry source
mado-ui doctor                       validate config, lock, imports and tags

All commands accept --cwd <path>.

add introduces missing files and never rewrites an existing target by default. A directly requested customized item still requires the explicit reset path, add <item> --overwrite. Customized dependencies are preserved when their upstream source is unchanged; an outdated or conflicting dependency directs the developer to update instead.

Updating owned source is a separate, explicit operation. update compares three states for every selected file:

hash recorded by the lock ↔ current local hash ↔ current registry hash
  • local equals current registry: keep the file and adopt current lock metadata;
  • local equals lock and registry changed: safely replace pristine source;
  • registry equals lock and local changed: preserve the customization because upstream did not change;
  • local and registry both diverged from lock: stop the complete transaction and point to diff.

Replacing a conflicting customization requires the deliberately narrower update <item> --overwrite. The flag applies only to item names written in the command, never implicitly to their dependencies; consequently, update --overwrite without item names is rejected. Without item names, a non-destructive update reconciles every item already present in the lock. Explicit names must already be installed. If their current dependency graph contains a new missing file, reconciliation may add that dependency after the complete plan passes validation.

Removing source is explicit and conservative. remove accepts one or more item names; it never means “remove everything”. Only items recorded as explicit installation roots may be selected. If another remaining root depends on a selected item, the command stops and asks for the dependent roots to be named in the same invocation. Once the complete plan is valid, the CLI deletes only pristine files named in the lock, retains dependencies still reachable from other roots, and removes newly orphaned dependencies. Missing owned files are forgotten, but locally modified files, shared lock targets, paths outside configured roots, symlinks and non-regular files stop the whole transaction. Directories and untracked files are never removed.

Use remove --dry-run to inspect the same deterministic plan without changing files or lock state:

npx @madojs/ui@latest remove tooltip --dry-run
npx @madojs/ui@latest remove tooltip

Use the stable moving tag for normal work, including months or years after initial installation:

npx @madojs/ui@latest add dialog
npx @madojs/ui@latest update button --dry-run
npx @madojs/ui@latest update button

@latest updates the command-line tool and registry selected for that invocation. It does not silently update source already owned by the project. The lock retains the exact registry version and original hashes for those files. Use an exact selector such as @0.1.1 only to reproduce an old installation or while reading a future immutable versioned catalog.

Stable releases alone move the npm latest tag. Release candidates may use next, and short-lived commit builds may use canary; neither channel changes what the live catalog installs. The registry's top-level compatibilityVersion is the compatibility generation. A generation change is an explicit migration boundary: add stops, and update must reconcile the entire installed set before the lock moves to the new generation. Generation transitions are monotonic: an older CLI cannot downgrade a lock written by a newer registry generation.

Project state

mado-ui init creates human-owned configuration:

{
  "$schema": "https://ui.madojs.dev/schema/v1/mado-ui.schema.json",
  "version": 1,
  "components": "src/shared/ui",
  "styles": "src/shared/styles",
  "templates": "src/templates"
}

The stable schema URL describes config format 1 independently of the npm release selected for a command. The immutable legacy raw.githubusercontent.com/madojs/ui/v0.1.0/... URL remains valid and is accepted; init never rewrites an existing human-owned configuration merely to replace its schema URL. The same canonical schema is also exported from the package as @madojs/ui/schema/config.json.

Successful installs update .mado-ui.lock.json. Lock format 2 records explicitItems, the direct dependency edges captured at installation, the registry version, registryCompatibility generation, source paths and SHA-256 hashes. That makes dependency cleanup deterministic even if a later registry release changes its graph. Commit both project-state files. The lock lets list, doctor, update and remove distinguish installed, customized, missing and upstream-changed files; it never prevents editing. Hashes normalize line endings, so Git's LF/CRLF checkout policy does not create false customizations.

Format-1 locks did not distinguish explicit requests from transitive dependencies. The CLI deliberately does not infer that history. doctor reports the migration and mutating commands stop until the original explicit roots are declared:

npx @madojs/ui@latest migrate badge dialog --dry-run
npx @madojs/ui@latest migrate badge dialog

Migration succeeds only when the named roots and their current dependency closure reproduce every item in the legacy lock. It changes lock metadata transactionally and never rewrites installed source.

templates is a neutral source root for copied route and layout recipes, normally split into pages/ and layouts/. It does not try to guess which domain owns a modular page, and the CLI never edits app.routes.ts. Existing version-1 configs without the field resolve {templates} to src/templates; adding the field explicitly is recommended before choosing a different location.

Library boundaries

Distribution is source-first; implementation remains native-first:

  • autonomous leaf widgets may use Mado components and Shadow DOM;
  • buttons, form controls, headings, lists and tables remain native HTML when wrapping them would weaken browser semantics;
  • blocks compose primitives;
  • templates are copied Mado page() and layout() recipes, not page-sized custom elements;
  • generated source imports only the public @madojs/mado API;
  • registry items cannot add browser runtime dependencies.

See the architecture decision and roadmap.

Contributing

Bug fixes, accessibility improvements, documentation and carefully scoped registry additions are welcome. Read CONTRIBUTING.md for the project boundaries, registry checklist and required verification gates.

Develop

npm ci
npm run registry:check
npm run typecheck
npm test
npm run catalog:dev
npm run catalog:release
npm run catalog:verify:release
npm run package:smoke
npm run verify

The CLI itself uses Node built-ins only. Development dependencies exist solely to typecheck, render and test the source that will be copied into applications. npm test includes a real-browser catalog smoke using installed Chrome. Set MADO_UI_BROWSER_CHANNEL or MADO_UI_BROWSER_PATH when Chrome is installed under a non-default name. Production deployment is intentionally local: after pushing a clean main, a maintainer authenticated through Wrangler OAuth runs npm run catalog:deploy; no Cloudflare credential is stored in GitHub.

About

Mado UI is an open-code source registry and zero-dependency Node CLI for Mado

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages