Skip to content

Latest commit

 

History

History
357 lines (301 loc) · 13.8 KB

File metadata and controls

357 lines (301 loc) · 13.8 KB

Templates

Mado UI templates are copied application source. They are not a page runtime, route generator or collection of page-sized custom elements. A template default-exports an ordinary Mado page() and becomes application-owned as soon as the CLI writes it.

The current template families contain:

  • application-shell: a responsive layout() recipe with a skip link, application brand, native navigation and exactly one route-content landmark;
  • authentication-shell: a focused layout() recipe for guest flows with a home link and exactly one route-content landmark;
  • list-data-page: a representative collection page composed from the existing page header, filter form, table, pagination and content-state contracts;
  • detail-page: a parameterized record overview with native breadcrumbs, grouped metadata, explicit actions and a real missing-record state;
  • edit-form-page: a parameterized native form using Mado form and mutation state around an explicit application-owned persistence seam;
  • settings-page: a native profile, locale and preferences form with explicit labels, descriptions and application-owned persistence.

No recipe chooses an identity provider, data access, API shape or domain language. Those decisions remain in the application.

Install

Initialize the neutral template target once, then install the recipes:

npx @madojs/ui@latest init
npx @madojs/ui@latest add \
  application-shell authentication-shell detail-page edit-form-page \
  list-data-page settings-page

With the default project paths, the copied application source is:

src/
├── styles/
│   ├── mado-ui-application-shell.css
│   ├── mado-ui-authentication-shell.css
│   ├── mado-ui-alert.css
│   ├── mado-ui-breadcrumbs.css
│   ├── mado-ui-button.css
│   ├── mado-ui-content-state.css
│   ├── mado-ui-description-list.css
│   ├── mado-ui-field.css
│   ├── mado-ui-filter-bar.css
│   ├── mado-ui-form-controls.css
│   ├── mado-ui-form-section.css
│   ├── mado-ui-layout.css
│   ├── mado-ui-navigation-list.css
│   ├── mado-ui-page-header.css
│   ├── mado-ui-pagination.css
│   ├── mado-ui-panel.css
│   ├── mado-ui-settings-row.css
│   ├── mado-ui-table.css
│   └── mado-ui-theme.css
└── 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

Import the installed styles once from the application's src/main.ts. The CLI prints this list after add, and mado-ui doctor checks every locked CSS file:

import "./styles/mado-ui-theme.css";
import "./styles/mado-ui-layout.css";
import "./styles/mado-ui-navigation-list.css";
import "./styles/mado-ui-application-shell.css";
import "./styles/mado-ui-authentication-shell.css";
import "./styles/mado-ui-alert.css";
import "./styles/mado-ui-breadcrumbs.css";
import "./styles/mado-ui-button.css";
import "./styles/mado-ui-content-state.css";
import "./styles/mado-ui-description-list.css";
import "./styles/mado-ui-field.css";
import "./styles/mado-ui-filter-bar.css";
import "./styles/mado-ui-form-controls.css";
import "./styles/mado-ui-form-section.css";
import "./styles/mado-ui-page-header.css";
import "./styles/mado-ui-pagination.css";
import "./styles/mado-ui-panel.css";
import "./styles/mado-ui-settings-row.css";
import "./styles/mado-ui-table.css";

If mado-ui.json uses another styles path, adjust only the import prefix. The CLI never edits main.ts or the route manifest.

Route the recipes

Both shell files are Mado layout pages. Mount them only through layout() in src/app.routes.ts; do not render a second shell around routes.view in main.ts.

// 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);

Here /auth/sign-in belongs to an explicit guest layout group, while /, /records and /settings belong to the guarded empty-prefix application group. Keep the explicit auth group before that catch-all group. guestOnly and requireAuth are application guards: neither copied shell knows how identity is stored or where redirects lead.

The shell intentionally contains three obvious copy-time decisions:

  1. replace the Application brand and decide whether it links to the public landing page or the application overview;
  2. replace navigationItems with the real application map;
  3. keep routeUrl() and data-link on internal navigation links.

Its <main> renders ${child} exactly once. Route pages should therefore own their content headings and sections, but must not introduce another main landmark.

The authentication shell has two copy-time decisions: replace its concise brand and home path, then keep routeUrl() and data-link on the internal link. It also renders ${child} exactly once. A sign-in, registration or recovery page owns its own form; the shell never owns credentials, session state or redirection.

If the application group belongs under /app, update navigationItems to /app, /app/records and /app/settings. Also update recordsPath (and the derived create/detail/edit URLs) in all three record-page recipes. Keeping one prefix across the route manifest and copied constants prevents breadcrumbs, Edit and Cancel links from leaving the guarded zone.

Adapt the list page

list-data.page.ts demonstrates composition, not a data architecture. Before shipping it:

  1. replace records, RecordSummary and the local filter with application-owned data;
  2. replace recordsPath and createRecordPath with real manifest paths;
  3. keep shareable filter and pagination state in the URL when that matches the product;
  4. preserve the native table caption, column and row headers;
  5. render an honest empty, loading or recoverable error state according to the application's actual async lifecycle.

The included search remains a native method="get" form. It works as a normal navigation without JavaScript and lets Mado's queryParam() reflect the same URL after takeover. The sample page deliberately contains no fetch(), resource() or API client because the registry cannot know the consumer's backend.

Adapt detail and edit pages

Both record pages take params.id from a dynamic route. Their small local record collections are readable fixtures, not a data layer. Replace each lookup with the owning module's resource or loaded page data, and keep the missing branch until the real loader has an equally honest not-found contract. Replace each copied recordsPath with the same route prefix used by the list page and application shell.

The detail recipe preserves document semantics:

  • the current breadcrumb is text with aria-current="page";
  • the page has one h1, while each panel has its own h2;
  • metadata remains a native dl and dates remain time;
  • Edit is an ordinary internal link, not a click handler.

The edit recipe keeps HTML constraints as the source of truth and uses useForm() only for reactive values, touched errors and submission state. Its mutation() wraps an exported PersistRecord function. The default function fails with an actionable error on purpose: a copied page must never claim that it saved data before the application connects its real persistence boundary. This recipe requires Mado >=0.17.0 <0.21.0. It relies on lifecycle-owned mutation() teardown and preserves native minlength/maxlength interaction state when .value mirrors form state.

Connect the page locally after copying:

// src/templates/pages/edit-form.page.ts
const persistRecord: PersistRecord = async (
  values,
  signal,
) => recordsConnector.update(values.id, values, { signal });

const editFormPage = createEditFormPage(persistRecord);

export default editFormPage;

Replace the recipe's unconfigured default export with that block. Alternatively, keep the copied factory and inject the module function from a thin route page:

import {
  createEditFormPage,
} from "../../templates/pages/edit-form.page";
import { updateRecord } from "./records.connector";

export default createEditFormPage((values, signal) =>
  updateRecord(values.id, values, { signal })
);

Mado disposes the in-flight mutation when its route is disposed; the copied page does not duplicate that lifecycle with a manual cleanup hook. It does not redirect, invalidate resource keys or reset edited values after success: those policies belong to the application route and data modules. Success and error feedback is tied to a snapshot of the submitted values. It disappears as soon as the current form becomes a different, unsaved draft; editing does not abort a POST that may already have reached the server.

Adapt the settings page

settings.page.ts is deliberately a native method="post" form. Its example values make the copied structure visible, but they are not user or account data. Before shipping it:

  1. replace the profile and regional defaults with values owned by the application;
  2. keep every control's explicit label and description relationship;
  3. remove, rename or extend settings to match the product rather than treating this list as a schema;
  4. connect the POST destination or progressively enhance submission at the application's action boundary;
  5. retain a real native-submit path whenever the deployment can support one.

The reset button restores the form's authored initial values. If an application later mirrors settings through reactive state, it must also make reset update that state or remove the reset action. The recipe has no @submit, mutation, success message or timer because it cannot honestly know whether anything was persisted.

Passwords, multi-factor authentication, active sessions and destructive account actions intentionally do not appear here. They need separate, product-specific threat models and confirmation flows.

Keep access policy in the route map

The authentication shell is presentation, not authentication. Put a guest guard on its layout() group and an authenticated guard on the application shell group, as in the route example above. The application owns session loading, redirects, return URLs and authorization. Do not add those concerns to either copied layout.

Guarded routes cannot be static snapshots. Public catalog preview routes use fixture content and no guards; a real application should test its guest and protected groups against its own identity service.

Shell customization

The application shell exposes a small set of copy-safe CSS properties:

--mado-ui-application-shell-background
--mado-ui-application-shell-border
--mado-ui-application-shell-brand-color
--mado-ui-application-shell-color
--mado-ui-application-shell-content-max-inline-size
--mado-ui-application-shell-focus-ring
--mado-ui-application-shell-header-background
--mado-ui-application-shell-header-padding
--mado-ui-application-shell-main-padding
--mado-ui-application-shell-sidebar-background
--mado-ui-application-shell-sidebar-padding
--mado-ui-application-shell-sidebar-width
--mado-ui-application-shell-skip-background
--mado-ui-application-shell-skip-border
--mado-ui-application-shell-skip-color
--mado-ui-application-shell-skip-radius

The authentication shell exposes equivalent focused-frame controls:

--mado-ui-authentication-shell-background
--mado-ui-authentication-shell-border
--mado-ui-authentication-shell-brand-color
--mado-ui-authentication-shell-color
--mado-ui-authentication-shell-content-max-inline-size
--mado-ui-authentication-shell-content-padding
--mado-ui-authentication-shell-content-padding-wide
--mado-ui-authentication-shell-focus-ring
--mado-ui-authentication-shell-header-max-inline-size
--mado-ui-authentication-shell-header-padding
--mado-ui-authentication-shell-header-padding-wide
--mado-ui-authentication-shell-main-padding
--mado-ui-authentication-shell-main-padding-wide
--mado-ui-authentication-shell-radius
--mado-ui-authentication-shell-shadow
--mado-ui-authentication-shell-skip-background
--mado-ui-authentication-shell-skip-border
--mado-ui-authentication-shell-skip-color
--mado-ui-authentication-shell-skip-radius
--mado-ui-authentication-shell-surface
--mado-ui-authentication-shell-wash

Both stylesheets fall back to the shared OKLCH theme tokens and system colors in forced-colors mode. Since the files are application-owned, structural changes do not require an override layer: edit the copied layout and stylesheet directly.

Boundary

  • Templates may depend on foundations, primitives and blocks.
  • Registry source imports only the public @madojs/mado API and browser platform APIs.
  • Templates never add a runtime dependency to @madojs/ui.
  • The CLI copies files and dependencies but never invents routes, guards, APIs or authentication.
  • Catalog previews run on isolated internal routes so the demonstration application and authentication shells cannot nest a second <main> inside the catalog shell.