@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
@@ -1,724 +0,0 @@
1
- # Mado · LLM pitfalls
2
-
3
- > Typical mistakes that AI assistants (Copilot, Claude, ChatGPT, Cursor)
4
- > make when generating Mado code. And how to fix them.
5
-
6
- This document is for **two audiences**:
7
-
8
- 1. **AI agents in the IDE** that read `AGENTS.md` / `.cursorrules` / `.github/copilot-instructions.md`. More detail on typical pitfalls is provided here.
9
- 2. **Humans** who received code from an AI with these errors and don't understand what's wrong.
10
-
11
- ---
12
-
13
- ## Pitfall #1: `${signal()}` instead of `${() => signal()}`
14
-
15
- **Symptom:** the value in the template is displayed but does not update when the signal changes.
16
-
17
- ```ts
18
- const count = signal(0);
19
-
20
- // ❌ AI often generates this
21
- html`<div>Count: ${count() * 2}</div>`;
22
- // → Will render "Count: 0" and never update again.
23
- // count() is read once when the TemplateResult is created.
24
-
25
- // ✅ Correct — getter function
26
- html`<div>Count: ${() => count() * 2}</div>`;
27
- // → Mado will create an effect() for this function and re-render when count changes.
28
-
29
- // ✅ Also correct — the signal itself is a function
30
- html`<div>Count: ${count}</div>`;
31
- ```
32
-
33
- **Rule:**
34
-
35
- - If the `${...}` contains an **expression** (something is done with the signal) — wrap it in `() => ...`.
36
- - If the `${...}` contains **the signal itself** — it can be used as-is.
37
-
38
- This applies to **child bindings** (text inside tags) and to **value attributes** (`@click`, `.prop`, `?attr`, regular attributes).
39
-
40
- ---
41
-
42
- ## Pitfall #2: `<button disabled=${loading}>` instead of `?disabled`
43
-
44
- **Symptom:** the button is not disabled, or is always disabled.
45
-
46
- ```ts
47
- const loading = signal(false);
48
-
49
- // ❌ This is setAttribute("disabled", "false") — the DOM treats this as disabled
50
- html`<button disabled=${loading()}>Save</button>`;
51
-
52
- // ✅ Correct — boolean binding (toggle attribute)
53
- html`<button ?disabled=${loading}>Save</button>`;
54
- ```
55
-
56
- **Rules for attributes:**
57
- | Prefix | What it does | When to use |
58
- |---|---|---|
59
- | `attr=` | `setAttribute("attr", value)` | strings, numbers, URLs |
60
- | `.attr=` | `el.attr = value` (DOM property) | objects, arrays, input `.value` |
61
- | `?attr=` | toggle attribute (by truthiness) | `disabled`, `hidden`, `checked`, etc |
62
- | `@evt=` | `addEventListener("evt", fn)` | event handlers |
63
-
64
- ---
65
-
66
- ## Pitfall #3: useState / useEffect style
67
-
68
- **Symptom:** AI generates React-like code that doesn't work in Mado.
69
-
70
- ```ts
71
- // ❌ AI often writes this
72
- function Counter() {
73
- const [count, setCount] = useState(0);
74
- useEffect(() => { console.log(count); }, [count]);
75
- return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
76
- }
77
-
78
- // ✅ Correct in Mado
79
- import { component, signal, effect, html } from "@madojs/mado";
80
-
81
- component("x-counter", () => {
82
- const count = signal(0);
83
- effect(() => console.log(count())); // auto-subscribe, disposed automatically
84
- return () => html`
85
- <button @click=${() => count.update(c => c + 1)}>${count}</button>
86
- `;
87
- });
88
- ```
89
-
90
- **Key differences:**
91
-
92
- - No hooks, no hook rules.
93
- - `signal()` can be created anywhere — in setup, in an effect, in a handler.
94
- - `effect()` sees what it read on its own — no dependency array needed.
95
- - A component is `component("x-name", setup)`, not a JSX function.
96
-
97
- ---
98
-
99
- ## Pitfall #4: `useEffect(() => { ... return cleanup })`
100
-
101
- **Symptom:** AI writes `return cleanup` inside an effect, expecting it to work like in React.
102
-
103
- ```ts
104
- // ❌ AI tries to write this
105
- component("x-timer", () => {
106
- effect(() => {
107
- const id = setInterval(..., 1000);
108
- return () => clearInterval(id); // will NOT work, use ctx.onDispose instead
109
- });
110
- return () => html`...`;
111
- });
112
-
113
- // ✅ Correct: cleanup via ctx.onDispose
114
- component("x-timer", (ctx) => {
115
- const id = setInterval(..., 1000);
116
- ctx.onDispose(() => clearInterval(id));
117
- return () => html`...`;
118
- });
119
- ```
120
-
121
- **Note:** `effect()` does support `return cleanup`, but this is a **per-run cleanup** (runs before the next effect execution), not an unmount cleanup. For unmount cleanup use `ctx.onDispose`.
122
-
123
- ---
124
-
125
- ## Pitfall #5: Component as a class or with a decorator
126
-
127
- **Symptom:** AI generates a Lit-style or vanilla WebComponent class.
128
-
129
- ```ts
130
- // ❌ AI: "let's do it like Lit"
131
- import { LitElement, html } from "lit";
132
- import { customElement, property } from "lit/decorators.js";
133
-
134
- @customElement('x-counter')
135
- class XCounter extends LitElement { ... }
136
-
137
- // ❌ AI: "let's do it vanilla style"
138
- class XCounter extends HTMLElement {
139
- connectedCallback() { ... }
140
- }
141
- customElements.define("x-counter", XCounter);
142
-
143
- // ✅ Correct: functional component()
144
- import { component, html, signal } from "@madojs/mado";
145
-
146
- component("x-counter", () => {
147
- const count = signal(0);
148
- return () => html`<button @click=${() => count.update(n => n + 1)}>${count}</button>`;
149
- });
150
- ```
151
-
152
- ---
153
-
154
- ## Pitfall #6: imports without the `.js` extension
155
-
156
- **Symptom:** TypeScript compiles, but the browser gets a 404.
157
-
158
- ```ts
159
- // ❌ AI often omits the extension
160
- import { foo } from "./bar";
161
- import { Home } from "./pages/home";
162
-
163
- // ✅ Correct: ES modules in the browser require the extension
164
- import { foo } from "./bar.js";
165
- import { Home } from "./pages/home.js";
166
- ```
167
-
168
- **Why `.js` and not `.ts`:** the browser receives already-compiled JS. TypeScript is smart enough to understand `./bar.js` as a reference to `./bar.ts` at compile time.
169
-
170
- ---
171
-
172
- ## Pitfall #7: lists via `.map()` without keys
173
-
174
- **Symptom:** when reordering elements, input focus is lost / CSS animations break / performance suffers on large lists.
175
-
176
- ```ts
177
- // ❌ Works, but not keyed: recreates DOM on every change
178
- html`<ul>
179
- ${() => items().map((t) => html`<li>${t.name}</li>`)}
180
- </ul>`;
181
-
182
- // ✅ Correct: each() with a key function
183
- import { each } from "@madojs/mado";
184
- html`<ul>
185
- ${() =>
186
- each(
187
- items(),
188
- (t) => t.id,
189
- (t) => html`<li>${t.name}</li>`,
190
- )}
191
- </ul>`;
192
- ```
193
-
194
- **Rule:** always use `each()` for lists of arrays with stable IDs. Reserve `.map()` only for static lists.
195
-
196
- ---
197
-
198
- ## Pitfall #8: `signal.value` or `count.get()`
199
-
200
- **Symptom:** AI writes an API in Vue or pre-v1 Solid style.
201
-
202
- ```ts
203
- const count = signal(0);
204
-
205
- // ❌ No such API
206
- count.value;
207
- count.value = 5;
208
- count.get();
209
-
210
- // ✅ Correct
211
- count(); // read
212
- count.set(5); // write
213
- count.update((n) => n + 1);
214
- count.peek(); // read without subscribing
215
- ```
216
-
217
- ---
218
-
219
- ## Pitfall #9: `provide(ApiCtx, value)` without host
220
-
221
- **Symptom:** TypeError when trying to provide context.
222
-
223
- ```ts
224
- // ❌ AI forgets host
225
- provide(ApiCtx, myApi);
226
- inject(ApiCtx);
227
-
228
- // ✅ Correct: first argument is host (the current component)
229
- component("x-app", ({ host }) => {
230
- provide(host, ApiCtx, myApi);
231
- return () => html`...`;
232
- });
233
-
234
- component("x-child", ({ host }) => {
235
- const api = inject(host, ApiCtx); // signal<value>
236
- return () => html`...`;
237
- });
238
- ```
239
-
240
- ---
241
-
242
- ## Pitfall #10: expecting SSR
243
-
244
- **Symptom:** AI writes code assuming the page is pre-rendered on the server.
245
-
246
- ```ts
247
- // ❌ This works only in the browser
248
- const userId = location.pathname.split("/")[2];
249
-
250
- // ❌ This too works only in the browser
251
- if (typeof window !== "undefined") { ... } // in Mado, window is ALWAYS available
252
- ```
253
-
254
- Mado **does not do SSR with hydration**. Code does not run on the server — there is only `bake` (static prerender at build time) and edge-prerender. Both replace user code with a linkedom environment, but this is **only** for generating HTML with meta tags, not for executing page logic.
255
-
256
- This means:
257
-
258
- - ✅ `window`, `document`, `location`, `fetch` — available without checks.
259
- - ❌ Don't write code that tries to "universally work on server and client".
260
- - ❌ Don't use Next.js patterns (`getServerSideProps`, `headers()`).
261
-
262
- ---
263
-
264
- ## Pitfall #11: `useForm()` with a zod/yup resolver
265
-
266
- **Symptom:** AI wants to plug in a validator.
267
-
268
- ```ts
269
- // ❌ No such API
270
- const f = useForm({ resolver: zodResolver(schema) });
271
-
272
- // ✅ Correct: HTML-like validation through useForm schema
273
- const f = useForm({
274
- email: { required: true, type: "email" },
275
- age: { required: true, type: "number", min: 18 },
276
- });
277
-
278
- // ✅ Or a custom function if HTML5 isn't enough
279
- const f = useForm(
280
- { name: { required: true } },
281
- {
282
- validate: (values) => {
283
- const errors: Record<string, string> = {};
284
- if (values.name && /\d/.test(values.name as string)) {
285
- errors.name = "Name must not contain digits";
286
- }
287
- return Object.keys(errors).length ? errors : null;
288
- },
289
- },
290
- );
291
- ```
292
-
293
- ---
294
-
295
- ## Pitfall #12: Tailwind / styled-components / CSS Modules
296
-
297
- **Symptom:** AI suggests standard React CSS solutions.
298
-
299
- Mado uses **Shadow DOM + `css\`\`` + CSS variables**. Global UI frameworks (Tailwind, Bootstrap-via-classes) **only work in light DOM** (`shadow: false`):
300
-
301
- ```ts
302
- // Light-DOM page/screen component, Tailwind classes work
303
- component(
304
- "x-admin-page",
305
- () => () => html`
306
- <section class="bg-white shadow-lg rounded-lg p-4">...</section>
307
- `,
308
- { shadow: false },
309
- );
310
-
311
- // Shadow-DOM component (default) — Tailwind does NOT work.
312
- // Use css`` or ::part() for external styling.
313
- component("x-button", () => () => html`<button><slot></slot></button>`, {
314
- styles: css`
315
- button {
316
- background: var(--button-bg, #2563eb);
317
- color: white;
318
- padding: 0.5rem 1rem;
319
- border-radius: 6px;
320
- }
321
- `,
322
- });
323
- ```
324
-
325
- **Themes and customization — via CSS variables**, not classes.
326
-
327
- ---
328
-
329
- ## Pitfall #13: `import * as Mado from "@madojs/mado"`
330
-
331
- **Symptom:** AI wants a namespace import.
332
-
333
- This works, but duplicates names and tree-shakes poorly. Named imports are preferred:
334
-
335
- ```ts
336
- // ✅ Canonical
337
- import { signal, html, component, css, page } from "@madojs/mado";
338
-
339
- // ⚠️ Works, but excessive
340
- import * as Mado from "@madojs/mado";
341
- Mado.signal(0);
342
- ```
343
-
344
- ---
345
-
346
- ## Pitfall #14: attempting to add a runtime dependency
347
-
348
- **Symptom:** AI suggests `npm install lodash` / `npm install date-fns` / etc.
349
-
350
- Mado is **zero runtime deps** by design. If AI wants to add:
351
-
352
- - **lodash** → use native JS (`Object.entries`, `Array.prototype`, `structuredClone`);
353
- - **date-fns** → use `Intl.DateTimeFormat` and `Intl.RelativeTimeFormat`;
354
- - **uuid** → `crypto.randomUUID()`;
355
- - **axios** → native `fetch` + `jsonFetcher()` from Mado;
356
- - **classnames** → native template literal or an object map.
357
-
358
- Any runtime dependency is a **violation of the framework's principles**. If you truly cannot avoid it — add it to the user project, not to the Mado core.
359
-
360
- ---
361
-
362
- ## Pitfall #15: inline `<style>` inside page templates
363
-
364
- **Symptom:** AI puts a large `<style>` directly inside a `html\`\`` page.
365
-
366
- ```ts
367
- // ❌ Works, but scales poorly and complicates cleanup
368
- page({
369
- view: () => html`
370
- <style>
371
- .panel {
372
- padding: 1rem;
373
- }
374
- </style>
375
- <section class="panel">...</section>
376
- `,
377
- });
378
-
379
- // ✅ Correct: component styles via css``
380
- component(
381
- "x-admin-panel",
382
- () => () => html` <section class="panel">...</section> `,
383
- {
384
- styles: css`
385
- .panel {
386
- padding: 1rem;
387
- }
388
- `,
389
- },
390
- );
391
- ```
392
-
393
- For backend admin route/page screens it is often appropriate to use `shadow: false`, so that
394
- global layout/form/table utilities work like a regular admin panel. But if
395
- the layout uses `<slot>` to project the page into the shell, keep the layout in
396
- Shadow DOM and keep the shell styles in `styles: css\`\``.
397
-
398
- ---
399
-
400
- ## Pitfall #16: Shadow DOM links without `data-link`
401
-
402
- **Symptom:** a link inside a Web Component causes a full page reload or is not
403
- prefetched.
404
-
405
- ```ts
406
- // ❌ Regular link: browser will perform a full reload
407
- html`<a href="/tickets/42">Open</a>`;
408
-
409
- // ✅ SPA navigation: router() will intercept the click even through Shadow DOM
410
- html`<a href="/tickets/42" data-link>Open</a>`;
411
- ```
412
-
413
- Mado finds the link via `event.composedPath()`, so `data-link` works
414
- inside Shadow DOM as well. Hover-prefetch uses the same path; `data-no-prefetch`
415
- disables prefetch for a specific link.
416
-
417
- ---
418
-
419
- ## Pitfall #17: `resource()` outside component setup
420
-
421
- **Symptom:** AI creates a resource in module scope to "reuse"
422
- data between pages.
423
-
424
- ```ts
425
- // ❌ No lifecycle cleanup, will emit dev-warning
426
- const tickets = resource(
427
- () => "tickets",
428
- () => api.listTickets(),
429
- );
430
-
431
- component("x-tickets", () => {
432
- return () => html`${() => tickets.data()?.length ?? 0}`;
433
- });
434
-
435
- // ✅ Create resource inside the component setup
436
- component("x-tickets", () => {
437
- const tickets = resource(
438
- () => "tickets",
439
- () => api.listTickets(),
440
- );
441
- return () => html`${() => tickets.data()?.length ?? 0}`;
442
- });
443
- ```
444
-
445
- This way invalidation subscriptions, abort controllers, and effects will be
446
- cleaned up when the component disconnects.
447
-
448
- ---
449
-
450
- ## Pitfall #18: assuming nested templates don't require cleanup
451
-
452
- **Symptom:** AI assembles a route outlet or conditional UI from nested
453
- `TemplateResult`s, and then old elements continue living below the new page.
454
-
455
- ```ts
456
- const view = signal(html`<x-home></x-home>`);
457
-
458
- // ✅ Normal pattern: nested TemplateResult can be returned from a child-binding
459
- html`${view}`;
460
- ```
461
-
462
- Starting from v0.3 this is guaranteed by regression tests: when a child-binding is
463
- replaced, Mado recursively disposes nested template instances/effects. If you see
464
- pages accumulating in `#app`, that is a core bug, not something you need to
465
- clean up manually.
466
-
467
- ---
468
-
469
- ## Pitfall #19: global CSS utilities inside Shadow DOM
470
-
471
- **Symptom:** the page looks "unstyled": `.page-head`, `.btn`,
472
- `.form-grid`, `.metric-grid` are not applied.
473
-
474
- ```ts
475
- // ❌ .page-head is declared globally, but x-dashboard defaults to Shadow DOM
476
- component(
477
- "x-dashboard",
478
- () => () => html`
479
- <header class="page-head">...</header>
480
- <div class="metric-grid">...</div>
481
- `,
482
- );
483
-
484
- // ✅ Page/layout/admin-shell components often should be Light DOM
485
- component(
486
- "x-dashboard",
487
- () => () => html`
488
- <header class="page-head">...</header>
489
- <div class="metric-grid">...</div>
490
- `,
491
- { shadow: false },
492
- );
493
- ```
494
-
495
- Rule: Shadow DOM — for leaf widgets and slot-based layouts, Light DOM — for
496
- route/page/admin-screen components that intentionally use shared
497
- layout/form/table utilities. Remember: `<slot>` only projects children in
498
- Shadow DOM; with `shadow: false` it is a regular element.
499
- More details: [`09-shadow-vs-light-dom.md`](./09-shadow-vs-light-dom.md).
500
-
501
- ---
502
-
503
- ## Pitfall #20: `host.getAttribute()` in render = not reactive
504
-
505
- **Symptom:** a component's appearance does not update when the parent changes an attribute.
506
-
507
- ```ts
508
- // ❌ host.getAttribute() in the render function is read once per render, but
509
- // the render only re-runs when its own signals change. External attribute
510
- // changes don't trigger a re-render.
511
- component("x-badge", ({ host }) => () => {
512
- const variant = host.getAttribute("variant") ?? "default";
513
- return html`<span class=${variant}>...</span>`;
514
- });
515
-
516
- // ✅ Correct: ctx.attr() — returns a reactive Signal<string>
517
- component("x-badge", ({ attr }) => {
518
- const variant = attr("variant", "default");
519
- return () => html`<span class=${() => `badge-${variant()}`}>...</span>`;
520
- });
521
- ```
522
-
523
- **Rule:** never read `host.getAttribute()` or `host.hasAttribute()` inside the
524
- render function for values that might change from outside. Use `ctx.attr()` —
525
- it returns a Signal that auto-updates via `attributeChangedCallback`.
526
-
527
- ---
528
-
529
- ## Pitfall #21: Shadow DOM `<button>` not submitting forms
530
-
531
- **Symptom:** clicking `<x-button type="submit">` inside a `<form>` does nothing.
532
-
533
- A `<button>` inside Shadow DOM is not part of the form's owner algorithm for
534
- `<form>` in Light DOM — this is a spec-level limitation, not a Mado bug.
535
-
536
- ```ts
537
- // ❌ The inner <button type="submit"> cannot trigger the parent <form>
538
- component("x-button", ({ host }) => {
539
- return () => html`<button type="submit"><slot></slot></button>`;
540
- });
541
-
542
- // ✅ Bridge via requestSubmit()
543
- component("x-button", ({ host, attr }) => {
544
- const disabled = attr("disabled");
545
-
546
- const handleClick = () => {
547
- const typeAttr = host.getAttribute("type");
548
- if (typeAttr === "button" || typeAttr === "reset") return;
549
- const form = host.closest("form");
550
- if (form && !host.hasAttribute("disabled")) form.requestSubmit();
551
- };
552
-
553
- return () => html`
554
- <button ?disabled=${() => disabled() !== ""} @click=${handleClick}>
555
- <slot></slot>
556
- </button>
557
- `;
558
- });
559
- ```
560
-
561
- See [`17-shadow-dom-forms.md`](./17-shadow-dom-forms.md) for the full recipe.
562
-
563
- ---
564
-
565
- ## Pitfall #22: `useForm()` with Shadow DOM custom inputs
566
-
567
- **Symptom:** `form.onInput` receives `undefined` for name/value from `<x-input>`.
568
-
569
- When a Shadow DOM input dispatches an `input` event, the browser retargets
570
- `e.target` from the inner `<input>` to the host `<x-input>`. But `<x-input>`
571
- (an HTMLElement) doesn't have `.name` or `.value` — so `useForm` gets nothing.
572
-
573
- ```ts
574
- // ❌ Missing proxy properties — useForm silently ignores events
575
- component("x-input", ({ host, attr }) => {
576
- const name = attr("name", "");
577
- return () => html`<input name=${name} />`;
578
- });
579
-
580
- // ✅ Add proxy properties for useForm compatibility
581
- component("x-input", ({ host, attr }) => {
582
- const name = attr("name", "");
583
-
584
- Object.defineProperty(host, "name", {
585
- get: () => host.getAttribute("name") ?? "",
586
- configurable: true,
587
- });
588
- Object.defineProperty(host, "value", {
589
- get: () => host.shadowRoot?.querySelector("input")?.value ?? "",
590
- configurable: true,
591
- });
592
-
593
- return () => html`<input name=${name} />`;
594
- });
595
- ```
596
-
597
- See [`17-shadow-dom-forms.md`](./17-shadow-dom-forms.md) for the full pattern.
598
-
599
- ---
600
-
601
- ## Pitfall #23: signal reads in async functions called from `view()` create effect cycles
602
-
603
- **Symptom:** `[mado] effect cycle detected: subscriber re-ran more than 100 times in one flush.`
604
-
605
- The router calls `page.view()` inside a reactive effect. Any signal read
606
- **synchronously** during `view()` subscribes the router's render effect. If that
607
- signal is then written (e.g. `loading.set(true)`) — the router re-runs `view()`,
608
- which reads the signal again → infinite loop.
609
-
610
- ```ts
611
- // ❌ INFINITE LOOP — loadMore reads signals inside the router's effect
612
- export default page({
613
- view: () => {
614
- const cursor = signal<string | null>("start");
615
- const loading = signal(false);
616
-
617
- const loadMore = async () => {
618
- if (cursor() === null || loading()) return; // ← subscribes render effect!
619
- loading.set(true); // ← re-triggers render → loadMore() → ∞
620
- const res = await fetch(`/api/items?cursor=${cursor()}`);
621
- // ...
622
- };
623
-
624
- loadMore(); // called synchronously during view()
625
- return html`...`;
626
- },
627
- });
628
-
629
- // ✅ CORRECT — wrap signal reads in untracked()
630
- import { untracked } from "@madojs/mado";
631
-
632
- export default page({
633
- view: () => {
634
- const cursor = signal<string | null>("start");
635
- const loading = signal(false);
636
-
637
- const loadMore = async () => {
638
- const c = untracked(() => cursor());
639
- if (c === null || untracked(() => loading())) return;
640
- loading.set(true);
641
- const res = await fetch(`/api/items?cursor=${c}`);
642
- // ...
643
- };
644
-
645
- loadMore();
646
- return html`...`;
647
- },
648
- });
649
- ```
650
-
651
- **Rule:** Any function that reads signals AND is called synchronously during
652
- `view()` initialization must use `untracked()` for those reads. This includes:
653
-
654
- - Data fetching / loadMore functions
655
- - IntersectionObserver callbacks set up during init
656
- - Timer/polling setup functions that check state
657
-
658
- Signals read inside the **returned template** (`html\`...\``) are fine — they are
659
- wrapped in a child-binding function `${() => ...}` which creates its own effect.
660
-
661
- ---
662
-
663
- ## Pitfall #24: `setInterval` / manual subscriptions in `page()` view without cleanup
664
-
665
- **Symptom:** After navigating away, timers/subscriptions keep running (zombie intervals,
666
- server logs show polling requests from pages the user already left).
667
-
668
- ```ts
669
- // ❌ ZOMBIE — interval survives navigation
670
- export default page({
671
- view: () => {
672
- const tick = signal(0);
673
- setInterval(() => tick.update((n) => n + 1), 3000); // never cleaned up!
674
- return html`<div>${tick}</div>`;
675
- },
676
- });
677
-
678
- // ✅ CORRECT — use onDispose for cleanup
679
- export default page({
680
- view: ({ onDispose }) => {
681
- const tick = signal(0);
682
- const id = setInterval(() => tick.update((n) => n + 1), 3000);
683
- onDispose(() => clearInterval(id));
684
- return html`<div>${tick}</div>`;
685
- },
686
- });
687
- ```
688
-
689
- **Note:** `resource()` and `effect()` created inside `view()` are automatically
690
- cleaned up on navigation (they register with the page lifecycle). Only raw
691
- browser APIs need explicit `onDispose()`:
692
-
693
- - `setInterval` / `setTimeout`
694
- - `addEventListener` (on window/document)
695
- - `WebSocket` / `EventSource`
696
- - `IntersectionObserver` / `ResizeObserver`
697
-
698
- ---
699
-
700
- ## Cheat-sheet for AI
701
-
702
- | If you want to do… | Correct in Mado |
703
- | ------------------------------------- | ------------------------------------------- |
704
- | `useState(0)` | `signal(0)` |
705
- | `useEffect(() => {...}, [a, b])` | `effect(() => {...})` (auto-deps) |
706
- | `useEffect(() => return cleanup, [])` | `ctx.onDispose(cleanup)` |
707
- | `useMemo(() => x, [a])` | `computed(() => x)` |
708
- | `useCallback(fn, [])` | ordinary function |
709
- | `useContext(Ctx)` | `inject(host, Ctx)` |
710
- | `useQuery(['key'], fn)` | `resource(() => 'key', fn)` |
711
- | `useMutation(fn)` | `mutation(fn, { invalidates: [...] })` |
712
- | `useRouter().push('/')` | `navigate('/')` |
713
- | `useRouter().query.q` | `queryParam('q')` |
714
- | `<input value={v} onChange={...}>` | `<input .value=${v} @input=${...}>` |
715
- | `{items.map(x => ...)}` | `${() => each(items, x => x.id, x => ...)}` |
716
- | `useForm({ resolver: zodResolver })` | `useForm({...}, { validate: (v) => ... })` |
717
- | `class extends HTMLElement` | `component('x-name', setup)` |
718
- | `@customElement('x')` | `component('x-name', setup)` |
719
- | `host.getAttribute('x')` in render | `ctx.attr('x', default)` (reactive) |
720
- | `jsonFetcher()` with auth | `apiFetcher()` (attaches Bearer token) |
721
- | `setInterval` in page view | `onDispose(() => clearInterval(id))` |
722
- | signal read in view() async init | `untracked(() => cursor())` |
723
-
724
- If something doesn't fit this list — open `src/` and **read 500 lines**. Seriously. Mado is intentionally small to be readable.