@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,776 @@
1
+ # Mado · LLM guide
2
+
3
+ > Pitfalls that AI assistants (Copilot, Claude, ChatGPT, Cursor) hit when
4
+ > generating Mado code, and a practical zero-history test you can hand to a
5
+ > fresh model.
6
+
7
+ Current stable release: **0.12.0**.
8
+
9
+ This document is for **two audiences**:
10
+
11
+ 1. **AI agents in the IDE** that read `AGENTS.md` / `.cursorrules` /
12
+ `.github/copilot-instructions.md`. The agent entrypoints are intentionally
13
+ short; this file is the long version.
14
+ 2. **Humans** who received code from an AI with these errors and don't
15
+ understand what's wrong.
16
+
17
+ If anything below doesn't fit a real Mado codebase, open `mado/src/` and read
18
+ the relevant 500 lines. Mado is intentionally small to be readable.
19
+
20
+ ---
21
+
22
+ ## Part 1 — Pitfalls
23
+
24
+ ### Pitfall #1: `${signal()}` instead of `${() => signal()}`
25
+
26
+ **Symptom:** the value renders once and never updates.
27
+
28
+ ```ts
29
+ const count = signal(0);
30
+
31
+ // ❌ AI often generates this
32
+ html`<div>Count: ${count() * 2}</div>`;
33
+ // → Renders "Count: 0" once. count() is read when the TemplateResult is built.
34
+
35
+ // ✅ Correct — getter function
36
+ html`<div>Count: ${() => count() * 2}</div>`;
37
+ // → Mado wraps this in an effect() and re-renders when count changes.
38
+
39
+ // ✅ Also correct — the signal itself is a function
40
+ html`<div>Count: ${count}</div>`;
41
+ ```
42
+
43
+ **Rule:**
44
+
45
+ - If `${...}` contains an **expression** (anything done with the signal) —
46
+ wrap it in `() => ...`.
47
+ - If `${...}` contains **the signal itself** — pass it as-is.
48
+
49
+ This applies to **child bindings** (text inside tags) and to **attribute
50
+ values** (`@click`, `.prop`, `?attr`, regular attributes).
51
+
52
+ ---
53
+
54
+ ### Pitfall #2: `<button disabled=${loading}>` instead of `?disabled`
55
+
56
+ **Symptom:** the button is not disabled, or is always disabled.
57
+
58
+ ```ts
59
+ const loading = signal(false);
60
+
61
+ // ❌ setAttribute("disabled", "false") — the DOM treats this as disabled
62
+ html`<button disabled=${loading()}>Save</button>`;
63
+
64
+ // ✅ boolean binding (toggles the attribute)
65
+ html`<button ?disabled=${loading}>Save</button>`;
66
+ ```
67
+
68
+ | Prefix | What it does | When to use |
69
+ | --------- | ------------------------------------- | ---------------------------------------- |
70
+ | `attr=` | `setAttribute("attr", value)` | strings, numbers, URLs |
71
+ | `.attr=` | `el.attr = value` (DOM property) | objects, arrays, `input.value` |
72
+ | `?attr=` | toggle attribute (by truthiness) | `disabled`, `hidden`, `checked`, etc. |
73
+ | `@evt=` | `addEventListener("evt", fn)` | event handlers |
74
+
75
+ ---
76
+
77
+ ### Pitfall #3: useState / useEffect style
78
+
79
+ **Symptom:** AI generates React-shaped code that doesn't work in Mado.
80
+
81
+ ```ts
82
+ // ❌ AI often writes this
83
+ function Counter() {
84
+ const [count, setCount] = useState(0);
85
+ useEffect(() => { console.log(count); }, [count]);
86
+ return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
87
+ }
88
+
89
+ // ✅ Correct in Mado
90
+ import { component, signal, effect, html } from "@madojs/mado";
91
+
92
+ component("x-counter", () => {
93
+ const count = signal(0);
94
+ effect(() => console.log(count())); // auto-subscribes, auto-disposed
95
+ return () => html`
96
+ <button @click=${() => count.update(c => c + 1)}>${count}</button>
97
+ `;
98
+ });
99
+ ```
100
+
101
+ Key differences:
102
+
103
+ - No hooks, no hook rules.
104
+ - `signal()` can be created anywhere — setup, effect, handler.
105
+ - `effect()` discovers its dependencies on its own; no dependency array.
106
+ - A component is `component("x-name", setup)`, not a JSX function.
107
+
108
+ ---
109
+
110
+ ### Pitfall #4: `useEffect(() => { ... return cleanup })` for unmount
111
+
112
+ **Symptom:** AI returns `cleanup` from an `effect()` expecting React-style
113
+ unmount cleanup.
114
+
115
+ ```ts
116
+ // ❌ This is per-run cleanup, NOT unmount cleanup
117
+ component("x-timer", () => {
118
+ effect(() => {
119
+ const id = setInterval(..., 1000);
120
+ return () => clearInterval(id); // runs before next effect re-run
121
+ });
122
+ return () => html`...`;
123
+ });
124
+
125
+ // ✅ Use ctx.onDispose for unmount
126
+ component("x-timer", (ctx) => {
127
+ const id = setInterval(..., 1000);
128
+ ctx.onDispose(() => clearInterval(id));
129
+ return () => html`...`;
130
+ });
131
+ ```
132
+
133
+ `effect()` does support `return cleanup`, but it runs before the next
134
+ execution of the same effect, not on disconnect. For unmount use
135
+ `ctx.onDispose`.
136
+
137
+ ---
138
+
139
+ ### Pitfall #5: Component as a class or with a decorator
140
+
141
+ **Symptom:** AI emits Lit-style or vanilla `HTMLElement` subclasses.
142
+
143
+ ```ts
144
+ // ❌ "let's do it like Lit"
145
+ @customElement('x-counter')
146
+ class XCounter extends LitElement { ... }
147
+
148
+ // ❌ "let's do it vanilla"
149
+ class XCounter extends HTMLElement {
150
+ connectedCallback() { ... }
151
+ }
152
+ customElements.define("x-counter", XCounter);
153
+
154
+ // ✅ Functional component()
155
+ import { component, html, signal } from "@madojs/mado";
156
+
157
+ component("x-counter", () => {
158
+ const count = signal(0);
159
+ return () => html`<button @click=${() => count.update(n => n + 1)}>${count}</button>`;
160
+ });
161
+ ```
162
+
163
+ ---
164
+
165
+ ### Pitfall #6: imports without the `.js` extension
166
+
167
+ **Symptom:** TypeScript compiles but the browser 404s.
168
+
169
+ ```ts
170
+ // ❌ AI often omits the extension
171
+ import { foo } from "./bar";
172
+ import { Home } from "./pages/home";
173
+
174
+ // ✅ ES modules in the browser require the extension
175
+ import { foo } from "./bar.js";
176
+ import { Home } from "./pages/home.js";
177
+ ```
178
+
179
+ TypeScript resolves `./bar.js` back to `./bar.ts` at compile time.
180
+
181
+ ---
182
+
183
+ ### Pitfall #7: lists via `.map()` without keys
184
+
185
+ **Symptom:** reordering items loses input focus / breaks CSS transitions /
186
+ gets slow.
187
+
188
+ ```ts
189
+ // ❌ Unkeyed — recreates DOM on every change
190
+ html`<ul>${() => items().map((t) => html`<li>${t.name}</li>`)}</ul>`;
191
+
192
+ // ✅ each() with a key function
193
+ import { each } from "@madojs/mado";
194
+ html`<ul>
195
+ ${() =>
196
+ each(
197
+ items(),
198
+ (t) => t.id,
199
+ (t) => html`<li>${t.name}</li>`,
200
+ )}
201
+ </ul>`;
202
+ ```
203
+
204
+ Use `each()` for any dynamic list with stable IDs. Reserve `.map()` for
205
+ static lists.
206
+
207
+ ---
208
+
209
+ ### Pitfall #8: `signal.value` or `count.get()`
210
+
211
+ ```ts
212
+ const count = signal(0);
213
+
214
+ // ❌ No such API
215
+ count.value;
216
+ count.value = 5;
217
+ count.get();
218
+
219
+ // ✅ Correct
220
+ count(); // read
221
+ count.set(5); // write
222
+ count.update((n) => n + 1);
223
+ count.peek(); // read without subscribing
224
+ ```
225
+
226
+ ---
227
+
228
+ ### Pitfall #9: `provide(Ctx, value)` without host
229
+
230
+ ```ts
231
+ // ❌ AI forgets host
232
+ provide(ApiCtx, myApi);
233
+ inject(ApiCtx);
234
+
235
+ // ✅ First argument is the host (the current component)
236
+ component("x-app", ({ host }) => {
237
+ provide(host, ApiCtx, myApi);
238
+ return () => html`...`;
239
+ });
240
+
241
+ component("x-child", ({ host }) => {
242
+ const api = inject(host, ApiCtx); // Signal<value>
243
+ return () => html`...`;
244
+ });
245
+ ```
246
+
247
+ ---
248
+
249
+ ### Pitfall #10: expecting SSR
250
+
251
+ **Symptom:** AI writes code assuming pages are pre-rendered on a server.
252
+
253
+ ```ts
254
+ // ❌ Don't do this — assumes universal/server rendering
255
+ if (typeof window !== "undefined") { ... }
256
+ ```
257
+
258
+ Mado **does not do SSR with hydration**. Page logic does not run on a
259
+ server — there is `mado static`, which renders the page in a real
260
+ Chromium at release time and freezes the resulting HTML (including
261
+ Shadow DOM via Declarative Shadow DOM). On first paint the live app
262
+ performs an **atomic takeover** of the snapshot — not hydration, not
263
+ reconciliation, not per-attribute diffing.
264
+
265
+ This means:
266
+
267
+ - ✅ `window`, `document`, `location`, `fetch` are always available.
268
+ - ❌ Don't gate logic on `typeof window`.
269
+ - ❌ Don't use Next.js patterns (`getServerSideProps`, `headers()`).
270
+ - ⚠️ `static.paths()` and `static.initialData()` run **in the snapshot
271
+ browser AND in the user bundle**. Keep them browser-safe and
272
+ secret-free.
273
+
274
+ ---
275
+
276
+ ### Pitfall #11: `useForm()` with a zod/yup resolver
277
+
278
+ ```ts
279
+ // ❌ No such API
280
+ const f = useForm({ resolver: zodResolver(schema) });
281
+
282
+ // ✅ HTML-style schema validation
283
+ const f = useForm({
284
+ email: { required: true, type: "email" },
285
+ age: { required: true, type: "number", min: 18 },
286
+ });
287
+
288
+ // ✅ Or a custom function when HTML5 isn't enough
289
+ const f = useForm(
290
+ { name: { required: true } },
291
+ {
292
+ validate: (values) => {
293
+ const errors: Record<string, string> = {};
294
+ if (values.name && /\d/.test(values.name as string)) {
295
+ errors.name = "Name must not contain digits";
296
+ }
297
+ return Object.keys(errors).length ? errors : null;
298
+ },
299
+ },
300
+ );
301
+ ```
302
+
303
+ ---
304
+
305
+ ### Pitfall #12: Tailwind / styled-components / CSS Modules
306
+
307
+ **Symptom:** AI suggests typical React CSS solutions.
308
+
309
+ Mado uses **Shadow DOM + `css\`\`` + CSS variables** by default. Global
310
+ class systems (Tailwind, Bootstrap) only reach a component if it opts out
311
+ of the shadow root via `{ shadow: false }`:
312
+
313
+ ```ts
314
+ // Light-DOM screen — Tailwind classes apply
315
+ component(
316
+ "x-admin-page",
317
+ () => () => html`
318
+ <section class="bg-white shadow-lg rounded-lg p-4">...</section>
319
+ `,
320
+ { shadow: false },
321
+ );
322
+
323
+ // Shadow-DOM component (default) — Tailwind does NOT reach inside.
324
+ // Style with css`` and customize from outside via ::part() / CSS variables.
325
+ component("x-button", () => () => html`<button><slot></slot></button>`, {
326
+ styles: css`
327
+ button {
328
+ background: var(--button-bg, #2563eb);
329
+ color: white;
330
+ padding: 0.5rem 1rem;
331
+ border-radius: 6px;
332
+ }
333
+ `,
334
+ });
335
+ ```
336
+
337
+ The full mental model lives in
338
+ [10-pages-and-components.md](./10-pages-and-components.md).
339
+
340
+ ---
341
+
342
+ ### Pitfall #13: `import * as Mado from "@madojs/mado"`
343
+
344
+ Works, but duplicates names and tree-shakes poorly. Prefer named imports:
345
+
346
+ ```ts
347
+ // ✅ Canonical
348
+ import { signal, html, component, css, page } from "@madojs/mado";
349
+
350
+ // ⚠️ Works, but heavy
351
+ import * as Mado from "@madojs/mado";
352
+ Mado.signal(0);
353
+ ```
354
+
355
+ ---
356
+
357
+ ### Pitfall #14: adding a runtime dependency
358
+
359
+ Mado is **zero runtime deps** by design. If AI proposes:
360
+
361
+ - **lodash** → use native JS (`Object.entries`, `structuredClone`, etc.);
362
+ - **date-fns** → `Intl.DateTimeFormat`, `Intl.RelativeTimeFormat`;
363
+ - **uuid** → `crypto.randomUUID()`;
364
+ - **axios** → native `fetch` + `jsonFetcher()` from Mado;
365
+ - **classnames** → template literal or an object map.
366
+
367
+ Runtime deps in user projects are fine. Runtime deps in `@madojs/mado`
368
+ core are not.
369
+
370
+ ---
371
+
372
+ ### Pitfall #15: inline `<style>` inside page templates
373
+
374
+ ```ts
375
+ // ❌ Works, but scales poorly and complicates cleanup
376
+ page({
377
+ view: () => html`
378
+ <style>.panel { padding: 1rem; }</style>
379
+ <section class="panel">...</section>
380
+ `,
381
+ });
382
+
383
+ // ✅ Move it into a component with css``
384
+ component(
385
+ "x-admin-panel",
386
+ () => () => html`<section class="panel">...</section>`,
387
+ {
388
+ styles: css`.panel { padding: 1rem; }`,
389
+ },
390
+ );
391
+ ```
392
+
393
+ For admin screens that intentionally use shared layout/form/table CSS,
394
+ make the page component `{ shadow: false }`. If the layout uses
395
+ `<slot>` to project the page, keep the layout in Shadow DOM and put
396
+ its chrome styles in `styles: css\`\``.
397
+
398
+ ---
399
+
400
+ ### Pitfall #16: Shadow DOM links without `data-link`
401
+
402
+ **Symptom:** clicking a link inside a Web Component causes a full reload,
403
+ or hover-prefetch never fires.
404
+
405
+ ```ts
406
+ // ❌ Browser will perform a full reload
407
+ html`<a href="/tickets/42">Open</a>`;
408
+
409
+ // ✅ Router intercepts the click, even across Shadow DOM
410
+ html`<a href=${routeUrl("/tickets/42")} data-link>Open</a>`;
411
+ ```
412
+
413
+ Mado finds the link via `event.composedPath()`, so `data-link` works
414
+ inside Shadow DOM. Hover-prefetch uses the same path. Use
415
+ `data-no-prefetch` to opt a single link out of prefetch.
416
+
417
+ `routeUrl()` resolves against `import.meta.env.BASE_URL`, so the same
418
+ code works at `/` or under a base path (`/docs/`).
419
+
420
+ ---
421
+
422
+ ### Pitfall #17: `resource()` outside component setup
423
+
424
+ **Symptom:** AI creates a resource in module scope to "reuse" data
425
+ between pages. No lifecycle cleanup, dev warning fires.
426
+
427
+ ```ts
428
+ // ❌ No cleanup; will emit a dev warning
429
+ const tickets = resource(
430
+ () => "tickets",
431
+ () => api.listTickets(),
432
+ );
433
+
434
+ component("x-tickets", () => {
435
+ return () => html`${() => tickets.data()?.length ?? 0}`;
436
+ });
437
+
438
+ // ✅ Inside the component setup
439
+ component("x-tickets", () => {
440
+ const tickets = resource(
441
+ () => "tickets",
442
+ () => api.listTickets(),
443
+ );
444
+ return () => html`${() => tickets.data()?.length ?? 0}`;
445
+ });
446
+ ```
447
+
448
+ This way invalidation subscriptions, abort controllers and effects are
449
+ disposed when the component disconnects.
450
+
451
+ ---
452
+
453
+ ### Pitfall #18: assuming nested templates leak
454
+
455
+ **Symptom:** AI manually clears `#app` "to be safe" before mounting a
456
+ new view.
457
+
458
+ ```ts
459
+ const view = signal(html`<x-home></x-home>`);
460
+
461
+ // ✅ Nested TemplateResult can be returned from a child binding
462
+ html`${view}`;
463
+ ```
464
+
465
+ Replaced child bindings recursively dispose nested template instances
466
+ and effects. This is a regression-tested guarantee since v0.3. If pages
467
+ seem to accumulate, that's a core bug, not something you patch in user
468
+ code.
469
+
470
+ ---
471
+
472
+ ### Pitfall #19: global CSS utilities inside Shadow DOM
473
+
474
+ **Symptom:** the page looks unstyled: `.page-head`, `.btn`,
475
+ `.form-grid` and friends are not applied.
476
+
477
+ ```ts
478
+ // ❌ .page-head is global, but x-dashboard defaults to Shadow DOM
479
+ component(
480
+ "x-dashboard",
481
+ () => () => html`
482
+ <header class="page-head">...</header>
483
+ <div class="metric-grid">...</div>
484
+ `,
485
+ );
486
+
487
+ // ✅ Page/layout/admin-shell components are typically Light DOM
488
+ component(
489
+ "x-dashboard",
490
+ () => () => html`
491
+ <header class="page-head">...</header>
492
+ <div class="metric-grid">...</div>
493
+ `,
494
+ { shadow: false },
495
+ );
496
+ ```
497
+
498
+ Rule of thumb: **Shadow DOM** for leaf widgets and slot-based layouts;
499
+ **Light DOM** for route/page/admin-screen components that intentionally
500
+ consume shared layout/form/table CSS. Remember that `<slot>` only
501
+ projects children in Shadow DOM. See
502
+ [10-pages-and-components.md](./10-pages-and-components.md).
503
+
504
+ ---
505
+
506
+ ### Pitfall #20: `host.getAttribute()` in render = not reactive
507
+
508
+ ```ts
509
+ // ❌ Read once per render; external attribute changes don't re-render
510
+ component("x-badge", ({ host }) => () => {
511
+ const variant = host.getAttribute("variant") ?? "default";
512
+ return html`<span class=${variant}>...</span>`;
513
+ });
514
+
515
+ // ✅ ctx.attr() returns a Signal<string> that updates via
516
+ // attributeChangedCallback
517
+ component("x-badge", ({ attr }) => {
518
+ const variant = attr("variant", "default");
519
+ return () => html`<span class=${() => `badge-${variant()}`}>...</span>`;
520
+ });
521
+ ```
522
+
523
+ Never read `host.getAttribute()` / `host.hasAttribute()` inside render
524
+ for values that may change from outside.
525
+
526
+ ---
527
+
528
+ ### Pitfall #21: Shadow DOM `<button>` not submitting forms
529
+
530
+ A `<button>` inside Shadow DOM is **not** part of the form owner
531
+ algorithm for a `<form>` in Light DOM — this is a spec limitation, not
532
+ a Mado bug.
533
+
534
+ ```ts
535
+ // ❌ Inner <button type="submit"> can't trigger the parent <form>
536
+ component("x-button", () => () => html`<button type="submit"><slot></slot></button>`);
537
+
538
+ // ✅ Bridge via requestSubmit()
539
+ component("x-button", ({ host, attr }) => {
540
+ const disabled = attr("disabled");
541
+
542
+ const handleClick = () => {
543
+ const typeAttr = host.getAttribute("type");
544
+ if (typeAttr === "button" || typeAttr === "reset") return;
545
+ const form = host.closest("form");
546
+ if (form && !host.hasAttribute("disabled")) form.requestSubmit();
547
+ };
548
+
549
+ return () => html`
550
+ <button ?disabled=${() => disabled() !== ""} @click=${handleClick}>
551
+ <slot></slot>
552
+ </button>
553
+ `;
554
+ });
555
+ ```
556
+
557
+ See [14-forms.md](./14-forms.md) for the full Shadow-DOM-input recipes.
558
+
559
+ ---
560
+
561
+ ### Pitfall #22: `useForm()` with Shadow DOM custom inputs
562
+
563
+ When a Shadow DOM input dispatches `input`, the browser retargets
564
+ `e.target` from the inner `<input>` to the host `<x-input>`. The host
565
+ is an `HTMLElement` and has no `.name` / `.value` — so `useForm`
566
+ silently sees `undefined`.
567
+
568
+ ```ts
569
+ // ❌ Missing proxy properties
570
+ component("x-input", ({ attr }) => {
571
+ const name = attr("name", "");
572
+ return () => html`<input name=${name} />`;
573
+ });
574
+
575
+ // ✅ Proxy name + value back to the host
576
+ component("x-input", ({ host, attr }) => {
577
+ const name = attr("name", "");
578
+
579
+ Object.defineProperty(host, "name", {
580
+ get: () => host.getAttribute("name") ?? "",
581
+ configurable: true,
582
+ });
583
+ Object.defineProperty(host, "value", {
584
+ get: () => host.shadowRoot?.querySelector("input")?.value ?? "",
585
+ configurable: true,
586
+ });
587
+
588
+ return () => html`<input name=${name} />`;
589
+ });
590
+ ```
591
+
592
+ Again, full pattern in [14-forms.md](./14-forms.md).
593
+
594
+ ---
595
+
596
+ ### Pitfall #23: signal reads in async functions called from `view()`
597
+
598
+ **Symptom:** `[mado] effect cycle detected: subscriber re-ran more than
599
+ 100 times in one flush.`
600
+
601
+ The router calls `page.view()` inside a reactive effect. Any signal
602
+ read **synchronously** during `view()` subscribes that render effect.
603
+ If the same signal is then written (e.g. `loading.set(true)`), the
604
+ router re-runs `view()`, which reads again → infinite loop.
605
+
606
+ ```ts
607
+ // ❌ INFINITE LOOP — loadMore reads signals inside the router's effect
608
+ export default page({
609
+ view: () => {
610
+ const cursor = signal<string | null>("start");
611
+ const loading = signal(false);
612
+
613
+ const loadMore = async () => {
614
+ if (cursor() === null || loading()) return; // ← subscribes render effect!
615
+ loading.set(true); // ← re-triggers render → ∞
616
+ // ...
617
+ };
618
+
619
+ loadMore(); // called synchronously during view()
620
+ return html`...`;
621
+ },
622
+ });
623
+
624
+ // ✅ Wrap synchronous signal reads in untracked()
625
+ import { untracked } from "@madojs/mado";
626
+
627
+ export default page({
628
+ view: () => {
629
+ const cursor = signal<string | null>("start");
630
+ const loading = signal(false);
631
+
632
+ const loadMore = async () => {
633
+ const c = untracked(() => cursor());
634
+ if (c === null || untracked(() => loading())) return;
635
+ loading.set(true);
636
+ // ...
637
+ };
638
+
639
+ loadMore();
640
+ return html`...`;
641
+ },
642
+ });
643
+ ```
644
+
645
+ Rule: any function that reads signals **and** is called synchronously
646
+ during `view()` must use `untracked()` for those reads. This includes
647
+ data fetching, IntersectionObserver callbacks set up during init, and
648
+ timer/polling setup. Signals read inside the **returned template**
649
+ (`html\`...\``) are fine — they sit inside a child binding `${() =>
650
+ ...}` that creates its own effect.
651
+
652
+ ---
653
+
654
+ ### Pitfall #24: `setInterval` / subscriptions in `page()` view without cleanup
655
+
656
+ **Symptom:** after navigating away, timers/subscriptions keep running
657
+ (zombie polling, server logs show requests from pages the user already
658
+ left).
659
+
660
+ ```ts
661
+ // ❌ Interval survives navigation
662
+ export default page({
663
+ view: () => {
664
+ const tick = signal(0);
665
+ setInterval(() => tick.update((n) => n + 1), 3000); // never cleaned up
666
+ return html`<div>${tick}</div>`;
667
+ },
668
+ });
669
+
670
+ // ✅ onDispose for cleanup
671
+ export default page({
672
+ view: ({ onDispose }) => {
673
+ const tick = signal(0);
674
+ const id = setInterval(() => tick.update((n) => n + 1), 3000);
675
+ onDispose(() => clearInterval(id));
676
+ return html`<div>${tick}</div>`;
677
+ },
678
+ });
679
+ ```
680
+
681
+ `resource()` and `effect()` created inside `view()` are cleaned up
682
+ automatically on navigation. Only raw browser APIs need explicit
683
+ `onDispose`:
684
+
685
+ - `setInterval` / `setTimeout`
686
+ - `addEventListener` on `window` / `document`
687
+ - `WebSocket` / `EventSource`
688
+ - `IntersectionObserver` / `ResizeObserver`
689
+
690
+ ---
691
+
692
+ ## Part 2 — Cheat-sheet
693
+
694
+ | If you want to do… | Correct in Mado |
695
+ | ------------------------------------- | -------------------------------------------- |
696
+ | `useState(0)` | `signal(0)` |
697
+ | `useEffect(() => {...}, [a, b])` | `effect(() => {...})` (auto-deps) |
698
+ | `useEffect(() => return cleanup, [])` | `ctx.onDispose(cleanup)` |
699
+ | `useMemo(() => x, [a])` | `computed(() => x)` |
700
+ | `useCallback(fn, [])` | ordinary function |
701
+ | `useContext(Ctx)` | `inject(host, Ctx)` |
702
+ | `useQuery(['key'], fn)` | `resource(() => 'key', fn)` |
703
+ | `useMutation(fn)` | `mutation(fn, { invalidates: [...] })` |
704
+ | `useRouter().push('/')` | `navigate('/')` |
705
+ | `useRouter().query.q` | `queryParam('q')` |
706
+ | `<input value={v} onChange={...}>` | `<input .value=${v} @input=${...}>` |
707
+ | `{items.map(x => ...)}` | `${() => each(items, x => x.id, x => ...)}` |
708
+ | `useForm({ resolver: zodResolver })` | `useForm({...}, { validate: (v) => ... })` |
709
+ | `class extends HTMLElement` | `component('x-name', setup)` |
710
+ | `@customElement('x')` | `component('x-name', setup)` |
711
+ | `host.getAttribute('x')` in render | `ctx.attr('x', default)` (reactive) |
712
+ | `jsonFetcher()` with auth | `apiFetcher()` (attaches Bearer token) |
713
+ | `setInterval` in page view | `onDispose(() => clearInterval(id))` |
714
+ | signal read in `view()` async init | `untracked(() => cursor())` |
715
+ | Internal `<a href>` in components | `<a data-link href=${routeUrl("/x")}>` |
716
+ | SSR / hydration | `mado static` (snapshot + atomic takeover) |
717
+
718
+ ---
719
+
720
+ ## Part 3 — Zero-history evaluation test
721
+
722
+ A practical validation script: can a **fresh** LLM (no prior Mado context)
723
+ write idiomatic Mado without falling back to React-shaped code?
724
+
725
+ ### Allowed context
726
+
727
+ For the first pass, give the agent only:
728
+
729
+ - `AGENTS.md`
730
+ - `README.md`
731
+ - `docs/en/40-llm-guide.md` (this file)
732
+ - specific files from the external `madojs-examples` workspace when the
733
+ agent asks for a larger app pattern
734
+
735
+ The agent may search targeted APIs in `mado/src/` when blocked, but it
736
+ should not load the whole framework into context.
737
+
738
+ ### Task
739
+
740
+ Build a small ticket-admin SPA aimed at a solo / backend developer.
741
+
742
+ Required behaviour:
743
+
744
+ - routes: `/`, `/tickets`, `/tickets/new`, `/tickets/:id`, `*`;
745
+ - in-memory mock API with realistic async delays;
746
+ - list page using `resource()`, `queryParam()` search + status filters,
747
+ `computed()`, and keyed `each()` rows;
748
+ - create + edit flows with `useForm()` + `mutation()` + `invalidates`;
749
+ - local UI state with `signal()`;
750
+ - slotted shell, metric, and badge components for a more realistic admin
751
+ UI;
752
+ - a smoke test importing the built example.
753
+
754
+ ### Failure checklist
755
+
756
+ After implementation, look for any of these and reject:
757
+
758
+ - JSX, `useState`, `useEffect`, `ref`, `$state`, or class-style
759
+ components;
760
+ - `${signal()}` or `${signal() + 1}` where a reactive child thunk is
761
+ required;
762
+ - `disabled=${...}` instead of `?disabled=${...}`;
763
+ - dynamic lists rendered with unkeyed array mapping instead of `each()`;
764
+ - ES-module imports without the `.js` extension;
765
+ - `resource()` created outside component setup;
766
+ - internal links without `data-link` and `routeUrl()`;
767
+ - new runtime dependencies or new public framework APIs;
768
+ - assumptions of SSR / hydration / `getServerSideProps`-style hooks.
769
+
770
+ ### What the test is really measuring
771
+
772
+ The hardest documentation pressure point is **lifecycle**: it should
773
+ never look acceptable to create long-lived `resource()` instances at
774
+ module scope or in route code that never cleans up. If the agent does
775
+ that, the docs need a sharper warning, not a less strict test.
776
+ </content>