@madojs/mado 0.11.1 → 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.
Files changed (219) hide show
  1. package/AGENTS.md +102 -25
  2. package/CHANGELOG.md +158 -1
  3. package/README.md +135 -172
  4. package/dist/src/component.d.ts +3 -2
  5. package/dist/src/component.js +98 -6
  6. package/dist/src/component.js.map +1 -1
  7. package/dist/src/css.js +34 -5
  8. package/dist/src/css.js.map +1 -1
  9. package/dist/src/head.d.ts +6 -6
  10. package/dist/src/head.js +6 -6
  11. package/dist/src/html/parser.d.ts +1 -1
  12. package/dist/src/html/parser.js +1 -1
  13. package/dist/src/html/template.js +24 -10
  14. package/dist/src/html/template.js.map +1 -1
  15. package/dist/src/index.d.ts +8 -4
  16. package/dist/src/index.js +13 -3
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/page.d.ts +62 -28
  19. package/dist/src/page.js +5 -0
  20. package/dist/src/page.js.map +1 -1
  21. package/dist/src/resource.js +2 -1
  22. package/dist/src/resource.js.map +1 -1
  23. package/dist/src/router/base.d.ts +65 -0
  24. package/dist/src/router/base.js +168 -0
  25. package/dist/src/router/base.js.map +1 -0
  26. package/dist/src/router/manifest.js +75 -31
  27. package/dist/src/router/manifest.js.map +1 -1
  28. package/dist/src/router/match.d.ts +16 -2
  29. package/dist/src/router/match.js +41 -1
  30. package/dist/src/router/match.js.map +1 -1
  31. package/dist/src/router/navigation.js +58 -9
  32. package/dist/src/router/navigation.js.map +1 -1
  33. package/dist/src/static-runtime.d.ts +81 -0
  34. package/dist/src/static-runtime.js +209 -0
  35. package/dist/src/static-runtime.js.map +1 -0
  36. package/dist/src/vite/index.d.ts +31 -3
  37. package/dist/src/vite/index.js +49 -18
  38. package/dist/src/vite/index.js.map +1 -1
  39. package/docs/README.md +5 -8
  40. package/docs/architecture/adr/0001-browser-static-snapshots.md +132 -0
  41. package/docs/en/00-the-mado-way.md +1 -1
  42. package/docs/en/01-quickstart.md +183 -0
  43. package/docs/en/10-pages-and-components.md +271 -0
  44. package/docs/en/11-templates-and-signals.md +211 -0
  45. package/docs/en/12-routing.md +229 -0
  46. package/docs/en/13-data.md +225 -0
  47. package/docs/en/14-forms.md +244 -0
  48. package/docs/en/15-static-snapshots.md +181 -0
  49. package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +53 -2
  50. package/docs/en/{13-deployment.md → 20-deployment.md} +59 -10
  51. package/docs/en/{14-testing.md → 22-testing.md} +6 -5
  52. package/docs/en/23-cookbook.md +292 -0
  53. package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +12 -3
  54. package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +3 -3
  55. package/docs/en/40-llm-guide.md +776 -0
  56. package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +15 -16
  57. package/docs/en/{05-why-mado.md → 42-why-mado.md} +1 -1
  58. package/docs/en/README.md +97 -27
  59. package/docs/recipes/nginx/Containerfile +26 -0
  60. package/docs/recipes/nginx/nginx.conf +42 -0
  61. package/llms.txt +246 -211
  62. package/package.json +17 -10
  63. package/scripts/bake.mjs +4 -569
  64. package/scripts/cli/generate.mjs +55 -5
  65. package/scripts/cli/help.mjs +11 -5
  66. package/scripts/cli/index.mjs +20 -4
  67. package/scripts/cli/init.mjs +7 -2
  68. package/scripts/cli/release.mjs +20 -68
  69. package/scripts/docs-lint.mjs +170 -0
  70. package/scripts/package-smoke.mjs +67 -9
  71. package/scripts/preview.mjs +95 -6
  72. package/scripts/size-budget.mjs +5 -1
  73. package/scripts/static/browser.mjs +654 -0
  74. package/scripts/static/discover.mjs +281 -0
  75. package/scripts/static/output.mjs +141 -0
  76. package/scripts/static/serialize.mjs +212 -0
  77. package/scripts/static/server.mjs +167 -0
  78. package/scripts/static.mjs +191 -0
  79. package/starters/default/README.md +57 -55
  80. package/starters/default/package.json +3 -9
  81. package/starters/default/src/app.routes.ts +20 -33
  82. package/starters/default/src/components/feature-card.component.ts +40 -0
  83. package/starters/default/src/components/live-counter.component.ts +57 -0
  84. package/starters/default/src/content/guides.ts +55 -0
  85. package/starters/default/src/main.ts +9 -13
  86. package/starters/default/src/pages/app.page.ts +41 -0
  87. package/starters/default/src/pages/guide.page.ts +40 -0
  88. package/starters/default/src/pages/home.page.ts +49 -0
  89. package/starters/default/src/pages/not-found.page.ts +18 -0
  90. package/starters/default/src/styles/document.css +39 -0
  91. package/starters/default/src/styles/reset.css +37 -0
  92. package/starters/default/src/styles/tokens.css +30 -0
  93. package/starters/default/src/styles.d.ts +3 -1
  94. package/starters/default/src/vite-env.d.ts +1 -1
  95. package/starters/default/vite.config.ts +14 -1
  96. package/starters/modular/README.md +142 -0
  97. package/starters/modular/index.html +13 -0
  98. package/starters/modular/package.json +30 -0
  99. package/starters/modular/src/app.routes.ts +39 -0
  100. package/starters/{default → modular}/src/layouts/app-shell.layout.ts +8 -5
  101. package/starters/{default → modular}/src/layouts/auth-shell.layout.ts +3 -3
  102. package/starters/modular/src/main.ts +16 -0
  103. package/starters/{default → modular}/src/modules/billing/pages/invoices-list.page.ts +3 -3
  104. package/starters/{default → modular}/src/modules/home/home.page.ts +5 -7
  105. package/starters/modular/src/modules/home/not-found.page.ts +14 -0
  106. package/starters/modular/src/styles.d.ts +1 -0
  107. package/starters/modular/src/vite-env.d.ts +1 -0
  108. package/starters/modular/tsconfig.json +24 -0
  109. package/starters/modular/vite.config.ts +131 -0
  110. package/TODO.md +0 -79
  111. package/docs/en/01-routing.md +0 -152
  112. package/docs/en/02-project-layout.md +0 -124
  113. package/docs/en/03-static-bake.md +0 -249
  114. package/docs/en/04-ide-setup.md +0 -162
  115. package/docs/en/07-llm-pitfalls.md +0 -724
  116. package/docs/en/08-llm-zero-history-test.md +0 -56
  117. package/docs/en/09-shadow-vs-light-dom.md +0 -174
  118. package/docs/en/11-layouts.md +0 -113
  119. package/docs/en/12-auth-and-api.md +0 -124
  120. package/docs/en/16-bake-cookbook.md +0 -100
  121. package/docs/en/17-shadow-dom-forms.md +0 -192
  122. package/docs/fr/00-the-mado-way.md +0 -85
  123. package/docs/fr/01-routing.md +0 -120
  124. package/docs/fr/02-project-layout.md +0 -84
  125. package/docs/fr/03-static-bake.md +0 -289
  126. package/docs/fr/04-ide-setup.md +0 -162
  127. package/docs/fr/05-why-mado.md +0 -193
  128. package/docs/fr/06-for-backenders.md +0 -438
  129. package/docs/fr/07-llm-pitfalls.md +0 -625
  130. package/docs/fr/08-llm-zero-history-test.md +0 -38
  131. package/docs/fr/09-shadow-vs-light-dom.md +0 -65
  132. package/docs/fr/10-app-architecture.md +0 -138
  133. package/docs/fr/11-layouts.md +0 -47
  134. package/docs/fr/12-auth-and-api.md +0 -76
  135. package/docs/fr/13-deployment.md +0 -57
  136. package/docs/fr/14-testing.md +0 -41
  137. package/docs/fr/15-error-handling.md +0 -50
  138. package/docs/fr/16-bake-cookbook.md +0 -88
  139. package/docs/fr/17-shadow-dom-forms.md +0 -196
  140. package/docs/fr/18-api-freeze-map.md +0 -63
  141. package/docs/fr/19-reactivity-ordering.md +0 -97
  142. package/docs/fr/20-v1-stability.md +0 -88
  143. package/docs/fr/README.md +0 -27
  144. package/docs/ru/00-the-mado-way.md +0 -84
  145. package/docs/ru/01-routing.md +0 -119
  146. package/docs/ru/02-project-layout.md +0 -84
  147. package/docs/ru/03-static-bake.md +0 -250
  148. package/docs/ru/04-ide-setup.md +0 -144
  149. package/docs/ru/05-why-mado.md +0 -193
  150. package/docs/ru/06-for-backenders.md +0 -428
  151. package/docs/ru/07-llm-pitfalls.md +0 -624
  152. package/docs/ru/08-llm-zero-history-test.md +0 -57
  153. package/docs/ru/09-shadow-vs-light-dom.md +0 -63
  154. package/docs/ru/10-app-architecture.md +0 -152
  155. package/docs/ru/11-layouts.md +0 -47
  156. package/docs/ru/12-auth-and-api.md +0 -75
  157. package/docs/ru/13-deployment.md +0 -66
  158. package/docs/ru/14-testing.md +0 -50
  159. package/docs/ru/15-error-handling.md +0 -56
  160. package/docs/ru/16-bake-cookbook.md +0 -95
  161. package/docs/ru/17-shadow-dom-forms.md +0 -193
  162. package/docs/ru/18-api-freeze-map.md +0 -64
  163. package/docs/ru/19-reactivity-ordering.md +0 -95
  164. package/docs/ru/20-v1-stability.md +0 -82
  165. package/docs/ru/README.md +0 -25
  166. package/docs/uk/00-the-mado-way.md +0 -82
  167. package/docs/uk/01-routing.md +0 -76
  168. package/docs/uk/02-project-layout.md +0 -73
  169. package/docs/uk/03-static-bake.md +0 -48
  170. package/docs/uk/04-ide-setup.md +0 -26
  171. package/docs/uk/05-why-mado.md +0 -34
  172. package/docs/uk/06-for-backenders.md +0 -55
  173. package/docs/uk/07-llm-pitfalls.md +0 -145
  174. package/docs/uk/08-llm-zero-history-test.md +0 -34
  175. package/docs/uk/09-shadow-vs-light-dom.md +0 -58
  176. package/docs/uk/10-app-architecture.md +0 -97
  177. package/docs/uk/11-layouts.md +0 -47
  178. package/docs/uk/12-auth-and-api.md +0 -70
  179. package/docs/uk/13-deployment.md +0 -40
  180. package/docs/uk/14-testing.md +0 -34
  181. package/docs/uk/15-error-handling.md +0 -32
  182. package/docs/uk/16-bake-cookbook.md +0 -36
  183. package/docs/uk/17-shadow-dom-forms.md +0 -193
  184. package/docs/uk/18-api-freeze-map.md +0 -61
  185. package/docs/uk/19-reactivity-ordering.md +0 -95
  186. package/docs/uk/20-v1-stability.md +0 -83
  187. package/docs/uk/README.md +0 -27
  188. package/starters/default/src/modules/home/not-found.page.ts +0 -11
  189. /package/docs/en/{15-error-handling.md → 21-error-handling.md} +0 -0
  190. /package/docs/en/{19-reactivity-ordering.md → 31-reactivity-ordering.md} +0 -0
  191. /package/starters/{default → modular}/.editorconfig +0 -0
  192. /package/starters/{default → modular}/eslint.config.mjs +0 -0
  193. /package/starters/{default → modular}/public/favicon.svg +0 -0
  194. /package/starters/{default → modular}/src/modules/auth/_contracts/auth-api.types.ts +0 -0
  195. /package/starters/{default → modular}/src/modules/auth/auth.connector.ts +0 -0
  196. /package/starters/{default → modular}/src/modules/auth/auth.guard.ts +0 -0
  197. /package/starters/{default → modular}/src/modules/auth/auth.public.ts +0 -0
  198. /package/starters/{default → modular}/src/modules/auth/auth.routes.ts +0 -0
  199. /package/starters/{default → modular}/src/modules/auth/auth.service.ts +0 -0
  200. /package/starters/{default → modular}/src/modules/auth/auth.types.ts +0 -0
  201. /package/starters/{default → modular}/src/modules/auth/login.page.ts +0 -0
  202. /package/starters/{default → modular}/src/modules/billing/_contracts/stripe.types.ts +0 -0
  203. /package/starters/{default → modular}/src/modules/billing/api/stripe.connector.ts +0 -0
  204. /package/starters/{default → modular}/src/modules/billing/billing.public.ts +0 -0
  205. /package/starters/{default → modular}/src/modules/billing/billing.routes.ts +0 -0
  206. /package/starters/{default → modular}/src/modules/billing/billing.types.ts +0 -0
  207. /package/starters/{default → modular}/src/modules/billing/components/invoice-status-badge.component.ts +0 -0
  208. /package/starters/{default → modular}/src/modules/billing/data/invoices.resource.ts +0 -0
  209. /package/starters/{default → modular}/src/modules/billing/pages/invoice-detail.page.ts +0 -0
  210. /package/starters/{default → modular}/src/shared/http/http-client.ts +0 -0
  211. /package/starters/{default → modular}/src/shared/http/http-error.ts +0 -0
  212. /package/starters/{default → modular}/src/shared/http/interceptors.ts +0 -0
  213. /package/starters/{default → modular}/src/shared/lib/format-date.ts +0 -0
  214. /package/starters/{default → modular}/src/shared/styles/content.css +0 -0
  215. /package/starters/{default → modular}/src/shared/styles/reset.css +0 -0
  216. /package/starters/{default → modular}/src/shared/styles/shell.css +0 -0
  217. /package/starters/{default → modular}/src/shared/styles/tokens.css +0 -0
  218. /package/starters/{default → modular}/src/shared/ui/x-button.component.ts +0 -0
  219. /package/starters/{default → modular}/src/shared/ui/x-spinner.component.ts +0 -0
@@ -0,0 +1,132 @@
1
+ # ADR 0001 — Browser-rendered static snapshots
2
+
3
+ **Status:** Accepted (0.12.0)
4
+ **Date:** 2026-06-22
5
+
6
+ ## Context
7
+
8
+ Mado started as a calm browser-native SPA framework for internal tools
9
+ and admin panels. Two new product requirements made that scope too
10
+ narrow:
11
+
12
+ 1. Public-facing surfaces (landing pages, docs sites, product pages)
13
+ need search engines and social preview bots to see a fully rendered
14
+ document on first request, without running JavaScript.
15
+ 2. The same component model that powers the live SPA must produce that
16
+ document — we do not want a second component runtime, a hydration
17
+ protocol, or a server renderer.
18
+
19
+ The conventional answers (Next/Nuxt SSR, Astro, Eleventy, Lit SSR,
20
+ Solid Start, …) each ship at least one of:
21
+
22
+ - a separate component model for the server,
23
+ - a hydration boundary that introduces a second source of truth,
24
+ - a build-time JSX/Vue/Svelte compiler,
25
+ - a Node runtime in production.
26
+
27
+ Mado is positioned as "calm native-first": we cannot adopt any of
28
+ those without breaking the positioning.
29
+
30
+ ## Decision
31
+
32
+ Adopt **browser-rendered static snapshots with Declarative Shadow
33
+ DOM** as the canonical SEO/static path:
34
+
35
+ 1. `mado release` runs `vite build`, then opens each declared static
36
+ route in a real headless Chromium pointed at the build output, and
37
+ serialises the rendered document — including the Shadow DOM as
38
+ Declarative Shadow DOM — into one HTML file per route under
39
+ `out/`.
40
+ 2. The client runtime treats the snapshot as an **atomic takeover**
41
+ target: a Mado component instance reattaches to the existing host
42
+ in place, with no second tree, no hydration, no diff.
43
+ 3. Routes that are not declared `static` stay SPA-only and are served
44
+ through the `_mado/spa.html` fallback the same way they were
45
+ before.
46
+ 4. Vite's `base` is the single source of truth for the public URL
47
+ prefix; the runtime reads it from `import.meta.env.BASE_URL`, the
48
+ CLI reads it from the build bridge (`_mado/build.json`) for
49
+ sitemap / canonical / preview, and the build bridge is dropped
50
+ before the artifact ships.
51
+ 5. Build-time seeds for static routes are strict `JsonValue` only,
52
+ validated by a path-aware walker so a `Date`, `Map`, `undefined`
53
+ field or cycle fails the build instead of silently corrupting the
54
+ first client render.
55
+
56
+ ## Alternatives rejected
57
+
58
+ - **SSR (Next / Nuxt / Solid Start).** Ships a second component model
59
+ and a Node production runtime. Hydration boundary is the largest
60
+ source of recurring bug classes in the ecosystem. Mado's
61
+ positioning rejects both.
62
+
63
+ - **Hydration via an island / partial hydration model (Astro,
64
+ Marko).** Splits the component graph into "static" and "interactive"
65
+ sub-graphs with framework-specific compilers. Adds a second mental
66
+ model for a public-site benefit that DSD already provides.
67
+
68
+ - **Author-time prerendering with `linkedom`/`jsdom`.** Cannot run
69
+ Shadow DOM, adoptedStyleSheets, layout, or any browser API.
70
+ Snapshots would diverge from what the browser actually renders.
71
+
72
+ - **External prerender service (Prerender.io and friends).** Adds an
73
+ external dependency, runs on each request, and Mado has no canonical
74
+ way to detect when to flush its cache.
75
+
76
+ - **Embed a Node runtime that pretends to be a browser.** Same
77
+ divergence problem as `linkedom`, plus a custom DOM polyfill we
78
+ would have to maintain forever.
79
+
80
+ ## Consequences
81
+
82
+ - A compatible Chromium is required at **release time** (not at
83
+ request time, not in production) for projects that declare any
84
+ static route. `mado release` documents how to install one
85
+ (Playwright-managed Chromium, system Chrome, or operator-provided
86
+ path) and CI gates the round-trip with `npx playwright install
87
+ --with-deps chromium` plus `MADO_REQUIRE_BROWSER=1`.
88
+
89
+ - `page.static.paths()` and `page.static.initialData()` must be
90
+ **browser-safe and secret-free**: they execute during the discovery
91
+ pass AND remain in the client bundle. The strict `JsonValue`
92
+ validator catches the common shapes that would otherwise leak.
93
+
94
+ - The framework now has a documented HTTP policy for snapshot capture:
95
+ documents, scripts, stylesheets and tracked resources are fatal;
96
+ favicon-class endpoints are ignored; fonts and paint frames are
97
+ quality hints with bounded timeouts.
98
+
99
+ - The product surface widens from "internal tools / admin panels" to
100
+ "sites and apps". Marketing copy, package metadata, GitHub topics
101
+ and the default starter all change accordingly. The previous
102
+ modular reference architecture is preserved as
103
+ `mado init --starter modular` for long-lived business apps.
104
+
105
+ ## Known limits
106
+
107
+ - Static routes cannot use `Date`, `Map`, `Set`, class instances,
108
+ `undefined` values, cycles, or non-finite numbers in their
109
+ `initialData`. The validator gives a path-aware error.
110
+
111
+ - `mado release` snapshots are deterministic only in a pinned
112
+ environment (same Chromium revision, same source tree). We document
113
+ that determinism contract; reproducing it across CI vendors is the
114
+ operator's responsibility.
115
+
116
+ - Static `paths()` runs at build time. We do not currently support
117
+ incremental re-snapshot from a webhook; a static-content change
118
+ requires a fresh release.
119
+
120
+ - Public client-only state inside a static route (canvas, media
121
+ controllers, transient timers) survives takeover but is not part of
122
+ the snapshot — it materialises only after JS executes.
123
+
124
+ ---
125
+
126
+ ## Original implementation plan (for history)
127
+
128
+ The remainder of this document is the original implementation plan
129
+ that drove the work up to 0.12.0. It is preserved verbatim for
130
+ historical context.
131
+
132
+ ---
@@ -106,7 +106,7 @@ belongs in `*.service.ts`.
106
106
 
107
107
  ### One way to declare routes
108
108
 
109
- See [`01-routing.md`](./01-routing.md).
109
+ See [`12-routing.md`](./12-routing.md).
110
110
 
111
111
  ## What we do NOT do
112
112
 
@@ -0,0 +1,183 @@
1
+ # Quickstart
2
+
3
+ > Goal: have a Mado app running locally, edited from VS Code, and shipped to
4
+ > a static host — in under 10 minutes.
5
+
6
+ ---
7
+
8
+ ## 1. Scaffold
9
+
10
+ Mado 0.12 ships with two starters: the **universal** starter (default) and
11
+ the **modular** reference architecture.
12
+
13
+ ```bash
14
+ # Universal starter (default) — ~15 files, one shared component
15
+ npm exec --package @madojs/mado -- mado init my-app
16
+
17
+ # Modular reference starter — auth shell, guarded zones, billing module
18
+ npm exec --package @madojs/mado -- mado init my-app --starter modular
19
+
20
+ cd my-app
21
+ npm install
22
+ ```
23
+
24
+ What you get either way:
25
+
26
+ - exactly one runtime dependency: `@madojs/mado`;
27
+ - `vite.config.ts` with `mado()` from `@madojs/mado/vite`;
28
+ - `src/main.ts` mounting the router into `#app`;
29
+ - `src/app.routes.ts` (a default export + a named `manifest` export);
30
+ - a `package.json` whose scripts wrap the Mado CLI.
31
+
32
+ ---
33
+
34
+ ## 2. The dev loop
35
+
36
+ ```bash
37
+ npm run dev # Vite dev server, fast HMR for templates/styles
38
+ npm run typecheck # tsc --noEmit
39
+ npm run test # node --test (if your project has tests)
40
+ npm run build # SPA build only (out/ without snapshots)
41
+ npm run release # typecheck + vite build + static snapshots + deploy files
42
+ npm run preview # serve out/ like a real static host
43
+ ```
44
+
45
+ `mado release` is the single command you ship. It produces one folder,
46
+ `out/`, that you can `rsync`/upload to any static host:
47
+
48
+ ```bash
49
+ npm run release
50
+ rsync -avz out/ user@server:/var/www/my-app/
51
+ ```
52
+
53
+ See [20-deployment.md](./20-deployment.md) for nginx / Cloudflare Pages /
54
+ GitHub Pages / S3 recipes.
55
+
56
+ ---
57
+
58
+ ## 3. Your first page
59
+
60
+ Two primitives, that's it (see [10-pages-and-components.md](./10-pages-and-components.md)
61
+ for the full mental model):
62
+
63
+ - **`page()`** — a route. Lives at a URL. Renders in light DOM. Has
64
+ `title` / `head` / `load` / `view` / optional `static`.
65
+ - **`component()`** — a reusable `<x-tag>`. Shadow DOM by default.
66
+
67
+ ```ts
68
+ // src/pages/home.page.ts
69
+ import { html, page } from "@madojs/mado";
70
+
71
+ export default page({
72
+ title: "Home",
73
+ head: () => ({ description: "Hello from Mado" }),
74
+ view: () => html`<h1>Hello, Mado</h1>`,
75
+ });
76
+ ```
77
+
78
+ ```ts
79
+ // src/app.routes.ts
80
+ import { routes } from "@madojs/mado";
81
+
82
+ export const manifest = {
83
+ "/": () => import("./pages/home.page"),
84
+ "*": () => import("./pages/not-found.page"),
85
+ };
86
+
87
+ export default routes(manifest);
88
+ ```
89
+
90
+ Always export both `default` (for the router) and `manifest` (so
91
+ `mado static` can discover bakeable pages).
92
+
93
+ Internal links must be base-aware:
94
+
95
+ ```ts
96
+ import { routeUrl } from "@madojs/mado";
97
+
98
+ html`<a data-link href=${routeUrl("/about")}>About</a>`;
99
+ ```
100
+
101
+ `data-link` lets the router intercept the click — including across
102
+ Shadow DOM. `routeUrl()` resolves against `import.meta.env.BASE_URL`,
103
+ so the same code works whether your app is hosted at `/` or at
104
+ `/docs/`.
105
+
106
+ ---
107
+
108
+ ## 4. IDE setup
109
+
110
+ Out of the box `html` and `css` are tagged-template strings. TypeScript
111
+ and most editors don't highlight them by default. The lit ecosystem's
112
+ tools work because Mado uses the same tag names and binding shapes.
113
+
114
+ ### VS Code (recommended)
115
+
116
+ 1. **Extensions → install [lit-plugin](https://marketplace.visualstudio.com/items?itemName=runem.lit-plugin)** (by `runem`).
117
+ 2. Import `html` / `css` directly — no rename:
118
+
119
+ ```ts
120
+ import { html, css } from "@madojs/mado";
121
+ ```
122
+
123
+ 3. (Optional) Recommended settings for Mado in `.vscode/settings.json`:
124
+
125
+ ```json
126
+ {
127
+ "lit-plugin.rules": {
128
+ "no-unknown-attribute": "off",
129
+ "no-incompatible-type-binding": "off"
130
+ }
131
+ }
132
+ ```
133
+
134
+ `no-incompatible-type-binding: off` is needed because Mado bindings
135
+ accept signals (`Signal<T>`) where the plugin expects raw values.
136
+
137
+ You get: HTML/CSS highlighting inside templates, attribute/event
138
+ auto-complete, go-to-definition for custom elements, typo checking.
139
+
140
+ ### WebStorm / IntelliJ
141
+
142
+ Native support for lit-style template literals since 2021. Nothing to
143
+ install. If highlighting is missing, restart the TS server.
144
+
145
+ ### Neovim / Helix
146
+
147
+ ```bash
148
+ npm install -g lit-html-server
149
+ ```
150
+
151
+ Wire it into your LSP config (`lit_html` in `lspconfig`).
152
+
153
+ ### Optional extras
154
+
155
+ - **Custom Elements Manifest** for auto-complete on your own
156
+ `<x-*>` components: `npx cem analyze --globs "src/**/*.ts"`.
157
+ - **Prettier 3+** formats `html`/`css` template literals via
158
+ `embeddedLanguageFormatting: "auto"` (default).
159
+ - **eslint-plugin-lit** + **eslint-plugin-wc** provide
160
+ template-aware lint rules. Not required.
161
+
162
+ ### What does NOT work today
163
+
164
+ - Type-checking of signal bindings: `<input .value=${count}>` is
165
+ flagged because the plugin expects a string, not a `Signal<number>`.
166
+ Suppress with `// @ts-expect-error` or the settings override above.
167
+ - The `each()` directive is recognised as a plain function — no
168
+ special-case checking inside.
169
+
170
+ Mado works without any IDE plugin — the templates remain valid TS,
171
+ they just won't be syntax-highlighted as HTML.
172
+
173
+ ---
174
+
175
+ ## 5. Where to go next
176
+
177
+ - [10 — Pages and components](./10-pages-and-components.md) — the one rule.
178
+ - [11 — Templates and signals](./11-templates-and-signals.md) — binding shapes.
179
+ - [12 — Routing](./12-routing.md) — manifest, layouts, guards, prefetch.
180
+ - [13 — Data](./13-data.md) — `resource()` / `mutation()` / auth.
181
+ - [14 — Forms](./14-forms.md) — `useForm()` + Shadow DOM input recipes.
182
+ - [15 — Static snapshots](./15-static-snapshots.md) — SEO without SSR.
183
+ - [16 — App architecture](./16-app-architecture.md) — the modular starter, ESLint boundaries.
@@ -0,0 +1,271 @@
1
+ # Pages and Components
2
+
3
+ > Mado has exactly two primitives. You decide which one to use by
4
+ > looking at the URL bar, not at the DOM tree.
5
+
6
+ This is the document that removes the most common Mado design
7
+ question. By the end of it you should never again have to think
8
+ "should this be a page or a component?", "should this be Shadow DOM
9
+ or Light DOM?", or "how do I share styles across components?". You
10
+ just write the thing.
11
+
12
+ ---
13
+
14
+ ## The one rule
15
+
16
+ | If the unit is… | Use |
17
+ | -------------------------------------------------------- | ----------- |
18
+ | Something the URL points to (a route, a layout, a 404) | `page()` |
19
+ | Something you would copy-paste under multiple URLs | `component()` |
20
+
21
+ That's it. Pages are the URLs your app exposes. Components are
22
+ re-usable Web Components.
23
+
24
+ A page can render any number of components. A component can never
25
+ participate in routing. Pages live in `src/pages/` (universal
26
+ starter) or in `src/modules/<name>/pages/` (modular starter).
27
+ Components live in `src/components/` or `src/modules/<name>/components/`.
28
+
29
+ You **never** wrap a page in `component()` — that breaks form
30
+ participation, shared CSS and the static snapshot capture. You
31
+ **never** route to a `component()` — only `page()` shows up in the
32
+ router manifest.
33
+
34
+ ---
35
+
36
+ ## You write the same TypeScript either way
37
+
38
+ ```ts
39
+ import { html, page, signal } from "@madojs/mado";
40
+
41
+ // PAGE — has a URL, owns route + load + head + view
42
+ export default page({
43
+ title: "Counter",
44
+ view: () => {
45
+ const n = signal(0);
46
+ return html`
47
+ <main>
48
+ <h1>Counter</h1>
49
+ <button @click=${() => n.set(n() + 1)}>Clicks: ${n}</button>
50
+ </main>
51
+ `;
52
+ },
53
+ });
54
+ ```
55
+
56
+ ```ts
57
+ import { component, css, html, signal } from "@madojs/mado";
58
+
59
+ // COMPONENT — reusable, has no URL
60
+ component(
61
+ "x-counter",
62
+ () => {
63
+ const n = signal(0);
64
+ return () => html`
65
+ <button @click=${() => n.set(n() + 1)}>Clicks: ${n}</button>
66
+ `;
67
+ },
68
+ { styles: css`button { padding: .5rem 1rem; }` },
69
+ );
70
+ ```
71
+
72
+ Same signals. Same `html\`\`` templates. Same lifecycle. Same data
73
+ model (`resource()`, `mutation()`, `useForm()`, `effect()`). The
74
+ only thing that changes is *where* the DOM lives.
75
+
76
+ ---
77
+
78
+ ## What changes under the hood
79
+
80
+ | Property | `page()` | `component()` (default) |
81
+ | ---------------------------------------------- | ------------ | ------------------------------ |
82
+ | DOM location | Light DOM | Open Shadow DOM |
83
+ | Sees global CSS (`shell.css`, `content.css`) | Yes | No (Shadow boundary) |
84
+ | Can use `<slot>` | No (uses `child`) | Yes |
85
+ | Participates in a parent `<form>` | Yes | Only via `attachInternals()` or `shadow:false` |
86
+ | Is captured by `mado static` | If `static: true \| { ... }` | Yes (DSD serialised) |
87
+ | Has a hyphenated custom-element tag | No | Yes (`x-foo`, `my-bar`) |
88
+ | Owns its own CSS isolation | No | Yes (via `css\`\``) |
89
+
90
+ That is the entire decision matrix. Anything beyond it is detail.
91
+
92
+ ---
93
+
94
+ ## `{ shadow: false }` — the only escape hatch you need
95
+
96
+ Sometimes a custom element MUST live in the light DOM. Two real
97
+ cases:
98
+
99
+ 1. **Form participation without `attachInternals()`.** A custom
100
+ input that should be submitted as part of a regular `<form>` and
101
+ whose author does not want to wire up `ElementInternals`.
102
+ 2. **Host-level CSS that the document must address by tag name**
103
+ (rare; usually solved by passing class attributes instead).
104
+
105
+ For both, declare `{ shadow: false }`:
106
+
107
+ ```ts
108
+ component(
109
+ "x-custom-input",
110
+ () => () => html`<input name="email" />`,
111
+ { shadow: false, styles: css`x-custom-input input { width: 100%; }` },
112
+ );
113
+ ```
114
+
115
+ That is the *only* place this option is justified. If you reach for
116
+ `shadow: false` because you want to share document-level CSS classes,
117
+ **stop** — you wanted a `page()`, not a component.
118
+
119
+ Page-shaped wrappers (layouts, route shells) are written with
120
+ `page({ view: ({ child }) => ... })`. They are not components.
121
+
122
+ ---
123
+
124
+ ## Decision table
125
+
126
+ | You are writing… | Reach for |
127
+ | --------------------------------------------- | --------------------------------------------- |
128
+ | A landing page | `page({ static: true, view })` |
129
+ | A dynamic SEO page (`/product/:slug`) | `page({ static: { paths, initialData } })` |
130
+ | An app screen behind auth | `page({ view })` (no `static`) |
131
+ | A shared shell that wraps several pages | `page({ view: ({ child }) => html\`...\` })` |
132
+ | A reusable button / badge / card / modal | `component("x-foo", setup, { styles })` |
133
+ | A custom form input | `component("x-input", setup, { shadow: false })` |
134
+ | A small inline render helper (no state) | a plain `(arg) => html\`...\`` function |
135
+
136
+ When in doubt, ask: **does this thing have a URL?** Yes → page.
137
+ No → component.
138
+
139
+ ---
140
+
141
+ ## Anti-patterns
142
+
143
+ These are the four mistakes Mado generators and AI assistants tend
144
+ to produce. Avoid them.
145
+
146
+ ### 1. Page inside a `component()`
147
+
148
+ ```ts
149
+ // ❌ Don't
150
+ component(
151
+ "x-home-page",
152
+ () => () => html`<h1>Home</h1><p>...</p>`,
153
+ );
154
+
155
+ // ✅ Do
156
+ export default page({
157
+ static: true,
158
+ title: "Home",
159
+ view: () => html`<h1>Home</h1><p>...</p>`,
160
+ });
161
+ ```
162
+
163
+ Why: the page becomes invisible to the router, the static snapshot
164
+ pipeline cannot find it, and shared CSS (`content.css`) does not
165
+ cross the Shadow boundary.
166
+
167
+ ### 2. Component used as a route
168
+
169
+ ```ts
170
+ // ❌ Don't
171
+ import "./components/billing-screen.component.js";
172
+ const manifest = { "/billing": () => html`<x-billing-screen/>` };
173
+
174
+ // ✅ Do
175
+ import billing from "./pages/billing.page.js";
176
+ const manifest = { "/billing": billing };
177
+ ```
178
+
179
+ Why: the router gives you `page.head()`, `page.load()`, the static
180
+ snapshot pipeline, the `data-mado-head` lifecycle and the seed
181
+ contract. A component does not.
182
+
183
+ ### 3. `shadow: false` to "fix" CSS in a route layout
184
+
185
+ ```ts
186
+ // ❌ Don't — this is a page wearing the wrong hat
187
+ component(
188
+ "x-app-shell",
189
+ () => ({ child }) => html`<header>...</header><main>${child}</main>`,
190
+ { shadow: false, styles: css`...` },
191
+ );
192
+
193
+ // ✅ Do
194
+ export default page({
195
+ view: ({ child }) => html`<header>...</header><main>${child}</main>`,
196
+ });
197
+ ```
198
+
199
+ Why: light-DOM components do not get the route `child`, are not
200
+ recognised by `layout()`, and the manifest cannot wrap them.
201
+
202
+ ### 4. Route-id state in layout view locals
203
+
204
+ ```ts
205
+ // ❌ Don't — `current` is shared across every page rendered by this layout
206
+ const current = signal<User | null>(null);
207
+ export default page({
208
+ view: ({ child }) => {
209
+ effect(() => loadUser(routePath()).then(current.set));
210
+ return html`<x-shell .user=${current}>${child}</x-shell>`;
211
+ },
212
+ });
213
+
214
+ // ✅ Do — put the resource in the page that needs it
215
+ const user = resource(() => `/api/users/${userId()}`, jsonFetcher<User>());
216
+ ```
217
+
218
+ Layouts are stateless wrappers. Per-page state belongs in the page
219
+ itself or in a `resource()` whose key is the page's identity.
220
+
221
+ ---
222
+
223
+ ## What about styles?
224
+
225
+ - `page()` lives in the light DOM. Use `content.css` / `shell.css`
226
+ from `src/styles/` (universal starter) or `src/shared/styles/`
227
+ (modular starter). Class selectors work normally.
228
+ - `component()` carries its own `css\`\``. CSS custom properties
229
+ (`--accent`, `--bg`) cross the Shadow boundary; class selectors
230
+ from the document do not.
231
+ - The shared design tokens (colours, spacing, type) live in
232
+ `tokens.css`. They are CSS custom properties and reach both pages
233
+ and components through `var(--...)`.
234
+
235
+ For the long form on style boundaries, see
236
+ [20-deployment.md](./20-deployment.md) (production tuning) and the
237
+ starter README files.
238
+
239
+ ---
240
+
241
+ ## What about routing and links?
242
+
243
+ Internal navigation is base-aware. Always use `routeUrl()` and
244
+ `data-link`:
245
+
246
+ ```ts
247
+ import { html, routeUrl } from "@madojs/mado";
248
+
249
+ html`<a data-link href=${routeUrl("/billing/invoices")}>Invoices</a>`;
250
+ html`<a data-link href=${routeUrl("/")}>Home</a>`; // → "/mado/" under base
251
+ ```
252
+
253
+ `data-link` opts the anchor into SPA navigation. A bare `<a href>`
254
+ performs a full document load — that is intentional for foreign
255
+ links and downloads. The router intercepts links inside Shadow DOM
256
+ too (it uses `event.composedPath()`).
257
+
258
+ Full router contract: [12-routing.md](./12-routing.md).
259
+
260
+ ---
261
+
262
+ ## Further reading
263
+
264
+ - [11-templates-and-signals.md](./11-templates-and-signals.md) — the
265
+ `html\`\`` parser, signals, `each`, reactive bindings.
266
+ - [12-routing.md](./12-routing.md) — `routes()`, layouts, guards,
267
+ `routeUrl`, `navigate`, prefetch.
268
+ - [15-static-snapshots.md](./15-static-snapshots.md) — when to mark
269
+ a page `static: true`.
270
+ - [14-forms.md](./14-forms.md) — `useForm()` + the only Shadow DOM
271
+ case where `shadow: false` is right.