@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,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`.
@@ -0,0 +1,225 @@
1
+ # Data
2
+
3
+ > `resource()` reads, `mutation()` writes, `jsonFetcher()` shapes
4
+ > the wire. Backends, auth and modules all sit on top of this layer.
5
+
6
+ Mado does not ship a generic HTTP client because the browser already
7
+ has one. Instead it ships two small primitives that wrap `fetch()`
8
+ into a cache + lifecycle:
9
+
10
+ - `resource()` — keyed cache, loading / error / data signals, glob
11
+ invalidation, automatic cleanup.
12
+ - `mutation()` — async action runner with the same loading / error /
13
+ data signals, plus declarative invalidation of related resources.
14
+
15
+ A third helper, `jsonFetcher()`, is the default body parser: parses
16
+ JSON, throws `HttpError` on `!ok`.
17
+
18
+ ## `resource()`
19
+
20
+ ```ts
21
+ import { resource, jsonFetcher } from "@madojs/mado";
22
+
23
+ const userId = signal(1);
24
+
25
+ const user = resource(
26
+ () => `/api/users/${userId()}`, // key (reactive — recomputed on change)
27
+ jsonFetcher<User>(), // how to load
28
+ { staleTime: 60_000 }, // cache age
29
+ );
30
+
31
+ user.data(); // User | undefined
32
+ user.error(); // Error | null
33
+ user.loading(); // boolean
34
+
35
+ await user.refresh();
36
+ user.mutate((u) => (u ? { ...u, name: "patched" } : u));
37
+ ```
38
+
39
+ Rules:
40
+
41
+ - The key is the cache identity. Same key → shared cache and shared
42
+ in-flight request, even across components.
43
+ - Distinct data needs distinct keys. Encode query params, auth scope
44
+ and tenant in the key string itself.
45
+ - `staleTime` is "how long after a successful fetch the cached value
46
+ is reused without a re-fetch". `0` means "always re-fetch on
47
+ remount".
48
+ - `resource()` inside a `component()` / `page()` cleans itself up
49
+ when the host leaves the DOM.
50
+
51
+ ## `mutation()`
52
+
53
+ ```ts
54
+ import { mutation } from "@madojs/mado";
55
+
56
+ const save = mutation<User, User>(
57
+ (u) =>
58
+ fetch("/api/users", {
59
+ method: "POST",
60
+ body: JSON.stringify(u),
61
+ }).then((r) => r.json()),
62
+ { invalidates: ["/api/users*"] },
63
+ );
64
+
65
+ await save.run(newUser); // throws on failure
66
+ save.loading(); // true while ANY run is in flight
67
+ save.error(); // last failure
68
+ save.data(); // last success
69
+ ```
70
+
71
+ - `run()` is concurrent by default. `loading()` stays true while any
72
+ run is in flight. Use `{ abortPrevious: true }` for
73
+ search-as-you-type ("latest request wins").
74
+ - `invalidates` runs after a successful mutation and is best-effort:
75
+ invalidation failures are logged but do not turn the mutation
76
+ itself into a failure.
77
+ - `invalidates` may also be a function of `(result, args)` for
78
+ dynamic keys.
79
+
80
+ ## `jsonFetcher()` and `HttpError`
81
+
82
+ ```ts
83
+ import { jsonFetcher, HttpError, resource } from "@madojs/mado";
84
+
85
+ const me = resource(() => "/api/auth/me", jsonFetcher<User>());
86
+
87
+ try {
88
+ await me.refresh();
89
+ } catch (err) {
90
+ if (err instanceof HttpError && err.status === 401) {
91
+ navigate("/login");
92
+ } else throw err;
93
+ }
94
+ ```
95
+
96
+ `HttpError` carries `status`, `url` and the parsed body (JSON when
97
+ possible, raw text otherwise). Use it for typed handling in pages
98
+ and guards.
99
+
100
+ ## Modular shape — connector → resource → page
101
+
102
+ The modular starter encodes one rule per layer; every business
103
+ module follows the same flow:
104
+
105
+ ```
106
+ connector → resource / mutation → page
107
+ ```
108
+
109
+ ```
110
+ src/shared/http/
111
+ http-client.ts # one fetch wrapper + query/body/error handling
112
+ http-error.ts # HttpError type
113
+ interceptors.ts # request/response hooks (auth header, retry, …)
114
+
115
+ src/modules/<name>/
116
+ <name>.connector.ts # external API contract, DTO -> domain
117
+ <name>.resource.ts # resource()/mutation() with cache keys
118
+ <name>.types.ts # domain types
119
+ pages/ # page()s — UI only
120
+ _contracts/ # backend DTO shapes, private to the connector
121
+ ```
122
+
123
+ Hard rules:
124
+
125
+ - Pages **never** import DTOs and **never** call `fetch()`.
126
+ - Connectors **never** import signals, resources, html, components,
127
+ pages or services.
128
+ - Public surface of a module is `<name>.public.ts`. Anything not
129
+ re-exported there is private.
130
+
131
+ The universal starter does not insist on this layering — for a small
132
+ app a single `src/lib/api.ts` is enough. Use the modular shape when
133
+ the app grows enough that "which file owns this fetch?" becomes a
134
+ real question.
135
+
136
+ ## Auth — service + guard
137
+
138
+ Auth state is an ES-module singleton built from the same primitives:
139
+
140
+ ```ts
141
+ // src/modules/auth/auth.service.ts
142
+ import { computed, signal } from "@madojs/mado";
143
+ import { authApi } from "./auth.connector";
144
+ import type { Credentials, User } from "./auth.types";
145
+
146
+ const _user = signal<User | null>(null);
147
+ const _token = signal<string | null>(null);
148
+
149
+ export const user = (): User | null => _user();
150
+ export const isAuthed = computed(() => _user() !== null);
151
+
152
+ export async function login(creds: Credentials): Promise<void> {
153
+ const res = await authApi.login(creds);
154
+ _token.set(res.token);
155
+ _user.set(res.user);
156
+ }
157
+
158
+ export async function logout(): Promise<void> {
159
+ try { await authApi.logout(); } catch { /* best effort */ }
160
+ _token.set(null);
161
+ _user.set(null);
162
+ }
163
+ ```
164
+
165
+ Expose only what other modules need through `auth.public.ts`.
166
+
167
+ The guard is a plain function returning `true | false | string`:
168
+
169
+ ```ts
170
+ // src/modules/auth/auth.guard.ts
171
+ import { isAuthed } from "./auth.service";
172
+
173
+ export function requireAuth(): boolean | string {
174
+ return isAuthed() ? true : "/login";
175
+ }
176
+ ```
177
+
178
+ Mount it in `src/app.routes.ts`:
179
+
180
+ ```ts
181
+ "/billing": layout({
182
+ layout: () => import("./layouts/app-shell.layout"),
183
+ guard: requireAuth,
184
+ routes: billingRoutes,
185
+ }),
186
+ ```
187
+
188
+ Full guard contract: [12-routing.md](./12-routing.md#guards).
189
+
190
+ ## Dev proxy
191
+
192
+ Configure backend proxying in `vite.config.ts`:
193
+
194
+ ```ts
195
+ import { defineConfig } from "vite";
196
+ import { mado } from "@madojs/mado/vite";
197
+
198
+ export default defineConfig({
199
+ plugins: [mado()],
200
+ server: {
201
+ proxy: { "/api": "http://localhost:3000" },
202
+ },
203
+ });
204
+ ```
205
+
206
+ The modular starter ships a dev-only in-memory mock for `/api/auth/*`
207
+ and `/api/billing/*` (`MADO_MOCK_API=0` to disable). Remove it once
208
+ your real backend is wired.
209
+
210
+ ## Rule of thumb
211
+
212
+ - `shared/http` knows HTTP.
213
+ - `*.connector.ts` knows one external system and one DTO shape.
214
+ - `*.resource.ts` knows cache keys and invalidation.
215
+ - `*.page.ts` knows UI.
216
+ - `*.public.ts` is the only cross-module surface.
217
+
218
+ ## Further reading
219
+
220
+ - [12-routing.md](./12-routing.md) — guards, layouts, `routeUrl`.
221
+ - [14-forms.md](./14-forms.md) — `useForm()`, then `mutation().run()`.
222
+ - [21-error-handling.md](./21-error-handling.md) — route / data /
223
+ action error boundaries.
224
+ - [16-app-architecture.md](./16-app-architecture.md) — the modular
225
+ starter shape end-to-end.