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