@madojs/mado 0.5.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.
- package/AGENTS.md +291 -0
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +371 -0
- package/ROADMAP.md +52 -0
- package/dist/src/component.d.ts +48 -0
- package/dist/src/component.js +140 -0
- package/dist/src/component.js.map +1 -0
- package/dist/src/context.d.ts +40 -0
- package/dist/src/context.js +67 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/css.d.ts +54 -0
- package/dist/src/css.js +137 -0
- package/dist/src/css.js.map +1 -0
- package/dist/src/devtools.d.ts +22 -0
- package/dist/src/devtools.js +63 -0
- package/dist/src/devtools.js.map +1 -0
- package/dist/src/diagnostics.d.ts +11 -0
- package/dist/src/diagnostics.js +28 -0
- package/dist/src/diagnostics.js.map +1 -0
- package/dist/src/each.d.ts +39 -0
- package/dist/src/each.js +35 -0
- package/dist/src/each.js.map +1 -0
- package/dist/src/forms.d.ts +71 -0
- package/dist/src/forms.js +161 -0
- package/dist/src/forms.js.map +1 -0
- package/dist/src/head.d.ts +19 -0
- package/dist/src/head.js +97 -0
- package/dist/src/head.js.map +1 -0
- package/dist/src/html/bindings.d.ts +78 -0
- package/dist/src/html/bindings.js +304 -0
- package/dist/src/html/bindings.js.map +1 -0
- package/dist/src/html/parser.d.ts +64 -0
- package/dist/src/html/parser.js +521 -0
- package/dist/src/html/parser.js.map +1 -0
- package/dist/src/html/template-types.d.ts +27 -0
- package/dist/src/html/template-types.js +8 -0
- package/dist/src/html/template-types.js.map +1 -0
- package/dist/src/html/template.d.ts +45 -0
- package/dist/src/html/template.js +119 -0
- package/dist/src/html/template.js.map +1 -0
- package/dist/src/html.d.ts +16 -0
- package/dist/src/html.js +16 -0
- package/dist/src/html.js.map +1 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.js +39 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lazy.d.ts +38 -0
- package/dist/src/lazy.js +73 -0
- package/dist/src/lazy.js.map +1 -0
- package/dist/src/lifecycle.d.ts +45 -0
- package/dist/src/lifecycle.js +66 -0
- package/dist/src/lifecycle.js.map +1 -0
- package/dist/src/page.d.ts +161 -0
- package/dist/src/page.js +38 -0
- package/dist/src/page.js.map +1 -0
- package/dist/src/persisted.d.ts +47 -0
- package/dist/src/persisted.js +119 -0
- package/dist/src/persisted.js.map +1 -0
- package/dist/src/resource.d.ts +120 -0
- package/dist/src/resource.js +275 -0
- package/dist/src/resource.js.map +1 -0
- package/dist/src/router/manifest.d.ts +56 -0
- package/dist/src/router/manifest.js +302 -0
- package/dist/src/router/manifest.js.map +1 -0
- package/dist/src/router/match.d.ts +62 -0
- package/dist/src/router/match.js +117 -0
- package/dist/src/router/match.js.map +1 -0
- package/dist/src/router/navigation.d.ts +89 -0
- package/dist/src/router/navigation.js +263 -0
- package/dist/src/router/navigation.js.map +1 -0
- package/dist/src/router.d.ts +13 -0
- package/dist/src/router.js +13 -0
- package/dist/src/router.js.map +1 -0
- package/dist/src/signal.d.ts +67 -0
- package/dist/src/signal.js +238 -0
- package/dist/src/signal.js.map +1 -0
- package/docs/README.md +12 -0
- package/docs/en/00-the-mado-way.md +106 -0
- package/docs/en/01-routing.md +204 -0
- package/docs/en/02-project-layout.md +58 -0
- package/docs/en/03-static-bake.md +251 -0
- package/docs/en/04-ide-setup.md +162 -0
- package/docs/en/05-why-mado.md +193 -0
- package/docs/en/06-for-backenders.md +422 -0
- package/docs/en/07-llm-pitfalls.md +486 -0
- package/docs/en/08-llm-zero-history-test.md +56 -0
- package/docs/en/09-shadow-vs-light-dom.md +122 -0
- package/docs/en/README.md +16 -0
- package/docs/fr/00-the-mado-way.md +108 -0
- package/docs/fr/01-routing.md +202 -0
- package/docs/fr/02-project-layout.md +58 -0
- package/docs/fr/03-static-bake.md +290 -0
- package/docs/fr/04-ide-setup.md +162 -0
- package/docs/fr/05-why-mado.md +193 -0
- package/docs/fr/06-for-backenders.md +432 -0
- package/docs/fr/07-llm-pitfalls.md +487 -0
- package/docs/fr/08-llm-zero-history-test.md +60 -0
- package/docs/fr/09-shadow-vs-light-dom.md +121 -0
- package/docs/fr/README.md +16 -0
- package/docs/ru/00-the-mado-way.md +93 -0
- package/docs/ru/01-routing.md +194 -0
- package/docs/ru/02-project-layout.md +57 -0
- package/docs/ru/03-static-bake.md +251 -0
- package/docs/ru/04-ide-setup.md +144 -0
- package/docs/ru/05-why-mado.md +193 -0
- package/docs/ru/06-for-backenders.md +422 -0
- package/docs/ru/07-llm-pitfalls.md +485 -0
- package/docs/ru/08-llm-zero-history-test.md +56 -0
- package/docs/ru/09-shadow-vs-light-dom.md +122 -0
- package/docs/ru/README.md +14 -0
- package/docs/uk/00-the-mado-way.md +54 -0
- package/docs/uk/01-routing.md +82 -0
- package/docs/uk/02-project-layout.md +46 -0
- package/docs/uk/03-static-bake.md +49 -0
- package/docs/uk/04-ide-setup.md +26 -0
- package/docs/uk/05-why-mado.md +34 -0
- package/docs/uk/06-for-backenders.md +50 -0
- package/docs/uk/07-llm-pitfalls.md +82 -0
- package/docs/uk/08-llm-zero-history-test.md +31 -0
- package/docs/uk/09-shadow-vs-light-dom.md +40 -0
- package/docs/uk/README.md +16 -0
- package/llms.txt +155 -0
- package/package.json +81 -0
- package/scripts/bake.mjs +406 -0
- package/scripts/bundle.mjs +146 -0
- package/scripts/cli.mjs +382 -0
- package/scripts/new.mjs +80 -0
- package/scripts/preview.mjs +176 -0
- package/scripts/release-notes.mjs +66 -0
- package/scripts/showcase-regression.mjs +392 -0
- package/server/serve.mjs +292 -0
- package/starters/crud/README.md +21 -0
- package/starters/crud/index.html +20 -0
- package/starters/crud/package.json +17 -0
- package/starters/crud/src/components/app-shell.ts +51 -0
- package/starters/crud/src/components/ticket-detail.ts +33 -0
- package/starters/crud/src/components/ticket-form.ts +69 -0
- package/starters/crud/src/components/ticket-list.ts +66 -0
- package/starters/crud/src/lib/api.ts +76 -0
- package/starters/crud/src/main.ts +12 -0
- package/starters/crud/src/pages/home.ts +18 -0
- package/starters/crud/src/pages/not-found.ts +12 -0
- package/starters/crud/src/pages/ticket-detail.ts +6 -0
- package/starters/crud/src/pages/ticket-new.ts +6 -0
- package/starters/crud/src/pages/tickets.ts +6 -0
- package/starters/crud/src/routes.ts +9 -0
- package/starters/crud/src/styles/global.ts +155 -0
- package/starters/crud/tsconfig.json +15 -0
- package/starters/minimal/README.md +19 -0
- package/starters/minimal/index.html +20 -0
- package/starters/minimal/package.json +17 -0
- package/starters/minimal/src/components/app-counter.ts +31 -0
- package/starters/minimal/src/main.ts +9 -0
- package/starters/minimal/src/pages/home.ts +18 -0
- package/starters/minimal/src/pages/not-found.ts +14 -0
- package/starters/minimal/src/routes.ts +6 -0
- package/starters/minimal/src/styles/global.ts +60 -0
- package/starters/minimal/tsconfig.json +15 -0
- package/templates/page-detail.ts +63 -0
- package/templates/page-form.ts +94 -0
- package/templates/page-list.ts +79 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
# Mado · Pièges LLM
|
|
2
|
+
|
|
3
|
+
> Erreurs typiques que les assistants IA (Copilot, Claude, ChatGPT, Cursor)
|
|
4
|
+
> commettent lors de la génération de code Mado. Et comment les corriger.
|
|
5
|
+
|
|
6
|
+
Ce document s'adresse à **deux publics** :
|
|
7
|
+
1. **Les agents IA dans l'IDE** qui lisent `AGENTS.md` / `.cursorrules` / `.github/copilot-instructions.md`. Plus de détails sur les pièges typiques sont fournis ici.
|
|
8
|
+
2. **Les humains** qui ont reçu du code d'une IA avec ces erreurs et ne comprennent pas ce qui ne va pas.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Piège #1 : `${signal()}` au lieu de `${() => signal()}`
|
|
13
|
+
|
|
14
|
+
**Symptôme :** la valeur dans le template s'affiche mais ne se met pas à jour quand le signal change.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
const count = signal(0);
|
|
18
|
+
|
|
19
|
+
// ❌ L'IA génère souvent ceci
|
|
20
|
+
html`<div>Compte : ${count() * 2}</div>`
|
|
21
|
+
// → Affichera "Compte : 0" et ne se mettra plus jamais à jour.
|
|
22
|
+
// count() est lu une seule fois quand le TemplateResult est créé.
|
|
23
|
+
|
|
24
|
+
// ✅ Correct — fonction getter
|
|
25
|
+
html`<div>Compte : ${() => count() * 2}</div>`
|
|
26
|
+
// → Mado créera un effect() pour cette fonction et re-rendra quand count change.
|
|
27
|
+
|
|
28
|
+
// ✅ Aussi correct — le signal lui-même est une fonction
|
|
29
|
+
html`<div>Compte : ${count}</div>`
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Règle :**
|
|
33
|
+
- Si `${...}` contient une **expression** (quelque chose est fait avec le signal) — enveloppez dans `() => ...`.
|
|
34
|
+
- Si `${...}` contient **le signal lui-même** — il peut être utilisé tel quel.
|
|
35
|
+
|
|
36
|
+
Ceci s'applique aux **bindings enfants** (texte à l'intérieur des tags) et aux **attributs de valeur** (`@click`, `.prop`, `?attr`, attributs ordinaires).
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Piège #2 : `<button disabled=${loading}>` au lieu de `?disabled`
|
|
41
|
+
|
|
42
|
+
**Symptôme :** le bouton n'est pas désactivé, ou est toujours désactivé.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const loading = signal(false);
|
|
46
|
+
|
|
47
|
+
// ❌ C'est setAttribute("disabled", "false") — le DOM traite ça comme disabled
|
|
48
|
+
html`<button disabled=${loading()}>Enregistrer</button>`
|
|
49
|
+
|
|
50
|
+
// ✅ Correct — binding booléen (basculer l'attribut)
|
|
51
|
+
html`<button ?disabled=${loading}>Enregistrer</button>`
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Règles pour les attributs :**
|
|
55
|
+
| Préfixe | Ce qu'il fait | Quand utiliser |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `attr=` | `setAttribute("attr", value)` | strings, nombres, URLs |
|
|
58
|
+
| `.attr=` | `el.attr = value` (propriété DOM) | objets, tableaux, `.value` d'input |
|
|
59
|
+
| `?attr=` | basculer l'attribut (par vérité) | `disabled`, `hidden`, `checked`, etc. |
|
|
60
|
+
| `@evt=` | `addEventListener("evt", fn)` | gestionnaires d'événements |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Piège #3 : style useState / useEffect
|
|
65
|
+
|
|
66
|
+
**Symptôme :** l'IA génère du code de style React qui ne fonctionne pas dans Mado.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// ❌ L'IA écrit souvent ceci
|
|
70
|
+
function Counter() {
|
|
71
|
+
const [count, setCount] = useState(0);
|
|
72
|
+
useEffect(() => { console.log(count); }, [count]);
|
|
73
|
+
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ✅ Correct dans Mado
|
|
77
|
+
import { component, signal, effect, html } from "@madojs/mado";
|
|
78
|
+
|
|
79
|
+
component("x-counter", () => {
|
|
80
|
+
const count = signal(0);
|
|
81
|
+
effect(() => console.log(count())); // auto-abonnement, disposé automatiquement
|
|
82
|
+
return () => html`
|
|
83
|
+
<button @click=${() => count.update(c => c + 1)}>${count}</button>
|
|
84
|
+
`;
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Différences clés :**
|
|
89
|
+
- Pas de hooks, pas de règles de hooks.
|
|
90
|
+
- `signal()` peut être créé n'importe où — dans le setup, dans un effect, dans un handler.
|
|
91
|
+
- `effect()` voit ce qu'il a lu de lui-même — pas besoin de tableau de dépendances.
|
|
92
|
+
- Un composant est `component("x-name", setup)`, pas une fonction JSX.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Piège #4 : `useEffect(() => { ... return cleanup })`
|
|
97
|
+
|
|
98
|
+
**Symptôme :** l'IA écrit `return cleanup` à l'intérieur d'un effect, attendant que ça fonctionne comme dans React.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// ❌ L'IA essaie d'écrire ceci
|
|
102
|
+
component("x-timer", () => {
|
|
103
|
+
effect(() => {
|
|
104
|
+
const id = setInterval(..., 1000);
|
|
105
|
+
return () => clearInterval(id); // NE fonctionnera PAS, utilisez ctx.onDispose à la place
|
|
106
|
+
});
|
|
107
|
+
return () => html`...`;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ✅ Correct : nettoyage via ctx.onDispose
|
|
111
|
+
component("x-timer", (ctx) => {
|
|
112
|
+
const id = setInterval(..., 1000);
|
|
113
|
+
ctx.onDispose(() => clearInterval(id));
|
|
114
|
+
return () => html`...`;
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Note :** `effect()` supporte bien `return cleanup`, mais c'est un **nettoyage par exécution** (s'exécute avant la prochaine exécution de l'effect), pas un nettoyage au démontage. Pour le nettoyage au démontage, utilisez `ctx.onDispose`.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Piège #5 : Composant comme classe ou avec un décorateur
|
|
123
|
+
|
|
124
|
+
**Symptôme :** l'IA génère une classe de style Lit ou vanilla WebComponent.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
// ❌ IA : "faisons comme Lit"
|
|
128
|
+
import { LitElement, html } from "lit";
|
|
129
|
+
import { customElement, property } from "lit/decorators.js";
|
|
130
|
+
|
|
131
|
+
@customElement('x-counter')
|
|
132
|
+
class XCounter extends LitElement { ... }
|
|
133
|
+
|
|
134
|
+
// ❌ IA : "faisons en style vanilla"
|
|
135
|
+
class XCounter extends HTMLElement {
|
|
136
|
+
connectedCallback() { ... }
|
|
137
|
+
}
|
|
138
|
+
customElements.define("x-counter", XCounter);
|
|
139
|
+
|
|
140
|
+
// ✅ Correct : component() fonctionnel
|
|
141
|
+
import { component, html, signal } from "@madojs/mado";
|
|
142
|
+
|
|
143
|
+
component("x-counter", () => {
|
|
144
|
+
const count = signal(0);
|
|
145
|
+
return () => html`<button @click=${() => count.update(n => n + 1)}>${count}</button>`;
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Piège #6 : imports sans l'extension `.js`
|
|
152
|
+
|
|
153
|
+
**Symptôme :** TypeScript compile, mais le navigateur obtient une 404.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// ❌ L'IA omet souvent l'extension
|
|
157
|
+
import { foo } from "./bar";
|
|
158
|
+
import { Home } from "./pages/home";
|
|
159
|
+
|
|
160
|
+
// ✅ Correct : les modules ES dans le navigateur nécessitent l'extension
|
|
161
|
+
import { foo } from "./bar.js";
|
|
162
|
+
import { Home } from "./pages/home.js";
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Pourquoi `.js` et pas `.ts` :** le navigateur reçoit du JS déjà compilé. TypeScript est assez intelligent pour comprendre `./bar.js` comme une référence à `./bar.ts` à la compilation.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Piège #7 : listes via `.map()` sans clés
|
|
170
|
+
|
|
171
|
+
**Symptôme :** lors du réordonnancement des éléments, le focus d'input est perdu / les animations CSS se cassent / les performances souffrent sur les grandes listes.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// ❌ Fonctionne, mais sans clé : recrée le DOM à chaque changement
|
|
175
|
+
html`<ul>${() => items().map(t => html`<li>${t.name}</li>`)}</ul>`
|
|
176
|
+
|
|
177
|
+
// ✅ Correct : each() avec une fonction de clé
|
|
178
|
+
import { each } from "@madojs/mado";
|
|
179
|
+
html`<ul>${() => each(items(), t => t.id, t => html`<li>${t.name}</li>`)}</ul>`
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Règle :** utilisez toujours `each()` pour les listes de tableaux avec des IDs stables. Réservez `.map()` uniquement pour les listes statiques.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Piège #8 : `signal.value` ou `count.get()`
|
|
187
|
+
|
|
188
|
+
**Symptôme :** l'IA écrit une API de style Vue ou Solid pre-v1.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const count = signal(0);
|
|
192
|
+
|
|
193
|
+
// ❌ Pas une telle API
|
|
194
|
+
count.value
|
|
195
|
+
count.value = 5
|
|
196
|
+
count.get()
|
|
197
|
+
|
|
198
|
+
// ✅ Correct
|
|
199
|
+
count() // lecture
|
|
200
|
+
count.set(5) // écriture
|
|
201
|
+
count.update(n => n + 1)
|
|
202
|
+
count.peek() // lecture sans abonnement
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Piège #9 : `provide(ApiCtx, value)` sans host
|
|
208
|
+
|
|
209
|
+
**Symptôme :** TypeError lors de la tentative de fournir le context.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
// ❌ L'IA oublie host
|
|
213
|
+
provide(ApiCtx, myApi);
|
|
214
|
+
inject(ApiCtx);
|
|
215
|
+
|
|
216
|
+
// ✅ Correct : le premier argument est host (le composant courant)
|
|
217
|
+
component("x-app", ({ host }) => {
|
|
218
|
+
provide(host, ApiCtx, myApi);
|
|
219
|
+
return () => html`...`;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
component("x-child", ({ host }) => {
|
|
223
|
+
const api = inject(host, ApiCtx); // signal<valeur>
|
|
224
|
+
return () => html`...`;
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Piège #10 : attendre du SSR
|
|
231
|
+
|
|
232
|
+
**Symptôme :** l'IA écrit du code en supposant que la page est pré-rendue côté serveur.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
// ❌ Ceci ne fonctionne que dans le navigateur
|
|
236
|
+
const userId = location.pathname.split("/")[2];
|
|
237
|
+
|
|
238
|
+
// ❌ Ceci aussi ne fonctionne que dans le navigateur
|
|
239
|
+
if (typeof window !== "undefined") { ... } // dans Mado, window est TOUJOURS disponible
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Mado **ne fait pas de SSR avec hydratation**. Le code ne s'exécute pas sur le serveur — il y a uniquement `bake` (prérendu statique au moment du build) et edge-prerender. Les deux remplacent le code utilisateur par un environnement linkedom, mais c'est **uniquement** pour générer du HTML avec des meta tags, pas pour exécuter la logique de page.
|
|
243
|
+
|
|
244
|
+
Cela signifie :
|
|
245
|
+
- ✅ `window`, `document`, `location`, `fetch` — disponibles sans vérifications.
|
|
246
|
+
- ❌ N'écrivez pas de code qui essaie de "fonctionner universellement sur serveur et client".
|
|
247
|
+
- ❌ N'utilisez pas les patterns Next.js (`getServerSideProps`, `headers()`).
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Piège #11 : `useForm()` avec un résolveur zod/yup
|
|
252
|
+
|
|
253
|
+
**Symptôme :** l'IA veut brancher un validateur.
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
// ❌ Pas une telle API
|
|
257
|
+
const f = useForm({ resolver: zodResolver(schema) });
|
|
258
|
+
|
|
259
|
+
// ✅ Correct : validation HTML5 via attributs
|
|
260
|
+
const f = useForm({
|
|
261
|
+
email: { required: true, type: "email" },
|
|
262
|
+
age: { required: true, type: "number", min: 18 },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ✅ Ou une fonction personnalisée si HTML5 ne suffit pas
|
|
266
|
+
const f = useForm(
|
|
267
|
+
{ name: { required: true } },
|
|
268
|
+
{
|
|
269
|
+
validate: (values) => {
|
|
270
|
+
const errors: Record<string, string> = {};
|
|
271
|
+
if (values.name && /\d/.test(values.name as string)) {
|
|
272
|
+
errors.name = "Le nom ne doit pas contenir de chiffres";
|
|
273
|
+
}
|
|
274
|
+
return Object.keys(errors).length ? errors : null;
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Piège #12 : Tailwind / styled-components / CSS Modules
|
|
283
|
+
|
|
284
|
+
**Symptôme :** l'IA suggère des solutions CSS React standard.
|
|
285
|
+
|
|
286
|
+
Mado utilise **Shadow DOM + `css\`\`` + variables CSS**. Les frameworks UI globaux (Tailwind,
|
|
287
|
+
Bootstrap-via-classes) **ne fonctionnent qu'en light DOM** (`shadow: false`) :
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
// Composant page/écran Light-DOM, les classes Tailwind fonctionnent
|
|
291
|
+
component("x-admin-page", () => () => html`
|
|
292
|
+
<section class="bg-white shadow-lg rounded-lg p-4">
|
|
293
|
+
...
|
|
294
|
+
</section>
|
|
295
|
+
`, { shadow: false });
|
|
296
|
+
|
|
297
|
+
// Composant Shadow-DOM (par défaut) — Tailwind ne fonctionne PAS.
|
|
298
|
+
// Utilisez css`` ou ::part() pour le stylage externe.
|
|
299
|
+
component("x-button", () => () => html`<button><slot></slot></button>`, {
|
|
300
|
+
styles: css`
|
|
301
|
+
button {
|
|
302
|
+
background: var(--button-bg, #2563eb);
|
|
303
|
+
color: white;
|
|
304
|
+
padding: .5rem 1rem;
|
|
305
|
+
border-radius: 6px;
|
|
306
|
+
}
|
|
307
|
+
`,
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Thèmes et personnalisation — via des variables CSS**, pas des classes.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Piège #13 : `import * as Mado from "@madojs/mado"`
|
|
316
|
+
|
|
317
|
+
**Symptôme :** l'IA veut un import de namespace.
|
|
318
|
+
|
|
319
|
+
Cela fonctionne, mais duplique les noms et se tree-shake mal. Les imports nommés sont préférés :
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
// ✅ Canonique
|
|
323
|
+
import { signal, html, component, css, page } from "@madojs/mado";
|
|
324
|
+
|
|
325
|
+
// ⚠️ Fonctionne, mais excessif
|
|
326
|
+
import * as Mado from "@madojs/mado";
|
|
327
|
+
Mado.signal(0);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Piège #14 : tentative d'ajouter une dépendance runtime
|
|
333
|
+
|
|
334
|
+
**Symptôme :** l'IA suggère `npm install lodash` / `npm install date-fns` / etc.
|
|
335
|
+
|
|
336
|
+
Mado est **zéro dépendances runtime** par conception. Si l'IA veut ajouter :
|
|
337
|
+
- **lodash** → utilisez du JS natif (`Object.entries`, `Array.prototype`, `structuredClone`) ;
|
|
338
|
+
- **date-fns** → utilisez `Intl.DateTimeFormat` et `Intl.RelativeTimeFormat` ;
|
|
339
|
+
- **uuid** → `crypto.randomUUID()` ;
|
|
340
|
+
- **axios** → `fetch` natif + `jsonFetcher()` de Mado ;
|
|
341
|
+
- **classnames** → template literal natif ou une map d'objet.
|
|
342
|
+
|
|
343
|
+
Toute dépendance runtime est une **violation des principes du framework**. Si vous ne pouvez vraiment pas l'éviter — ajoutez-la au projet utilisateur, pas au cœur de Mado.
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Piège #15 : `<style>` inline dans les templates de page
|
|
348
|
+
|
|
349
|
+
**Symptôme :** l'IA met un grand `<style>` directement dans une page `html\`\``.
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
// ❌ Fonctionne, mais se met à l'échelle difficilement et complique le nettoyage
|
|
353
|
+
page({
|
|
354
|
+
view: () => html`
|
|
355
|
+
<style>.panel { padding: 1rem; }</style>
|
|
356
|
+
<section class="panel">...</section>
|
|
357
|
+
`,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// ✅ Correct : styles de composant via css``
|
|
361
|
+
component("x-admin-panel", () => () => html`
|
|
362
|
+
<section class="panel">...</section>
|
|
363
|
+
`, {
|
|
364
|
+
styles: css`
|
|
365
|
+
.panel { padding: 1rem; }
|
|
366
|
+
`,
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Pour les écrans route/page d'admin backend, il est souvent approprié d'utiliser `shadow: false`,
|
|
371
|
+
afin que les utilitaires globaux de layout/form/table fonctionnent comme un panneau d'admin
|
|
372
|
+
ordinaire. Mais si le layout utilise `<slot>` pour projeter la page dans le shell, gardez le
|
|
373
|
+
layout en Shadow DOM et gardez les styles du shell dans `styles: css\`\``.
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Piège #16 : liens Shadow DOM sans `data-link`
|
|
378
|
+
|
|
379
|
+
**Symptôme :** un lien à l'intérieur d'un Web Component provoque un rechargement complet de
|
|
380
|
+
la page ou n'est pas préchargé.
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
// ❌ Lien ordinaire : le navigateur effectuera un rechargement complet
|
|
384
|
+
html`<a href="/tickets/42">Ouvrir</a>`
|
|
385
|
+
|
|
386
|
+
// ✅ Navigation SPA : router() interceptera le clic même à travers Shadow DOM
|
|
387
|
+
html`<a href="/tickets/42" data-link>Ouvrir</a>`
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Mado trouve le lien via `event.composedPath()`, donc `data-link` fonctionne aussi à l'intérieur
|
|
391
|
+
de Shadow DOM. Le hover-prefetch utilise le même chemin ; `data-no-prefetch` désactive le
|
|
392
|
+
prefetch pour un lien spécifique.
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Piège #17 : `resource()` en dehors du setup du composant
|
|
397
|
+
|
|
398
|
+
**Symptôme :** l'IA crée une resource dans le scope du module pour "réutiliser" des données
|
|
399
|
+
entre les pages.
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
// ❌ Pas de nettoyage du lifecycle, générera un avertissement dev
|
|
403
|
+
const tickets = resource(() => "tickets", () => api.listTickets());
|
|
404
|
+
|
|
405
|
+
component("x-tickets", () => {
|
|
406
|
+
return () => html`${() => tickets.data()?.length ?? 0}`;
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ✅ Créer la resource à l'intérieur du setup du composant
|
|
410
|
+
component("x-tickets", () => {
|
|
411
|
+
const tickets = resource(() => "tickets", () => api.listTickets());
|
|
412
|
+
return () => html`${() => tickets.data()?.length ?? 0}`;
|
|
413
|
+
});
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Ainsi, les abonnements d'invalidation, les abort controllers et les effects seront
|
|
417
|
+
nettoyés quand le composant se déconnecte.
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## Piège #18 : supposer que les templates imbriqués ne nécessitent pas de nettoyage
|
|
422
|
+
|
|
423
|
+
**Symptôme :** l'IA assemble un outlet de route ou une UI conditionnelle à partir de
|
|
424
|
+
`TemplateResult`s imbriqués, et les anciens éléments continuent de vivre sous la nouvelle page.
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
const view = signal(html`<x-home></x-home>`);
|
|
428
|
+
|
|
429
|
+
// ✅ Pattern normal : un TemplateResult imbriqué peut être retourné depuis un binding enfant
|
|
430
|
+
html`${view}`
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
À partir de v0.3, ceci est garanti par des tests de régression : quand un binding enfant est
|
|
434
|
+
remplacé, Mado dispose récursivement les instances/effects de template imbriqués. Si vous
|
|
435
|
+
voyez des pages s'accumuler dans `#app`, c'est un bug core, pas quelque chose que vous devez
|
|
436
|
+
nettoyer manuellement.
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## Piège #19 : utilitaires CSS globaux dans Shadow DOM
|
|
441
|
+
|
|
442
|
+
**Symptôme :** la page a l'air "sans style" : `.page-head`, `.btn`, `.form-grid`,
|
|
443
|
+
`.metric-grid` ne sont pas appliqués.
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
// ❌ .page-head est déclaré globalement, mais x-dashboard utilise Shadow DOM par défaut
|
|
447
|
+
component("x-dashboard", () => () => html`
|
|
448
|
+
<header class="page-head">...</header>
|
|
449
|
+
<div class="metric-grid">...</div>
|
|
450
|
+
`);
|
|
451
|
+
|
|
452
|
+
// ✅ Les composants page/layout/admin-shell doivent souvent être Light DOM
|
|
453
|
+
component("x-dashboard", () => () => html`
|
|
454
|
+
<header class="page-head">...</header>
|
|
455
|
+
<div class="metric-grid">...</div>
|
|
456
|
+
`, { shadow: false });
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Règle : Shadow DOM — pour les widgets feuilles et les layouts basés sur slot, Light DOM — pour
|
|
460
|
+
les composants route/page/admin-screen qui utilisent intentionnellement des utilitaires partagés
|
|
461
|
+
de layout/form/table. Rappel : `<slot>` ne projette les enfants qu'en Shadow DOM ; avec
|
|
462
|
+
`shadow: false` c'est un élément ordinaire.
|
|
463
|
+
Plus de détails : [`09-shadow-vs-light-dom.md`](./09-shadow-vs-light-dom.md).
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Aide-mémoire pour l'IA
|
|
468
|
+
|
|
469
|
+
| Si vous voulez faire… | Correct dans Mado |
|
|
470
|
+
|---|---|
|
|
471
|
+
| `useState(0)` | `signal(0)` |
|
|
472
|
+
| `useEffect(() => {...}, [a, b])` | `effect(() => {...})` (auto-dépendances) |
|
|
473
|
+
| `useEffect(() => return cleanup, [])` | `ctx.onDispose(cleanup)` |
|
|
474
|
+
| `useMemo(() => x, [a])` | `computed(() => x)` |
|
|
475
|
+
| `useCallback(fn, [])` | fonction ordinaire |
|
|
476
|
+
| `useContext(Ctx)` | `inject(host, Ctx)` |
|
|
477
|
+
| `useQuery(['key'], fn)` | `resource(() => 'key', fn)` |
|
|
478
|
+
| `useMutation(fn)` | `mutation(fn, { invalidates: [...] })` |
|
|
479
|
+
| `useRouter().push('/')` | `navigate('/')` |
|
|
480
|
+
| `useRouter().query.q` | `queryParam('q')` |
|
|
481
|
+
| `<input value={v} onChange={...}>` | `<input .value=${v} @input=${...}>` |
|
|
482
|
+
| `{items.map(x => ...)}` | `${() => each(items, x => x.id, x => ...)}` |
|
|
483
|
+
| `useForm({ resolver: zodResolver })` | `useForm({...}, { validate: (v) => ... })` |
|
|
484
|
+
| `class extends HTMLElement` | `component('x-name', setup)` |
|
|
485
|
+
| `@customElement('x')` | `component('x-name', setup)` |
|
|
486
|
+
|
|
487
|
+
Si quelque chose ne rentre pas dans cette liste — ouvrez `src/` et **lisez 500 lignes**. Sérieusement. Mado est intentionnellement petit pour être lisible.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Test LLM sans historique
|
|
2
|
+
|
|
3
|
+
Ce document définit un test de validation pratique pour Mado.
|
|
4
|
+
|
|
5
|
+
La question n'est pas "un LLM peut-il générer du code frontend ?" Il le peut. La question est :
|
|
6
|
+
un LLM fraîchement initialisé peut-il écrire du Mado idiomatique sans retomber dans du code
|
|
7
|
+
de forme React ?
|
|
8
|
+
|
|
9
|
+
## Contexte autorisé
|
|
10
|
+
|
|
11
|
+
Pour le premier passage, donnez à l'agent uniquement :
|
|
12
|
+
|
|
13
|
+
- `AGENTS.md`
|
|
14
|
+
- `README.md`
|
|
15
|
+
- `docs/ru/07-llm-pitfalls.md`
|
|
16
|
+
- `examples/basic/README.md` si un tour minimal de l'API est nécessaire
|
|
17
|
+
- des fichiers `examples/showcase/**` spécifiques uniquement quand l'agent demande un pattern
|
|
18
|
+
d'application plus grande
|
|
19
|
+
|
|
20
|
+
L'agent peut rechercher des API ciblées dans `src/` quand il est bloqué, mais ne doit pas
|
|
21
|
+
charger tout le framework dans le context.
|
|
22
|
+
|
|
23
|
+
## Tâche
|
|
24
|
+
|
|
25
|
+
Construire `examples/tickets` : une petite SPA d'administration de tickets pour un développeur
|
|
26
|
+
solo/backend.
|
|
27
|
+
|
|
28
|
+
Comportement requis :
|
|
29
|
+
|
|
30
|
+
- routes : `/`, `/tickets`, `/tickets/new`, `/tickets/:id`, `*` ;
|
|
31
|
+
- API mock en mémoire avec des délais async réalistes ;
|
|
32
|
+
- page de liste avec `resource()`, `queryParam()` filtres de recherche/statut, `computed()`,
|
|
33
|
+
et des lignes `each()` avec clés ;
|
|
34
|
+
- flux de création et d'édition avec `useForm()` + `mutation()` + `invalidates` ;
|
|
35
|
+
- état UI local avec `signal()` ;
|
|
36
|
+
- composants shell, metric et badge avec slot pour une UI admin plus réaliste ;
|
|
37
|
+
- test de smoke important le build de l'exemple.
|
|
38
|
+
|
|
39
|
+
## Liste de contrôle des échecs
|
|
40
|
+
|
|
41
|
+
Cherchez ces éléments après l'implémentation :
|
|
42
|
+
|
|
43
|
+
- JSX, `useState`, `useEffect`, `ref`, `$state`, ou composants de style classe ;
|
|
44
|
+
- `${signal()}` ou `${signal() + 1}` là où un thunk enfant réactif est requis ;
|
|
45
|
+
- `disabled=${...}` au lieu de `?disabled=${...}` ;
|
|
46
|
+
- listes dynamiques rendues avec un mapping de tableau sans clé au lieu de `each()` ;
|
|
47
|
+
- imports ESM navigateur sans `.js` ;
|
|
48
|
+
- `resource()` créé en dehors du setup du composant ;
|
|
49
|
+
- nouvelles dépendances runtime ou nouvelles API publiques du framework.
|
|
50
|
+
|
|
51
|
+
## Notes de résultats
|
|
52
|
+
|
|
53
|
+
L'implémentation actuelle de `examples/tickets` n'a pas nécessité de nouvelles API publiques ni
|
|
54
|
+
de dépendances runtime.
|
|
55
|
+
|
|
56
|
+
Le principal point de pression dans la documentation reste le lifecycle : les anciens exemples
|
|
57
|
+
peuvent donner l'impression qu'il est acceptable de créer `resource()` directement dans
|
|
58
|
+
`page.view()`. L'exemple tickets utilise plutôt des composants wrapper au niveau page, de sorte
|
|
59
|
+
que les resources sont enregistrées à l'intérieur du setup du composant et se nettoient avec
|
|
60
|
+
le composant.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Shadow DOM vs Light DOM
|
|
2
|
+
|
|
3
|
+
Les composants Mado utilisent Shadow DOM par défaut. C'est un bon défaut pour les widgets
|
|
4
|
+
autonomes, mais ce n'est pas le bon défaut pour chaque composant dans une application.
|
|
5
|
+
|
|
6
|
+
## Règle générale
|
|
7
|
+
|
|
8
|
+
Utilisez **Shadow DOM** pour les widgets feuilles :
|
|
9
|
+
|
|
10
|
+
- boutons, badges, cartes, métriques ;
|
|
11
|
+
- modals, toasts, petits composants visuels ;
|
|
12
|
+
- widgets d'intégration qui ne devraient pas hériter du CSS de l'app accidentellement ;
|
|
13
|
+
- composants dont le stylage doit appartenir au composant lui-même.
|
|
14
|
+
|
|
15
|
+
Utilisez **Light DOM** (`{ shadow: false }`) pour la structure de l'app qui veut partager
|
|
16
|
+
les utilitaires CSS globaux :
|
|
17
|
+
|
|
18
|
+
- composants route/page ;
|
|
19
|
+
- écrans admin avec des layouts denses de tableau/formulaire ;
|
|
20
|
+
- écrans riches en données avec des tableaux et des formulaires ;
|
|
21
|
+
- composants qui partagent intentionnellement les utilitaires globaux de layout, formulaire et
|
|
22
|
+
tableau ;
|
|
23
|
+
- endroits où les enfants doivent simplement rester dans le DOM normal du document.
|
|
24
|
+
|
|
25
|
+
## Le piège
|
|
26
|
+
|
|
27
|
+
Le CSS global ne franchit pas une frontière Shadow DOM.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// global.ts
|
|
31
|
+
export const globalStyles = css`
|
|
32
|
+
.page-head { display: flex; justify-content: space-between; }
|
|
33
|
+
.metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); }
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
// ❌ .page-head et .metric-grid ne s'appliqueront pas à l'intérieur du shadowRoot de x-dashboard
|
|
37
|
+
component("x-dashboard", () => () => html`
|
|
38
|
+
<header class="page-head">...</header>
|
|
39
|
+
<div class="metric-grid">...</div>
|
|
40
|
+
`);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Corrigez en rendant le composant route/page Light DOM :
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
component("x-dashboard", () => () => html`
|
|
47
|
+
<header class="page-head">...</header>
|
|
48
|
+
<div class="metric-grid">...</div>
|
|
49
|
+
`, {
|
|
50
|
+
shadow: false,
|
|
51
|
+
styles: css`
|
|
52
|
+
x-dashboard { display: block; }
|
|
53
|
+
x-dashboard .panel { padding: 1rem; }
|
|
54
|
+
`,
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Maintenant les utilitaires globaux et les styles locaux scopés fonctionnent tous les deux.
|
|
59
|
+
|
|
60
|
+
## Comment les styles se comportent
|
|
61
|
+
|
|
62
|
+
- `styles: css\`\`` en Shadow DOM est adopté dans le shadowRoot du composant.
|
|
63
|
+
- `styles: css\`\`` avec `shadow: false` est scopé au nom du tag et adopté globalement.
|
|
64
|
+
- Les propriétés CSS personnalisées (`--accent`, `--bg`, etc.) traversent les frontières Shadow DOM.
|
|
65
|
+
- Les sélecteurs de classe comme `.btn`, `.form-grid`, `.page-head` ne traversent **pas** les
|
|
66
|
+
frontières Shadow DOM.
|
|
67
|
+
- Les enfants slottés conservent leurs propres styles de document ; le composant shadow ne peut
|
|
68
|
+
les cibler qu'à travers `::slotted(...)`.
|
|
69
|
+
- `<slot>` projette les enfants uniquement en Shadow DOM. Dans un composant `shadow: false`,
|
|
70
|
+
c'est juste un élément `<slot>` normal et ne déplacera pas les enfants à cet endroit dans
|
|
71
|
+
votre layout.
|
|
72
|
+
|
|
73
|
+
## Forme recommandée de l'application
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// racine et pages : Light DOM
|
|
77
|
+
component("x-app", setup, { shadow: false });
|
|
78
|
+
component("x-users-page", setup, { shadow: false });
|
|
79
|
+
|
|
80
|
+
// layout basé sur slot : Shadow DOM par défaut, car il possède la grille du shell
|
|
81
|
+
component("x-app-layout", setup);
|
|
82
|
+
|
|
83
|
+
// widgets feuilles : Shadow DOM par défaut
|
|
84
|
+
component("x-status-badge", setup);
|
|
85
|
+
component("x-stat-card", setup);
|
|
86
|
+
component("x-toast-stack", setup);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Cela donne aux écrans d'admin backend un CSS prévisible tout en préservant l'encapsulation
|
|
90
|
+
pour les widgets réutilisables et les shells basés sur slot.
|
|
91
|
+
|
|
92
|
+
Si un layout n'a pas besoin de projection slot et doit être entièrement stylé par du CSS
|
|
93
|
+
global, `shadow: false` peut rester un bon choix. S'il contient `<slot>`, gardez Shadow DOM
|
|
94
|
+
et mettez les styles du shell dans `styles: css\`\``.
|
|
95
|
+
|
|
96
|
+
## Routage et liens
|
|
97
|
+
|
|
98
|
+
`data-link` fonctionne à l'intérieur de Shadow DOM. Le router utilise `event.composedPath()`,
|
|
99
|
+
donc l'interception de clic et le hover-prefetch peuvent voir les liens depuis les shadow roots
|
|
100
|
+
ouverts.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
component("x-card-link", () => () => html`
|
|
104
|
+
<a href="/app/accounts" data-link>Comptes</a>
|
|
105
|
+
`);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Le lien peut être en Shadow DOM ; la navigation reste SPA.
|
|
109
|
+
|
|
110
|
+
## Leçon du Showcase
|
|
111
|
+
|
|
112
|
+
`examples/showcase` utilise cette séparation délibérément :
|
|
113
|
+
|
|
114
|
+
- `x-app` et les pages de route CRM sont Light DOM ;
|
|
115
|
+
- `x-app-layout` garde Shadow DOM car il possède un shell sidebar/contenu basé sur slot ;
|
|
116
|
+
- les utilitaires de tableau/formulaire/page vivent dans `styles/global.ts` ;
|
|
117
|
+
- les composants feuilles comme `x-stat-card`, `x-status-badge`, `x-modal` et `x-toast-stack`
|
|
118
|
+
gardent Shadow DOM.
|
|
119
|
+
|
|
120
|
+
Si une page a soudainement l'air sans style, vérifiez si elle utilise des classes globales
|
|
121
|
+
à l'intérieur d'un composant Shadow DOM. C'est généralement le problème.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Mado docs — Français
|
|
2
|
+
|
|
3
|
+
Documentation française.
|
|
4
|
+
|
|
5
|
+
| Section | Source |
|
|
6
|
+
|---|---|
|
|
7
|
+
| The Mado way | [00-the-mado-way.md](./00-the-mado-way.md) |
|
|
8
|
+
| Routing | [01-routing.md](./01-routing.md) |
|
|
9
|
+
| Project layout | [02-project-layout.md](./02-project-layout.md) |
|
|
10
|
+
| Static bake & SEO | [03-static-bake.md](./03-static-bake.md) |
|
|
11
|
+
| IDE setup | [04-ide-setup.md](./04-ide-setup.md) |
|
|
12
|
+
| Why Mado | [05-why-mado.md](./05-why-mado.md) |
|
|
13
|
+
| For backenders | [06-for-backenders.md](./06-for-backenders.md) |
|
|
14
|
+
| LLM pitfalls | [07-llm-pitfalls.md](./07-llm-pitfalls.md) |
|
|
15
|
+
| LLM zero-history test | [08-llm-zero-history-test.md](./08-llm-zero-history-test.md) |
|
|
16
|
+
| Shadow DOM vs Light DOM | [09-shadow-vs-light-dom.md](./09-shadow-vs-light-dom.md) |
|