@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,486 @@
|
|
|
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
|
+
1. **AI agents in the IDE** that read `AGENTS.md` / `.cursorrules` / `.github/copilot-instructions.md`. More detail on typical pitfalls is provided here.
|
|
8
|
+
2. **Humans** who received code from an AI with these errors and don't understand what's wrong.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Pitfall #1: `${signal()}` instead of `${() => signal()}`
|
|
13
|
+
|
|
14
|
+
**Symptom:** the value in the template is displayed but does not update when the signal changes.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
const count = signal(0);
|
|
18
|
+
|
|
19
|
+
// ❌ AI often generates this
|
|
20
|
+
html`<div>Count: ${count() * 2}</div>`
|
|
21
|
+
// → Will render "Count: 0" and never update again.
|
|
22
|
+
// count() is read once when the TemplateResult is created.
|
|
23
|
+
|
|
24
|
+
// ✅ Correct — getter function
|
|
25
|
+
html`<div>Count: ${() => count() * 2}</div>`
|
|
26
|
+
// → Mado will create an effect() for this function and re-render when count changes.
|
|
27
|
+
|
|
28
|
+
// ✅ Also correct — the signal itself is a function
|
|
29
|
+
html`<div>Count: ${count}</div>`
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Rule:**
|
|
33
|
+
- If the `${...}` contains an **expression** (something is done with the signal) — wrap it in `() => ...`.
|
|
34
|
+
- If the `${...}` contains **the signal itself** — it can be used as-is.
|
|
35
|
+
|
|
36
|
+
This applies to **child bindings** (text inside tags) and to **value attributes** (`@click`, `.prop`, `?attr`, regular attributes).
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Pitfall #2: `<button disabled=${loading}>` instead of `?disabled`
|
|
41
|
+
|
|
42
|
+
**Symptom:** the button is not disabled, or is always disabled.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const loading = signal(false);
|
|
46
|
+
|
|
47
|
+
// ❌ This is setAttribute("disabled", "false") — the DOM treats this as disabled
|
|
48
|
+
html`<button disabled=${loading()}>Save</button>`
|
|
49
|
+
|
|
50
|
+
// ✅ Correct — boolean binding (toggle attribute)
|
|
51
|
+
html`<button ?disabled=${loading}>Save</button>`
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Rules for attributes:**
|
|
55
|
+
| Prefix | What it does | When to use |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `attr=` | `setAttribute("attr", value)` | strings, numbers, URLs |
|
|
58
|
+
| `.attr=` | `el.attr = value` (DOM property) | objects, arrays, input `.value` |
|
|
59
|
+
| `?attr=` | toggle attribute (by truthiness) | `disabled`, `hidden`, `checked`, etc |
|
|
60
|
+
| `@evt=` | `addEventListener("evt", fn)` | event handlers |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Pitfall #3: useState / useEffect style
|
|
65
|
+
|
|
66
|
+
**Symptom:** AI generates React-like code that doesn't work in Mado.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// ❌ AI often writes this
|
|
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 in 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-subscribe, disposed automatically
|
|
82
|
+
return () => html`
|
|
83
|
+
<button @click=${() => count.update(c => c + 1)}>${count}</button>
|
|
84
|
+
`;
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Key differences:**
|
|
89
|
+
- No hooks, no hook rules.
|
|
90
|
+
- `signal()` can be created anywhere — in setup, in an effect, in a handler.
|
|
91
|
+
- `effect()` sees what it read on its own — no dependency array needed.
|
|
92
|
+
- A component is `component("x-name", setup)`, not a JSX function.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Pitfall #4: `useEffect(() => { ... return cleanup })`
|
|
97
|
+
|
|
98
|
+
**Symptom:** AI writes `return cleanup` inside an effect, expecting it to work like in React.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// ❌ AI tries to write this
|
|
102
|
+
component("x-timer", () => {
|
|
103
|
+
effect(() => {
|
|
104
|
+
const id = setInterval(..., 1000);
|
|
105
|
+
return () => clearInterval(id); // will NOT work, use ctx.onDispose instead
|
|
106
|
+
});
|
|
107
|
+
return () => html`...`;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ✅ Correct: cleanup 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()` 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`.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Pitfall #5: Component as a class or with a decorator
|
|
123
|
+
|
|
124
|
+
**Symptom:** AI generates a Lit-style or vanilla WebComponent class.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
// ❌ AI: "let's do it like 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
|
+
// ❌ AI: "let's do it vanilla style"
|
|
135
|
+
class XCounter extends HTMLElement {
|
|
136
|
+
connectedCallback() { ... }
|
|
137
|
+
}
|
|
138
|
+
customElements.define("x-counter", XCounter);
|
|
139
|
+
|
|
140
|
+
// ✅ Correct: functional component()
|
|
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
|
+
## Pitfall #6: imports without the `.js` extension
|
|
152
|
+
|
|
153
|
+
**Symptom:** TypeScript compiles, but the browser gets a 404.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// ❌ AI often omits the extension
|
|
157
|
+
import { foo } from "./bar";
|
|
158
|
+
import { Home } from "./pages/home";
|
|
159
|
+
|
|
160
|
+
// ✅ Correct: ES modules in the browser require the extension
|
|
161
|
+
import { foo } from "./bar.js";
|
|
162
|
+
import { Home } from "./pages/home.js";
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**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.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Pitfall #7: lists via `.map()` without keys
|
|
170
|
+
|
|
171
|
+
**Symptom:** when reordering elements, input focus is lost / CSS animations break / performance suffers on large lists.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// ❌ Works, but not keyed: recreates DOM on every change
|
|
175
|
+
html`<ul>${() => items().map(t => html`<li>${t.name}</li>`)}</ul>`
|
|
176
|
+
|
|
177
|
+
// ✅ Correct: each() with a key function
|
|
178
|
+
import { each } from "@madojs/mado";
|
|
179
|
+
html`<ul>${() => each(items(), t => t.id, t => html`<li>${t.name}</li>`)}</ul>`
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Rule:** always use `each()` for lists of arrays with stable IDs. Reserve `.map()` only for static lists.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Pitfall #8: `signal.value` or `count.get()`
|
|
187
|
+
|
|
188
|
+
**Symptom:** AI writes an API in Vue or pre-v1 Solid style.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const count = signal(0);
|
|
192
|
+
|
|
193
|
+
// ❌ No such API
|
|
194
|
+
count.value
|
|
195
|
+
count.value = 5
|
|
196
|
+
count.get()
|
|
197
|
+
|
|
198
|
+
// ✅ Correct
|
|
199
|
+
count() // read
|
|
200
|
+
count.set(5) // write
|
|
201
|
+
count.update(n => n + 1)
|
|
202
|
+
count.peek() // read without subscribing
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Pitfall #9: `provide(ApiCtx, value)` without host
|
|
208
|
+
|
|
209
|
+
**Symptom:** TypeError when trying to provide context.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
// ❌ AI forgets host
|
|
213
|
+
provide(ApiCtx, myApi);
|
|
214
|
+
inject(ApiCtx);
|
|
215
|
+
|
|
216
|
+
// ✅ Correct: first argument is host (the current component)
|
|
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<value>
|
|
224
|
+
return () => html`...`;
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Pitfall #10: expecting SSR
|
|
231
|
+
|
|
232
|
+
**Symptom:** AI writes code assuming the page is pre-rendered on the server.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
// ❌ This works only in the browser
|
|
236
|
+
const userId = location.pathname.split("/")[2];
|
|
237
|
+
|
|
238
|
+
// ❌ This too works only in the browser
|
|
239
|
+
if (typeof window !== "undefined") { ... } // in Mado, window is ALWAYS available
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
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.
|
|
243
|
+
|
|
244
|
+
This means:
|
|
245
|
+
- ✅ `window`, `document`, `location`, `fetch` — available without checks.
|
|
246
|
+
- ❌ Don't write code that tries to "universally work on server and client".
|
|
247
|
+
- ❌ Don't use Next.js patterns (`getServerSideProps`, `headers()`).
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Pitfall #11: `useForm()` with a zod/yup resolver
|
|
252
|
+
|
|
253
|
+
**Symptom:** AI wants to plug in a validator.
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
// ❌ No such API
|
|
257
|
+
const f = useForm({ resolver: zodResolver(schema) });
|
|
258
|
+
|
|
259
|
+
// ✅ Correct: HTML5 validation via attributes
|
|
260
|
+
const f = useForm({
|
|
261
|
+
email: { required: true, type: "email" },
|
|
262
|
+
age: { required: true, type: "number", min: 18 },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ✅ Or a custom function if HTML5 isn't enough
|
|
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 = "Name must not contain digits";
|
|
273
|
+
}
|
|
274
|
+
return Object.keys(errors).length ? errors : null;
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Pitfall #12: Tailwind / styled-components / CSS Modules
|
|
283
|
+
|
|
284
|
+
**Symptom:** AI suggests standard React CSS solutions.
|
|
285
|
+
|
|
286
|
+
Mado uses **Shadow DOM + `css\`\`` + CSS variables**. Global UI frameworks (Tailwind, Bootstrap-via-classes) **only work in light DOM** (`shadow: false`):
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
// Light-DOM page/screen component, Tailwind classes work
|
|
290
|
+
component("x-admin-page", () => () => html`
|
|
291
|
+
<section class="bg-white shadow-lg rounded-lg p-4">
|
|
292
|
+
...
|
|
293
|
+
</section>
|
|
294
|
+
`, { shadow: false });
|
|
295
|
+
|
|
296
|
+
// Shadow-DOM component (default) — Tailwind does NOT work.
|
|
297
|
+
// Use css`` or ::part() for external styling.
|
|
298
|
+
component("x-button", () => () => html`<button><slot></slot></button>`, {
|
|
299
|
+
styles: css`
|
|
300
|
+
button {
|
|
301
|
+
background: var(--button-bg, #2563eb);
|
|
302
|
+
color: white;
|
|
303
|
+
padding: .5rem 1rem;
|
|
304
|
+
border-radius: 6px;
|
|
305
|
+
}
|
|
306
|
+
`,
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Themes and customization — via CSS variables**, not classes.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Pitfall #13: `import * as Mado from "@madojs/mado"`
|
|
315
|
+
|
|
316
|
+
**Symptom:** AI wants a namespace import.
|
|
317
|
+
|
|
318
|
+
This works, but duplicates names and tree-shakes poorly. Named imports are preferred:
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
// ✅ Canonical
|
|
322
|
+
import { signal, html, component, css, page } from "@madojs/mado";
|
|
323
|
+
|
|
324
|
+
// ⚠️ Works, but excessive
|
|
325
|
+
import * as Mado from "@madojs/mado";
|
|
326
|
+
Mado.signal(0);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Pitfall #14: attempting to add a runtime dependency
|
|
332
|
+
|
|
333
|
+
**Symptom:** AI suggests `npm install lodash` / `npm install date-fns` / etc.
|
|
334
|
+
|
|
335
|
+
Mado is **zero runtime deps** by design. If AI wants to add:
|
|
336
|
+
- **lodash** → use native JS (`Object.entries`, `Array.prototype`, `structuredClone`);
|
|
337
|
+
- **date-fns** → use `Intl.DateTimeFormat` and `Intl.RelativeTimeFormat`;
|
|
338
|
+
- **uuid** → `crypto.randomUUID()`;
|
|
339
|
+
- **axios** → native `fetch` + `jsonFetcher()` from Mado;
|
|
340
|
+
- **classnames** → native template literal or an object map.
|
|
341
|
+
|
|
342
|
+
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.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Pitfall #15: inline `<style>` inside page templates
|
|
347
|
+
|
|
348
|
+
**Symptom:** AI puts a large `<style>` directly inside a `html\`\`` page.
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
// ❌ Works, but scales poorly and complicates cleanup
|
|
352
|
+
page({
|
|
353
|
+
view: () => html`
|
|
354
|
+
<style>.panel { padding: 1rem; }</style>
|
|
355
|
+
<section class="panel">...</section>
|
|
356
|
+
`,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// ✅ Correct: component styles via css``
|
|
360
|
+
component("x-admin-panel", () => () => html`
|
|
361
|
+
<section class="panel">...</section>
|
|
362
|
+
`, {
|
|
363
|
+
styles: css`
|
|
364
|
+
.panel { padding: 1rem; }
|
|
365
|
+
`,
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
For backend admin route/page screens it is often appropriate to use `shadow: false`, so that
|
|
370
|
+
global layout/form/table utilities work like a regular admin panel. But if
|
|
371
|
+
the layout uses `<slot>` to project the page into the shell, keep the layout in
|
|
372
|
+
Shadow DOM and keep the shell styles in `styles: css\`\``.
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Pitfall #16: Shadow DOM links without `data-link`
|
|
377
|
+
|
|
378
|
+
**Symptom:** a link inside a Web Component causes a full page reload or is not
|
|
379
|
+
prefetched.
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
// ❌ Regular link: browser will perform a full reload
|
|
383
|
+
html`<a href="/tickets/42">Open</a>`
|
|
384
|
+
|
|
385
|
+
// ✅ SPA navigation: router() will intercept the click even through Shadow DOM
|
|
386
|
+
html`<a href="/tickets/42" data-link>Open</a>`
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Mado finds the link via `event.composedPath()`, so `data-link` works
|
|
390
|
+
inside Shadow DOM as well. Hover-prefetch uses the same path; `data-no-prefetch`
|
|
391
|
+
disables prefetch for a specific link.
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Pitfall #17: `resource()` outside component setup
|
|
396
|
+
|
|
397
|
+
**Symptom:** AI creates a resource in module scope to "reuse"
|
|
398
|
+
data between pages.
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
// ❌ No lifecycle cleanup, will emit dev-warning
|
|
402
|
+
const tickets = resource(() => "tickets", () => api.listTickets());
|
|
403
|
+
|
|
404
|
+
component("x-tickets", () => {
|
|
405
|
+
return () => html`${() => tickets.data()?.length ?? 0}`;
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// ✅ Create resource inside the component setup
|
|
409
|
+
component("x-tickets", () => {
|
|
410
|
+
const tickets = resource(() => "tickets", () => api.listTickets());
|
|
411
|
+
return () => html`${() => tickets.data()?.length ?? 0}`;
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
This way invalidation subscriptions, abort controllers, and effects will be
|
|
416
|
+
cleaned up when the component disconnects.
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## Pitfall #18: assuming nested templates don't require cleanup
|
|
421
|
+
|
|
422
|
+
**Symptom:** AI assembles a route outlet or conditional UI from nested
|
|
423
|
+
`TemplateResult`s, and then old elements continue living below the new page.
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
const view = signal(html`<x-home></x-home>`);
|
|
427
|
+
|
|
428
|
+
// ✅ Normal pattern: nested TemplateResult can be returned from a child-binding
|
|
429
|
+
html`${view}`
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Starting from v0.3 this is guaranteed by regression tests: when a child-binding is
|
|
433
|
+
replaced, Mado recursively disposes nested template instances/effects. If you see
|
|
434
|
+
pages accumulating in `#app`, that is a core bug, not something you need to
|
|
435
|
+
clean up manually.
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## Pitfall #19: global CSS utilities inside Shadow DOM
|
|
440
|
+
|
|
441
|
+
**Symptom:** the page looks "unstyled": `.page-head`, `.btn`,
|
|
442
|
+
`.form-grid`, `.metric-grid` are not applied.
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
// ❌ .page-head is declared globally, but x-dashboard defaults to Shadow DOM
|
|
446
|
+
component("x-dashboard", () => () => html`
|
|
447
|
+
<header class="page-head">...</header>
|
|
448
|
+
<div class="metric-grid">...</div>
|
|
449
|
+
`);
|
|
450
|
+
|
|
451
|
+
// ✅ Page/layout/admin-shell components often should be Light DOM
|
|
452
|
+
component("x-dashboard", () => () => html`
|
|
453
|
+
<header class="page-head">...</header>
|
|
454
|
+
<div class="metric-grid">...</div>
|
|
455
|
+
`, { shadow: false });
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Rule: Shadow DOM — for leaf widgets and slot-based layouts, Light DOM — for
|
|
459
|
+
route/page/admin-screen components that intentionally use shared
|
|
460
|
+
layout/form/table utilities. Remember: `<slot>` only projects children in
|
|
461
|
+
Shadow DOM; with `shadow: false` it is a regular element.
|
|
462
|
+
More details: [`09-shadow-vs-light-dom.md`](./09-shadow-vs-light-dom.md).
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Cheat-sheet for AI
|
|
467
|
+
|
|
468
|
+
| If you want to do… | Correct in Mado |
|
|
469
|
+
|---|---|
|
|
470
|
+
| `useState(0)` | `signal(0)` |
|
|
471
|
+
| `useEffect(() => {...}, [a, b])` | `effect(() => {...})` (auto-deps) |
|
|
472
|
+
| `useEffect(() => return cleanup, [])` | `ctx.onDispose(cleanup)` |
|
|
473
|
+
| `useMemo(() => x, [a])` | `computed(() => x)` |
|
|
474
|
+
| `useCallback(fn, [])` | ordinary function |
|
|
475
|
+
| `useContext(Ctx)` | `inject(host, Ctx)` |
|
|
476
|
+
| `useQuery(['key'], fn)` | `resource(() => 'key', fn)` |
|
|
477
|
+
| `useMutation(fn)` | `mutation(fn, { invalidates: [...] })` |
|
|
478
|
+
| `useRouter().push('/')` | `navigate('/')` |
|
|
479
|
+
| `useRouter().query.q` | `queryParam('q')` |
|
|
480
|
+
| `<input value={v} onChange={...}>` | `<input .value=${v} @input=${...}>` |
|
|
481
|
+
| `{items.map(x => ...)}` | `${() => each(items, x => x.id, x => ...)}` |
|
|
482
|
+
| `useForm({ resolver: zodResolver })` | `useForm({...}, { validate: (v) => ... })` |
|
|
483
|
+
| `class extends HTMLElement` | `component('x-name', setup)` |
|
|
484
|
+
| `@customElement('x')` | `component('x-name', setup)` |
|
|
485
|
+
|
|
486
|
+
If something doesn't fit this list — open `src/` and **read 500 lines**. Seriously. Mado is intentionally small to be readable.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# LLM zero-history test
|
|
2
|
+
|
|
3
|
+
This document defines a practical validation test for Mado.
|
|
4
|
+
|
|
5
|
+
The question is not "can an LLM generate frontend code?" It can. The question is:
|
|
6
|
+
can a fresh LLM write idiomatic Mado without falling back to React-shaped code?
|
|
7
|
+
|
|
8
|
+
## Allowed context
|
|
9
|
+
|
|
10
|
+
For the first pass, give the agent only:
|
|
11
|
+
|
|
12
|
+
- `AGENTS.md`
|
|
13
|
+
- `README.md`
|
|
14
|
+
- `docs/ru/07-llm-pitfalls.md`
|
|
15
|
+
- `examples/basic/README.md` if a minimal API tour is needed
|
|
16
|
+
- specific `examples/showcase/**` files only when the agent asks for a larger app pattern
|
|
17
|
+
|
|
18
|
+
The agent may search targeted APIs in `src/` when blocked, but should not load
|
|
19
|
+
the whole framework into context.
|
|
20
|
+
|
|
21
|
+
## Task
|
|
22
|
+
|
|
23
|
+
Build `examples/tickets`: a small ticket-admin SPA for a solo/backend developer.
|
|
24
|
+
|
|
25
|
+
Required behavior:
|
|
26
|
+
|
|
27
|
+
- routes: `/`, `/tickets`, `/tickets/new`, `/tickets/:id`, `*`;
|
|
28
|
+
- in-memory mock API with realistic async delays;
|
|
29
|
+
- list page with `resource()`, `queryParam()` search/status filters, `computed()`,
|
|
30
|
+
and keyed `each()` rows;
|
|
31
|
+
- create and edit flows with `useForm()` + `mutation()` + `invalidates`;
|
|
32
|
+
- local UI state with `signal()`;
|
|
33
|
+
- slotted shell, metric, and badge components for a more realistic admin UI;
|
|
34
|
+
- smoke test importing the built example.
|
|
35
|
+
|
|
36
|
+
## Failure checklist
|
|
37
|
+
|
|
38
|
+
Look for these after implementation:
|
|
39
|
+
|
|
40
|
+
- JSX, `useState`, `useEffect`, `ref`, `$state`, or class-style components;
|
|
41
|
+
- `${signal()}` or `${signal() + 1}` where a reactive child thunk is required;
|
|
42
|
+
- `disabled=${...}` instead of `?disabled=${...}`;
|
|
43
|
+
- dynamic lists rendered with unkeyed array mapping instead of `each()`;
|
|
44
|
+
- browser ESM imports without `.js`;
|
|
45
|
+
- `resource()` created outside component setup;
|
|
46
|
+
- new runtime dependencies or new public framework APIs.
|
|
47
|
+
|
|
48
|
+
## Result notes
|
|
49
|
+
|
|
50
|
+
The current `examples/tickets` implementation did not require new public APIs or
|
|
51
|
+
runtime dependencies.
|
|
52
|
+
|
|
53
|
+
The main documentation pressure point remains lifecycle: older examples can make
|
|
54
|
+
it look acceptable to create `resource()` directly in `page.view()`. The tickets
|
|
55
|
+
example uses page-level wrapper components instead, so resources are registered
|
|
56
|
+
inside component setup and clean up with the component.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Shadow DOM vs Light DOM
|
|
2
|
+
|
|
3
|
+
Mado components use Shadow DOM by default. This is a good default for
|
|
4
|
+
self-contained widgets, but it is not the right default for every component in
|
|
5
|
+
an application.
|
|
6
|
+
|
|
7
|
+
## Rule of Thumb
|
|
8
|
+
|
|
9
|
+
Use **Shadow DOM** for leaf widgets:
|
|
10
|
+
|
|
11
|
+
- buttons, badges, cards, metrics;
|
|
12
|
+
- modals, toasts, small visual components;
|
|
13
|
+
- embed widgets that should not inherit app CSS accidentally;
|
|
14
|
+
- components whose styling should be owned by the component itself.
|
|
15
|
+
|
|
16
|
+
Use **Light DOM** (`{ shadow: false }`) for app structure that wants to share
|
|
17
|
+
global CSS utilities:
|
|
18
|
+
|
|
19
|
+
- route/page components;
|
|
20
|
+
- admin screens with dense table/form layouts;
|
|
21
|
+
- data-heavy screens with tables and forms;
|
|
22
|
+
- components that intentionally share global layout, form and table utilities;
|
|
23
|
+
- places where children should simply remain normal document DOM.
|
|
24
|
+
|
|
25
|
+
## The Footgun
|
|
26
|
+
|
|
27
|
+
Global CSS does not cross a Shadow DOM boundary.
|
|
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 and .metric-grid will not apply inside x-dashboard shadowRoot
|
|
37
|
+
component("x-dashboard", () => () => html`
|
|
38
|
+
<header class="page-head">...</header>
|
|
39
|
+
<div class="metric-grid">...</div>
|
|
40
|
+
`);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Fix it by making the route/page component 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
|
+
Now global utilities and local scoped styles both work.
|
|
59
|
+
|
|
60
|
+
## How Styles Behave
|
|
61
|
+
|
|
62
|
+
- `styles: css\`\`` in Shadow DOM is adopted into the component shadowRoot.
|
|
63
|
+
- `styles: css\`\`` with `shadow: false` is scoped to the tag name and adopted
|
|
64
|
+
globally.
|
|
65
|
+
- CSS custom properties (`--accent`, `--bg`, etc.) cross Shadow DOM boundaries.
|
|
66
|
+
- Class selectors like `.btn`, `.form-grid`, `.page-head` do **not** cross
|
|
67
|
+
Shadow DOM boundaries.
|
|
68
|
+
- Slotted children keep their own document styles; the shadow component can only
|
|
69
|
+
target them through `::slotted(...)`.
|
|
70
|
+
- `<slot>` projects children only in Shadow DOM. In a `shadow: false` component
|
|
71
|
+
it is just a normal `<slot>` element and will not move children into that
|
|
72
|
+
place in your layout.
|
|
73
|
+
|
|
74
|
+
## Recommended App Shape
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// root and pages: Light DOM
|
|
78
|
+
component("x-app", setup, { shadow: false });
|
|
79
|
+
component("x-users-page", setup, { shadow: false });
|
|
80
|
+
|
|
81
|
+
// slot-based layout: Shadow DOM default, because it owns the shell grid
|
|
82
|
+
component("x-app-layout", setup);
|
|
83
|
+
|
|
84
|
+
// leaf widgets: Shadow DOM default
|
|
85
|
+
component("x-status-badge", setup);
|
|
86
|
+
component("x-stat-card", setup);
|
|
87
|
+
component("x-toast-stack", setup);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This gives backend-admin screens predictable CSS while preserving encapsulation
|
|
91
|
+
for reusable widgets and slot-based shells.
|
|
92
|
+
|
|
93
|
+
If a layout does not need slot projection and should be styled entirely by
|
|
94
|
+
global CSS, `shadow: false` can still be a good choice. If it contains
|
|
95
|
+
`<slot>`, keep Shadow DOM and put the shell styles in that component.
|
|
96
|
+
|
|
97
|
+
## Routing and Links
|
|
98
|
+
|
|
99
|
+
`data-link` works inside Shadow DOM. The router uses `event.composedPath()`, so
|
|
100
|
+
click interception and hover-prefetch can see links from open shadow roots.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
component("x-card-link", () => () => html`
|
|
104
|
+
<a href="/app/accounts" data-link>Accounts</a>
|
|
105
|
+
`);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The link can be in Shadow DOM; navigation still stays SPA.
|
|
109
|
+
|
|
110
|
+
## Showcase Lesson
|
|
111
|
+
|
|
112
|
+
`examples/showcase` uses this split deliberately:
|
|
113
|
+
|
|
114
|
+
- `x-app` and CRM route pages are Light DOM;
|
|
115
|
+
- `x-app-layout` keeps Shadow DOM because it owns a slot-based sidebar/content
|
|
116
|
+
shell;
|
|
117
|
+
- table/form/page utilities live in `styles/global.ts`;
|
|
118
|
+
- leaf components such as `x-stat-card`, `x-status-badge`, `x-modal`, and
|
|
119
|
+
`x-toast-stack` keep Shadow DOM.
|
|
120
|
+
|
|
121
|
+
If a page suddenly looks unstyled, check whether it uses global classes inside a
|
|
122
|
+
Shadow DOM component. That is usually the issue.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Mado docs — English
|
|
2
|
+
|
|
3
|
+
English documentation set.
|
|
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) |
|