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