@madojs/mado 0.6.0 → 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 +208 -1
- package/dist/src/component.d.ts +17 -4
- package/dist/src/component.js +26 -4
- package/dist/src/component.js.map +1 -1
- package/dist/src/resource.js +11 -0
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/manifest.js +29 -2
- package/dist/src/router/manifest.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 +76 -22
- package/scripts/bundle.mjs +24 -1
- package/scripts/cli.mjs +98 -45
- package/scripts/preview.mjs +104 -10
- package/server/serve.mjs +80 -7
- package/starters/admin/index.html +10 -3
- package/starters/admin/package.json +3 -1
- 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
- package/starters/admin/src/pages/admin/order-detail.ts +4 -2
- package/starters/admin/src/pages/home.ts +10 -1
- package/starters/crud/index.html +12 -4
- package/starters/crud/package.json +3 -1
- package/starters/crud/src/pages/home.ts +16 -0
- package/starters/minimal/index.html +12 -4
- package/starters/minimal/package.json +2 -0
- package/starters/minimal/src/pages/home.ts +17 -0
|
@@ -1,43 +1,74 @@
|
|
|
1
|
-
// <x-input label name type placeholder required value @input @blur>
|
|
1
|
+
// <x-input label name type placeholder required value error @input @blur>
|
|
2
2
|
//
|
|
3
3
|
// Labeled input that proxies its events. Use inside `useForm()`:
|
|
4
4
|
//
|
|
5
5
|
// <x-input name="email" type="email" required
|
|
6
6
|
// @input=${form.onInput} @blur=${form.onBlur}></x-input>
|
|
7
|
+
//
|
|
8
|
+
// Shadow DOM integration notes:
|
|
9
|
+
// - `name` and `value` are exposed as DOM properties on the host so that
|
|
10
|
+
// event retargeting (e.target → <x-input>) still works with useForm().
|
|
11
|
+
// useForm.onInput reads e.target.name and e.target.value — without these
|
|
12
|
+
// getters the form silently receives undefined.
|
|
13
|
+
// - The inner <input> dispatches its events with `composed: true` (native
|
|
14
|
+
// behaviour), so @input/@blur bubble through the shadow boundary.
|
|
7
15
|
|
|
8
16
|
import { component, css, html } from "@madojs/mado";
|
|
9
17
|
|
|
10
18
|
component(
|
|
11
19
|
"x-input",
|
|
12
|
-
({ host }) =>
|
|
13
|
-
const label =
|
|
14
|
-
const name =
|
|
15
|
-
const type =
|
|
16
|
-
const placeholder =
|
|
17
|
-
const required =
|
|
18
|
-
const value =
|
|
19
|
-
const error =
|
|
20
|
+
({ host, attr }) => {
|
|
21
|
+
const label = attr("label", "");
|
|
22
|
+
const name = attr("name", "");
|
|
23
|
+
const type = attr("type", "text");
|
|
24
|
+
const placeholder = attr("placeholder", "");
|
|
25
|
+
const required = attr("required");
|
|
26
|
+
const value = attr("value", "");
|
|
27
|
+
const error = attr("error");
|
|
28
|
+
|
|
29
|
+
// Proxy properties so useForm().onInput can read e.target.name / .value
|
|
30
|
+
// even after Shadow DOM retargets e.target from <input> to <x-input>.
|
|
31
|
+
Object.defineProperty(host, "name", {
|
|
32
|
+
get: () => host.getAttribute("name") ?? "",
|
|
33
|
+
configurable: true,
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(host, "value", {
|
|
36
|
+
get: () => host.shadowRoot?.querySelector("input")?.value ?? "",
|
|
37
|
+
set: (v: string) => {
|
|
38
|
+
const input = host.shadowRoot?.querySelector("input");
|
|
39
|
+
if (input) input.value = v;
|
|
40
|
+
},
|
|
41
|
+
configurable: true,
|
|
42
|
+
});
|
|
20
43
|
|
|
21
|
-
return html`
|
|
44
|
+
return () => html`
|
|
22
45
|
<label>
|
|
23
|
-
${
|
|
24
|
-
|
|
25
|
-
|
|
46
|
+
${() =>
|
|
47
|
+
label()
|
|
48
|
+
? html`<span class="lbl"
|
|
49
|
+
>${label}${() =>
|
|
50
|
+
required() !== "" ? html`<em>*</em>` : null}</span
|
|
51
|
+
>`
|
|
52
|
+
: null}
|
|
26
53
|
<input
|
|
27
54
|
name=${name}
|
|
28
55
|
type=${type}
|
|
29
56
|
placeholder=${placeholder}
|
|
30
|
-
?required=${required}
|
|
57
|
+
?required=${() => required() !== ""}
|
|
31
58
|
.value=${value}
|
|
32
|
-
|
|
33
|
-
${error ? html`<small class="err">${error}</small>` : null}
|
|
59
|
+
/>
|
|
60
|
+
${() => (error() ? html`<small class="err">${error}</small>` : null)}
|
|
34
61
|
</label>
|
|
35
62
|
`;
|
|
36
63
|
},
|
|
37
64
|
{
|
|
38
65
|
styles: css`
|
|
39
|
-
:host {
|
|
40
|
-
|
|
66
|
+
:host {
|
|
67
|
+
display: block;
|
|
68
|
+
}
|
|
69
|
+
label {
|
|
70
|
+
display: block;
|
|
71
|
+
}
|
|
41
72
|
.lbl {
|
|
42
73
|
display: block;
|
|
43
74
|
font-size: 12px;
|
|
@@ -71,4 +102,4 @@ component(
|
|
|
71
102
|
}
|
|
72
103
|
`,
|
|
73
104
|
},
|
|
74
|
-
);
|
|
105
|
+
);
|
|
@@ -71,9 +71,9 @@ export function createApiClient(baseUrl: string) {
|
|
|
71
71
|
credentials: "include",
|
|
72
72
|
});
|
|
73
73
|
if (!res.ok) return false;
|
|
74
|
-
const data = (await res.json().catch(() => null)) as
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
const data = (await res.json().catch(() => null)) as {
|
|
75
|
+
accessToken?: string;
|
|
76
|
+
} | null;
|
|
77
77
|
if (!data?.accessToken) return false;
|
|
78
78
|
accessToken.set(data.accessToken);
|
|
79
79
|
return true;
|
|
@@ -118,7 +118,11 @@ export function createApiClient(baseUrl: string) {
|
|
|
118
118
|
}
|
|
119
119
|
if (!res.ok) {
|
|
120
120
|
const body = await res.json().catch(() => null);
|
|
121
|
-
throw new ApiError(
|
|
121
|
+
throw new ApiError(
|
|
122
|
+
res.status,
|
|
123
|
+
body,
|
|
124
|
+
`HTTP ${res.status} ${res.statusText}`,
|
|
125
|
+
);
|
|
122
126
|
}
|
|
123
127
|
if (res.status === 204) return null as unknown as T;
|
|
124
128
|
return (await res.json()) as T;
|
|
@@ -131,3 +135,50 @@ export function createApiClient(baseUrl: string) {
|
|
|
131
135
|
|
|
132
136
|
/** Default app-wide client. Change the base URL via mado.config.json dev.proxy. */
|
|
133
137
|
export const api = createApiClient("/api");
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Fetcher for resource() that attaches the Bearer token automatically.
|
|
141
|
+
* Use this instead of jsonFetcher() for protected endpoints:
|
|
142
|
+
*
|
|
143
|
+
* const stats = resource(() => "/api/admin/stats", apiFetcher<Stats>());
|
|
144
|
+
*
|
|
145
|
+
* Unlike jsonFetcher(), this:
|
|
146
|
+
* - Sends the access token from memory (no cookie-based auth needed).
|
|
147
|
+
* - Does NOT prepend a base URL — the key is the full URL (matches
|
|
148
|
+
* resource key semantics).
|
|
149
|
+
* - Throws ApiError on non-2xx (consistent with api()).
|
|
150
|
+
*/
|
|
151
|
+
export function apiFetcher<T>(): (
|
|
152
|
+
url: string,
|
|
153
|
+
signal: AbortSignal,
|
|
154
|
+
) => Promise<T> {
|
|
155
|
+
return async (url, abortSignal) => {
|
|
156
|
+
const token = accessToken();
|
|
157
|
+
const headers = new Headers();
|
|
158
|
+
headers.set("accept", "application/json");
|
|
159
|
+
if (token) headers.set("authorization", `Bearer ${token}`);
|
|
160
|
+
|
|
161
|
+
const res = await fetch(url, {
|
|
162
|
+
signal: abortSignal,
|
|
163
|
+
headers,
|
|
164
|
+
credentials: "include",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
let body: unknown = null;
|
|
169
|
+
try {
|
|
170
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
171
|
+
body = ct.includes("json") ? await res.json() : await res.text();
|
|
172
|
+
} catch {
|
|
173
|
+
body = null;
|
|
174
|
+
}
|
|
175
|
+
throw new ApiError(
|
|
176
|
+
res.status,
|
|
177
|
+
body,
|
|
178
|
+
`HTTP ${res.status} ${res.statusText}`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (res.status === 204) return null as unknown as T;
|
|
182
|
+
return (await res.json()) as T;
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Order detail page. Reads :id from params, fetches the order, and shows
|
|
2
2
|
// inline loading/error/empty/ready states.
|
|
3
3
|
|
|
4
|
-
import { html, jsonFetcher, page, resource } from "@madojs/mado";
|
|
4
|
+
import { each, html, jsonFetcher, page, resource } from "@madojs/mado";
|
|
5
5
|
|
|
6
6
|
interface OrderDetail {
|
|
7
7
|
id: string;
|
|
@@ -46,7 +46,9 @@ export default page<{ id: string }>({
|
|
|
46
46
|
</tr>
|
|
47
47
|
</thead>
|
|
48
48
|
<tbody>
|
|
49
|
-
${
|
|
49
|
+
${each(
|
|
50
|
+
o.items,
|
|
51
|
+
(it) => it.sku,
|
|
50
52
|
(it) => html`
|
|
51
53
|
<tr style="border-bottom:1px solid var(--border);">
|
|
52
54
|
<td style="padding:8px 0;">${it.sku}</td>
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// Public landing page. Demonstrates that the marketing surface can live
|
|
2
2
|
// alongside the admin app without a guard, and can be baked for SEO.
|
|
3
|
+
//
|
|
4
|
+
// `bake` is declared so that `mado bake` (and `mado release`) actually
|
|
5
|
+
// prerender at least one static page out of the box. Without it the
|
|
6
|
+
// release output ships only the SPA shell with no SEO-friendly HTML
|
|
7
|
+
// for crawlers landing on "/".
|
|
3
8
|
|
|
4
9
|
import { html, page } from "@madojs/mado";
|
|
5
10
|
|
|
@@ -13,6 +18,10 @@ export default page({
|
|
|
13
18
|
type: "website",
|
|
14
19
|
},
|
|
15
20
|
}),
|
|
21
|
+
bake: {
|
|
22
|
+
paths: () => [{}],
|
|
23
|
+
data: () => ({}),
|
|
24
|
+
},
|
|
16
25
|
view: () => html`
|
|
17
26
|
<main style="max-width:720px;margin:0 auto;padding:64px 24px;">
|
|
18
27
|
<h1>__APP_NAME__</h1>
|
|
@@ -22,4 +31,4 @@ export default page({
|
|
|
22
31
|
</p>
|
|
23
32
|
</main>
|
|
24
33
|
`,
|
|
25
|
-
});
|
|
34
|
+
});
|
package/starters/crud/index.html
CHANGED
|
@@ -4,17 +4,25 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>__APP_NAME__</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
8
|
+
<!--
|
|
9
|
+
Paths below MUST be root-absolute (start with "/"), not "./...".
|
|
10
|
+
Mado is an SPA: hard-refreshing /users/42 still serves this same
|
|
11
|
+
index.html, and the browser resolves "./dist/main.js" against the
|
|
12
|
+
current URL → /users/dist/main.js → 404 → blank page.
|
|
13
|
+
Root-absolute paths always resolve to /dist/main.js regardless of route.
|
|
14
|
+
-->
|
|
7
15
|
<script type="importmap">
|
|
8
16
|
{
|
|
9
17
|
"imports": {
|
|
10
|
-
"@madojs/mado": "
|
|
11
|
-
"@madojs/mado/": "
|
|
18
|
+
"@madojs/mado": "/node_modules/@madojs/mado/dist/src/index.js",
|
|
19
|
+
"@madojs/mado/": "/node_modules/@madojs/mado/dist/src/"
|
|
12
20
|
}
|
|
13
21
|
}
|
|
14
22
|
</script>
|
|
15
23
|
</head>
|
|
16
24
|
<body>
|
|
17
25
|
<div id="app"></div>
|
|
18
|
-
<script type="module" src="
|
|
26
|
+
<script type="module" src="/dist/main.js"></script>
|
|
19
27
|
</body>
|
|
20
|
-
</html>
|
|
28
|
+
</html>
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
+
// Public landing page. `bake` is declared so that `mado bake` (and
|
|
2
|
+
// `mado release`) actually prerender at least one SEO-friendly HTML page
|
|
3
|
+
// out of the box. Without it the build output ships only the SPA shell.
|
|
4
|
+
|
|
1
5
|
import { html, page } from "@madojs/mado";
|
|
2
6
|
|
|
3
7
|
export default page({
|
|
4
8
|
title: "__APP_NAME__",
|
|
9
|
+
head: () => ({
|
|
10
|
+
description: "A CRUD scaffold built with Mado.",
|
|
11
|
+
og: {
|
|
12
|
+
title: "__APP_NAME__",
|
|
13
|
+
description: "A CRUD scaffold built with Mado.",
|
|
14
|
+
type: "website",
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
bake: {
|
|
18
|
+
paths: () => [{}],
|
|
19
|
+
data: () => ({}),
|
|
20
|
+
},
|
|
5
21
|
view: () => html`
|
|
6
22
|
<section class="page">
|
|
7
23
|
<div class="hero">
|
|
@@ -4,17 +4,25 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>__APP_NAME__</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
8
|
+
<!--
|
|
9
|
+
Paths below MUST be root-absolute (start with "/"), not "./...".
|
|
10
|
+
Mado is an SPA: hard-refreshing any nested route still serves this same
|
|
11
|
+
index.html, and the browser resolves "./dist/main.js" against the
|
|
12
|
+
current URL → /some/nested/dist/main.js → 404 → blank page.
|
|
13
|
+
Root-absolute paths always resolve to /dist/main.js regardless of route.
|
|
14
|
+
-->
|
|
7
15
|
<script type="importmap">
|
|
8
16
|
{
|
|
9
17
|
"imports": {
|
|
10
|
-
"@madojs/mado": "
|
|
11
|
-
"@madojs/mado/": "
|
|
18
|
+
"@madojs/mado": "/node_modules/@madojs/mado/dist/src/index.js",
|
|
19
|
+
"@madojs/mado/": "/node_modules/@madojs/mado/dist/src/"
|
|
12
20
|
}
|
|
13
21
|
}
|
|
14
22
|
</script>
|
|
15
23
|
</head>
|
|
16
24
|
<body>
|
|
17
25
|
<div id="app"></div>
|
|
18
|
-
<script type="module" src="
|
|
26
|
+
<script type="module" src="/dist/main.js"></script>
|
|
19
27
|
</body>
|
|
20
|
-
</html>
|
|
28
|
+
</html>
|
|
@@ -1,7 +1,24 @@
|
|
|
1
|
+
// Public landing page. `bake` is declared so that `mado bake` (and
|
|
2
|
+
// `mado release`) actually prerender a static HTML page out of the box.
|
|
3
|
+
// Without it the build output ships only the SPA shell — bots that hit
|
|
4
|
+
// "/" see an empty <body> instead of the welcome content.
|
|
5
|
+
|
|
1
6
|
import { html, page } from "@madojs/mado";
|
|
2
7
|
|
|
3
8
|
export default page({
|
|
4
9
|
title: "__APP_NAME__",
|
|
10
|
+
head: () => ({
|
|
11
|
+
description: "A minimal Mado starter app.",
|
|
12
|
+
og: {
|
|
13
|
+
title: "__APP_NAME__",
|
|
14
|
+
description: "A minimal Mado starter app.",
|
|
15
|
+
type: "website",
|
|
16
|
+
},
|
|
17
|
+
}),
|
|
18
|
+
bake: {
|
|
19
|
+
paths: () => [{}],
|
|
20
|
+
data: () => ({}),
|
|
21
|
+
},
|
|
5
22
|
view: () => html`
|
|
6
23
|
<main class="shell">
|
|
7
24
|
<section class="panel">
|