@madojs/mado 0.10.1 → 0.11.1
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 +24 -26
- package/CHANGELOG.md +95 -0
- package/README.md +22 -47
- package/TODO.md +52 -48
- package/dist/src/component.d.ts +2 -1
- package/dist/src/component.js +5 -2
- package/dist/src/component.js.map +1 -1
- package/dist/src/each.d.ts +1 -1
- package/dist/src/each.js +1 -1
- package/dist/src/each.js.map +1 -1
- package/dist/src/html/bindings.js +3 -3
- package/dist/src/html/bindings.js.map +1 -1
- package/dist/src/index.d.ts +11 -6
- package/dist/src/index.js +5 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/lazy.d.ts +1 -1
- package/dist/src/lazy.js +1 -1
- package/dist/src/lazy.js.map +1 -1
- package/dist/src/page.d.ts +17 -21
- package/dist/src/page.js +7 -12
- package/dist/src/page.js.map +1 -1
- package/dist/src/router/manifest.d.ts +1 -1
- package/dist/src/router/manifest.js +21 -13
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +2 -2
- package/dist/src/router/match.js +3 -3
- package/dist/src/router/match.js.map +1 -1
- package/dist/src/router/navigation.js +1 -1
- package/dist/src/router/navigation.js.map +1 -1
- package/dist/src/vite/index.d.ts +10 -0
- package/dist/src/vite/index.js +33 -0
- package/dist/src/vite/index.js.map +1 -0
- package/docs/en/00-the-mado-way.md +25 -12
- package/docs/en/01-routing.md +90 -142
- package/docs/en/02-project-layout.md +59 -53
- package/docs/en/03-static-bake.md +5 -6
- package/docs/en/05-why-mado.md +6 -6
- package/docs/en/06-for-backenders.md +18 -22
- package/docs/en/08-llm-zero-history-test.md +9 -14
- package/docs/en/09-shadow-vs-light-dom.md +28 -36
- package/docs/en/10-app-architecture.md +158 -96
- package/docs/en/11-layouts.md +22 -24
- package/docs/en/12-auth-and-api.md +89 -182
- package/docs/en/13-deployment.md +18 -22
- package/docs/en/14-testing.md +4 -4
- package/docs/en/16-bake-cookbook.md +11 -12
- package/docs/en/18-api-freeze-map.md +6 -4
- package/docs/en/20-v1-stability.md +1 -1
- package/docs/fr/00-the-mado-way.md +55 -90
- package/docs/fr/01-routing.md +70 -152
- package/docs/fr/02-project-layout.md +61 -42
- package/docs/fr/03-static-bake.md +1 -1
- package/docs/fr/05-why-mado.md +6 -6
- package/docs/fr/06-for-backenders.md +7 -7
- package/docs/fr/08-llm-zero-history-test.md +21 -48
- package/docs/fr/09-shadow-vs-light-dom.md +43 -162
- package/docs/fr/10-app-architecture.md +110 -33
- package/docs/fr/11-layouts.md +24 -12
- package/docs/fr/12-auth-and-api.md +63 -22
- package/docs/fr/13-deployment.md +7 -10
- package/docs/fr/14-testing.md +1 -1
- package/docs/fr/16-bake-cookbook.md +2 -2
- package/docs/fr/18-api-freeze-map.md +1 -1
- package/docs/fr/20-v1-stability.md +1 -1
- package/docs/recipes/nginx/README.md +13 -0
- package/docs/ru/00-the-mado-way.md +53 -75
- package/docs/ru/01-routing.md +68 -143
- package/docs/ru/02-project-layout.md +61 -41
- package/docs/ru/03-static-bake.md +2 -2
- package/docs/ru/05-why-mado.md +6 -6
- package/docs/ru/06-for-backenders.md +7 -7
- package/docs/ru/08-llm-zero-history-test.md +9 -14
- package/docs/ru/09-shadow-vs-light-dom.md +43 -178
- package/docs/ru/10-app-architecture.md +115 -63
- package/docs/ru/11-layouts.md +24 -24
- package/docs/ru/12-auth-and-api.md +57 -35
- package/docs/ru/13-deployment.md +7 -11
- package/docs/ru/14-testing.md +1 -1
- package/docs/ru/16-bake-cookbook.md +12 -6
- package/docs/ru/18-api-freeze-map.md +5 -3
- package/docs/ru/20-v1-stability.md +1 -1
- package/docs/uk/00-the-mado-way.md +70 -44
- package/docs/uk/01-routing.md +41 -47
- package/docs/uk/02-project-layout.md +68 -41
- package/docs/uk/03-static-bake.md +1 -2
- package/docs/uk/06-for-backenders.md +3 -3
- package/docs/uk/08-llm-zero-history-test.md +22 -24
- package/docs/uk/09-shadow-vs-light-dom.md +37 -86
- package/docs/uk/10-app-architecture.md +72 -31
- package/docs/uk/11-layouts.md +25 -12
- package/docs/uk/12-auth-and-api.md +58 -22
- package/docs/uk/13-deployment.md +4 -3
- package/docs/uk/14-testing.md +1 -1
- package/docs/uk/18-api-freeze-map.md +1 -1
- package/docs/uk/20-v1-stability.md +1 -1
- package/llms.txt +14 -15
- package/package.json +18 -11
- package/scripts/_config.mjs +15 -161
- package/scripts/bake.mjs +74 -63
- package/scripts/cli/generate.mjs +348 -0
- package/scripts/cli/help.mjs +27 -0
- package/scripts/cli/index.mjs +79 -0
- package/scripts/cli/init.mjs +153 -0
- package/scripts/cli/release.mjs +152 -0
- package/scripts/cli/run.mjs +96 -0
- package/scripts/cli.mjs +2 -621
- package/scripts/package-smoke.mjs +4 -1
- package/scripts/preview.mjs +13 -37
- package/scripts/size-budget.mjs +5 -2
- package/scripts/vite.default.mjs +11 -0
- package/starters/default/.editorconfig +12 -0
- package/starters/default/README.md +74 -0
- package/starters/default/eslint.config.mjs +256 -0
- package/starters/default/index.html +13 -0
- package/starters/default/package.json +30 -0
- package/starters/default/public/favicon.svg +4 -0
- package/starters/default/src/app.routes.ts +39 -0
- package/starters/default/src/layouts/app-shell.layout.ts +35 -0
- package/starters/default/src/layouts/auth-shell.layout.ts +17 -0
- package/starters/default/src/main.ts +16 -0
- package/starters/default/src/modules/auth/_contracts/auth-api.types.ts +17 -0
- package/starters/default/src/modules/auth/auth.connector.ts +45 -0
- package/starters/default/src/modules/auth/auth.guard.ts +22 -0
- package/starters/default/src/modules/auth/auth.public.ts +9 -0
- package/starters/default/src/modules/auth/auth.routes.ts +8 -0
- package/starters/default/src/modules/auth/auth.service.ts +71 -0
- package/starters/default/src/modules/auth/auth.types.ts +15 -0
- package/starters/default/src/modules/auth/login.page.ts +62 -0
- package/starters/default/src/modules/billing/_contracts/stripe.types.ts +17 -0
- package/starters/default/src/modules/billing/api/stripe.connector.ts +71 -0
- package/starters/default/src/modules/billing/billing.public.ts +5 -0
- package/starters/default/src/modules/billing/billing.routes.ts +9 -0
- package/starters/default/src/modules/billing/billing.types.ts +15 -0
- package/starters/default/src/modules/billing/components/invoice-status-badge.component.ts +43 -0
- package/starters/default/src/modules/billing/data/invoices.resource.ts +35 -0
- package/starters/default/src/modules/billing/pages/invoice-detail.page.ts +70 -0
- package/starters/default/src/modules/billing/pages/invoices-list.page.ts +73 -0
- package/starters/default/src/modules/home/home.page.ts +34 -0
- package/starters/default/src/modules/home/not-found.page.ts +11 -0
- package/starters/default/src/shared/http/http-client.ts +86 -0
- package/starters/default/src/shared/http/http-error.ts +37 -0
- package/starters/default/src/shared/http/interceptors.ts +59 -0
- package/starters/default/src/shared/lib/format-date.ts +19 -0
- package/starters/default/src/shared/styles/content.css +70 -0
- package/starters/default/src/shared/styles/reset.css +32 -0
- package/starters/default/src/shared/styles/shell.css +57 -0
- package/starters/default/src/shared/styles/tokens.css +44 -0
- package/starters/default/src/shared/ui/x-button.component.ts +49 -0
- package/starters/default/src/shared/ui/x-spinner.component.ts +22 -0
- package/starters/default/src/styles.d.ts +1 -0
- package/starters/default/src/vite-env.d.ts +1 -0
- package/starters/default/tsconfig.json +24 -0
- package/starters/default/vite.config.ts +9 -0
- package/MADO_V1_PLAN.md +0 -179
- package/ROADMAP.md +0 -178
- package/dist/src/html.d.ts +0 -18
- package/dist/src/html.js +0 -17
- package/dist/src/html.js.map +0 -1
- package/dist/src/router.d.ts +0 -13
- package/dist/src/router.js +0 -13
- package/dist/src/router.js.map +0 -1
- package/scripts/bundle.mjs +0 -212
- package/scripts/llm-zero-history-smoke.mjs +0 -93
- package/scripts/new.mjs +0 -80
- package/scripts/showcase-regression.mjs +0 -392
- package/server/serve.mjs +0 -455
- package/starters/admin/README.md +0 -63
- package/starters/admin/index.html +0 -28
- package/starters/admin/mado.config.json +0 -22
- package/starters/admin/package.json +0 -24
- package/starters/admin/public/favicon.svg +0 -4
- package/starters/admin/src/components/x-button.ts +0 -82
- package/starters/admin/src/components/x-input.ts +0 -105
- package/starters/admin/src/layouts/app.ts +0 -101
- package/starters/admin/src/layouts/auth.ts +0 -41
- package/starters/admin/src/lib/api.ts +0 -184
- package/starters/admin/src/lib/auth.ts +0 -83
- package/starters/admin/src/main.ts +0 -15
- package/starters/admin/src/pages/admin/dashboard.ts +0 -48
- package/starters/admin/src/pages/admin/order-detail.ts +0 -80
- package/starters/admin/src/pages/admin/orders.ts +0 -117
- package/starters/admin/src/pages/home.ts +0 -34
- package/starters/admin/src/pages/login.ts +0 -70
- package/starters/admin/src/pages/not-found.ts +0 -12
- package/starters/admin/src/routes.ts +0 -40
- package/starters/admin/src/styles/global.ts +0 -86
- package/starters/admin/tsconfig.json +0 -15
- package/starters/crud/README.md +0 -33
- package/starters/crud/index.html +0 -28
- package/starters/crud/mado.config.json +0 -20
- package/starters/crud/package.json +0 -24
- package/starters/crud/src/components/app-shell.ts +0 -56
- package/starters/crud/src/components/ticket-detail.ts +0 -33
- package/starters/crud/src/components/ticket-form.ts +0 -69
- package/starters/crud/src/components/ticket-list.ts +0 -66
- package/starters/crud/src/lib/api.ts +0 -76
- package/starters/crud/src/main.ts +0 -9
- package/starters/crud/src/pages/home.ts +0 -34
- package/starters/crud/src/pages/not-found.ts +0 -12
- package/starters/crud/src/pages/ticket-detail.ts +0 -7
- package/starters/crud/src/pages/ticket-new.ts +0 -7
- package/starters/crud/src/pages/tickets.ts +0 -7
- package/starters/crud/src/routes.ts +0 -11
- package/starters/crud/src/styles/global.ts +0 -155
- package/starters/crud/tsconfig.json +0 -15
- package/starters/minimal/README.md +0 -21
- package/starters/minimal/index.html +0 -28
- package/starters/minimal/mado.config.json +0 -20
- package/starters/minimal/package.json +0 -24
- package/starters/minimal/src/components/app-counter.ts +0 -31
- package/starters/minimal/src/main.ts +0 -9
- package/starters/minimal/src/pages/home.ts +0 -35
- package/starters/minimal/src/pages/not-found.ts +0 -14
- package/starters/minimal/src/routes.ts +0 -8
- package/starters/minimal/src/styles/global.ts +0 -60
- package/starters/minimal/tsconfig.json +0 -15
- package/templates/page-detail.ts +0 -63
- package/templates/page-form.ts +0 -94
- package/templates/page-list.ts +0 -79
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// One http client for the whole app. Modules' *.connector.ts files build on
|
|
2
|
+
// top of this and never call fetch() directly. This is what lets you swap
|
|
3
|
+
// auth, retries or base URLs in one place.
|
|
4
|
+
|
|
5
|
+
import { HttpError } from "./http-error";
|
|
6
|
+
import { applyRequestInterceptors, applyResponseInterceptors } from "./interceptors";
|
|
7
|
+
|
|
8
|
+
export interface HttpRequest {
|
|
9
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
10
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
11
|
+
body?: unknown;
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const baseUrl = (() => {
|
|
17
|
+
if (typeof window === "undefined") return "";
|
|
18
|
+
const meta = document.querySelector('meta[name="api-base"]');
|
|
19
|
+
return meta?.getAttribute("content") ?? "";
|
|
20
|
+
})();
|
|
21
|
+
|
|
22
|
+
function buildUrl(path: string, query?: HttpRequest["query"]): string {
|
|
23
|
+
const url = new URL(path, baseUrl || window.location.origin);
|
|
24
|
+
if (query) {
|
|
25
|
+
for (const [k, v] of Object.entries(query)) {
|
|
26
|
+
if (v !== undefined) url.searchParams.set(k, String(v));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return url.toString().replace(window.location.origin, "");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function request<T>(path: string, init: HttpRequest = {}): Promise<T> {
|
|
33
|
+
const url = buildUrl(path, init.query);
|
|
34
|
+
|
|
35
|
+
let req: RequestInit = {
|
|
36
|
+
method: init.method ?? "GET",
|
|
37
|
+
headers: {
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
Accept: "application/json",
|
|
40
|
+
...(init.headers ?? {}),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
if (init.body !== undefined) req.body = JSON.stringify(init.body);
|
|
44
|
+
if (init.signal) req.signal = init.signal;
|
|
45
|
+
|
|
46
|
+
req = await applyRequestInterceptors(url, req);
|
|
47
|
+
|
|
48
|
+
let res: Response;
|
|
49
|
+
try {
|
|
50
|
+
res = await fetch(url, req);
|
|
51
|
+
} catch (cause) {
|
|
52
|
+
throw new HttpError(0, "NETWORK_ERROR", "Network request failed", { cause });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
res = await applyResponseInterceptors(res);
|
|
56
|
+
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
let payload: unknown;
|
|
59
|
+
try {
|
|
60
|
+
payload = await res.json();
|
|
61
|
+
} catch {
|
|
62
|
+
payload = await res.text().catch(() => "");
|
|
63
|
+
}
|
|
64
|
+
const message =
|
|
65
|
+
(payload && typeof payload === "object" && "message" in payload
|
|
66
|
+
? String((payload as { message: unknown }).message)
|
|
67
|
+
: res.statusText) || "Request failed";
|
|
68
|
+
throw new HttpError(res.status, `HTTP_${res.status}`, message, { payload });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (res.status === 204) return undefined as T;
|
|
72
|
+
return (await res.json()) as T;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const httpClient = {
|
|
76
|
+
get: <T>(path: string, init?: Omit<HttpRequest, "method" | "body">) =>
|
|
77
|
+
request<T>(path, { ...init, method: "GET" }),
|
|
78
|
+
post: <T>(path: string, body?: unknown, init?: Omit<HttpRequest, "method" | "body">) =>
|
|
79
|
+
request<T>(path, { ...init, method: "POST", body }),
|
|
80
|
+
put: <T>(path: string, body?: unknown, init?: Omit<HttpRequest, "method" | "body">) =>
|
|
81
|
+
request<T>(path, { ...init, method: "PUT", body }),
|
|
82
|
+
patch: <T>(path: string, body?: unknown, init?: Omit<HttpRequest, "method" | "body">) =>
|
|
83
|
+
request<T>(path, { ...init, method: "PATCH", body }),
|
|
84
|
+
delete: <T>(path: string, init?: Omit<HttpRequest, "method" | "body">) =>
|
|
85
|
+
request<T>(path, { ...init, method: "DELETE" }),
|
|
86
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// One error shape for the entire app. Connectors may subclass HttpError for
|
|
2
|
+
// their own provider-specific codes.
|
|
3
|
+
|
|
4
|
+
export class HttpError extends Error {
|
|
5
|
+
readonly status: number;
|
|
6
|
+
readonly code: string;
|
|
7
|
+
readonly payload?: unknown;
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
status: number,
|
|
11
|
+
code: string,
|
|
12
|
+
message: string,
|
|
13
|
+
options?: { payload?: unknown; cause?: unknown },
|
|
14
|
+
) {
|
|
15
|
+
super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);
|
|
16
|
+
this.name = "HttpError";
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.payload = options?.payload;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get isNetwork(): boolean {
|
|
23
|
+
return this.status === 0;
|
|
24
|
+
}
|
|
25
|
+
get isUnauthorized(): boolean {
|
|
26
|
+
return this.status === 401;
|
|
27
|
+
}
|
|
28
|
+
get isForbidden(): boolean {
|
|
29
|
+
return this.status === 403;
|
|
30
|
+
}
|
|
31
|
+
get isNotFound(): boolean {
|
|
32
|
+
return this.status === 404;
|
|
33
|
+
}
|
|
34
|
+
get isServer(): boolean {
|
|
35
|
+
return this.status >= 500;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// HTTP interceptors. Register here once, every connector benefits.
|
|
2
|
+
//
|
|
3
|
+
// Auth interceptor is wired by modules/auth at boot time via registerAuthToken.
|
|
4
|
+
|
|
5
|
+
type RequestInterceptor = (url: string, init: RequestInit) => RequestInit | Promise<RequestInit>;
|
|
6
|
+
type ResponseInterceptor = (res: Response) => Response | Promise<Response>;
|
|
7
|
+
|
|
8
|
+
const requestInterceptors: RequestInterceptor[] = [];
|
|
9
|
+
const responseInterceptors: ResponseInterceptor[] = [];
|
|
10
|
+
|
|
11
|
+
export function addRequestInterceptor(fn: RequestInterceptor): () => void {
|
|
12
|
+
requestInterceptors.push(fn);
|
|
13
|
+
return () => {
|
|
14
|
+
const i = requestInterceptors.indexOf(fn);
|
|
15
|
+
if (i >= 0) requestInterceptors.splice(i, 1);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function addResponseInterceptor(fn: ResponseInterceptor): () => void {
|
|
20
|
+
responseInterceptors.push(fn);
|
|
21
|
+
return () => {
|
|
22
|
+
const i = responseInterceptors.indexOf(fn);
|
|
23
|
+
if (i >= 0) responseInterceptors.splice(i, 1);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function applyRequestInterceptors(
|
|
28
|
+
url: string,
|
|
29
|
+
init: RequestInit,
|
|
30
|
+
): Promise<RequestInit> {
|
|
31
|
+
let current = init;
|
|
32
|
+
for (const fn of requestInterceptors) {
|
|
33
|
+
current = await fn(url, current);
|
|
34
|
+
}
|
|
35
|
+
return current;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function applyResponseInterceptors(res: Response): Promise<Response> {
|
|
39
|
+
let current = res;
|
|
40
|
+
for (const fn of responseInterceptors) {
|
|
41
|
+
current = await fn(current);
|
|
42
|
+
}
|
|
43
|
+
return current;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Convenience: a token provider the auth module plugs into.
|
|
47
|
+
let getToken: (() => string | null) | null = null;
|
|
48
|
+
|
|
49
|
+
export function registerAuthTokenProvider(provider: () => string | null): void {
|
|
50
|
+
getToken = provider;
|
|
51
|
+
addRequestInterceptor((_url, init) => {
|
|
52
|
+
const token = getToken?.();
|
|
53
|
+
if (!token) return init;
|
|
54
|
+
return {
|
|
55
|
+
...init,
|
|
56
|
+
headers: { ...(init.headers as Record<string, string>), Authorization: `Bearer ${token}` },
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Pure utilities only. No DOM, no signals, no Mado runtime.
|
|
2
|
+
|
|
3
|
+
const fmt = new Intl.DateTimeFormat(undefined, {
|
|
4
|
+
year: "numeric",
|
|
5
|
+
month: "short",
|
|
6
|
+
day: "2-digit",
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export function formatDate(input: string | number | Date): string {
|
|
10
|
+
const date = input instanceof Date ? input : new Date(input);
|
|
11
|
+
return fmt.format(date);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatMoney(amount: number, currency: string): string {
|
|
15
|
+
return new Intl.NumberFormat(undefined, {
|
|
16
|
+
style: "currency",
|
|
17
|
+
currency,
|
|
18
|
+
}).format(amount);
|
|
19
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* Light-DOM content styles for pages: forms, tables, prose and simple states.
|
|
2
|
+
* Shadow DOM leaf components own their styles in component css`` blocks. */
|
|
3
|
+
|
|
4
|
+
section {
|
|
5
|
+
display: grid;
|
|
6
|
+
gap: var(--space-4);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
h1,
|
|
10
|
+
p {
|
|
11
|
+
margin-block-start: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
form {
|
|
15
|
+
display: grid;
|
|
16
|
+
gap: var(--space-3);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
label {
|
|
20
|
+
display: grid;
|
|
21
|
+
gap: var(--space-1);
|
|
22
|
+
font-weight: 500;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
input {
|
|
26
|
+
width: 100%;
|
|
27
|
+
padding: var(--space-2) var(--space-3);
|
|
28
|
+
border: 1px solid var(--color-border);
|
|
29
|
+
border-radius: var(--radius-sm);
|
|
30
|
+
background: var(--color-bg);
|
|
31
|
+
color: var(--color-text);
|
|
32
|
+
font: inherit;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
table.data {
|
|
36
|
+
width: 100%;
|
|
37
|
+
border-collapse: collapse;
|
|
38
|
+
background: var(--color-bg);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
table.data th,
|
|
42
|
+
table.data td {
|
|
43
|
+
padding: var(--space-2) var(--space-3);
|
|
44
|
+
border-bottom: 1px solid var(--color-border);
|
|
45
|
+
text-align: left;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
table.data th {
|
|
49
|
+
color: var(--color-text-muted);
|
|
50
|
+
font-size: 0.875rem;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
dl {
|
|
55
|
+
display: grid;
|
|
56
|
+
grid-template-columns: max-content 1fr;
|
|
57
|
+
gap: var(--space-2) var(--space-4);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
dt {
|
|
61
|
+
color: var(--color-text-muted);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
dd {
|
|
65
|
+
margin: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.error {
|
|
69
|
+
color: var(--color-danger);
|
|
70
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* Minimal reset for light DOM only. Does NOT pierce shadow roots. */
|
|
2
|
+
|
|
3
|
+
*,
|
|
4
|
+
*::before,
|
|
5
|
+
*::after {
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
html,
|
|
10
|
+
body {
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
background: var(--color-bg);
|
|
14
|
+
color: var(--color-text);
|
|
15
|
+
font-family: var(--font-family);
|
|
16
|
+
font-size: var(--font-size-base);
|
|
17
|
+
line-height: var(--line-height-base);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
a {
|
|
21
|
+
color: var(--color-primary);
|
|
22
|
+
text-decoration: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
a:hover {
|
|
26
|
+
text-decoration: underline;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
button {
|
|
30
|
+
font-family: inherit;
|
|
31
|
+
font-size: inherit;
|
|
32
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* App-zone shell styles. These classes are used by src/layouts/*.layout.ts.
|
|
2
|
+
* Page content and reusable components do not belong here. */
|
|
3
|
+
|
|
4
|
+
.layout--app {
|
|
5
|
+
display: grid;
|
|
6
|
+
grid-template-rows: auto 1fr;
|
|
7
|
+
min-height: 100vh;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.app-header {
|
|
11
|
+
display: flex;
|
|
12
|
+
gap: var(--space-4);
|
|
13
|
+
align-items: center;
|
|
14
|
+
padding: var(--space-3) var(--space-5);
|
|
15
|
+
border-bottom: 1px solid var(--color-border);
|
|
16
|
+
background: var(--color-surface);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.app-header .brand {
|
|
20
|
+
margin-right: auto;
|
|
21
|
+
font-weight: 600;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.app-header nav {
|
|
25
|
+
display: flex;
|
|
26
|
+
gap: var(--space-3);
|
|
27
|
+
align-items: center;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.app-header .who {
|
|
31
|
+
color: var(--color-text-muted);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.app-main {
|
|
35
|
+
padding: var(--space-5);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.layout--auth {
|
|
39
|
+
display: grid;
|
|
40
|
+
min-height: 100vh;
|
|
41
|
+
gap: var(--space-5);
|
|
42
|
+
place-items: center;
|
|
43
|
+
padding: var(--space-5);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.layout--auth .brand {
|
|
47
|
+
font-size: 1.2rem;
|
|
48
|
+
font-weight: 600;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.auth-main {
|
|
52
|
+
width: min(360px, 100%);
|
|
53
|
+
padding: var(--space-5);
|
|
54
|
+
border: 1px solid var(--color-border);
|
|
55
|
+
border-radius: var(--radius-md);
|
|
56
|
+
background: var(--color-surface);
|
|
57
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* Design tokens. Imported once by src/main.ts.
|
|
2
|
+
* Every component reads these via var(--token-name) and can be retargeted
|
|
3
|
+
* here without touching component code. */
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
/* Color */
|
|
7
|
+
--color-bg: #ffffff;
|
|
8
|
+
--color-surface: #f8fafc;
|
|
9
|
+
--color-border: #e2e8f0;
|
|
10
|
+
--color-text: #0f172a;
|
|
11
|
+
--color-text-muted: #64748b;
|
|
12
|
+
--color-primary: #2563eb;
|
|
13
|
+
--color-primary-contrast: #ffffff;
|
|
14
|
+
--color-danger: #dc2626;
|
|
15
|
+
--color-success: #16a34a;
|
|
16
|
+
|
|
17
|
+
/* Spacing scale */
|
|
18
|
+
--space-1: 0.25rem;
|
|
19
|
+
--space-2: 0.5rem;
|
|
20
|
+
--space-3: 0.75rem;
|
|
21
|
+
--space-4: 1rem;
|
|
22
|
+
--space-5: 1.5rem;
|
|
23
|
+
--space-6: 2rem;
|
|
24
|
+
|
|
25
|
+
/* Radii */
|
|
26
|
+
--radius-sm: 4px;
|
|
27
|
+
--radius-md: 8px;
|
|
28
|
+
|
|
29
|
+
/* Typography */
|
|
30
|
+
--font-family:
|
|
31
|
+
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
32
|
+
--font-size-base: 14px;
|
|
33
|
+
--line-height-base: 1.5;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@media (prefers-color-scheme: dark) {
|
|
37
|
+
:root {
|
|
38
|
+
--color-bg: #0b1220;
|
|
39
|
+
--color-surface: #111827;
|
|
40
|
+
--color-border: #1f2937;
|
|
41
|
+
--color-text: #e5e7eb;
|
|
42
|
+
--color-text-muted: #9ca3af;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Reusable UI brick. Pure presentation: no business state, no signals
|
|
2
|
+
// reaching out to services. Token-driven look, themable via tokens.css.
|
|
3
|
+
//
|
|
4
|
+
// Reactive attributes via ctx.attr(name, default?) — no MutationObserver
|
|
5
|
+
// boilerplate.
|
|
6
|
+
|
|
7
|
+
import { component, css, html } from "@madojs/mado";
|
|
8
|
+
|
|
9
|
+
component(
|
|
10
|
+
"x-button",
|
|
11
|
+
({ attr }) => {
|
|
12
|
+
const variant = attr("variant", "primary"); // "primary" | "ghost"
|
|
13
|
+
const disabled = attr("disabled", "false");
|
|
14
|
+
return () => html`
|
|
15
|
+
<button
|
|
16
|
+
class=${() => `btn btn--${variant()}`}
|
|
17
|
+
?disabled=${() => disabled() !== "false"}
|
|
18
|
+
>
|
|
19
|
+
<slot></slot>
|
|
20
|
+
</button>
|
|
21
|
+
`;
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
styles: css`
|
|
25
|
+
:host {
|
|
26
|
+
display: inline-block;
|
|
27
|
+
}
|
|
28
|
+
.btn {
|
|
29
|
+
padding: var(--space-2) var(--space-4);
|
|
30
|
+
border-radius: var(--radius-md);
|
|
31
|
+
border: 1px solid transparent;
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
}
|
|
34
|
+
.btn--primary {
|
|
35
|
+
background: var(--color-primary);
|
|
36
|
+
color: var(--color-primary-contrast);
|
|
37
|
+
}
|
|
38
|
+
.btn--ghost {
|
|
39
|
+
background: transparent;
|
|
40
|
+
border-color: var(--color-border);
|
|
41
|
+
color: var(--color-text);
|
|
42
|
+
}
|
|
43
|
+
.btn[disabled] {
|
|
44
|
+
opacity: 0.55;
|
|
45
|
+
cursor: not-allowed;
|
|
46
|
+
}
|
|
47
|
+
`,
|
|
48
|
+
},
|
|
49
|
+
);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { component, css, html } from "@madojs/mado";
|
|
2
|
+
|
|
3
|
+
component("x-spinner", () => () => html`<div class="dot" aria-label="Loading"></div>`, {
|
|
4
|
+
styles: css`
|
|
5
|
+
:host {
|
|
6
|
+
display: inline-block;
|
|
7
|
+
}
|
|
8
|
+
.dot {
|
|
9
|
+
width: 1rem;
|
|
10
|
+
height: 1rem;
|
|
11
|
+
border-radius: 50%;
|
|
12
|
+
border: 2px solid var(--color-border);
|
|
13
|
+
border-top-color: var(--color-primary);
|
|
14
|
+
animation: spin 0.7s linear infinite;
|
|
15
|
+
}
|
|
16
|
+
@keyframes spin {
|
|
17
|
+
to {
|
|
18
|
+
transform: rotate(360deg);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`,
|
|
22
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "*.css";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noImplicitOverride": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"exactOptionalPropertyTypes": true,
|
|
11
|
+
"noFallthroughCasesInSwitch": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"useDefineForClassFields": true,
|
|
17
|
+
"experimentalDecorators": false,
|
|
18
|
+
"isolatedModules": true,
|
|
19
|
+
"verbatimModuleSyntax": true,
|
|
20
|
+
"noEmit": true
|
|
21
|
+
},
|
|
22
|
+
"include": ["src/**/*.ts", "src/**/*.d.ts"],
|
|
23
|
+
"exclude": ["node_modules", "out"]
|
|
24
|
+
}
|