@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,485 @@
|
|
|
1
|
+
# Mado · LLM pitfalls
|
|
2
|
+
|
|
3
|
+
> Типичные ошибки, которые AI-ассистенты (Copilot, Claude, ChatGPT, Cursor)
|
|
4
|
+
> делают при генерации Mado-кода. И как их исправлять.
|
|
5
|
+
|
|
6
|
+
Этот документ — для **двух аудиторий**:
|
|
7
|
+
1. **AI-агентов в IDE**, которые читают `AGENTS.md` / `.cursorrules` / `.github/copilot-instructions.md`. Здесь больше деталей по типичным граблям.
|
|
8
|
+
2. **Людей**, которые получили от AI код с этими ошибками и не понимают, что не так.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Pitfall #1: `${signal()}` вместо `${() => signal()}`
|
|
13
|
+
|
|
14
|
+
**Симптом:** значение в шаблоне отображается, но не обновляется при изменении сигнала.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
const count = signal(0);
|
|
18
|
+
|
|
19
|
+
// ❌ AI генерирует это часто
|
|
20
|
+
html`<div>Count: ${count() * 2}</div>`
|
|
21
|
+
// → Отрисует "Count: 0", и больше никогда не обновится.
|
|
22
|
+
// count() прочитан один раз в момент создания TemplateResult.
|
|
23
|
+
|
|
24
|
+
// ✅ Правильно — функция-геттер
|
|
25
|
+
html`<div>Count: ${() => count() * 2}</div>`
|
|
26
|
+
// → Mado создаст effect() на эту функцию, при изменении count перерисует.
|
|
27
|
+
|
|
28
|
+
// ✅ Тоже правильно — сам сигнал является функцией
|
|
29
|
+
html`<div>Count: ${count}</div>`
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Правило:**
|
|
33
|
+
- Если в `${...}` есть **выражение** (что-то делает с сигналом) — оборачивай в `() => ...`.
|
|
34
|
+
- Если в `${...}` **сам сигнал** — можно как есть.
|
|
35
|
+
|
|
36
|
+
Это работает для **child-биндингов** (текст внутри тегов) и для **value-атрибутов** (`@click`, `.prop`, `?attr`, обычных атрибутов).
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Pitfall #2: `<button disabled=${loading}>` вместо `?disabled`
|
|
41
|
+
|
|
42
|
+
**Симптом:** кнопка не disable'ится либо disable'ится всегда.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const loading = signal(false);
|
|
46
|
+
|
|
47
|
+
// ❌ Это setAttribute("disabled", "false") — DOM воспринимает это как disabled
|
|
48
|
+
html`<button disabled=${loading()}>Save</button>`
|
|
49
|
+
|
|
50
|
+
// ✅ Правильно — boolean-биндинг (toggle attribute)
|
|
51
|
+
html`<button ?disabled=${loading}>Save</button>`
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Правило для атрибутов:**
|
|
55
|
+
| Префикс | Что делает | Когда использовать |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `attr=` | `setAttribute("attr", value)` | строки, числа, URL |
|
|
58
|
+
| `.attr=` | `el.attr = value` (DOM property) | объекты, массивы, `.value` инпута |
|
|
59
|
+
| `?attr=` | toggle attribute (по truthy) | `disabled`, `hidden`, `checked`, etc |
|
|
60
|
+
| `@evt=` | `addEventListener("evt", fn)` | обработчики событий |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Pitfall #3: useState / useEffect-стиль
|
|
65
|
+
|
|
66
|
+
**Симптом:** AI генерирует React-подобный код, который не работает в Mado.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// ❌ AI часто это пишет
|
|
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
|
+
// ✅ Правильно в 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, dispose автоматически
|
|
82
|
+
return () => html`
|
|
83
|
+
<button @click=${() => count.update(c => c + 1)}>${count}</button>
|
|
84
|
+
`;
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Ключевые отличия:**
|
|
89
|
+
- Нет хуков, нет правил хуков.
|
|
90
|
+
- `signal()` можно создавать где угодно — в setup, в effect, в обработчике.
|
|
91
|
+
- `effect()` сам видит, что прочитал — не нужен dependency array.
|
|
92
|
+
- Компонент = `component("x-name", setup)`, не JSX-функция.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Pitfall #4: `useEffect(() => { ... return cleanup })`
|
|
97
|
+
|
|
98
|
+
**Симптом:** AI пишет `return cleanup` в effect, ожидая что это сработает как в React.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// ❌ AI пытается это написать
|
|
102
|
+
component("x-timer", () => {
|
|
103
|
+
effect(() => {
|
|
104
|
+
const id = setInterval(..., 1000);
|
|
105
|
+
return () => clearInterval(id); // НЕ сработает, нужно через onDispose
|
|
106
|
+
});
|
|
107
|
+
return () => html`...`;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ✅ Правильно: cleanup через ctx.onDispose
|
|
111
|
+
component("x-timer", (ctx) => {
|
|
112
|
+
const id = setInterval(..., 1000);
|
|
113
|
+
ctx.onDispose(() => clearInterval(id));
|
|
114
|
+
return () => html`...`;
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Примечание:** `effect()` действительно поддерживает `return cleanup`, но это **per-run cleanup** (выполнится при следующем прогоне effect'а), а не при unmount. Для unmount-cleanup используй `ctx.onDispose`.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Pitfall #5: Компонент как класс или с декоратором
|
|
123
|
+
|
|
124
|
+
**Симптом:** AI генерирует Lit-style или vanilla WebComponent класс.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
// ❌ AI: "сделаем как в 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: "сделаем как vanilla"
|
|
135
|
+
class XCounter extends HTMLElement {
|
|
136
|
+
connectedCallback() { ... }
|
|
137
|
+
}
|
|
138
|
+
customElements.define("x-counter", XCounter);
|
|
139
|
+
|
|
140
|
+
// ✅ Правильно: функциональный 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: импорт без расширения `.js`
|
|
152
|
+
|
|
153
|
+
**Симптом:** TypeScript компилирует, но в браузере 404.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// ❌ AI часто опускает расширение
|
|
157
|
+
import { foo } from "./bar";
|
|
158
|
+
import { Home } from "./pages/home";
|
|
159
|
+
|
|
160
|
+
// ✅ Правильно: ES-модули в браузере требуют расширение
|
|
161
|
+
import { foo } from "./bar.js";
|
|
162
|
+
import { Home } from "./pages/home.js";
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Почему `.js`, а не `.ts`:** в браузер уходит уже скомпилированный JS. TypeScript достаточно умён, чтобы понимать `./bar.js` как ссылку на `./bar.ts` при компиляции.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Pitfall #7: списки через `.map()` без ключей
|
|
170
|
+
|
|
171
|
+
**Симптом:** при перестановке элементов теряется фокус инпутов / ломаются CSS-анимации / тормозит на больших списках.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// ❌ Работает, но не keyed: пересоздаёт DOM на каждое изменение
|
|
175
|
+
html`<ul>${() => items().map(t => html`<li>${t.name}</li>`)}</ul>`
|
|
176
|
+
|
|
177
|
+
// ✅ Правильно: each() с key-функцией
|
|
178
|
+
import { each } from "@madojs/mado";
|
|
179
|
+
html`<ul>${() => each(items(), t => t.id, t => html`<li>${t.name}</li>`)}</ul>`
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Правило:** всегда используй `each()` для списков из массивов с устойчивыми ID. `.map()` оставь только для статичных списков.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Pitfall #8: `signal.value` или `count.get()`
|
|
187
|
+
|
|
188
|
+
**Симптом:** AI пишет API в стиле Vue или Solid pre-v1.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const count = signal(0);
|
|
192
|
+
|
|
193
|
+
// ❌ Нет такого API
|
|
194
|
+
count.value
|
|
195
|
+
count.value = 5
|
|
196
|
+
count.get()
|
|
197
|
+
|
|
198
|
+
// ✅ Правильно
|
|
199
|
+
count() // прочитать
|
|
200
|
+
count.set(5) // записать
|
|
201
|
+
count.update(n => n + 1)
|
|
202
|
+
count.peek() // прочитать без подписки
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Pitfall #9: `provide(ApiCtx, value)` без host
|
|
208
|
+
|
|
209
|
+
**Симптом:** TypeError при попытке поднять контекст.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
// ❌ AI забывает host
|
|
213
|
+
provide(ApiCtx, myApi);
|
|
214
|
+
inject(ApiCtx);
|
|
215
|
+
|
|
216
|
+
// ✅ Правильно: первый аргумент — host (текущий компонент)
|
|
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: ожидание SSR
|
|
231
|
+
|
|
232
|
+
**Симптом:** AI пишет код, предполагая, что страница пререндерится на сервере.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
// ❌ Это работает только в браузере
|
|
236
|
+
const userId = location.pathname.split("/")[2];
|
|
237
|
+
|
|
238
|
+
// ❌ Это тоже только в браузере
|
|
239
|
+
if (typeof window !== "undefined") { ... } // в Mado window есть ВСЕГДА
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Mado **не делает SSR с гидрацией**. На сервере код не выполняется — есть только `bake` (статический prerender на build) и edge-prerender. Оба заменяют user code на linkedom-окружение, но это **только** для генерации HTML с meta-тегами, не для выполнения логики страницы.
|
|
243
|
+
|
|
244
|
+
Это значит:
|
|
245
|
+
- ✅ `window`, `document`, `location`, `fetch` — доступны без проверок.
|
|
246
|
+
- ❌ Не пиши код, который пытается «универсально работать на сервере и клиенте».
|
|
247
|
+
- ❌ Не используй паттерны Next.js (`getServerSideProps`, `headers()`).
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Pitfall #11: `useForm()` с zod/yup-резолвером
|
|
252
|
+
|
|
253
|
+
**Симптом:** AI хочет подключить валидатор.
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
// ❌ Нет такого API
|
|
257
|
+
const f = useForm({ resolver: zodResolver(schema) });
|
|
258
|
+
|
|
259
|
+
// ✅ Правильно: HTML5-валидация атрибутами
|
|
260
|
+
const f = useForm({
|
|
261
|
+
email: { required: true, type: "email" },
|
|
262
|
+
age: { required: true, type: "number", min: 18 },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ✅ Или кастомная функция, если HTML5 не хватает
|
|
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 = "Имя не должно содержать цифры";
|
|
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
|
+
**Симптом:** AI предлагает стандартные React-CSS-решения.
|
|
285
|
+
|
|
286
|
+
Mado использует **Shadow DOM + `css\`\`` + CSS variables**. Глобальные UI-фреймворки (Tailwind, Bootstrap-через-классы) **работают только в light DOM** (`shadow: false`):
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
// Light-DOM page/screen компонент, Tailwind-классы работают
|
|
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 компонент (default) — Tailwind НЕ работает.
|
|
297
|
+
// Используй css`` или ::part() для внешней стилизации.
|
|
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
|
+
**Темы и кастомизация — через CSS variables**, а не классы.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Pitfall #13: `import * as Mado from "@madojs/mado"`
|
|
315
|
+
|
|
316
|
+
**Симптом:** AI хочет namespace-import.
|
|
317
|
+
|
|
318
|
+
Это работает, но дублирует имена и плохо tree-shake'ится. Лучше named-import:
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
// ✅ Канонично
|
|
322
|
+
import { signal, html, component, css, page } from "@madojs/mado";
|
|
323
|
+
|
|
324
|
+
// ⚠️ Работает, но избыточно
|
|
325
|
+
import * as Mado from "@madojs/mado";
|
|
326
|
+
Mado.signal(0);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Pitfall #14: попытка добавить runtime-зависимость
|
|
332
|
+
|
|
333
|
+
**Симптом:** AI предлагает `npm install lodash` / `npm install date-fns` / etc.
|
|
334
|
+
|
|
335
|
+
Mado — **zero runtime deps** by design. Если AI хочет добавить:
|
|
336
|
+
- **lodash** → используй нативный JS (`Object.entries`, `Array.prototype`, `structuredClone`);
|
|
337
|
+
- **date-fns** → используй `Intl.DateTimeFormat` и `Intl.RelativeTimeFormat`;
|
|
338
|
+
- **uuid** → `crypto.randomUUID()`;
|
|
339
|
+
- **axios** → нативный `fetch` + `jsonFetcher()` из Mado;
|
|
340
|
+
- **classnames** → нативный template literal или объект-mапа.
|
|
341
|
+
|
|
342
|
+
Любая runtime-зависимость — **нарушение принципа фреймворка**. Если без неё реально нельзя — добавляй в пользовательский проект, не в Mado core.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Pitfall #15: inline `<style>` внутри page-шаблонов
|
|
347
|
+
|
|
348
|
+
**Симптом:** AI кладёт большой `<style>` прямо в `html\`\`` страницы.
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
// ❌ Работает, но плохо масштабируется и усложняет cleanup
|
|
352
|
+
page({
|
|
353
|
+
view: () => html`
|
|
354
|
+
<style>.panel { padding: 1rem; }</style>
|
|
355
|
+
<section class="panel">...</section>
|
|
356
|
+
`,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// ✅ Правильно: стили компонента через 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
|
+
Для backend-admin route/page экранов часто уместен `shadow: false`, чтобы
|
|
370
|
+
глобальные layout/form/table utilities работали как обычная админка. Но если
|
|
371
|
+
layout использует `<slot>` для проекции страницы внутрь shell, оставь layout в
|
|
372
|
+
Shadow DOM и держи shell-стили в `styles: css\`\``.
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Pitfall #16: Shadow DOM links без `data-link`
|
|
377
|
+
|
|
378
|
+
**Симптом:** ссылка внутри Web Component перезагружает страницу или не
|
|
379
|
+
prefetch'ится.
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
// ❌ Обычная ссылка: браузер сделает full reload
|
|
383
|
+
html`<a href="/tickets/42">Open</a>`
|
|
384
|
+
|
|
385
|
+
// ✅ SPA-навигация: router() перехватит click даже через Shadow DOM
|
|
386
|
+
html`<a href="/tickets/42" data-link>Open</a>`
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Mado ищет ссылку через `event.composedPath()`, поэтому `data-link` работает
|
|
390
|
+
и внутри Shadow DOM. Hover-prefetch использует тот же путь; `data-no-prefetch`
|
|
391
|
+
отключает prefetch для конкретной ссылки.
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Pitfall #17: `resource()` вне component setup
|
|
396
|
+
|
|
397
|
+
**Симптом:** AI создаёт resource в module scope, чтобы "переиспользовать"
|
|
398
|
+
данные между страницами.
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
// ❌ Нет lifecycle cleanup, будет dev-warning
|
|
402
|
+
const tickets = resource(() => "tickets", () => api.listTickets());
|
|
403
|
+
|
|
404
|
+
component("x-tickets", () => {
|
|
405
|
+
return () => html`${() => tickets.data()?.length ?? 0}`;
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// ✅ Создавай resource внутри setup компонента
|
|
409
|
+
component("x-tickets", () => {
|
|
410
|
+
const tickets = resource(() => "tickets", () => api.listTickets());
|
|
411
|
+
return () => html`${() => tickets.data()?.length ?? 0}`;
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Так подписки на invalidation, abort controller и effects будут очищены при
|
|
416
|
+
disconnect компонента.
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## Pitfall #18: предположение, что nested templates не требуют cleanup
|
|
421
|
+
|
|
422
|
+
**Симптом:** AI собирает route outlet или conditional UI из вложенных
|
|
423
|
+
`TemplateResult`, а потом старые элементы продолжают жить ниже новой страницы.
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
const view = signal(html`<x-home></x-home>`);
|
|
427
|
+
|
|
428
|
+
// ✅ Нормальный паттерн: вложенный TemplateResult можно возвращать из child-binding
|
|
429
|
+
html`${view}`
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Начиная с v0.3 это закреплено регрессиями: при замене child-binding Mado
|
|
433
|
+
dispose'ит вложенные template instances/effects рекурсивно. Если видишь
|
|
434
|
+
накопление страниц в `#app`, это баг ядра, а не "нужно руками чистить DOM".
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Pitfall #19: global CSS utilities внутри Shadow DOM
|
|
439
|
+
|
|
440
|
+
**Симптом:** страница выглядит “без стилей”: `.page-head`, `.btn`,
|
|
441
|
+
`.form-grid`, `.metric-grid` не применяются.
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
// ❌ .page-head объявлен глобально, но x-dashboard по умолчанию Shadow DOM
|
|
445
|
+
component("x-dashboard", () => () => html`
|
|
446
|
+
<header class="page-head">...</header>
|
|
447
|
+
<div class="metric-grid">...</div>
|
|
448
|
+
`);
|
|
449
|
+
|
|
450
|
+
// ✅ Page/layout/admin-shell компоненты часто должны быть Light DOM
|
|
451
|
+
component("x-dashboard", () => () => html`
|
|
452
|
+
<header class="page-head">...</header>
|
|
453
|
+
<div class="metric-grid">...</div>
|
|
454
|
+
`, { shadow: false });
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Правило: Shadow DOM — для leaf widgets и slot-based layouts, Light DOM — для
|
|
458
|
+
route/page/admin-screen компонентов, которые намеренно используют общие
|
|
459
|
+
layout/form/table utilities. Не забывай: `<slot>` проецирует детей только в
|
|
460
|
+
Shadow DOM; при `shadow: false` это обычный элемент.
|
|
461
|
+
Подробнее: [`09-shadow-vs-light-dom.md`](./09-shadow-vs-light-dom.md).
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Cheat-sheet для AI
|
|
466
|
+
|
|
467
|
+
| Если хочешь сделать… | Правильно в Mado |
|
|
468
|
+
|---|---|
|
|
469
|
+
| `useState(0)` | `signal(0)` |
|
|
470
|
+
| `useEffect(() => {...}, [a, b])` | `effect(() => {...})` (auto-deps) |
|
|
471
|
+
| `useEffect(() => return cleanup, [])` | `ctx.onDispose(cleanup)` |
|
|
472
|
+
| `useMemo(() => x, [a])` | `computed(() => x)` |
|
|
473
|
+
| `useCallback(fn, [])` | обычная функция |
|
|
474
|
+
| `useContext(Ctx)` | `inject(host, Ctx)` |
|
|
475
|
+
| `useQuery(['key'], fn)` | `resource(() => 'key', fn)` |
|
|
476
|
+
| `useMutation(fn)` | `mutation(fn, { invalidates: [...] })` |
|
|
477
|
+
| `useRouter().push('/')` | `navigate('/')` |
|
|
478
|
+
| `useRouter().query.q` | `queryParam('q')` |
|
|
479
|
+
| `<input value={v} onChange={...}>` | `<input .value=${v} @input=${...}>` |
|
|
480
|
+
| `{items.map(x => ...)}` | `${() => each(items, x => x.id, x => ...)}` |
|
|
481
|
+
| `useForm({ resolver: zodResolver })` | `useForm({...}, { validate: (v) => ... })` |
|
|
482
|
+
| `class extends HTMLElement` | `component('x-name', setup)` |
|
|
483
|
+
| `@customElement('x')` | `component('x-name', setup)` |
|
|
484
|
+
|
|
485
|
+
Если что-то не подходит из этого списка — открой `src/` и **прочитай 500 строк**. Это серьёзно. Mado специально маленький, чтобы быть читаемым.
|
|
@@ -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,14 @@
|
|
|
1
|
+
# Mado docs — Русский
|
|
2
|
+
|
|
3
|
+
| Раздел | Файл |
|
|
4
|
+
|---|---|
|
|
5
|
+
| Принципы | [00-the-mado-way.md](./00-the-mado-way.md) |
|
|
6
|
+
| Routing | [01-routing.md](./01-routing.md) |
|
|
7
|
+
| Project layout | [02-project-layout.md](./02-project-layout.md) |
|
|
8
|
+
| Static bake & SEO | [03-static-bake.md](./03-static-bake.md) |
|
|
9
|
+
| IDE setup | [04-ide-setup.md](./04-ide-setup.md) |
|
|
10
|
+
| Why Mado | [05-why-mado.md](./05-why-mado.md) |
|
|
11
|
+
| For backenders | [06-for-backenders.md](./06-for-backenders.md) |
|
|
12
|
+
| LLM pitfalls | [07-llm-pitfalls.md](./07-llm-pitfalls.md) |
|
|
13
|
+
| LLM zero-history test | [08-llm-zero-history-test.md](./08-llm-zero-history-test.md) |
|
|
14
|
+
| Shadow DOM vs Light DOM | [09-shadow-vs-light-dom.md](./09-shadow-vs-light-dom.md) |
|