@madojs/mado 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +102 -25
- package/CHANGELOG.md +185 -1
- package/README.md +137 -172
- package/dist/src/component.d.ts +3 -2
- package/dist/src/component.js +98 -6
- package/dist/src/component.js.map +1 -1
- package/dist/src/css.js +34 -5
- package/dist/src/css.js.map +1 -1
- package/dist/src/head.d.ts +6 -6
- package/dist/src/head.js +6 -6
- package/dist/src/html/bindings.js +3 -3
- package/dist/src/html/bindings.js.map +1 -1
- package/dist/src/html/parser.d.ts +1 -1
- package/dist/src/html/parser.js +1 -1
- package/dist/src/html/template.js +24 -10
- package/dist/src/html/template.js.map +1 -1
- package/dist/src/index.d.ts +8 -4
- package/dist/src/index.js +13 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/page.d.ts +62 -28
- package/dist/src/page.js +5 -0
- package/dist/src/page.js.map +1 -1
- package/dist/src/resource.js +2 -1
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/base.d.ts +65 -0
- package/dist/src/router/base.js +168 -0
- package/dist/src/router/base.js.map +1 -0
- package/dist/src/router/manifest.js +75 -31
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +16 -2
- package/dist/src/router/match.js +41 -1
- package/dist/src/router/match.js.map +1 -1
- package/dist/src/router/navigation.js +58 -9
- package/dist/src/router/navigation.js.map +1 -1
- package/dist/src/static-runtime.d.ts +81 -0
- package/dist/src/static-runtime.js +209 -0
- package/dist/src/static-runtime.js.map +1 -0
- package/dist/src/vite/index.d.ts +31 -3
- package/dist/src/vite/index.js +49 -18
- package/dist/src/vite/index.js.map +1 -1
- package/docs/README.md +5 -8
- package/docs/architecture/adr/0001-browser-static-snapshots.md +132 -0
- package/docs/en/00-the-mado-way.md +1 -1
- package/docs/en/01-quickstart.md +183 -0
- package/docs/en/10-pages-and-components.md +271 -0
- package/docs/en/11-templates-and-signals.md +211 -0
- package/docs/en/12-routing.md +229 -0
- package/docs/en/13-data.md +225 -0
- package/docs/en/14-forms.md +244 -0
- package/docs/en/15-static-snapshots.md +181 -0
- package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +53 -2
- package/docs/en/{13-deployment.md → 20-deployment.md} +59 -10
- package/docs/en/{14-testing.md → 22-testing.md} +6 -5
- package/docs/en/23-cookbook.md +292 -0
- package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +12 -3
- package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +3 -3
- package/docs/en/40-llm-guide.md +776 -0
- package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +15 -16
- package/docs/en/{05-why-mado.md → 42-why-mado.md} +1 -1
- package/docs/en/README.md +97 -27
- package/docs/recipes/nginx/Containerfile +26 -0
- package/docs/recipes/nginx/nginx.conf +42 -0
- package/llms.txt +246 -211
- package/package.json +17 -10
- package/scripts/bake.mjs +4 -568
- package/scripts/cli/generate.mjs +55 -5
- package/scripts/cli/help.mjs +11 -5
- package/scripts/cli/index.mjs +20 -4
- package/scripts/cli/init.mjs +7 -2
- package/scripts/cli/release.mjs +20 -68
- package/scripts/docs-lint.mjs +170 -0
- package/scripts/package-smoke.mjs +67 -9
- package/scripts/preview.mjs +95 -6
- package/scripts/size-budget.mjs +5 -1
- package/scripts/static/browser.mjs +654 -0
- package/scripts/static/discover.mjs +281 -0
- package/scripts/static/output.mjs +141 -0
- package/scripts/static/serialize.mjs +212 -0
- package/scripts/static/server.mjs +167 -0
- package/scripts/static.mjs +191 -0
- package/starters/default/README.md +57 -55
- package/starters/default/package.json +3 -9
- package/starters/default/src/app.routes.ts +20 -33
- package/starters/default/src/components/feature-card.component.ts +40 -0
- package/starters/default/src/components/live-counter.component.ts +57 -0
- package/starters/default/src/content/guides.ts +55 -0
- package/starters/default/src/main.ts +9 -13
- package/starters/default/src/pages/app.page.ts +41 -0
- package/starters/default/src/pages/guide.page.ts +40 -0
- package/starters/default/src/pages/home.page.ts +49 -0
- package/starters/default/src/pages/not-found.page.ts +18 -0
- package/starters/default/src/styles/document.css +39 -0
- package/starters/default/src/styles/reset.css +37 -0
- package/starters/default/src/styles/tokens.css +30 -0
- package/starters/default/src/styles.d.ts +3 -1
- package/starters/default/src/vite-env.d.ts +1 -1
- package/starters/default/vite.config.ts +14 -1
- package/starters/modular/README.md +142 -0
- package/starters/modular/index.html +13 -0
- package/starters/modular/package.json +30 -0
- package/starters/modular/src/app.routes.ts +39 -0
- package/starters/{default → modular}/src/layouts/app-shell.layout.ts +8 -5
- package/starters/{default → modular}/src/layouts/auth-shell.layout.ts +3 -3
- package/starters/modular/src/main.ts +16 -0
- package/starters/{default → modular}/src/modules/billing/pages/invoices-list.page.ts +3 -3
- package/starters/{default → modular}/src/modules/home/home.page.ts +5 -7
- package/starters/modular/src/modules/home/not-found.page.ts +14 -0
- package/starters/modular/src/styles.d.ts +1 -0
- package/starters/modular/src/vite-env.d.ts +1 -0
- package/starters/modular/tsconfig.json +24 -0
- package/starters/modular/vite.config.ts +131 -0
- package/TODO.md +0 -79
- package/docs/en/01-routing.md +0 -152
- package/docs/en/02-project-layout.md +0 -124
- package/docs/en/03-static-bake.md +0 -249
- package/docs/en/04-ide-setup.md +0 -162
- package/docs/en/07-llm-pitfalls.md +0 -724
- package/docs/en/08-llm-zero-history-test.md +0 -56
- package/docs/en/09-shadow-vs-light-dom.md +0 -174
- package/docs/en/11-layouts.md +0 -113
- package/docs/en/12-auth-and-api.md +0 -124
- package/docs/en/16-bake-cookbook.md +0 -100
- package/docs/en/17-shadow-dom-forms.md +0 -192
- package/docs/fr/00-the-mado-way.md +0 -85
- package/docs/fr/01-routing.md +0 -120
- package/docs/fr/02-project-layout.md +0 -84
- package/docs/fr/03-static-bake.md +0 -289
- package/docs/fr/04-ide-setup.md +0 -162
- package/docs/fr/05-why-mado.md +0 -193
- package/docs/fr/06-for-backenders.md +0 -438
- package/docs/fr/07-llm-pitfalls.md +0 -625
- package/docs/fr/08-llm-zero-history-test.md +0 -38
- package/docs/fr/09-shadow-vs-light-dom.md +0 -65
- package/docs/fr/10-app-architecture.md +0 -138
- package/docs/fr/11-layouts.md +0 -47
- package/docs/fr/12-auth-and-api.md +0 -76
- package/docs/fr/13-deployment.md +0 -57
- package/docs/fr/14-testing.md +0 -41
- package/docs/fr/15-error-handling.md +0 -50
- package/docs/fr/16-bake-cookbook.md +0 -88
- package/docs/fr/17-shadow-dom-forms.md +0 -196
- package/docs/fr/18-api-freeze-map.md +0 -63
- package/docs/fr/19-reactivity-ordering.md +0 -97
- package/docs/fr/20-v1-stability.md +0 -88
- package/docs/fr/README.md +0 -27
- package/docs/ru/00-the-mado-way.md +0 -84
- package/docs/ru/01-routing.md +0 -119
- package/docs/ru/02-project-layout.md +0 -84
- package/docs/ru/03-static-bake.md +0 -250
- package/docs/ru/04-ide-setup.md +0 -144
- package/docs/ru/05-why-mado.md +0 -193
- package/docs/ru/06-for-backenders.md +0 -428
- package/docs/ru/07-llm-pitfalls.md +0 -624
- package/docs/ru/08-llm-zero-history-test.md +0 -57
- package/docs/ru/09-shadow-vs-light-dom.md +0 -63
- package/docs/ru/10-app-architecture.md +0 -152
- package/docs/ru/11-layouts.md +0 -47
- package/docs/ru/12-auth-and-api.md +0 -75
- package/docs/ru/13-deployment.md +0 -66
- package/docs/ru/14-testing.md +0 -50
- package/docs/ru/15-error-handling.md +0 -56
- package/docs/ru/16-bake-cookbook.md +0 -95
- package/docs/ru/17-shadow-dom-forms.md +0 -193
- package/docs/ru/18-api-freeze-map.md +0 -64
- package/docs/ru/19-reactivity-ordering.md +0 -95
- package/docs/ru/20-v1-stability.md +0 -82
- package/docs/ru/README.md +0 -25
- package/docs/uk/00-the-mado-way.md +0 -82
- package/docs/uk/01-routing.md +0 -76
- package/docs/uk/02-project-layout.md +0 -73
- package/docs/uk/03-static-bake.md +0 -48
- package/docs/uk/04-ide-setup.md +0 -26
- package/docs/uk/05-why-mado.md +0 -34
- package/docs/uk/06-for-backenders.md +0 -55
- package/docs/uk/07-llm-pitfalls.md +0 -145
- package/docs/uk/08-llm-zero-history-test.md +0 -34
- package/docs/uk/09-shadow-vs-light-dom.md +0 -58
- package/docs/uk/10-app-architecture.md +0 -97
- package/docs/uk/11-layouts.md +0 -47
- package/docs/uk/12-auth-and-api.md +0 -70
- package/docs/uk/13-deployment.md +0 -40
- package/docs/uk/14-testing.md +0 -34
- package/docs/uk/15-error-handling.md +0 -32
- package/docs/uk/16-bake-cookbook.md +0 -36
- package/docs/uk/17-shadow-dom-forms.md +0 -193
- package/docs/uk/18-api-freeze-map.md +0 -61
- package/docs/uk/19-reactivity-ordering.md +0 -95
- package/docs/uk/20-v1-stability.md +0 -83
- package/docs/uk/README.md +0 -27
- package/starters/default/src/modules/home/not-found.page.ts +0 -11
- /package/docs/en/{15-error-handling.md → 21-error-handling.md} +0 -0
- /package/docs/en/{19-reactivity-ordering.md → 31-reactivity-ordering.md} +0 -0
- /package/starters/{default → modular}/.editorconfig +0 -0
- /package/starters/{default → modular}/eslint.config.mjs +0 -0
- /package/starters/{default → modular}/public/favicon.svg +0 -0
- /package/starters/{default → modular}/src/modules/auth/_contracts/auth-api.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.connector.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.guard.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.public.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.routes.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.service.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/login.page.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/_contracts/stripe.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/api/stripe.connector.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.public.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.routes.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/components/invoice-status-badge.component.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/data/invoices.resource.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/pages/invoice-detail.page.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/http-client.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/http-error.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/interceptors.ts +0 -0
- /package/starters/{default → modular}/src/shared/lib/format-date.ts +0 -0
- /package/starters/{default → modular}/src/shared/styles/content.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/reset.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/shell.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/tokens.css +0 -0
- /package/starters/{default → modular}/src/shared/ui/x-button.component.ts +0 -0
- /package/starters/{default → modular}/src/shared/ui/x-spinner.component.ts +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Browser-safe content module. `paths()` and `initialData()` in
|
|
2
|
+
// `guide.page.ts` import from here, so this file MUST stay in the
|
|
3
|
+
// client bundle — no Node-only APIs, no secrets, no env reads.
|
|
4
|
+
//
|
|
5
|
+
// In a larger app you would replace this with a markdown loader or a
|
|
6
|
+
// public JSON file fetched at build time.
|
|
7
|
+
// `Guide` is a JsonValue-safe shape: only plain string fields so it can
|
|
8
|
+
// be passed to `static.initialData` and survive the snapshot serializer.
|
|
9
|
+
// The explicit index signature satisfies Mado's `JsonValue` constraint
|
|
10
|
+
// without any runtime cost.
|
|
11
|
+
export type Guide = {
|
|
12
|
+
slug: string;
|
|
13
|
+
title: string;
|
|
14
|
+
summary: string;
|
|
15
|
+
body: string;
|
|
16
|
+
[key: string]: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const guides: Guide[] = [
|
|
20
|
+
{
|
|
21
|
+
slug: "components",
|
|
22
|
+
title: "Web Components are the app shell",
|
|
23
|
+
summary:
|
|
24
|
+
"Every visual unit in Mado is a real custom element with its own open shadow root.",
|
|
25
|
+
body:
|
|
26
|
+
"Mado does not ship a virtual DOM. The same custom element you " +
|
|
27
|
+
"register with `component()` runs in the live SPA and inside the " +
|
|
28
|
+
"snapshotted Declarative Shadow DOM the static release produces.",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
slug: "signals",
|
|
32
|
+
title: "Signals are the reactivity model",
|
|
33
|
+
summary:
|
|
34
|
+
"Read with `count()`, write with `count.set()`, derive with `computed()`.",
|
|
35
|
+
body:
|
|
36
|
+
"Signals are getter functions. Subscriptions are automatic inside " +
|
|
37
|
+
"`html\\`...\\`` template fragments and `effect()` callbacks, and " +
|
|
38
|
+
"released when the surrounding component or page unmounts.",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
slug: "snapshots",
|
|
42
|
+
title: "Browser-rendered static snapshots",
|
|
43
|
+
summary:
|
|
44
|
+
"`mado release` runs your app in a real Chromium and freezes the " +
|
|
45
|
+
"rendered HTML — including the Shadow DOM — into one file per route.",
|
|
46
|
+
body:
|
|
47
|
+
"The snapshot ships as Declarative Shadow DOM. On first paint the " +
|
|
48
|
+
"client component re-attaches to the same host with zero hydration " +
|
|
49
|
+
"boundary and zero duplicated trees.",
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export function findGuide(slug: string): Guide | undefined {
|
|
54
|
+
return guides.find((g) => g.slug === slug);
|
|
55
|
+
}
|
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import "./shared/styles/shell.css";
|
|
5
|
-
import "./shared/styles/content.css";
|
|
6
|
-
|
|
1
|
+
// Single entrypoint: import the app routes and render them into the
|
|
2
|
+
// `#app` host declared in `index.html`. There is no separate "bootstrap"
|
|
3
|
+
// step — the router IS the application.
|
|
7
4
|
import { html, render } from "@madojs/mado";
|
|
8
|
-
|
|
9
|
-
import
|
|
5
|
+
import "./components/feature-card.component";
|
|
6
|
+
import "./components/live-counter.component";
|
|
10
7
|
import appRoutes from "./app.routes";
|
|
8
|
+
import "./styles/tokens.css";
|
|
9
|
+
import "./styles/reset.css";
|
|
10
|
+
import "./styles/document.css";
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
// router view into #app.
|
|
14
|
-
await initAuth();
|
|
15
|
-
|
|
16
|
-
render(html`${appRoutes.view}`, document.getElementById("app")!);
|
|
12
|
+
render(html`${appRoutes.view}`, document.getElementById("app")!);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// SPA-only interactive route. No `static:` declaration, so
|
|
2
|
+
// `mado release` keeps it out of the sitemap and serves it through
|
|
3
|
+
// the `_mado/spa.html` fallback at runtime — the canonical home for
|
|
4
|
+
// in-app surfaces that should not appear in search results.
|
|
5
|
+
import { html, page, routeUrl, signal } from "@madojs/mado";
|
|
6
|
+
|
|
7
|
+
export default page({
|
|
8
|
+
title: "App",
|
|
9
|
+
head: () => ({
|
|
10
|
+
description: "A live SPA route demonstrating signals and components.",
|
|
11
|
+
}),
|
|
12
|
+
view: () => {
|
|
13
|
+
const name = signal("world");
|
|
14
|
+
return html`
|
|
15
|
+
<main class="document">
|
|
16
|
+
<p><a data-link href=${routeUrl("/")}>← Home</a></p>
|
|
17
|
+
<h1>Hello, ${() => name()}!</h1>
|
|
18
|
+
<p>
|
|
19
|
+
<label>
|
|
20
|
+
Your name:
|
|
21
|
+
<input
|
|
22
|
+
type="text"
|
|
23
|
+
.value=${() => name()}
|
|
24
|
+
@input=${(e: Event) =>
|
|
25
|
+
name.set((e.target as HTMLInputElement).value)}
|
|
26
|
+
/>
|
|
27
|
+
</label>
|
|
28
|
+
</p>
|
|
29
|
+
<p>The same component model runs in the static landing AND here:</p>
|
|
30
|
+
<feature-card title="Live + static, one model">
|
|
31
|
+
Each <code>feature-card</code> below the title is a real custom
|
|
32
|
+
element with its own open shadow root. It works inside the
|
|
33
|
+
server-rendered snapshot and inside this SPA-only route without
|
|
34
|
+
changing a single line.
|
|
35
|
+
</feature-card>
|
|
36
|
+
<p>And here is a tiny reactive component you can drive locally:</p>
|
|
37
|
+
<p><live-counter></live-counter></p>
|
|
38
|
+
</main>
|
|
39
|
+
`;
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Dynamic static page. `static.paths()` enumerates the slugs that
|
|
2
|
+
// the snapshot pipeline materialises at build time; `initialData()`
|
|
3
|
+
// seeds the rendered HTML so neither the snapshot nor the live boot
|
|
4
|
+
// has to re-fetch the guide body.
|
|
5
|
+
import { html, page, routeUrl } from "@madojs/mado";
|
|
6
|
+
import { findGuide, guides, type Guide } from "../content/guides";
|
|
7
|
+
|
|
8
|
+
type Params = { slug: string };
|
|
9
|
+
|
|
10
|
+
export default page<Params, Guide | null, Guide | null>({
|
|
11
|
+
static: {
|
|
12
|
+
paths: () => guides.map((g) => ({ slug: g.slug })),
|
|
13
|
+
initialData: (params) => findGuide(params.slug) ?? null,
|
|
14
|
+
},
|
|
15
|
+
title: (params) => findGuide(params.slug)?.title ?? "Guide not found",
|
|
16
|
+
head: (_params, seed) => {
|
|
17
|
+
if (!seed) return { title: "Guide not found" };
|
|
18
|
+
return { title: seed.title, description: seed.summary };
|
|
19
|
+
},
|
|
20
|
+
view: ({ data }) => {
|
|
21
|
+
if (!data) {
|
|
22
|
+
return html`
|
|
23
|
+
<main class="document">
|
|
24
|
+
<h1>Guide not found</h1>
|
|
25
|
+
<p><a data-link href=${routeUrl("/")}>Back home</a></p>
|
|
26
|
+
</main>
|
|
27
|
+
`;
|
|
28
|
+
}
|
|
29
|
+
return html`
|
|
30
|
+
<main class="document">
|
|
31
|
+
<p><a data-link href=${routeUrl("/")}>← Home</a></p>
|
|
32
|
+
<article>
|
|
33
|
+
<h1>${data.title}</h1>
|
|
34
|
+
<p class="lead">${data.summary}</p>
|
|
35
|
+
<p>${data.body}</p>
|
|
36
|
+
</article>
|
|
37
|
+
</main>
|
|
38
|
+
`;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Public landing page. `static: true` opts the route into the
|
|
2
|
+
// browser-rendered snapshot pipeline — `mado release` captures this
|
|
3
|
+
// view into `out/index.html` with the rendered Shadow DOM inlined.
|
|
4
|
+
import { html, page, routeUrl } from "@madojs/mado";
|
|
5
|
+
import { guides } from "../content/guides";
|
|
6
|
+
|
|
7
|
+
export default page({
|
|
8
|
+
static: true,
|
|
9
|
+
title: "Mado — a calm native-first web framework",
|
|
10
|
+
head: () => ({
|
|
11
|
+
description:
|
|
12
|
+
"Build with real Web Components, signals, routing, data and forms. " +
|
|
13
|
+
"Ship live SPAs and browser-rendered static documents from one " +
|
|
14
|
+
"component model.",
|
|
15
|
+
}),
|
|
16
|
+
view: () => html`
|
|
17
|
+
<main class="document">
|
|
18
|
+
<header class="hero">
|
|
19
|
+
<h1>Mado</h1>
|
|
20
|
+
<p class="lead">
|
|
21
|
+
A calm native-first web framework for sites and apps.
|
|
22
|
+
</p>
|
|
23
|
+
<p>
|
|
24
|
+
<strong>One component model. One page model. One release command.</strong>
|
|
25
|
+
</p>
|
|
26
|
+
</header>
|
|
27
|
+
|
|
28
|
+
<section class="features">
|
|
29
|
+
${guides.map(
|
|
30
|
+
(guide) => html`
|
|
31
|
+
<feature-card title=${guide.title}>
|
|
32
|
+
${guide.summary}
|
|
33
|
+
<a data-link href=${routeUrl(`/guide/${guide.slug}`)}>
|
|
34
|
+
Read the guide →
|
|
35
|
+
</a>
|
|
36
|
+
</feature-card>
|
|
37
|
+
`,
|
|
38
|
+
)}
|
|
39
|
+
</section>
|
|
40
|
+
|
|
41
|
+
<section class="cta">
|
|
42
|
+
<h2>Open the app</h2>
|
|
43
|
+
<p>
|
|
44
|
+
<a data-link href=${routeUrl("/app")}>Launch the live SPA →</a>
|
|
45
|
+
</p>
|
|
46
|
+
</section>
|
|
47
|
+
</main>
|
|
48
|
+
`,
|
|
49
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Catch-all 404. Not statically captured; the SPA shell serves it
|
|
2
|
+
// whenever a deep link does not match a known route.
|
|
3
|
+
import { html, page, routeUrl } from "@madojs/mado";
|
|
4
|
+
|
|
5
|
+
export default page({
|
|
6
|
+
title: "Page not found",
|
|
7
|
+
head: () => ({
|
|
8
|
+
description: "The page you are looking for does not exist.",
|
|
9
|
+
meta: [{ name: "robots", content: "noindex" }],
|
|
10
|
+
}),
|
|
11
|
+
view: ({ path }) => html`
|
|
12
|
+
<main class="document">
|
|
13
|
+
<h1>404</h1>
|
|
14
|
+
<p>No page is registered for <code>${() => path()}</code>.</p>
|
|
15
|
+
<p><a data-link href=${routeUrl("/")}>Back home</a></p>
|
|
16
|
+
</main>
|
|
17
|
+
`,
|
|
18
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* Document-level layout: header, sections, hero. Component styles
|
|
2
|
+
live inside their own shadow roots, this stylesheet styles the
|
|
3
|
+
light-DOM wrappers only. */
|
|
4
|
+
.document {
|
|
5
|
+
max-width: 56rem;
|
|
6
|
+
margin: 0 auto;
|
|
7
|
+
padding: 3rem 1.5rem;
|
|
8
|
+
display: grid;
|
|
9
|
+
gap: 2.5rem;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.hero h1 {
|
|
13
|
+
margin: 0 0 0.5rem;
|
|
14
|
+
font-size: 3rem;
|
|
15
|
+
letter-spacing: -0.01em;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.hero .lead {
|
|
19
|
+
margin: 0 0 0.75rem;
|
|
20
|
+
font-size: 1.25rem;
|
|
21
|
+
color: var(--mado-fg-muted);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.features {
|
|
25
|
+
display: grid;
|
|
26
|
+
gap: 1rem;
|
|
27
|
+
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.cta {
|
|
31
|
+
padding: 1.5rem;
|
|
32
|
+
border: 1px dashed var(--mado-border);
|
|
33
|
+
border-radius: var(--mado-radius);
|
|
34
|
+
background: var(--mado-surface);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
article p {
|
|
38
|
+
margin: 0.75rem 0;
|
|
39
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* Tiny modern reset. Keep it boring so nothing here surprises a new
|
|
2
|
+
contributor. */
|
|
3
|
+
*,
|
|
4
|
+
*::before,
|
|
5
|
+
*::after {
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
html,
|
|
10
|
+
body {
|
|
11
|
+
margin: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
background: var(--mado-bg);
|
|
16
|
+
color: var(--mado-fg);
|
|
17
|
+
font-family: var(--mado-font-body);
|
|
18
|
+
line-height: 1.5;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
a {
|
|
22
|
+
color: var(--mado-accent);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
button,
|
|
26
|
+
input,
|
|
27
|
+
select,
|
|
28
|
+
textarea {
|
|
29
|
+
font: inherit;
|
|
30
|
+
color: inherit;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
code {
|
|
34
|
+
background: var(--mado-surface);
|
|
35
|
+
padding: 0.05rem 0.3rem;
|
|
36
|
+
border-radius: 4px;
|
|
37
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* Design tokens. Override these to re-skin the app without touching
|
|
2
|
+
component CSS. Custom properties penetrate shadow roots, so every
|
|
3
|
+
component reads them automatically. */
|
|
4
|
+
:root {
|
|
5
|
+
--mado-bg: #fafafa;
|
|
6
|
+
--mado-surface: #ffffff;
|
|
7
|
+
--mado-surface-hover: #f1f5f9;
|
|
8
|
+
--mado-fg: #0f172a;
|
|
9
|
+
--mado-fg-muted: #475569;
|
|
10
|
+
--mado-border: #e2e8f0;
|
|
11
|
+
--mado-accent: #1d4ed8;
|
|
12
|
+
|
|
13
|
+
--mado-radius: 0.625rem;
|
|
14
|
+
|
|
15
|
+
--mado-font-body:
|
|
16
|
+
system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial,
|
|
17
|
+
sans-serif;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@media (prefers-color-scheme: dark) {
|
|
21
|
+
:root {
|
|
22
|
+
--mado-bg: #0b1220;
|
|
23
|
+
--mado-surface: #111827;
|
|
24
|
+
--mado-surface-hover: #1f2937;
|
|
25
|
+
--mado-fg: #e2e8f0;
|
|
26
|
+
--mado-fg-muted: #94a3b8;
|
|
27
|
+
--mado-border: #1f2937;
|
|
28
|
+
--mado-accent: #60a5fa;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
/// <reference types="vite/client" />
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -2,7 +2,20 @@ import { defineConfig } from "vite";
|
|
|
2
2
|
import { mado } from "@madojs/mado/vite";
|
|
3
3
|
|
|
4
4
|
export default defineConfig({
|
|
5
|
-
plugins: [
|
|
5
|
+
plugins: [
|
|
6
|
+
mado({
|
|
7
|
+
// Public origin used to build absolute URLs for static snapshots
|
|
8
|
+
// (sitemap, canonical, OpenGraph). Combined with Vite's `base`,
|
|
9
|
+
// the canonical for a route is `site + base + pathname`.
|
|
10
|
+
//
|
|
11
|
+
// REQUIRED when any page declares `static`. Set to your deployment
|
|
12
|
+
// origin (e.g. https://your-app.example) before running
|
|
13
|
+
// `mado release`. Override per environment via:
|
|
14
|
+
// mado release --base-url https://staging.example
|
|
15
|
+
// MADO_SITE=https://staging.example mado release
|
|
16
|
+
// site: "https://your-app.example",
|
|
17
|
+
}),
|
|
18
|
+
],
|
|
6
19
|
css: {
|
|
7
20
|
transformer: "lightningcss",
|
|
8
21
|
},
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Mado modular starter
|
|
2
|
+
|
|
3
|
+
Reference architecture for long-lived modular business applications:
|
|
4
|
+
admin panels, internal tools, SaaS backoffice. The starter ships the
|
|
5
|
+
opinions a team of more than one person will eventually adopt anyway —
|
|
6
|
+
auth shell, guarded zones, module boundaries, HTTP client and
|
|
7
|
+
contracts.
|
|
8
|
+
|
|
9
|
+
If you are building a website, a landing page, a docs site or any
|
|
10
|
+
small interactive app, prefer the universal starter:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
mado init my-app # universal default
|
|
14
|
+
mado init my-app --starter modular # this starter
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Both starters target the same Mado runtime. They only differ in how
|
|
18
|
+
much structure is pre-created.
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install
|
|
24
|
+
npm run dev # Vite dev server
|
|
25
|
+
npm run build # Vite production SPA build
|
|
26
|
+
npm run release # vite build + browser-rendered snapshots → out/
|
|
27
|
+
npm run preview # serve out/ like a real static host
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`npm run release` requires a public origin so it can build absolute
|
|
31
|
+
canonical URLs. Set it once in `vite.config.ts`:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
mado({ site: "https://your-app.example" })
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
…or override per environment:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
mado release --base-url https://staging.example
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The public home page is captured as a static document; auth and
|
|
44
|
+
billing surfaces stay SPA-only and are served through the
|
|
45
|
+
`_mado/spa.html` fallback.
|
|
46
|
+
|
|
47
|
+
## Dev API mock
|
|
48
|
+
|
|
49
|
+
`vite.config.ts` mounts a tiny in-memory mock for the endpoints used by
|
|
50
|
+
this starter so `npm run dev` is runnable out of the box:
|
|
51
|
+
|
|
52
|
+
| Method + path | Behaviour |
|
|
53
|
+
| ---------------------------------------- | ---------------------------------------------- |
|
|
54
|
+
| `POST /api/auth/login` | accepts `demo@mado.dev` / `demo` |
|
|
55
|
+
| `GET /api/auth/me` | current user (after login) |
|
|
56
|
+
| `POST /api/auth/logout` | clears the in-memory session |
|
|
57
|
+
| `GET /api/billing/stripe/invoices` | lists six seeded invoices |
|
|
58
|
+
| `GET /api/billing/stripe/invoices/:id` | returns a single invoice |
|
|
59
|
+
| `POST /api/billing/stripe/invoices/:id/pay` | flips status to `paid` |
|
|
60
|
+
|
|
61
|
+
The mock only runs under `vite dev`, so `mado release` ships a clean
|
|
62
|
+
bundle that talks to your real backend. Disable the mock with
|
|
63
|
+
`MADO_MOCK_API=0` or remove the `devApiMock()` plugin entry.
|
|
64
|
+
|
|
65
|
+
When you wire up your real backend, mirror the request/response shape
|
|
66
|
+
in `auth.connector.ts` and `stripe.connector.ts`. The contract types in
|
|
67
|
+
`modules/<name>/_contracts/` are the source of truth.
|
|
68
|
+
|
|
69
|
+
## Shape
|
|
70
|
+
|
|
71
|
+
```txt
|
|
72
|
+
public/ static assets copied by Vite
|
|
73
|
+
src/
|
|
74
|
+
main.ts imports global CSS and mounts the router
|
|
75
|
+
app.routes.ts app map: zones, layouts, guards and modules
|
|
76
|
+
layouts/ app-zone shells
|
|
77
|
+
shared/
|
|
78
|
+
http/ HTTP client and interceptors
|
|
79
|
+
lib/ pure utilities
|
|
80
|
+
styles/ tokens, reset, shell and content CSS
|
|
81
|
+
ui/ reusable x-* components
|
|
82
|
+
modules/ business modules
|
|
83
|
+
<name>/
|
|
84
|
+
<name>.routes.ts
|
|
85
|
+
<name>.public.ts
|
|
86
|
+
<name>.types.ts
|
|
87
|
+
pages/
|
|
88
|
+
data/
|
|
89
|
+
api/
|
|
90
|
+
components/
|
|
91
|
+
_contracts/
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## CSS Contract
|
|
95
|
+
|
|
96
|
+
- `tokens.css` defines CSS custom properties and is safe for Shadow DOM
|
|
97
|
+
components through `var(...)`.
|
|
98
|
+
- `reset.css` normalizes the document/light DOM surface.
|
|
99
|
+
- `shell.css` styles app-zone layouts from `src/layouts/`.
|
|
100
|
+
- `content.css` styles page-level light DOM: forms, tables, prose and
|
|
101
|
+
simple states.
|
|
102
|
+
- Reusable leaf components keep their own styles in ``css`...` ``
|
|
103
|
+
inside `component()` options.
|
|
104
|
+
|
|
105
|
+
Vite uses Lightning CSS for CSS transforms/minification in this
|
|
106
|
+
starter.
|
|
107
|
+
|
|
108
|
+
## Internal links
|
|
109
|
+
|
|
110
|
+
Always emit internal links through `routeUrl()` so they pick up the
|
|
111
|
+
deployed Vite `base` automatically:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { routeUrl } from "@madojs/mado";
|
|
115
|
+
|
|
116
|
+
html`<a data-link href=${routeUrl("/billing/invoices")}>Invoices</a>`
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The router only intercepts anchors that opt in with `data-link`; bare
|
|
120
|
+
`<a href>` falls through to a full document load.
|
|
121
|
+
|
|
122
|
+
## Generate files
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm run new -- module billing
|
|
126
|
+
npm run new -- page billing/pages/invoices-list
|
|
127
|
+
npm run new -- connector billing/api/stripe
|
|
128
|
+
npm run new -- resource billing/data/invoices
|
|
129
|
+
npm run new -- service billing/cart
|
|
130
|
+
npm run new -- form billing/invoice
|
|
131
|
+
npm run new -- component billing/components/invoice-status-badge
|
|
132
|
+
npm run new -- guard billing/billing
|
|
133
|
+
npm run new -- layout admin-shell
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The generator writes files only. Wire new routes in
|
|
137
|
+
`src/app.routes.ts` or the module route map by hand.
|
|
138
|
+
|
|
139
|
+
## More
|
|
140
|
+
|
|
141
|
+
The full architecture guide lives in the framework docs:
|
|
142
|
+
https://github.com/madojs/mado/tree/main/docs/en
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" href="/favicon.svg" />
|
|
7
|
+
<title>Mado App</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "mado dev",
|
|
8
|
+
"build": "mado build",
|
|
9
|
+
"typecheck": "mado typecheck",
|
|
10
|
+
"test": "npm run typecheck && npm run lint",
|
|
11
|
+
"release": "mado release",
|
|
12
|
+
"preview": "mado preview",
|
|
13
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
14
|
+
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
|
15
|
+
"new": "mado new"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@madojs/mado": "__MADOJS_VERSION__"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
22
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
23
|
+
"eslint": "^9.0.0",
|
|
24
|
+
"eslint-plugin-boundaries": "^5.0.0",
|
|
25
|
+
"lightningcss": "^1.32.0",
|
|
26
|
+
"playwright-core": "^1.47.0",
|
|
27
|
+
"typescript": "^5.5.0",
|
|
28
|
+
"vite": "^8.0.16"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Single source of truth for the application route table.
|
|
2
|
+
//
|
|
3
|
+
// This file is the APP MAP. Reading it = understanding the app.
|
|
4
|
+
//
|
|
5
|
+
// One rule:
|
|
6
|
+
// Modules export plain `routes` maps. The choice of SHELL and GUARD for
|
|
7
|
+
// each zone of the app is made HERE, by wrapping a module's routes with
|
|
8
|
+
// a `layout({...})` block.
|
|
9
|
+
//
|
|
10
|
+
// `manifest` is exported separately so `mado static` can discover pages that
|
|
11
|
+
// declare `static`.
|
|
12
|
+
|
|
13
|
+
import { layout, routes } from "@madojs/mado";
|
|
14
|
+
|
|
15
|
+
import { requireAuth } from "./modules/auth/auth.public";
|
|
16
|
+
import { authRoutes } from "./modules/auth/auth.routes";
|
|
17
|
+
import { billingRoutes } from "./modules/billing/billing.routes";
|
|
18
|
+
|
|
19
|
+
export const manifest = {
|
|
20
|
+
// Public landing (no shell, no guard).
|
|
21
|
+
"/": () => import("./modules/home/home.page"),
|
|
22
|
+
|
|
23
|
+
// AUTH ZONE — centered card, no guard.
|
|
24
|
+
"/login": layout({
|
|
25
|
+
layout: () => import("./layouts/auth-shell.layout"),
|
|
26
|
+
routes: authRoutes,
|
|
27
|
+
}),
|
|
28
|
+
|
|
29
|
+
// APP ZONE — header + nav, guarded by requireAuth.
|
|
30
|
+
"/billing": layout({
|
|
31
|
+
layout: () => import("./layouts/app-shell.layout"),
|
|
32
|
+
guard: requireAuth,
|
|
33
|
+
routes: billingRoutes,
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
"*": () => import("./modules/home/not-found.page"),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default routes(manifest);
|
|
@@ -9,27 +9,30 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Layouts live in src/layouts/ because they describe APP ZONES, not domains.
|
|
11
11
|
|
|
12
|
-
import { html, page } from "@madojs/mado";
|
|
12
|
+
import { html, page, routeUrl } from "@madojs/mado";
|
|
13
13
|
|
|
14
14
|
import { isAuthed, logout, user } from "../modules/auth/auth.public";
|
|
15
15
|
import "../shared/ui/x-button.component";
|
|
16
16
|
|
|
17
17
|
export default page({
|
|
18
18
|
title: "Mado App",
|
|
19
|
+
// Every internal anchor uses `data-link` + `routeUrl()` so SPA
|
|
20
|
+
// navigation intercepts the click and the href stays correct under
|
|
21
|
+
// any Vite `base` (e.g. deploying the app under `/admin/`).
|
|
19
22
|
view: ({ child }) => html`
|
|
20
23
|
<div class="layout layout--app">
|
|
21
24
|
<header class="app-header">
|
|
22
|
-
<a href
|
|
25
|
+
<a data-link href=${routeUrl("/")} class="brand">Mado App</a>
|
|
23
26
|
<nav>
|
|
24
|
-
<a href
|
|
27
|
+
<a data-link href=${routeUrl("/billing/invoices")}>Invoices</a>
|
|
25
28
|
${() =>
|
|
26
29
|
isAuthed()
|
|
27
30
|
? html`<span class="who">${() => user()?.email ?? ""}</span>
|
|
28
31
|
<x-button variant="ghost" @click=${logout}>Sign out</x-button>`
|
|
29
|
-
: html`<a href
|
|
32
|
+
: html`<a data-link href=${routeUrl("/login")}>Sign in</a>`}
|
|
30
33
|
</nav>
|
|
31
34
|
</header>
|
|
32
35
|
<main class="app-main">${child}</main>
|
|
33
36
|
</div>
|
|
34
37
|
`,
|
|
35
|
-
});
|
|
38
|
+
});
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
// auth ZONE. If tomorrow a `password-reset` module appears, it will reuse
|
|
5
5
|
// this same shell.
|
|
6
6
|
|
|
7
|
-
import { html, page } from "@madojs/mado";
|
|
7
|
+
import { html, page, routeUrl } from "@madojs/mado";
|
|
8
8
|
|
|
9
9
|
export default page({
|
|
10
10
|
title: "Sign in",
|
|
11
11
|
view: ({ child }) => html`
|
|
12
12
|
<div class="layout layout--auth">
|
|
13
|
-
<a href
|
|
13
|
+
<a data-link href=${routeUrl("/")} class="brand">Mado App</a>
|
|
14
14
|
<main class="auth-main">${child}</main>
|
|
15
15
|
</div>
|
|
16
16
|
`,
|
|
17
|
-
});
|
|
17
|
+
});
|