@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
|
@@ -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
|
+
}
|