@madojs/mado 0.6.1 → 0.7.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 +82 -30
- package/CHANGELOG.md +98 -3
- package/dist/src/component.d.ts +17 -4
- package/dist/src/component.js +26 -4
- package/dist/src/component.js.map +1 -1
- package/docs/en/07-llm-pitfalls.md +197 -60
- package/docs/en/08-llm-zero-history-test.md +1 -1
- package/docs/en/17-shadow-dom-forms.md +192 -0
- package/docs/en/README.md +20 -19
- package/docs/fr/07-llm-pitfalls.md +196 -60
- package/docs/fr/17-shadow-dom-forms.md +196 -0
- package/docs/fr/README.md +20 -19
- package/docs/ru/07-llm-pitfalls.md +198 -61
- package/docs/ru/08-llm-zero-history-test.md +39 -38
- package/docs/ru/09-shadow-vs-light-dom.md +97 -81
- package/docs/ru/17-shadow-dom-forms.md +193 -0
- package/docs/ru/README.md +20 -19
- package/docs/uk/07-llm-pitfalls.md +64 -3
- package/docs/uk/17-shadow-dom-forms.md +193 -0
- package/docs/uk/README.md +20 -19
- package/llms.txt +50 -1
- package/package.json +2 -2
- package/scripts/bake.mjs +25 -19
- package/scripts/cli.mjs +22 -33
- package/starters/admin/src/components/x-button.ts +40 -13
- package/starters/admin/src/components/x-input.ts +50 -19
- package/starters/admin/src/lib/api.ts +55 -4
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
> делают при генерации Mado-кода. И как их исправлять.
|
|
5
5
|
|
|
6
6
|
Этот документ — для **двух аудиторий**:
|
|
7
|
+
|
|
7
8
|
1. **AI-агентов в IDE**, которые читают `AGENTS.md` / `.cursorrules` / `.github/copilot-instructions.md`. Здесь больше деталей по типичным граблям.
|
|
8
9
|
2. **Людей**, которые получили от AI код с этими ошибками и не понимают, что не так.
|
|
9
10
|
|
|
@@ -17,19 +18,20 @@
|
|
|
17
18
|
const count = signal(0);
|
|
18
19
|
|
|
19
20
|
// ❌ AI генерирует это часто
|
|
20
|
-
html`<div>Count: ${count() * 2}</div
|
|
21
|
+
html`<div>Count: ${count() * 2}</div>`;
|
|
21
22
|
// → Отрисует "Count: 0", и больше никогда не обновится.
|
|
22
23
|
// count() прочитан один раз в момент создания TemplateResult.
|
|
23
24
|
|
|
24
25
|
// ✅ Правильно — функция-геттер
|
|
25
|
-
html`<div>Count: ${() => count() * 2}</div
|
|
26
|
+
html`<div>Count: ${() => count() * 2}</div>`;
|
|
26
27
|
// → Mado создаст effect() на эту функцию, при изменении count перерисует.
|
|
27
28
|
|
|
28
29
|
// ✅ Тоже правильно — сам сигнал является функцией
|
|
29
|
-
html`<div>Count: ${count}</div
|
|
30
|
+
html`<div>Count: ${count}</div>`;
|
|
30
31
|
```
|
|
31
32
|
|
|
32
33
|
**Правило:**
|
|
34
|
+
|
|
33
35
|
- Если в `${...}` есть **выражение** (что-то делает с сигналом) — оборачивай в `() => ...`.
|
|
34
36
|
- Если в `${...}` **сам сигнал** — можно как есть.
|
|
35
37
|
|
|
@@ -45,10 +47,10 @@ html`<div>Count: ${count}</div>`
|
|
|
45
47
|
const loading = signal(false);
|
|
46
48
|
|
|
47
49
|
// ❌ Это setAttribute("disabled", "false") — DOM воспринимает это как disabled
|
|
48
|
-
html`<button disabled=${loading()}>Save</button
|
|
50
|
+
html`<button disabled=${loading()}>Save</button>`;
|
|
49
51
|
|
|
50
52
|
// ✅ Правильно — boolean-биндинг (toggle attribute)
|
|
51
|
-
html`<button ?disabled=${loading}>Save</button
|
|
53
|
+
html`<button ?disabled=${loading}>Save</button>`;
|
|
52
54
|
```
|
|
53
55
|
|
|
54
56
|
**Правило для атрибутов:**
|
|
@@ -86,6 +88,7 @@ component("x-counter", () => {
|
|
|
86
88
|
```
|
|
87
89
|
|
|
88
90
|
**Ключевые отличия:**
|
|
91
|
+
|
|
89
92
|
- Нет хуков, нет правил хуков.
|
|
90
93
|
- `signal()` можно создавать где угодно — в setup, в effect, в обработчике.
|
|
91
94
|
- `effect()` сам видит, что прочитал — не нужен dependency array.
|
|
@@ -93,7 +96,7 @@ component("x-counter", () => {
|
|
|
93
96
|
|
|
94
97
|
---
|
|
95
98
|
|
|
96
|
-
## Pitfall #4: `useEffect(() => { ... return cleanup })`
|
|
99
|
+
## Pitfall #4: `useEffect(() => { ... return cleanup })`
|
|
97
100
|
|
|
98
101
|
**Симптом:** AI пишет `return cleanup` в effect, ожидая что это сработает как в React.
|
|
99
102
|
|
|
@@ -172,11 +175,20 @@ import { Home } from "./pages/home.js";
|
|
|
172
175
|
|
|
173
176
|
```ts
|
|
174
177
|
// ❌ Работает, но не keyed: пересоздаёт DOM на каждое изменение
|
|
175
|
-
html`<ul
|
|
178
|
+
html`<ul>
|
|
179
|
+
${() => items().map((t) => html`<li>${t.name}</li>`)}
|
|
180
|
+
</ul>`;
|
|
176
181
|
|
|
177
182
|
// ✅ Правильно: each() с key-функцией
|
|
178
183
|
import { each } from "@madojs/mado";
|
|
179
|
-
html`<ul
|
|
184
|
+
html`<ul>
|
|
185
|
+
${() =>
|
|
186
|
+
each(
|
|
187
|
+
items(),
|
|
188
|
+
(t) => t.id,
|
|
189
|
+
(t) => html`<li>${t.name}</li>`,
|
|
190
|
+
)}
|
|
191
|
+
</ul>`;
|
|
180
192
|
```
|
|
181
193
|
|
|
182
194
|
**Правило:** всегда используй `each()` для списков из массивов с устойчивыми ID. `.map()` оставь только для статичных списков.
|
|
@@ -191,15 +203,15 @@ html`<ul>${() => each(items(), t => t.id, t => html`<li>${t.name}</li>`)}</ul>`
|
|
|
191
203
|
const count = signal(0);
|
|
192
204
|
|
|
193
205
|
// ❌ Нет такого API
|
|
194
|
-
count.value
|
|
195
|
-
count.value = 5
|
|
196
|
-
count.get()
|
|
206
|
+
count.value;
|
|
207
|
+
count.value = 5;
|
|
208
|
+
count.get();
|
|
197
209
|
|
|
198
210
|
// ✅ Правильно
|
|
199
|
-
count()
|
|
200
|
-
count.set(5)
|
|
201
|
-
count.update(n => n + 1)
|
|
202
|
-
count.peek()
|
|
211
|
+
count(); // прочитать
|
|
212
|
+
count.set(5); // записать
|
|
213
|
+
count.update((n) => n + 1);
|
|
214
|
+
count.peek(); // прочитать без подписки
|
|
203
215
|
```
|
|
204
216
|
|
|
205
217
|
---
|
|
@@ -220,7 +232,7 @@ component("x-app", ({ host }) => {
|
|
|
220
232
|
});
|
|
221
233
|
|
|
222
234
|
component("x-child", ({ host }) => {
|
|
223
|
-
const api = inject(host, ApiCtx);
|
|
235
|
+
const api = inject(host, ApiCtx); // signal<value>
|
|
224
236
|
return () => html`...`;
|
|
225
237
|
});
|
|
226
238
|
```
|
|
@@ -242,6 +254,7 @@ if (typeof window !== "undefined") { ... } // в Mado window есть ВСЕГ
|
|
|
242
254
|
Mado **не делает SSR с гидрацией**. На сервере код не выполняется — есть только `bake` (статический prerender на build) и edge-prerender. Оба заменяют user code на linkedom-окружение, но это **только** для генерации HTML с meta-тегами, не для выполнения логики страницы.
|
|
243
255
|
|
|
244
256
|
Это значит:
|
|
257
|
+
|
|
245
258
|
- ✅ `window`, `document`, `location`, `fetch` — доступны без проверок.
|
|
246
259
|
- ❌ Не пиши код, который пытается «универсально работать на сервере и клиенте».
|
|
247
260
|
- ❌ Не используй паттерны Next.js (`getServerSideProps`, `headers()`).
|
|
@@ -259,7 +272,7 @@ const f = useForm({ resolver: zodResolver(schema) });
|
|
|
259
272
|
// ✅ Правильно: HTML5-валидация атрибутами
|
|
260
273
|
const f = useForm({
|
|
261
274
|
email: { required: true, type: "email" },
|
|
262
|
-
age:
|
|
275
|
+
age: { required: true, type: "number", min: 18 },
|
|
263
276
|
});
|
|
264
277
|
|
|
265
278
|
// ✅ Или кастомная функция, если HTML5 не хватает
|
|
@@ -287,11 +300,13 @@ Mado использует **Shadow DOM + `css\`\`` + CSS variables**. Глоба
|
|
|
287
300
|
|
|
288
301
|
```ts
|
|
289
302
|
// Light-DOM page/screen компонент, Tailwind-классы работают
|
|
290
|
-
component(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
`,
|
|
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
|
+
);
|
|
295
310
|
|
|
296
311
|
// Shadow-DOM компонент (default) — Tailwind НЕ работает.
|
|
297
312
|
// Используй css`` или ::part() для внешней стилизации.
|
|
@@ -300,7 +315,7 @@ component("x-button", () => () => html`<button><slot></slot></button>`, {
|
|
|
300
315
|
button {
|
|
301
316
|
background: var(--button-bg, #2563eb);
|
|
302
317
|
color: white;
|
|
303
|
-
padding: .5rem 1rem;
|
|
318
|
+
padding: 0.5rem 1rem;
|
|
304
319
|
border-radius: 6px;
|
|
305
320
|
}
|
|
306
321
|
`,
|
|
@@ -333,6 +348,7 @@ Mado.signal(0);
|
|
|
333
348
|
**Симптом:** AI предлагает `npm install lodash` / `npm install date-fns` / etc.
|
|
334
349
|
|
|
335
350
|
Mado — **zero runtime deps** by design. Если AI хочет добавить:
|
|
351
|
+
|
|
336
352
|
- **lodash** → используй нативный JS (`Object.entries`, `Array.prototype`, `structuredClone`);
|
|
337
353
|
- **date-fns** → используй `Intl.DateTimeFormat` и `Intl.RelativeTimeFormat`;
|
|
338
354
|
- **uuid** → `crypto.randomUUID()`;
|
|
@@ -351,19 +367,27 @@ Mado — **zero runtime deps** by design. Если AI хочет добавит
|
|
|
351
367
|
// ❌ Работает, но плохо масштабируется и усложняет cleanup
|
|
352
368
|
page({
|
|
353
369
|
view: () => html`
|
|
354
|
-
<style
|
|
370
|
+
<style>
|
|
371
|
+
.panel {
|
|
372
|
+
padding: 1rem;
|
|
373
|
+
}
|
|
374
|
+
</style>
|
|
355
375
|
<section class="panel">...</section>
|
|
356
376
|
`,
|
|
357
377
|
});
|
|
358
378
|
|
|
359
379
|
// ✅ Правильно: стили компонента через css``
|
|
360
|
-
component(
|
|
361
|
-
|
|
362
|
-
`,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
+
);
|
|
367
391
|
```
|
|
368
392
|
|
|
369
393
|
Для backend-admin route/page экранов часто уместен `shadow: false`, чтобы
|
|
@@ -380,10 +404,10 @@ prefetch'ится.
|
|
|
380
404
|
|
|
381
405
|
```ts
|
|
382
406
|
// ❌ Обычная ссылка: браузер сделает full reload
|
|
383
|
-
html`<a href="/tickets/42">Open</a
|
|
407
|
+
html`<a href="/tickets/42">Open</a>`;
|
|
384
408
|
|
|
385
409
|
// ✅ SPA-навигация: router() перехватит click даже через Shadow DOM
|
|
386
|
-
html`<a href="/tickets/42" data-link>Open</a
|
|
410
|
+
html`<a href="/tickets/42" data-link>Open</a>`;
|
|
387
411
|
```
|
|
388
412
|
|
|
389
413
|
Mado ищет ссылку через `event.composedPath()`, поэтому `data-link` работает
|
|
@@ -399,7 +423,10 @@ Mado ищет ссылку через `event.composedPath()`, поэтому `da
|
|
|
399
423
|
|
|
400
424
|
```ts
|
|
401
425
|
// ❌ Нет lifecycle cleanup, будет dev-warning
|
|
402
|
-
const tickets = resource(
|
|
426
|
+
const tickets = resource(
|
|
427
|
+
() => "tickets",
|
|
428
|
+
() => api.listTickets(),
|
|
429
|
+
);
|
|
403
430
|
|
|
404
431
|
component("x-tickets", () => {
|
|
405
432
|
return () => html`${() => tickets.data()?.length ?? 0}`;
|
|
@@ -407,7 +434,10 @@ component("x-tickets", () => {
|
|
|
407
434
|
|
|
408
435
|
// ✅ Создавай resource внутри setup компонента
|
|
409
436
|
component("x-tickets", () => {
|
|
410
|
-
const tickets = resource(
|
|
437
|
+
const tickets = resource(
|
|
438
|
+
() => "tickets",
|
|
439
|
+
() => api.listTickets(),
|
|
440
|
+
);
|
|
411
441
|
return () => html`${() => tickets.data()?.length ?? 0}`;
|
|
412
442
|
});
|
|
413
443
|
```
|
|
@@ -426,7 +456,7 @@ disconnect компонента.
|
|
|
426
456
|
const view = signal(html`<x-home></x-home>`);
|
|
427
457
|
|
|
428
458
|
// ✅ Нормальный паттерн: вложенный TemplateResult можно возвращать из child-binding
|
|
429
|
-
html`${view}
|
|
459
|
+
html`${view}`;
|
|
430
460
|
```
|
|
431
461
|
|
|
432
462
|
Начиная с v0.3 это закреплено регрессиями: при замене child-binding Mado
|
|
@@ -442,16 +472,23 @@ dispose'ит вложенные template instances/effects рекурсивно.
|
|
|
442
472
|
|
|
443
473
|
```ts
|
|
444
474
|
// ❌ .page-head объявлен глобально, но x-dashboard по умолчанию Shadow DOM
|
|
445
|
-
component(
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
475
|
+
component(
|
|
476
|
+
"x-dashboard",
|
|
477
|
+
() => () => html`
|
|
478
|
+
<header class="page-head">...</header>
|
|
479
|
+
<div class="metric-grid">...</div>
|
|
480
|
+
`,
|
|
481
|
+
);
|
|
449
482
|
|
|
450
483
|
// ✅ Page/layout/admin-shell компоненты часто должны быть Light DOM
|
|
451
|
-
component(
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
484
|
+
component(
|
|
485
|
+
"x-dashboard",
|
|
486
|
+
() => () => html`
|
|
487
|
+
<header class="page-head">...</header>
|
|
488
|
+
<div class="metric-grid">...</div>
|
|
489
|
+
`,
|
|
490
|
+
{ shadow: false },
|
|
491
|
+
);
|
|
455
492
|
```
|
|
456
493
|
|
|
457
494
|
Правило: Shadow DOM — для leaf widgets и slot-based layouts, Light DOM — для
|
|
@@ -462,24 +499,124 @@ Shadow DOM; при `shadow: false` это обычный элемент.
|
|
|
462
499
|
|
|
463
500
|
---
|
|
464
501
|
|
|
502
|
+
## Pitfall #20: `host.getAttribute()` в render = не реактивно
|
|
503
|
+
|
|
504
|
+
**Симптом:** внешний вид компонента не обновляется при изменении атрибута родителем.
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
// ❌ host.getAttribute() в render-функции читается один раз, но
|
|
508
|
+
// render перезапускается только при изменении его собственных сигналов.
|
|
509
|
+
// Внешние изменения атрибута не триггерят перерисовку.
|
|
510
|
+
component("x-badge", ({ host }) => () => {
|
|
511
|
+
const variant = host.getAttribute("variant") ?? "default";
|
|
512
|
+
return html`<span class=${variant}>...</span>`;
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// ✅ Правильно: ctx.attr() — возвращает реактивный Signal<string>
|
|
516
|
+
component("x-badge", ({ attr }) => {
|
|
517
|
+
const variant = attr("variant", "default");
|
|
518
|
+
return () => html`<span class=${() => `badge-${variant()}`}>...</span>`;
|
|
519
|
+
});
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
**Правило:** никогда не читайте `host.getAttribute()` или `host.hasAttribute()` внутри
|
|
523
|
+
render-функции для значений, которые могут измениться снаружи. Используйте `ctx.attr()` —
|
|
524
|
+
он возвращает Signal, который автоматически обновляется через `attributeChangedCallback`.
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## Pitfall #21: Shadow DOM `<button>` не сабмитит формы
|
|
529
|
+
|
|
530
|
+
**Симптом:** клик по `<x-button type="submit">` внутри `<form>` ничего не делает.
|
|
531
|
+
|
|
532
|
+
`<button>` внутри Shadow DOM не участвует в алгоритме form-owner для
|
|
533
|
+
`<form>` в Light DOM — это ограничение спецификации, не баг Mado.
|
|
534
|
+
|
|
535
|
+
```ts
|
|
536
|
+
// ❌ Внутренняя <button type="submit"> не может триггерить родительскую <form>
|
|
537
|
+
component("x-button", ({ host }) => {
|
|
538
|
+
return () => html`<button type="submit"><slot></slot></button>`;
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// ✅ Мост через requestSubmit()
|
|
542
|
+
component("x-button", ({ host, attr }) => {
|
|
543
|
+
const disabled = attr("disabled");
|
|
544
|
+
|
|
545
|
+
const handleClick = () => {
|
|
546
|
+
const typeAttr = host.getAttribute("type");
|
|
547
|
+
if (typeAttr === "button" || typeAttr === "reset") return;
|
|
548
|
+
const form = host.closest("form");
|
|
549
|
+
if (form && !host.hasAttribute("disabled")) form.requestSubmit();
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
return () => html`
|
|
553
|
+
<button ?disabled=${() => disabled() !== ""} @click=${handleClick}>
|
|
554
|
+
<slot></slot>
|
|
555
|
+
</button>
|
|
556
|
+
`;
|
|
557
|
+
});
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
Подробнее: [`17-shadow-dom-forms.md`](./17-shadow-dom-forms.md).
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## Pitfall #22: `useForm()` с Shadow DOM кастомными input
|
|
565
|
+
|
|
566
|
+
**Симптом:** `form.onInput` получает `undefined` для name/value от `<x-input>`.
|
|
567
|
+
|
|
568
|
+
Когда Shadow DOM input диспатчит `input` событие, браузер ретаргетирует
|
|
569
|
+
`e.target` с внутреннего `<input>` на host `<x-input>`. Но `<x-input>`
|
|
570
|
+
(HTMLElement) не имеет `.name` или `.value` — поэтому `useForm` ничего не получает.
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
// ❌ Нет proxy-свойств — useForm тихо игнорирует события
|
|
574
|
+
component("x-input", ({ host, attr }) => {
|
|
575
|
+
const name = attr("name", "");
|
|
576
|
+
return () => html`<input name=${name} />`;
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// ✅ Добавить proxy-свойства для совместимости с useForm
|
|
580
|
+
component("x-input", ({ host, attr }) => {
|
|
581
|
+
const name = attr("name", "");
|
|
582
|
+
|
|
583
|
+
Object.defineProperty(host, "name", {
|
|
584
|
+
get: () => host.getAttribute("name") ?? "",
|
|
585
|
+
configurable: true,
|
|
586
|
+
});
|
|
587
|
+
Object.defineProperty(host, "value", {
|
|
588
|
+
get: () => host.shadowRoot?.querySelector("input")?.value ?? "",
|
|
589
|
+
configurable: true,
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
return () => html`<input name=${name} />`;
|
|
593
|
+
});
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
Подробнее: [`17-shadow-dom-forms.md`](./17-shadow-dom-forms.md).
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
465
600
|
## Cheat-sheet для AI
|
|
466
601
|
|
|
467
|
-
| Если хочешь сделать…
|
|
468
|
-
|
|
469
|
-
| `useState(0)`
|
|
470
|
-
| `useEffect(() => {...}, [a, b])`
|
|
471
|
-
| `useEffect(() => return cleanup, [])` | `ctx.onDispose(cleanup)`
|
|
472
|
-
| `useMemo(() => x, [a])`
|
|
473
|
-
| `useCallback(fn, [])`
|
|
474
|
-
| `useContext(Ctx)`
|
|
475
|
-
| `useQuery(['key'], fn)`
|
|
476
|
-
| `useMutation(fn)`
|
|
477
|
-
| `useRouter().push('/')`
|
|
478
|
-
| `useRouter().query.q`
|
|
479
|
-
| `<input value={v} onChange={...}>`
|
|
480
|
-
| `{items.map(x => ...)}`
|
|
481
|
-
| `useForm({ resolver: zodResolver })`
|
|
482
|
-
| `class extends HTMLElement`
|
|
483
|
-
| `@customElement('x')`
|
|
602
|
+
| Если хочешь сделать… | Правильно в Mado |
|
|
603
|
+
| ------------------------------------- | ------------------------------------------- |
|
|
604
|
+
| `useState(0)` | `signal(0)` |
|
|
605
|
+
| `useEffect(() => {...}, [a, b])` | `effect(() => {...})` (auto-deps) |
|
|
606
|
+
| `useEffect(() => return cleanup, [])` | `ctx.onDispose(cleanup)` |
|
|
607
|
+
| `useMemo(() => x, [a])` | `computed(() => x)` |
|
|
608
|
+
| `useCallback(fn, [])` | обычная функция |
|
|
609
|
+
| `useContext(Ctx)` | `inject(host, Ctx)` |
|
|
610
|
+
| `useQuery(['key'], fn)` | `resource(() => 'key', fn)` |
|
|
611
|
+
| `useMutation(fn)` | `mutation(fn, { invalidates: [...] })` |
|
|
612
|
+
| `useRouter().push('/')` | `navigate('/')` |
|
|
613
|
+
| `useRouter().query.q` | `queryParam('q')` |
|
|
614
|
+
| `<input value={v} onChange={...}>` | `<input .value=${v} @input=${...}>` |
|
|
615
|
+
| `{items.map(x => ...)}` | `${() => each(items, x => x.id, x => ...)}` |
|
|
616
|
+
| `useForm({ resolver: zodResolver })` | `useForm({...}, { validate: (v) => ... })` |
|
|
617
|
+
| `class extends HTMLElement` | `component('x-name', setup)` |
|
|
618
|
+
| `@customElement('x')` | `component('x-name', setup)` |
|
|
619
|
+
| `host.getAttribute('x')` в render | `ctx.attr('x', default)` (реактивно) |
|
|
620
|
+
| `jsonFetcher()` с авторизацией | `apiFetcher()` (прикрепляет Bearer токен) |
|
|
484
621
|
|
|
485
622
|
Если что-то не подходит из этого списка — открой `src/` и **прочитай 500 строк**. Это серьёзно. Mado специально маленький, чтобы быть читаемым.
|
|
@@ -1,56 +1,57 @@
|
|
|
1
|
-
# LLM
|
|
1
|
+
# Тест LLM с нулевой историей
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Этот документ определяет практический валидационный тест для Mado.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Вопрос не в том, «может ли LLM генерировать фронтенд-код?» — может. Вопрос в том:
|
|
6
|
+
может ли свежая LLM написать идиоматичный Mado-код без скатывания в React-подобные
|
|
7
|
+
паттерны?
|
|
7
8
|
|
|
8
|
-
##
|
|
9
|
+
## Допустимый контекст
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
При первом проходе предоставьте агенту только:
|
|
11
12
|
|
|
12
13
|
- `AGENTS.md`
|
|
13
14
|
- `README.md`
|
|
14
|
-
- `docs/
|
|
15
|
-
- `examples/basic/README.md`
|
|
16
|
-
-
|
|
15
|
+
- `docs/en/07-llm-pitfalls.md`
|
|
16
|
+
- `examples/basic/README.md` если нужен минимальный обзор API
|
|
17
|
+
- конкретные файлы из `examples/showcase/**` только если агент сам попросит паттерн более крупного приложения
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
Агент может искать целевые API в `src/` когда заблокирован, но не должен
|
|
20
|
+
загружать весь фреймворк в контекст.
|
|
20
21
|
|
|
21
|
-
##
|
|
22
|
+
## Задание
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
Построить `examples/tickets`: маленький ticket-admin SPA для соло/бекенд-разработчика.
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
Требуемое поведение:
|
|
26
27
|
|
|
27
|
-
-
|
|
28
|
-
- in-memory mock API
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
- slotted shell, metric
|
|
34
|
-
- smoke
|
|
28
|
+
- маршруты: `/`, `/tickets`, `/tickets/new`, `/tickets/:id`, `*`;
|
|
29
|
+
- in-memory mock API с реалистичными асинхронными задержками;
|
|
30
|
+
- страница списка с `resource()`, `queryParam()` фильтрами поиска/статуса,
|
|
31
|
+
`computed()` и `each()` с ключами по строкам;
|
|
32
|
+
- сценарии создания и редактирования с `useForm()` + `mutation()` + `invalidates`;
|
|
33
|
+
- локальный UI-стейт через `signal()`;
|
|
34
|
+
- slotted shell, metric и badge компоненты для более реалистичного admin UI;
|
|
35
|
+
- smoke-тест, импортирующий собранный пример.
|
|
35
36
|
|
|
36
|
-
##
|
|
37
|
+
## Чеклист ошибок
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
Ищите следующее после реализации:
|
|
39
40
|
|
|
40
|
-
- JSX, `useState`, `useEffect`, `ref`, `$state
|
|
41
|
-
- `${signal()}`
|
|
42
|
-
- `disabled=${...}`
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
- `resource()
|
|
46
|
-
-
|
|
41
|
+
- JSX, `useState`, `useEffect`, `ref`, `$state` или class-style компоненты;
|
|
42
|
+
- `${signal()}` или `${signal() + 1}` где требуется реактивный child thunk;
|
|
43
|
+
- `disabled=${...}` вместо `?disabled=${...}`;
|
|
44
|
+
- динамические списки, отрендеренные через unkeyed `.map()` вместо `each()`;
|
|
45
|
+
- ESM-импорты в браузере без `.js`;
|
|
46
|
+
- `resource()`, созданный вне component setup;
|
|
47
|
+
- новые runtime-зависимости или новые публичные API фреймворка.
|
|
47
48
|
|
|
48
|
-
##
|
|
49
|
+
## Заметки по результатам
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
runtime
|
|
51
|
+
Текущая реализация `examples/tickets` не потребовала новых публичных API или
|
|
52
|
+
runtime-зависимостей.
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
Основная болевая точка в документации остается lifecycle: старые примеры могут
|
|
55
|
+
создать впечатление, что создание `resource()` прямо в `page.view()` допустимо.
|
|
56
|
+
Пример tickets использует page-level wrapper-компоненты вместо этого, поэтому
|
|
57
|
+
ресурсы регистрируются внутри component setup и очищаются вместе с компонентом.
|