@madojs/mado 0.11.1 → 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.
- package/AGENTS.md +102 -25
- package/CHANGELOG.md +158 -1
- package/README.md +135 -172
- package/dist/src/component.d.ts +3 -2
- package/dist/src/component.js +98 -6
- package/dist/src/component.js.map +1 -1
- package/dist/src/css.js +34 -5
- package/dist/src/css.js.map +1 -1
- package/dist/src/head.d.ts +6 -6
- package/dist/src/head.js +6 -6
- package/dist/src/html/parser.d.ts +1 -1
- package/dist/src/html/parser.js +1 -1
- package/dist/src/html/template.js +24 -10
- package/dist/src/html/template.js.map +1 -1
- package/dist/src/index.d.ts +8 -4
- package/dist/src/index.js +13 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/page.d.ts +62 -28
- package/dist/src/page.js +5 -0
- package/dist/src/page.js.map +1 -1
- package/dist/src/resource.js +2 -1
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/base.d.ts +65 -0
- package/dist/src/router/base.js +168 -0
- package/dist/src/router/base.js.map +1 -0
- package/dist/src/router/manifest.js +75 -31
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +16 -2
- package/dist/src/router/match.js +41 -1
- package/dist/src/router/match.js.map +1 -1
- package/dist/src/router/navigation.js +58 -9
- package/dist/src/router/navigation.js.map +1 -1
- package/dist/src/static-runtime.d.ts +81 -0
- package/dist/src/static-runtime.js +209 -0
- package/dist/src/static-runtime.js.map +1 -0
- package/dist/src/vite/index.d.ts +31 -3
- package/dist/src/vite/index.js +49 -18
- package/dist/src/vite/index.js.map +1 -1
- package/docs/README.md +5 -8
- package/docs/architecture/adr/0001-browser-static-snapshots.md +132 -0
- package/docs/en/00-the-mado-way.md +1 -1
- package/docs/en/01-quickstart.md +183 -0
- package/docs/en/10-pages-and-components.md +271 -0
- package/docs/en/11-templates-and-signals.md +211 -0
- package/docs/en/12-routing.md +229 -0
- package/docs/en/13-data.md +225 -0
- package/docs/en/14-forms.md +244 -0
- package/docs/en/15-static-snapshots.md +181 -0
- package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +53 -2
- package/docs/en/{13-deployment.md → 20-deployment.md} +59 -10
- package/docs/en/{14-testing.md → 22-testing.md} +6 -5
- package/docs/en/23-cookbook.md +292 -0
- package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +12 -3
- package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +3 -3
- package/docs/en/40-llm-guide.md +776 -0
- package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +15 -16
- package/docs/en/{05-why-mado.md → 42-why-mado.md} +1 -1
- package/docs/en/README.md +97 -27
- package/docs/recipes/nginx/Containerfile +26 -0
- package/docs/recipes/nginx/nginx.conf +42 -0
- package/llms.txt +246 -211
- package/package.json +17 -10
- package/scripts/bake.mjs +4 -569
- package/scripts/cli/generate.mjs +55 -5
- package/scripts/cli/help.mjs +11 -5
- package/scripts/cli/index.mjs +20 -4
- package/scripts/cli/init.mjs +7 -2
- package/scripts/cli/release.mjs +20 -68
- package/scripts/docs-lint.mjs +170 -0
- package/scripts/package-smoke.mjs +67 -9
- package/scripts/preview.mjs +95 -6
- package/scripts/size-budget.mjs +5 -1
- package/scripts/static/browser.mjs +654 -0
- package/scripts/static/discover.mjs +281 -0
- package/scripts/static/output.mjs +141 -0
- package/scripts/static/serialize.mjs +212 -0
- package/scripts/static/server.mjs +167 -0
- package/scripts/static.mjs +191 -0
- package/starters/default/README.md +57 -55
- package/starters/default/package.json +3 -9
- package/starters/default/src/app.routes.ts +20 -33
- package/starters/default/src/components/feature-card.component.ts +40 -0
- package/starters/default/src/components/live-counter.component.ts +57 -0
- package/starters/default/src/content/guides.ts +55 -0
- package/starters/default/src/main.ts +9 -13
- package/starters/default/src/pages/app.page.ts +41 -0
- package/starters/default/src/pages/guide.page.ts +40 -0
- package/starters/default/src/pages/home.page.ts +49 -0
- package/starters/default/src/pages/not-found.page.ts +18 -0
- package/starters/default/src/styles/document.css +39 -0
- package/starters/default/src/styles/reset.css +37 -0
- package/starters/default/src/styles/tokens.css +30 -0
- package/starters/default/src/styles.d.ts +3 -1
- package/starters/default/src/vite-env.d.ts +1 -1
- package/starters/default/vite.config.ts +14 -1
- package/starters/modular/README.md +142 -0
- package/starters/modular/index.html +13 -0
- package/starters/modular/package.json +30 -0
- package/starters/modular/src/app.routes.ts +39 -0
- package/starters/{default → modular}/src/layouts/app-shell.layout.ts +8 -5
- package/starters/{default → modular}/src/layouts/auth-shell.layout.ts +3 -3
- package/starters/modular/src/main.ts +16 -0
- package/starters/{default → modular}/src/modules/billing/pages/invoices-list.page.ts +3 -3
- package/starters/{default → modular}/src/modules/home/home.page.ts +5 -7
- package/starters/modular/src/modules/home/not-found.page.ts +14 -0
- package/starters/modular/src/styles.d.ts +1 -0
- package/starters/modular/src/vite-env.d.ts +1 -0
- package/starters/modular/tsconfig.json +24 -0
- package/starters/modular/vite.config.ts +131 -0
- package/TODO.md +0 -79
- package/docs/en/01-routing.md +0 -152
- package/docs/en/02-project-layout.md +0 -124
- package/docs/en/03-static-bake.md +0 -249
- package/docs/en/04-ide-setup.md +0 -162
- package/docs/en/07-llm-pitfalls.md +0 -724
- package/docs/en/08-llm-zero-history-test.md +0 -56
- package/docs/en/09-shadow-vs-light-dom.md +0 -174
- package/docs/en/11-layouts.md +0 -113
- package/docs/en/12-auth-and-api.md +0 -124
- package/docs/en/16-bake-cookbook.md +0 -100
- package/docs/en/17-shadow-dom-forms.md +0 -192
- package/docs/fr/00-the-mado-way.md +0 -85
- package/docs/fr/01-routing.md +0 -120
- package/docs/fr/02-project-layout.md +0 -84
- package/docs/fr/03-static-bake.md +0 -289
- package/docs/fr/04-ide-setup.md +0 -162
- package/docs/fr/05-why-mado.md +0 -193
- package/docs/fr/06-for-backenders.md +0 -438
- package/docs/fr/07-llm-pitfalls.md +0 -625
- package/docs/fr/08-llm-zero-history-test.md +0 -38
- package/docs/fr/09-shadow-vs-light-dom.md +0 -65
- package/docs/fr/10-app-architecture.md +0 -138
- package/docs/fr/11-layouts.md +0 -47
- package/docs/fr/12-auth-and-api.md +0 -76
- package/docs/fr/13-deployment.md +0 -57
- package/docs/fr/14-testing.md +0 -41
- package/docs/fr/15-error-handling.md +0 -50
- package/docs/fr/16-bake-cookbook.md +0 -88
- package/docs/fr/17-shadow-dom-forms.md +0 -196
- package/docs/fr/18-api-freeze-map.md +0 -63
- package/docs/fr/19-reactivity-ordering.md +0 -97
- package/docs/fr/20-v1-stability.md +0 -88
- package/docs/fr/README.md +0 -27
- package/docs/ru/00-the-mado-way.md +0 -84
- package/docs/ru/01-routing.md +0 -119
- package/docs/ru/02-project-layout.md +0 -84
- package/docs/ru/03-static-bake.md +0 -250
- package/docs/ru/04-ide-setup.md +0 -144
- package/docs/ru/05-why-mado.md +0 -193
- package/docs/ru/06-for-backenders.md +0 -428
- package/docs/ru/07-llm-pitfalls.md +0 -624
- package/docs/ru/08-llm-zero-history-test.md +0 -57
- package/docs/ru/09-shadow-vs-light-dom.md +0 -63
- package/docs/ru/10-app-architecture.md +0 -152
- package/docs/ru/11-layouts.md +0 -47
- package/docs/ru/12-auth-and-api.md +0 -75
- package/docs/ru/13-deployment.md +0 -66
- package/docs/ru/14-testing.md +0 -50
- package/docs/ru/15-error-handling.md +0 -56
- package/docs/ru/16-bake-cookbook.md +0 -95
- package/docs/ru/17-shadow-dom-forms.md +0 -193
- package/docs/ru/18-api-freeze-map.md +0 -64
- package/docs/ru/19-reactivity-ordering.md +0 -95
- package/docs/ru/20-v1-stability.md +0 -82
- package/docs/ru/README.md +0 -25
- package/docs/uk/00-the-mado-way.md +0 -82
- package/docs/uk/01-routing.md +0 -76
- package/docs/uk/02-project-layout.md +0 -73
- package/docs/uk/03-static-bake.md +0 -48
- package/docs/uk/04-ide-setup.md +0 -26
- package/docs/uk/05-why-mado.md +0 -34
- package/docs/uk/06-for-backenders.md +0 -55
- package/docs/uk/07-llm-pitfalls.md +0 -145
- package/docs/uk/08-llm-zero-history-test.md +0 -34
- package/docs/uk/09-shadow-vs-light-dom.md +0 -58
- package/docs/uk/10-app-architecture.md +0 -97
- package/docs/uk/11-layouts.md +0 -47
- package/docs/uk/12-auth-and-api.md +0 -70
- package/docs/uk/13-deployment.md +0 -40
- package/docs/uk/14-testing.md +0 -34
- package/docs/uk/15-error-handling.md +0 -32
- package/docs/uk/16-bake-cookbook.md +0 -36
- package/docs/uk/17-shadow-dom-forms.md +0 -193
- package/docs/uk/18-api-freeze-map.md +0 -61
- package/docs/uk/19-reactivity-ordering.md +0 -95
- package/docs/uk/20-v1-stability.md +0 -83
- package/docs/uk/README.md +0 -27
- package/starters/default/src/modules/home/not-found.page.ts +0 -11
- /package/docs/en/{15-error-handling.md → 21-error-handling.md} +0 -0
- /package/docs/en/{19-reactivity-ordering.md → 31-reactivity-ordering.md} +0 -0
- /package/starters/{default → modular}/.editorconfig +0 -0
- /package/starters/{default → modular}/eslint.config.mjs +0 -0
- /package/starters/{default → modular}/public/favicon.svg +0 -0
- /package/starters/{default → modular}/src/modules/auth/_contracts/auth-api.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.connector.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.guard.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.public.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.routes.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.service.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/login.page.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/_contracts/stripe.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/api/stripe.connector.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.public.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.routes.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/components/invoice-status-badge.component.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/data/invoices.resource.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/pages/invoice-detail.page.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/http-client.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/http-error.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/interceptors.ts +0 -0
- /package/starters/{default → modular}/src/shared/lib/format-date.ts +0 -0
- /package/starters/{default → modular}/src/shared/styles/content.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/reset.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/shell.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/tokens.css +0 -0
- /package/starters/{default → modular}/src/shared/ui/x-button.component.ts +0 -0
- /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.
|