@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.
Files changed (221) hide show
  1. package/AGENTS.md +102 -25
  2. package/CHANGELOG.md +185 -1
  3. package/README.md +137 -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/bindings.js +3 -3
  12. package/dist/src/html/bindings.js.map +1 -1
  13. package/dist/src/html/parser.d.ts +1 -1
  14. package/dist/src/html/parser.js +1 -1
  15. package/dist/src/html/template.js +24 -10
  16. package/dist/src/html/template.js.map +1 -1
  17. package/dist/src/index.d.ts +8 -4
  18. package/dist/src/index.js +13 -3
  19. package/dist/src/index.js.map +1 -1
  20. package/dist/src/page.d.ts +62 -28
  21. package/dist/src/page.js +5 -0
  22. package/dist/src/page.js.map +1 -1
  23. package/dist/src/resource.js +2 -1
  24. package/dist/src/resource.js.map +1 -1
  25. package/dist/src/router/base.d.ts +65 -0
  26. package/dist/src/router/base.js +168 -0
  27. package/dist/src/router/base.js.map +1 -0
  28. package/dist/src/router/manifest.js +75 -31
  29. package/dist/src/router/manifest.js.map +1 -1
  30. package/dist/src/router/match.d.ts +16 -2
  31. package/dist/src/router/match.js +41 -1
  32. package/dist/src/router/match.js.map +1 -1
  33. package/dist/src/router/navigation.js +58 -9
  34. package/dist/src/router/navigation.js.map +1 -1
  35. package/dist/src/static-runtime.d.ts +81 -0
  36. package/dist/src/static-runtime.js +209 -0
  37. package/dist/src/static-runtime.js.map +1 -0
  38. package/dist/src/vite/index.d.ts +31 -3
  39. package/dist/src/vite/index.js +49 -18
  40. package/dist/src/vite/index.js.map +1 -1
  41. package/docs/README.md +5 -8
  42. package/docs/architecture/adr/0001-browser-static-snapshots.md +132 -0
  43. package/docs/en/00-the-mado-way.md +1 -1
  44. package/docs/en/01-quickstart.md +183 -0
  45. package/docs/en/10-pages-and-components.md +271 -0
  46. package/docs/en/11-templates-and-signals.md +211 -0
  47. package/docs/en/12-routing.md +229 -0
  48. package/docs/en/13-data.md +225 -0
  49. package/docs/en/14-forms.md +244 -0
  50. package/docs/en/15-static-snapshots.md +181 -0
  51. package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +53 -2
  52. package/docs/en/{13-deployment.md → 20-deployment.md} +59 -10
  53. package/docs/en/{14-testing.md → 22-testing.md} +6 -5
  54. package/docs/en/23-cookbook.md +292 -0
  55. package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +12 -3
  56. package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +3 -3
  57. package/docs/en/40-llm-guide.md +776 -0
  58. package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +15 -16
  59. package/docs/en/{05-why-mado.md → 42-why-mado.md} +1 -1
  60. package/docs/en/README.md +97 -27
  61. package/docs/recipes/nginx/Containerfile +26 -0
  62. package/docs/recipes/nginx/nginx.conf +42 -0
  63. package/llms.txt +246 -211
  64. package/package.json +17 -10
  65. package/scripts/bake.mjs +4 -568
  66. package/scripts/cli/generate.mjs +55 -5
  67. package/scripts/cli/help.mjs +11 -5
  68. package/scripts/cli/index.mjs +20 -4
  69. package/scripts/cli/init.mjs +7 -2
  70. package/scripts/cli/release.mjs +20 -68
  71. package/scripts/docs-lint.mjs +170 -0
  72. package/scripts/package-smoke.mjs +67 -9
  73. package/scripts/preview.mjs +95 -6
  74. package/scripts/size-budget.mjs +5 -1
  75. package/scripts/static/browser.mjs +654 -0
  76. package/scripts/static/discover.mjs +281 -0
  77. package/scripts/static/output.mjs +141 -0
  78. package/scripts/static/serialize.mjs +212 -0
  79. package/scripts/static/server.mjs +167 -0
  80. package/scripts/static.mjs +191 -0
  81. package/starters/default/README.md +57 -55
  82. package/starters/default/package.json +3 -9
  83. package/starters/default/src/app.routes.ts +20 -33
  84. package/starters/default/src/components/feature-card.component.ts +40 -0
  85. package/starters/default/src/components/live-counter.component.ts +57 -0
  86. package/starters/default/src/content/guides.ts +55 -0
  87. package/starters/default/src/main.ts +9 -13
  88. package/starters/default/src/pages/app.page.ts +41 -0
  89. package/starters/default/src/pages/guide.page.ts +40 -0
  90. package/starters/default/src/pages/home.page.ts +49 -0
  91. package/starters/default/src/pages/not-found.page.ts +18 -0
  92. package/starters/default/src/styles/document.css +39 -0
  93. package/starters/default/src/styles/reset.css +37 -0
  94. package/starters/default/src/styles/tokens.css +30 -0
  95. package/starters/default/src/styles.d.ts +3 -1
  96. package/starters/default/src/vite-env.d.ts +1 -1
  97. package/starters/default/vite.config.ts +14 -1
  98. package/starters/modular/README.md +142 -0
  99. package/starters/modular/index.html +13 -0
  100. package/starters/modular/package.json +30 -0
  101. package/starters/modular/src/app.routes.ts +39 -0
  102. package/starters/{default → modular}/src/layouts/app-shell.layout.ts +8 -5
  103. package/starters/{default → modular}/src/layouts/auth-shell.layout.ts +3 -3
  104. package/starters/modular/src/main.ts +16 -0
  105. package/starters/{default → modular}/src/modules/billing/pages/invoices-list.page.ts +3 -3
  106. package/starters/{default → modular}/src/modules/home/home.page.ts +5 -7
  107. package/starters/modular/src/modules/home/not-found.page.ts +14 -0
  108. package/starters/modular/src/styles.d.ts +1 -0
  109. package/starters/modular/src/vite-env.d.ts +1 -0
  110. package/starters/modular/tsconfig.json +24 -0
  111. package/starters/modular/vite.config.ts +131 -0
  112. package/TODO.md +0 -79
  113. package/docs/en/01-routing.md +0 -152
  114. package/docs/en/02-project-layout.md +0 -124
  115. package/docs/en/03-static-bake.md +0 -249
  116. package/docs/en/04-ide-setup.md +0 -162
  117. package/docs/en/07-llm-pitfalls.md +0 -724
  118. package/docs/en/08-llm-zero-history-test.md +0 -56
  119. package/docs/en/09-shadow-vs-light-dom.md +0 -174
  120. package/docs/en/11-layouts.md +0 -113
  121. package/docs/en/12-auth-and-api.md +0 -124
  122. package/docs/en/16-bake-cookbook.md +0 -100
  123. package/docs/en/17-shadow-dom-forms.md +0 -192
  124. package/docs/fr/00-the-mado-way.md +0 -85
  125. package/docs/fr/01-routing.md +0 -120
  126. package/docs/fr/02-project-layout.md +0 -84
  127. package/docs/fr/03-static-bake.md +0 -289
  128. package/docs/fr/04-ide-setup.md +0 -162
  129. package/docs/fr/05-why-mado.md +0 -193
  130. package/docs/fr/06-for-backenders.md +0 -438
  131. package/docs/fr/07-llm-pitfalls.md +0 -625
  132. package/docs/fr/08-llm-zero-history-test.md +0 -38
  133. package/docs/fr/09-shadow-vs-light-dom.md +0 -65
  134. package/docs/fr/10-app-architecture.md +0 -138
  135. package/docs/fr/11-layouts.md +0 -47
  136. package/docs/fr/12-auth-and-api.md +0 -76
  137. package/docs/fr/13-deployment.md +0 -57
  138. package/docs/fr/14-testing.md +0 -41
  139. package/docs/fr/15-error-handling.md +0 -50
  140. package/docs/fr/16-bake-cookbook.md +0 -88
  141. package/docs/fr/17-shadow-dom-forms.md +0 -196
  142. package/docs/fr/18-api-freeze-map.md +0 -63
  143. package/docs/fr/19-reactivity-ordering.md +0 -97
  144. package/docs/fr/20-v1-stability.md +0 -88
  145. package/docs/fr/README.md +0 -27
  146. package/docs/ru/00-the-mado-way.md +0 -84
  147. package/docs/ru/01-routing.md +0 -119
  148. package/docs/ru/02-project-layout.md +0 -84
  149. package/docs/ru/03-static-bake.md +0 -250
  150. package/docs/ru/04-ide-setup.md +0 -144
  151. package/docs/ru/05-why-mado.md +0 -193
  152. package/docs/ru/06-for-backenders.md +0 -428
  153. package/docs/ru/07-llm-pitfalls.md +0 -624
  154. package/docs/ru/08-llm-zero-history-test.md +0 -57
  155. package/docs/ru/09-shadow-vs-light-dom.md +0 -63
  156. package/docs/ru/10-app-architecture.md +0 -152
  157. package/docs/ru/11-layouts.md +0 -47
  158. package/docs/ru/12-auth-and-api.md +0 -75
  159. package/docs/ru/13-deployment.md +0 -66
  160. package/docs/ru/14-testing.md +0 -50
  161. package/docs/ru/15-error-handling.md +0 -56
  162. package/docs/ru/16-bake-cookbook.md +0 -95
  163. package/docs/ru/17-shadow-dom-forms.md +0 -193
  164. package/docs/ru/18-api-freeze-map.md +0 -64
  165. package/docs/ru/19-reactivity-ordering.md +0 -95
  166. package/docs/ru/20-v1-stability.md +0 -82
  167. package/docs/ru/README.md +0 -25
  168. package/docs/uk/00-the-mado-way.md +0 -82
  169. package/docs/uk/01-routing.md +0 -76
  170. package/docs/uk/02-project-layout.md +0 -73
  171. package/docs/uk/03-static-bake.md +0 -48
  172. package/docs/uk/04-ide-setup.md +0 -26
  173. package/docs/uk/05-why-mado.md +0 -34
  174. package/docs/uk/06-for-backenders.md +0 -55
  175. package/docs/uk/07-llm-pitfalls.md +0 -145
  176. package/docs/uk/08-llm-zero-history-test.md +0 -34
  177. package/docs/uk/09-shadow-vs-light-dom.md +0 -58
  178. package/docs/uk/10-app-architecture.md +0 -97
  179. package/docs/uk/11-layouts.md +0 -47
  180. package/docs/uk/12-auth-and-api.md +0 -70
  181. package/docs/uk/13-deployment.md +0 -40
  182. package/docs/uk/14-testing.md +0 -34
  183. package/docs/uk/15-error-handling.md +0 -32
  184. package/docs/uk/16-bake-cookbook.md +0 -36
  185. package/docs/uk/17-shadow-dom-forms.md +0 -193
  186. package/docs/uk/18-api-freeze-map.md +0 -61
  187. package/docs/uk/19-reactivity-ordering.md +0 -95
  188. package/docs/uk/20-v1-stability.md +0 -83
  189. package/docs/uk/README.md +0 -27
  190. package/starters/default/src/modules/home/not-found.page.ts +0 -11
  191. /package/docs/en/{15-error-handling.md → 21-error-handling.md} +0 -0
  192. /package/docs/en/{19-reactivity-ordering.md → 31-reactivity-ordering.md} +0 -0
  193. /package/starters/{default → modular}/.editorconfig +0 -0
  194. /package/starters/{default → modular}/eslint.config.mjs +0 -0
  195. /package/starters/{default → modular}/public/favicon.svg +0 -0
  196. /package/starters/{default → modular}/src/modules/auth/_contracts/auth-api.types.ts +0 -0
  197. /package/starters/{default → modular}/src/modules/auth/auth.connector.ts +0 -0
  198. /package/starters/{default → modular}/src/modules/auth/auth.guard.ts +0 -0
  199. /package/starters/{default → modular}/src/modules/auth/auth.public.ts +0 -0
  200. /package/starters/{default → modular}/src/modules/auth/auth.routes.ts +0 -0
  201. /package/starters/{default → modular}/src/modules/auth/auth.service.ts +0 -0
  202. /package/starters/{default → modular}/src/modules/auth/auth.types.ts +0 -0
  203. /package/starters/{default → modular}/src/modules/auth/login.page.ts +0 -0
  204. /package/starters/{default → modular}/src/modules/billing/_contracts/stripe.types.ts +0 -0
  205. /package/starters/{default → modular}/src/modules/billing/api/stripe.connector.ts +0 -0
  206. /package/starters/{default → modular}/src/modules/billing/billing.public.ts +0 -0
  207. /package/starters/{default → modular}/src/modules/billing/billing.routes.ts +0 -0
  208. /package/starters/{default → modular}/src/modules/billing/billing.types.ts +0 -0
  209. /package/starters/{default → modular}/src/modules/billing/components/invoice-status-badge.component.ts +0 -0
  210. /package/starters/{default → modular}/src/modules/billing/data/invoices.resource.ts +0 -0
  211. /package/starters/{default → modular}/src/modules/billing/pages/invoice-detail.page.ts +0 -0
  212. /package/starters/{default → modular}/src/shared/http/http-client.ts +0 -0
  213. /package/starters/{default → modular}/src/shared/http/http-error.ts +0 -0
  214. /package/starters/{default → modular}/src/shared/http/interceptors.ts +0 -0
  215. /package/starters/{default → modular}/src/shared/lib/format-date.ts +0 -0
  216. /package/starters/{default → modular}/src/shared/styles/content.css +0 -0
  217. /package/starters/{default → modular}/src/shared/styles/reset.css +0 -0
  218. /package/starters/{default → modular}/src/shared/styles/shell.css +0 -0
  219. /package/starters/{default → modular}/src/shared/styles/tokens.css +0 -0
  220. /package/starters/{default → modular}/src/shared/ui/x-button.component.ts +0 -0
  221. /package/starters/{default → modular}/src/shared/ui/x-spinner.component.ts +0 -0
@@ -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.
@@ -0,0 +1,211 @@
1
+ # Templates and signals
2
+
3
+ > Reactivity in Mado is one primitive — `signal()` — composed into
4
+ > three reading patterns. Templates are tagged `html\`\`` literals
5
+ > the browser understands directly.
6
+
7
+ By the end of this page you should be able to read and write any
8
+ Mado view without surprises.
9
+
10
+ ## Signals
11
+
12
+ A signal is a getter function with a `.set()` method.
13
+
14
+ ```ts
15
+ import { signal, computed, effect } from "@madojs/mado";
16
+
17
+ const count = signal(0);
18
+
19
+ count(); // 0 — read
20
+ count.set(5); // — write
21
+ count.update((n) => n + 1); // 6 — derived write
22
+ count.peek(); // 6 — read without subscribing
23
+
24
+ const doubled = computed(() => count() * 2);
25
+ doubled(); // 12
26
+
27
+ effect(() => console.log("count =", count()));
28
+ // → "count = 6"
29
+ count.set(7);
30
+ // → "count = 7"
31
+ ```
32
+
33
+ Rules of thumb:
34
+
35
+ - A signal is a **function**: `count()`, not `count.value`.
36
+ - `effect()` and `computed()` track every signal read **during their
37
+ callback**. There is no dependency array.
38
+ - `computed()` is lazy and de-duplicates: if its result is `Object.is`-
39
+ equal to the previous one, subscribers do not re-run.
40
+ - `untracked(() => ...)` reads without subscribing. Use it inside
41
+ async callbacks created from a synchronous `view()` so the router's
42
+ render effect does not accidentally subscribe.
43
+ - `batch(() => { a.set(...); b.set(...); })` flushes once after the
44
+ callback returns. `flushSync()` flushes pending effects immediately.
45
+
46
+ `effect()` cleanups run before the next run and on disposal. Inside a
47
+ component or a page they happen automatically when the host leaves
48
+ the DOM; outside, register the disposer through
49
+ `getCurrentLifecycle()` or pass an explicit one to a
50
+ `createLifecycle()` you own.
51
+
52
+ ## Templates
53
+
54
+ `html\`\`` parses once per template literal into a static fragment +
55
+ binding indices, then patches only the changing slots on each
56
+ re-render.
57
+
58
+ ```ts
59
+ import { html } from "@madojs/mado";
60
+
61
+ html`<h1>Hello, ${name}</h1>`;
62
+ ```
63
+
64
+ Five binding shapes — and only these:
65
+
66
+ | Shape | Meaning |
67
+ | -------------------- | ----------------------------------------------------------------------- |
68
+ | `${value}` | Child content: text, nodes, arrays, nested `html`, `each()` results. |
69
+ | `attr=${v}` | HTML attribute. String / number / falsy (`false`/`null`/`undefined` → remove). |
70
+ | `.prop=${v}` | DOM property. Use for `.value` of inputs, arrays, objects, numbers. |
71
+ | `?attr=${flag}` | Boolean attribute. `true` → present, `false` → absent (`?disabled`). |
72
+ | `@event=${fn}` | Event listener. Removed automatically on re-render. |
73
+
74
+ ### The single most common mistake
75
+
76
+ ```ts
77
+ const count = signal(0);
78
+
79
+ // ❌ NOT REACTIVE — count() is read once at render
80
+ html`<div>${count() * 2}</div>`
81
+
82
+ // ✅ REACTIVE — the function is re-invoked on signal change
83
+ html`<div>${() => count() * 2}</div>`
84
+
85
+ // ✅ ALSO OK — the signal itself IS a function
86
+ html`<div>${count}</div>`
87
+ ```
88
+
89
+ Rule of thumb: if you wrote `signal()` (with parens) inside `${...}`,
90
+ wrap the whole expression in `() => ...`.
91
+
92
+ ### Lists — `each(items, keyFn, renderFn)`
93
+
94
+ ```ts
95
+ import { each } from "@madojs/mado";
96
+
97
+ html`<ul>
98
+ ${() =>
99
+ each(
100
+ users(),
101
+ (u) => u.id,
102
+ (u) => html`<li>${u.name}</li>`,
103
+ )}
104
+ </ul>`;
105
+ ```
106
+
107
+ `each()` is keyed: rows keep DOM identity across re-orders, so input
108
+ focus, scroll position and component state survive. A plain `.map()`
109
+ works but is not keyed; avoid it for anything but throwaway lists.
110
+
111
+ ### Directives
112
+
113
+ Inline helpers you import alongside `html`:
114
+
115
+ - `unsafeHTML(string)` — interpolate trusted HTML.
116
+ - `ref((el) => …)` — get a callback when the element mounts.
117
+ - `classMap({ active: isActive(), error: hasError() })` — toggle
118
+ class names declaratively.
119
+ - `styleMap({ color: theme().fg, "--bg": theme().bg })` — set
120
+ inline styles, including custom properties.
121
+
122
+ ### Parser hard errors
123
+
124
+ Mado refuses templates it cannot represent safely. Two cases you
125
+ will hit:
126
+
127
+ ```ts
128
+ // ❌ slots inside RAW_TEXT elements
129
+ html`<textarea>${draft}</textarea>`;
130
+ html`<title>${title}</title>`;
131
+
132
+ // ✅ use properties / page APIs
133
+ html`<textarea .value=${draft}></textarea>`;
134
+ page({ title: ({ id }) => `User ${id}`, view: () => html`<main></main>` });
135
+
136
+ // ❌ nested SVG-only templates (namespace context is lost)
137
+ html`<svg>${html`<circle r="5"></circle>`}</svg>`;
138
+
139
+ // ✅ keep SVG internals in one template
140
+ html`<svg viewBox="0 0 10 10"><circle r="5"></circle></svg>`;
141
+ ```
142
+
143
+ No dynamic `${...}` child slots inside `<script>`, `<style>`,
144
+ `<textarea>` or `<title>`.
145
+
146
+ ## How the two compose
147
+
148
+ ```ts
149
+ import { html, page, signal } from "@madojs/mado";
150
+
151
+ export default page({
152
+ title: "Counter",
153
+ view: () => {
154
+ // 1. local state
155
+ const n = signal(0);
156
+
157
+ // 2. event handlers
158
+ const inc = () => n.update((x) => x + 1);
159
+
160
+ // 3. view — values that change are wrapped in arrow functions
161
+ return html`
162
+ <main>
163
+ <h1>Counter</h1>
164
+ <button @click=${inc} ?disabled=${() => n() >= 10}>${n}</button>
165
+ </main>
166
+ `;
167
+ },
168
+ });
169
+ ```
170
+
171
+ A page's `view()` runs **once** when the route commits; the returned
172
+ template stays mounted until the route changes. Every reactive slot
173
+ re-evaluates independently — Mado never re-runs the whole view.
174
+
175
+ ## Lifecycle reads
176
+
177
+ Inside a `component(setup, …)` you get the lifecycle through `ctx`:
178
+
179
+ ```ts
180
+ component("x-timer", (ctx) => {
181
+ const tick = signal(0);
182
+ const id = setInterval(() => tick.update((n) => n + 1), 1000);
183
+ ctx.onDispose(() => clearInterval(id));
184
+ return () => html`<span>${tick}</span>`;
185
+ });
186
+ ```
187
+
188
+ Inside a `page({ view })` use the same `onDispose` from the view
189
+ context:
190
+
191
+ ```ts
192
+ export default page({
193
+ view: ({ onDispose }) => {
194
+ const handler = () => { /* … */ };
195
+ window.addEventListener("resize", handler);
196
+ onDispose?.(() => window.removeEventListener("resize", handler));
197
+ return html`<main>…</main>`;
198
+ },
199
+ });
200
+ ```
201
+
202
+ `resource()`, `effect()` and `mutation()` subscribe to the active
203
+ lifecycle automatically — you do not write cleanup for them.
204
+
205
+ ## Further reading
206
+
207
+ - [10-pages-and-components.md](./10-pages-and-components.md) — the
208
+ page vs component decision.
209
+ - [13-data.md](./13-data.md) — `resource()` and `mutation()`.
210
+ - [31-reactivity-ordering.md](./31-reactivity-ordering.md) — signal
211
+ scheduling, equality and teardown guarantees.
@@ -0,0 +1,229 @@
1
+ # Routing
2
+
3
+ > One app map. No folder scanners. No special path syntax. Layouts
4
+ > are pages that render `${child}`.
5
+
6
+ Mado does not infer routes from files. Route composition lives in
7
+ one file — `src/app.routes.ts` — so the whole map is readable at a
8
+ glance.
9
+
10
+ ## The app manifest
11
+
12
+ ```ts
13
+ // src/app.routes.ts
14
+ import { layout, routes } from "@madojs/mado";
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
+ "/": () => import("./modules/home/home.page"),
21
+ "/login": layout({
22
+ layout: () => import("./layouts/auth-shell.layout"),
23
+ routes: authRoutes,
24
+ }),
25
+ "/billing": layout({
26
+ layout: () => import("./layouts/app-shell.layout"),
27
+ guard: requireAuth,
28
+ routes: billingRoutes,
29
+ }),
30
+ "*": () => import("./modules/home/not-found.page"),
31
+ };
32
+
33
+ export default routes(manifest);
34
+ ```
35
+
36
+ Opening `app.routes.ts` shows the whole app: public pages, auth
37
+ zone, protected zones, guards and shells.
38
+
39
+ Exporting `manifest` is **required**: `mado static` discovers
40
+ static routes by reading it.
41
+
42
+ ## Module routes
43
+
44
+ Modules export plain route maps. They do not decide which shell
45
+ wraps them; that happens in `app.routes.ts`.
46
+
47
+ ```ts
48
+ // src/modules/billing/billing.routes.ts
49
+ export const billingRoutes = {
50
+ "/invoices": () => import("./pages/invoices-list.page"),
51
+ "/invoices/:id": () => import("./pages/invoice-detail.page"),
52
+ };
53
+ ```
54
+
55
+ When mounted under `"/billing"` the public URLs become
56
+ `/billing/invoices` and `/billing/invoices/:id`.
57
+
58
+ ## Layouts (the one blessed path)
59
+
60
+ A layout is a `page({ view })` that renders `${ctx.child}` somewhere:
61
+
62
+ ```ts
63
+ // src/layouts/app-shell.layout.ts
64
+ import { html, page } from "@madojs/mado";
65
+
66
+ export default page({
67
+ view: ({ child }) => html`
68
+ <header class="app-header">…</header>
69
+ <main class="app-main">${child}</main>
70
+ `,
71
+ });
72
+ ```
73
+
74
+ Mount it through `layout({ layout, routes, guard? })` in
75
+ `app.routes.ts`. That is the **only** canonical place to declare a
76
+ layout.
77
+
78
+ Two corollaries that prevent the classic "navigation appears below
79
+ content" bug:
80
+
81
+ - **Order matters.** Outer manifest entries wrap inner ones.
82
+ - **One shell per group.** If a subtree needs a different shell,
83
+ open a new `layout({...})` group; do not branch inside one shell.
84
+
85
+ Two acceptable alternatives exist but are escape hatches, not the
86
+ default:
87
+
88
+ - *Single shell in `main.ts`* — `render(html\`<x-shell>${routes.view}</x-shell>\`)`.
89
+ Caveat: every route lives inside one shell; no centred login page,
90
+ no marketing landing without admin chrome.
91
+ - *Per-page wrapping inside `view`* — each page repeats the shell.
92
+ Caveat: the first time someone forgets it the layout disappears.
93
+ Do not start with this.
94
+
95
+ Layouts are stateless wrappers. Per-page state belongs in the page
96
+ or in a `resource()` keyed by the page's identity — never in layout
97
+ view locals.
98
+
99
+ ## What goes on the right side
100
+
101
+ ### Lazy page import (preferred)
102
+
103
+ ```ts
104
+ "/users/:id": () => import("./modules/users/pages/user-profile.page"),
105
+ ```
106
+
107
+ Vite emits a separate chunk per dynamic import in production.
108
+
109
+ ### Eager page
110
+
111
+ ```ts
112
+ import home from "./modules/home/home.page";
113
+
114
+ export const manifest = { "/": home };
115
+ ```
116
+
117
+ Use only for tiny critical pages.
118
+
119
+ ### Layout group
120
+
121
+ ```ts
122
+ "/admin": layout({
123
+ layout: () => import("./layouts/app-shell.layout"),
124
+ guard: requireAuth,
125
+ routes: adminRoutes,
126
+ }),
127
+ ```
128
+
129
+ ## Page contract
130
+
131
+ ```ts
132
+ import { html, page } from "@madojs/mado";
133
+
134
+ export default page<{ id: string }>({
135
+ title: ({ id }) => `User ${id}`,
136
+ view: ({ params }) => html`<h1>${params.id}</h1>`,
137
+ });
138
+ ```
139
+
140
+ If a lazy route does not default-export `page({...})`, `routes()`
141
+ throws a clear error at load time.
142
+
143
+ ## Guards
144
+
145
+ A guard is a function returning `true | false | string | { redirect, replace? } | { halt }`:
146
+
147
+ ```ts
148
+ export function requireAuth(): boolean | string {
149
+ return isAuthed() ? true : "/login";
150
+ }
151
+ ```
152
+
153
+ Guards apply outer → inner: layout-group guards run before page
154
+ guards. Static routes refuse all guards (they are public by
155
+ definition).
156
+
157
+ ## Internal links — `routeUrl()` + `data-link`
158
+
159
+ Vite's `base` flows into `appBase` and `routeUrl()` automatically.
160
+ Internal links MUST use both:
161
+
162
+ ```ts
163
+ import { html, routeUrl } from "@madojs/mado";
164
+
165
+ html`<a data-link href=${routeUrl("/users/42")}>User 42</a>`;
166
+ html`<a data-link href=${routeUrl("/")}>Home</a>`; // → "/mado/" under base
167
+ ```
168
+
169
+ - `routeUrl(path)` returns a base-prefixed URL (preserves query and
170
+ hash).
171
+ - `data-link` opts the anchor into SPA navigation. A bare
172
+ `<a href>` performs a full document load — intentional for
173
+ foreign URLs and downloads.
174
+
175
+ The router intercepts links inside open Shadow DOM (it uses
176
+ `event.composedPath()`).
177
+
178
+ ## Programmatic navigation
179
+
180
+ ```ts
181
+ import appRoutes from "./app.routes";
182
+ import { navigate } from "@madojs/mado";
183
+
184
+ appRoutes.navigate("/billing/invoices");
185
+ appRoutes.navigate("/billing/invoices?page=2");
186
+ appRoutes.navigate("/login", { replace: true });
187
+
188
+ // Standalone helper — dispatches popstate, every active router updates.
189
+ navigate("/users/42");
190
+ ```
191
+
192
+ Both accept route paths (no base prefix). The active base is
193
+ re-applied internally before `history.pushState`.
194
+
195
+ ## Query parameters
196
+
197
+ ```ts
198
+ import { queryParam } from "@madojs/mado";
199
+
200
+ const page = queryParam("page", "1");
201
+ page(); // current value (reactive)
202
+ page.set("2"); // history.replaceState
203
+ page.set("3", { push: true }); // history.pushState
204
+ page.set(null); // delete the parameter
205
+ ```
206
+
207
+ `queryParam()` returns a `Signal<string>`. Reading inside a
208
+ template subscribes; updating triggers a re-render.
209
+
210
+ ## Prefetch
211
+
212
+ Hover-prefetch is on by default for `<a data-link>`. Programmatic
213
+ prefetch:
214
+
215
+ ```ts
216
+ import { prefetchPath } from "@madojs/mado";
217
+ prefetchPath("/billing/invoices/123");
218
+ ```
219
+
220
+ Foreign-base links (outside the active prefix) are ignored — the
221
+ manifest only knows base-free route paths.
222
+
223
+ ## What is intentionally absent
224
+
225
+ - No auto-scan of page folders.
226
+ - No filesystem route syntax (`[id]`, `(group)`, `_layout`).
227
+ - No server routes in the client manifest.
228
+ - No hidden layout discovery — app zones are explicit in
229
+ `app.routes.ts`.