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.
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-pageDuring 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-pageinit 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/templatesAdding 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".
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 spinnerThe 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.
combobox copies a root-ref Mado helper and its opt-in style recipe, resolving
the native form-controls and theme dependencies:
mado-ui add comboboxAuthor 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 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 tooltipImport 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.
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 menuImport 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 installs a small light-DOM helper and an opt-in style recipe:
mado-ui add tabsImport 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 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 toolbarThe 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 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-pagesrc/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.
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:devRegistry 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.
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 tooltipUse 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.
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 dialogMigration 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.
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()andlayout()recipes, not page-sized custom elements; - generated source imports only the public
@madojs/madoAPI; - registry items cannot add browser runtime dependencies.
See the architecture decision and roadmap.
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.
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 verifyThe 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.