@madojs/mado 0.10.1 → 0.11.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 +24 -26
- package/CHANGELOG.md +68 -0
- package/README.md +18 -45
- 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/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 +67 -57
- 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
package/server/serve.mjs
DELETED
|
@@ -1,455 +0,0 @@
|
|
|
1
|
-
// Tiny static server on node:http with dev features (ETag + HMR through SSE).
|
|
2
|
-
// No dependencies.
|
|
3
|
-
//
|
|
4
|
-
// PORT=5173 node server/serve.mjs
|
|
5
|
-
// NO_HMR=1 node server/serve.mjs # disable HMR
|
|
6
|
-
// node server/serve.mjs basic # mount examples/basic/ at /
|
|
7
|
-
// EXAMPLE=showcase node server/serve.mjs # mount examples/showcase/ at /
|
|
8
|
-
//
|
|
9
|
-
// Without EXAMPLE, / serves examples/index.html when running inside the Mado
|
|
10
|
-
// repository, or ./index.html when running inside a generated app.
|
|
11
|
-
// With EXAMPLE, all extensionless and /index.html requests fall back to
|
|
12
|
-
// examples/<EXAMPLE>/index.html so the client router works from root, just
|
|
13
|
-
// like a production SPA deploy.
|
|
14
|
-
|
|
15
|
-
import { createServer, request as httpRequest } from "node:http";
|
|
16
|
-
import { request as httpsRequest } from "node:https";
|
|
17
|
-
import { readFile, readdir, readFile as readFileAsync, stat } from "node:fs/promises";
|
|
18
|
-
import { watch, existsSync, readFileSync } from "node:fs";
|
|
19
|
-
import { extname, join, resolve, sep } from "node:path";
|
|
20
|
-
import { createHash } from "node:crypto";
|
|
21
|
-
|
|
22
|
-
const ROOT = resolve(process.cwd());
|
|
23
|
-
|
|
24
|
-
// Optional mado.config.json — used for dev.proxy and dev.port. Read with a
|
|
25
|
-
// hand-rolled JSON parse to avoid a circular dep with scripts/_config.mjs
|
|
26
|
-
// (this server is launched from cli.mjs and runs in its own Node process).
|
|
27
|
-
const CONFIG = (() => {
|
|
28
|
-
try {
|
|
29
|
-
const file = join(ROOT, "mado.config.json");
|
|
30
|
-
if (!existsSync(file)) return {};
|
|
31
|
-
return JSON.parse(readFileSync(file, "utf8")) ?? {};
|
|
32
|
-
} catch {
|
|
33
|
-
return {};
|
|
34
|
-
}
|
|
35
|
-
})();
|
|
36
|
-
const PROXY_RULES = Object.entries(CONFIG.dev?.proxy ?? {}); // [["/api", "http://localhost:3000"], ...]
|
|
37
|
-
|
|
38
|
-
// Tiny argv parser. Supports --flag, --flag=value, --flag value. The first
|
|
39
|
-
// non-flag positional is the EXAMPLE name (legacy behavior). Anything that
|
|
40
|
-
// looks like a flag is consumed before we pick the positional, so calls like
|
|
41
|
-
// `mado dev -- --host 127.0.0.1` work as documented.
|
|
42
|
-
function parseArgs(argv) {
|
|
43
|
-
const flags = {};
|
|
44
|
-
const positional = [];
|
|
45
|
-
for (let i = 0; i < argv.length; i++) {
|
|
46
|
-
const a = argv[i];
|
|
47
|
-
if (a === "--") continue;
|
|
48
|
-
if (a.startsWith("--")) {
|
|
49
|
-
const eq = a.indexOf("=");
|
|
50
|
-
if (eq >= 0) {
|
|
51
|
-
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
52
|
-
} else {
|
|
53
|
-
const name = a.slice(2);
|
|
54
|
-
const next = argv[i + 1];
|
|
55
|
-
if (next !== undefined && !next.startsWith("-")) {
|
|
56
|
-
flags[name] = next;
|
|
57
|
-
i++;
|
|
58
|
-
} else {
|
|
59
|
-
flags[name] = true;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
} else {
|
|
63
|
-
positional.push(a);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return { flags, positional };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const { flags: CLI_FLAGS, positional: CLI_POSITIONAL } = parseArgs(
|
|
70
|
-
process.argv.slice(2),
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
const PORT = Number(CLI_FLAGS.port ?? process.env.PORT ?? CONFIG.dev?.port ?? 5173);
|
|
74
|
-
// HOST is opt-in. Default to "localhost" (loopback) which is friendlier in
|
|
75
|
-
// sandboxes that disallow binding 0.0.0.0 (EPERM on listen). Users can opt
|
|
76
|
-
// into LAN exposure with `mado dev --host 0.0.0.0` or HOST=0.0.0.0.
|
|
77
|
-
const HOST = String(CLI_FLAGS.host ?? process.env.HOST ?? CONFIG.dev?.host ?? "localhost");
|
|
78
|
-
const HMR = CLI_FLAGS.hmr !== false && process.env.NO_HMR !== "1";
|
|
79
|
-
|
|
80
|
-
const EXAMPLE = CLI_POSITIONAL[0] ?? process.env.MADO_EXAMPLE ?? process.env.EXAMPLE ?? "";
|
|
81
|
-
const EXAMPLE_DIR = EXAMPLE
|
|
82
|
-
? resolve(join(ROOT, "examples", EXAMPLE))
|
|
83
|
-
: "";
|
|
84
|
-
const EXAMPLE_INDEX = EXAMPLE ? join(EXAMPLE_DIR, "index.html") : "";
|
|
85
|
-
const EXAMPLES_INDEX = join(ROOT, "examples", "index.html");
|
|
86
|
-
const APP_INDEX = join(ROOT, "index.html");
|
|
87
|
-
const DEFAULT_INDEX = existsSync(EXAMPLES_INDEX) ? EXAMPLES_INDEX : APP_INDEX;
|
|
88
|
-
|
|
89
|
-
if (EXAMPLE) {
|
|
90
|
-
if (!existsSync(EXAMPLE_INDEX)) {
|
|
91
|
-
console.error(
|
|
92
|
-
`[serve] EXAMPLE=${EXAMPLE}: file not found: ${EXAMPLE_INDEX}`,
|
|
93
|
-
);
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const MIME = {
|
|
99
|
-
".html": "text/html; charset=utf-8",
|
|
100
|
-
".js": "text/javascript; charset=utf-8",
|
|
101
|
-
".mjs": "text/javascript; charset=utf-8",
|
|
102
|
-
".css": "text/css; charset=utf-8",
|
|
103
|
-
".json": "application/json; charset=utf-8",
|
|
104
|
-
".svg": "image/svg+xml",
|
|
105
|
-
".ico": "image/x-icon",
|
|
106
|
-
".map": "application/json; charset=utf-8",
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
// ---------- HMR through Server-Sent Events ----------
|
|
110
|
-
//
|
|
111
|
-
// Open SSE connections live in a Set. Any file change broadcasts "reload".
|
|
112
|
-
// The client reloads the page. This is deliberately full reload rather than
|
|
113
|
-
// true HMR: we do not need preserved state, and the behavior stays simple.
|
|
114
|
-
|
|
115
|
-
const sseClients = new Set();
|
|
116
|
-
|
|
117
|
-
function broadcast(event, data) {
|
|
118
|
-
for (const res of sseClients) {
|
|
119
|
-
res.write(`event: ${event}\ndata: ${data}\n\n`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (HMR) {
|
|
124
|
-
// Debounce reload: tsc -w often changes several files in a row.
|
|
125
|
-
let timer = null;
|
|
126
|
-
const trigger = () => {
|
|
127
|
-
clearTimeout(timer);
|
|
128
|
-
timer = setTimeout(() => {
|
|
129
|
-
console.log(`[hmr] reload ${sseClients.size} client(s)`);
|
|
130
|
-
broadcast("reload", Date.now());
|
|
131
|
-
}, 80);
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
for (const dir of ["dist", "examples"]) {
|
|
135
|
-
try {
|
|
136
|
-
watch(join(ROOT, dir), { recursive: true }, trigger);
|
|
137
|
-
} catch {
|
|
138
|
-
/* dir may not exist on startup */
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const HMR_CLIENT = `
|
|
144
|
-
// Mado HMR client (auto-injected by serve.mjs)
|
|
145
|
-
(() => {
|
|
146
|
-
if (window.__madoHmr) return;
|
|
147
|
-
window.__madoHmr = true;
|
|
148
|
-
const es = new EventSource('/__hmr');
|
|
149
|
-
es.addEventListener('reload', () => location.reload());
|
|
150
|
-
es.addEventListener('error', () => {
|
|
151
|
-
// The server may be gone; try reconnecting after 1s.
|
|
152
|
-
setTimeout(() => location.reload(), 1000);
|
|
153
|
-
});
|
|
154
|
-
})();
|
|
155
|
-
`.trim();
|
|
156
|
-
|
|
157
|
-
// ---------- Server ----------
|
|
158
|
-
|
|
159
|
-
const server = createServer(async (req, res) => {
|
|
160
|
-
const started = Date.now();
|
|
161
|
-
let pathname = "/";
|
|
162
|
-
let reason = "";
|
|
163
|
-
res.on("finish", () => {
|
|
164
|
-
const ms = Date.now() - started;
|
|
165
|
-
const type = res.getHeader("content-type") ?? "-";
|
|
166
|
-
const suffix = reason ? ` ${reason}` : "";
|
|
167
|
-
console.log(
|
|
168
|
-
`[serve] ${req.method ?? "GET"} ${pathname} ${res.statusCode} ${ms}ms ${type}${suffix}`,
|
|
169
|
-
);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
174
|
-
pathname = decodeURIComponent(url.pathname);
|
|
175
|
-
|
|
176
|
-
// Dev proxy: forward matching prefixes to an upstream backend, so the
|
|
177
|
-
// browser can reach the SPA and the API on a single origin without CORS.
|
|
178
|
-
const proxyRule = PROXY_RULES.find(([prefix]) => pathname.startsWith(prefix));
|
|
179
|
-
if (proxyRule) {
|
|
180
|
-
const [prefix, upstream] = proxyRule;
|
|
181
|
-
await proxyForward({ req, res, prefix, upstream, pathname, search: url.search });
|
|
182
|
-
reason = `proxy → ${upstream}`;
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// SSE endpoint for HMR.
|
|
187
|
-
if (pathname === "/__hmr") {
|
|
188
|
-
res.writeHead(200, {
|
|
189
|
-
"content-type": "text/event-stream",
|
|
190
|
-
"cache-control": "no-cache",
|
|
191
|
-
connection: "keep-alive",
|
|
192
|
-
});
|
|
193
|
-
res.write("retry: 1000\n\n");
|
|
194
|
-
sseClients.add(res);
|
|
195
|
-
console.log(`[hmr] client connected (${sseClients.size})`);
|
|
196
|
-
req.on("close", () => {
|
|
197
|
-
sseClients.delete(res);
|
|
198
|
-
console.log(`[hmr] client disconnected (${sseClients.size})`);
|
|
199
|
-
});
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// A mounted example owns root and SPA fallback. Otherwise serve the
|
|
204
|
-
// examples index page.
|
|
205
|
-
const fallbackIndex = EXAMPLE ? EXAMPLE_INDEX : DEFAULT_INDEX;
|
|
206
|
-
|
|
207
|
-
if (pathname === "/") {
|
|
208
|
-
// Resolved through fallback below.
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const filePath =
|
|
212
|
-
pathname === "/" ? fallbackIndex : resolve(join(ROOT, pathname));
|
|
213
|
-
|
|
214
|
-
if (filePath !== fallbackIndex) {
|
|
215
|
-
if (!filePath.startsWith(ROOT + sep) && filePath !== ROOT) {
|
|
216
|
-
reason = "forbidden path";
|
|
217
|
-
res.writeHead(403).end("forbidden");
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
let target = filePath;
|
|
223
|
-
try {
|
|
224
|
-
const s = await stat(target);
|
|
225
|
-
if (s.isDirectory()) target = join(target, "index.html");
|
|
226
|
-
} catch {
|
|
227
|
-
// Public assets: in production `mado release` copies public/* into out/.
|
|
228
|
-
// In dev we surface them from public/ directly so favicon.svg, robots.txt,
|
|
229
|
-
// og-image.png, etc. don't 404 (and so dev/prod behavior matches).
|
|
230
|
-
const publicCandidate = resolve(join(ROOT, "public", pathname));
|
|
231
|
-
if (
|
|
232
|
-
publicCandidate.startsWith(resolve(join(ROOT, "public")) + sep) &&
|
|
233
|
-
existsSync(publicCandidate)
|
|
234
|
-
) {
|
|
235
|
-
try {
|
|
236
|
-
const ps = await stat(publicCandidate);
|
|
237
|
-
if (!ps.isDirectory()) {
|
|
238
|
-
target = publicCandidate;
|
|
239
|
-
reason = "public/";
|
|
240
|
-
} else if (!extname(pathname)) {
|
|
241
|
-
target = fallbackIndex;
|
|
242
|
-
} else {
|
|
243
|
-
reason = "file not found";
|
|
244
|
-
res.writeHead(404).end("not found");
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
} catch {
|
|
248
|
-
target = fallbackIndex;
|
|
249
|
-
}
|
|
250
|
-
} else if (!extname(pathname)) {
|
|
251
|
-
target = fallbackIndex;
|
|
252
|
-
} else {
|
|
253
|
-
reason = "file not found";
|
|
254
|
-
res.writeHead(404).end("not found");
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
let data = await readFile(target);
|
|
260
|
-
|
|
261
|
-
// ETag: content hash. If-None-Match → 304.
|
|
262
|
-
const etag = `"${createHash("sha1").update(data).digest("base64url")}"`;
|
|
263
|
-
if (req.headers["if-none-match"] === etag) {
|
|
264
|
-
res.writeHead(304, { etag });
|
|
265
|
-
res.end();
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// HMR injector: add the client script before </body>.
|
|
270
|
-
const type =
|
|
271
|
-
MIME[extname(target).toLowerCase()] ?? "application/octet-stream";
|
|
272
|
-
|
|
273
|
-
if (type.startsWith("text/html")) {
|
|
274
|
-
let text = data.toString("utf8");
|
|
275
|
-
// modulepreload hints: tell the browser to fetch the framework core and
|
|
276
|
-
// the mounted example's pages while HTML is still being parsed.
|
|
277
|
-
const preload = await buildPreloadHints();
|
|
278
|
-
if (preload) {
|
|
279
|
-
text = text.replace(/<\/head>/i, `${preload}\n </head>`);
|
|
280
|
-
}
|
|
281
|
-
if (HMR) {
|
|
282
|
-
text = text.replace(
|
|
283
|
-
/<\/body>/i,
|
|
284
|
-
`<script>${HMR_CLIENT}</script>\n </body>`,
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
data = Buffer.from(text);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
res.writeHead(200, {
|
|
291
|
-
"content-type": type,
|
|
292
|
-
etag,
|
|
293
|
-
"cache-control": "no-cache",
|
|
294
|
-
});
|
|
295
|
-
res.end(data);
|
|
296
|
-
} catch (err) {
|
|
297
|
-
reason = "unhandled error";
|
|
298
|
-
console.error("[serve] error:", err);
|
|
299
|
-
res.writeHead(500).end(String(err));
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// ---------- modulepreload hints ----------
|
|
304
|
-
//
|
|
305
|
-
// Simple heuristic, no AST parsing:
|
|
306
|
-
// 1. /dist/src/index.js — framework core, always needed
|
|
307
|
-
// 2. /dist/examples/<EXAMPLE>/main.js — app entry
|
|
308
|
-
// 3. /dist/examples/<EXAMPLE>/routes.js — route manifest
|
|
309
|
-
// 4. /dist/examples/<EXAMPLE>/pages/*.js — all pages
|
|
310
|
-
// (without them the first router click waterfalls)
|
|
311
|
-
//
|
|
312
|
-
// Disable with PRELOAD=0. Limit to the core with PRELOAD=core.
|
|
313
|
-
// Default is full (all pages).
|
|
314
|
-
|
|
315
|
-
const PRELOAD = process.env.PRELOAD ?? "full";
|
|
316
|
-
|
|
317
|
-
let cachedPreloadHints = null;
|
|
318
|
-
let cachedPreloadAt = 0;
|
|
319
|
-
const PRELOAD_CACHE_MS = HMR ? 1000 : 60_000;
|
|
320
|
-
|
|
321
|
-
async function buildPreloadHints() {
|
|
322
|
-
if (PRELOAD === "0" || PRELOAD === "off" || PRELOAD === "false") return "";
|
|
323
|
-
const now = Date.now();
|
|
324
|
-
if (cachedPreloadHints !== null && now - cachedPreloadAt < PRELOAD_CACHE_MS) {
|
|
325
|
-
return cachedPreloadHints;
|
|
326
|
-
}
|
|
327
|
-
const hrefs = [];
|
|
328
|
-
// core
|
|
329
|
-
if (existsSync(join(ROOT, "dist/src/index.js"))) {
|
|
330
|
-
hrefs.push("/dist/src/index.js");
|
|
331
|
-
}
|
|
332
|
-
if (EXAMPLE) {
|
|
333
|
-
const exampleDist = join(ROOT, "dist", "examples", EXAMPLE);
|
|
334
|
-
for (const f of ["main.js", "routes.js"]) {
|
|
335
|
-
if (existsSync(join(exampleDist, f))) {
|
|
336
|
-
hrefs.push(`/dist/examples/${EXAMPLE}/${f}`);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
if (PRELOAD === "full") {
|
|
340
|
-
const pagesDir = join(exampleDist, "pages");
|
|
341
|
-
if (existsSync(pagesDir)) {
|
|
342
|
-
try {
|
|
343
|
-
for (const file of await readdir(pagesDir)) {
|
|
344
|
-
if (file.endsWith(".js")) {
|
|
345
|
-
hrefs.push(`/dist/examples/${EXAMPLE}/pages/${file}`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
} catch {
|
|
349
|
-
/* ignore */
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
} else if (!existsSync(EXAMPLES_INDEX)) {
|
|
354
|
-
if (existsSync(join(ROOT, "dist/main.js"))) {
|
|
355
|
-
hrefs.push("/dist/main.js");
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
cachedPreloadHints = hrefs
|
|
359
|
-
.map((h) => ` <link rel="modulepreload" href="${h}">`)
|
|
360
|
-
.join("\n");
|
|
361
|
-
cachedPreloadAt = now;
|
|
362
|
-
return cachedPreloadHints;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
server.on("error", (err) => {
|
|
366
|
-
if (err.code === "EPERM" || err.code === "EACCES") {
|
|
367
|
-
console.error(
|
|
368
|
-
`[serve] failed to bind ${HOST}:${PORT}: ${err.message}\n` +
|
|
369
|
-
`[serve] tip: this sandbox may disallow binding "${HOST}".\n` +
|
|
370
|
-
`[serve] try: mado dev --host 127.0.0.1 (or HOST=127.0.0.1)`,
|
|
371
|
-
);
|
|
372
|
-
} else {
|
|
373
|
-
console.error(`[serve] failed to listen on ${HOST}:${PORT}: ${err.message}`);
|
|
374
|
-
}
|
|
375
|
-
process.exit(1);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
async function proxyForward({ req, res, prefix, upstream, pathname, search }) {
|
|
379
|
-
// Strip the prefix only if the upstream URL itself ends with `/`; otherwise
|
|
380
|
-
// forward the full pathname so the backend sees /api/...
|
|
381
|
-
let upstreamUrl;
|
|
382
|
-
try {
|
|
383
|
-
upstreamUrl = new URL(upstream);
|
|
384
|
-
} catch {
|
|
385
|
-
res.writeHead(502).end(`bad upstream: ${upstream}`);
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
const target = new URL(upstream);
|
|
389
|
-
// Compose path: <upstream.pathname rstrip "/"> + <pathname> + <search>
|
|
390
|
-
const tail = pathname; // keep the original /api/... so backends route normally
|
|
391
|
-
target.pathname = (target.pathname.replace(/\/$/, "")) + tail;
|
|
392
|
-
target.search = search;
|
|
393
|
-
|
|
394
|
-
const lib = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
395
|
-
const upstreamReq = lib(
|
|
396
|
-
target,
|
|
397
|
-
{
|
|
398
|
-
method: req.method,
|
|
399
|
-
headers: {
|
|
400
|
-
...req.headers,
|
|
401
|
-
host: target.host,
|
|
402
|
-
},
|
|
403
|
-
},
|
|
404
|
-
(upstreamRes) => {
|
|
405
|
-
// Forward status and headers, then pipe the body.
|
|
406
|
-
res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
|
|
407
|
-
upstreamRes.pipe(res);
|
|
408
|
-
},
|
|
409
|
-
);
|
|
410
|
-
upstreamReq.on("error", (err) => {
|
|
411
|
-
console.error(`[serve] proxy error for ${pathname} → ${target.href}:`, err.message);
|
|
412
|
-
if (!res.headersSent) {
|
|
413
|
-
res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
|
|
414
|
-
res.end(`proxy upstream unavailable: ${target.host}\n${err.message}`);
|
|
415
|
-
} else {
|
|
416
|
-
res.end();
|
|
417
|
-
}
|
|
418
|
-
});
|
|
419
|
-
req.pipe(upstreamReq);
|
|
420
|
-
// Reference unused arg so lint is happy.
|
|
421
|
-
void prefix;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
server.listen(PORT, HOST, () => {
|
|
425
|
-
const distReady = existsSync(join(ROOT, "dist/src/index.js"))
|
|
426
|
-
|| existsSync(join(ROOT, "dist/main.js"));
|
|
427
|
-
const mount = EXAMPLE
|
|
428
|
-
? `examples/${EXAMPLE}/ -> /`
|
|
429
|
-
: existsSync(EXAMPLES_INDEX)
|
|
430
|
-
? "examples/index.html landing"
|
|
431
|
-
: "index.html app";
|
|
432
|
-
// Show "localhost" in the URL when bound to 0.0.0.0 — easier to click.
|
|
433
|
-
const urlHost = HOST === "0.0.0.0" || HOST === "::" ? "localhost" : HOST;
|
|
434
|
-
console.log("");
|
|
435
|
-
console.log("Mado dev server");
|
|
436
|
-
console.log(` url: http://${urlHost}:${PORT}/`);
|
|
437
|
-
console.log(` host: ${HOST}`);
|
|
438
|
-
console.log(` root: ${ROOT}`);
|
|
439
|
-
console.log(` mount: ${mount}`);
|
|
440
|
-
console.log(` hmr: ${HMR ? "on" : "off"}`);
|
|
441
|
-
console.log(` preload: ${PRELOAD}`);
|
|
442
|
-
console.log(` dist: ${distReady ? "ready" : "missing (run mado build)"}`);
|
|
443
|
-
if (PROXY_RULES.length > 0) {
|
|
444
|
-
console.log(" proxy:");
|
|
445
|
-
for (const [prefix, upstream] of PROXY_RULES) {
|
|
446
|
-
console.log(` ${prefix.padEnd(10)} → ${upstream}`);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
if (!EXAMPLE && existsSync(EXAMPLES_INDEX)) {
|
|
450
|
-
console.log(" try: mado serve basic");
|
|
451
|
-
console.log(" mado serve showcase");
|
|
452
|
-
console.log(" mado serve tickets");
|
|
453
|
-
}
|
|
454
|
-
console.log("");
|
|
455
|
-
});
|
package/starters/admin/README.md
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
# __APP_NAME__
|
|
2
|
-
|
|
3
|
-
A starter Mado admin app: nested routes, a guarded admin shell, a blessed API
|
|
4
|
-
client, and a one-shot release pipeline.
|
|
5
|
-
|
|
6
|
-
## What you get
|
|
7
|
-
|
|
8
|
-
- `src/main.ts` — 8 lines: mount the router into `#app`. Layouts are NOT
|
|
9
|
-
declared here, only in `src/routes.ts`.
|
|
10
|
-
- `src/routes.ts` — nested manifest with three groups:
|
|
11
|
-
- `/` → public landing (bakeable),
|
|
12
|
-
- `/login` → centered `auth` layout,
|
|
13
|
-
- `/admin` → `app` layout, **guarded** by `requireAuth`.
|
|
14
|
-
- `src/layouts/app.ts` — admin shell (top bar + sidebar + content slot).
|
|
15
|
-
- `src/layouts/auth.ts` — centered card for sign-in.
|
|
16
|
-
- `src/lib/api.ts` — `createApiClient(baseUrl)` with bearer token, 401-refresh
|
|
17
|
-
retry, JSON in/out and a typed `ApiError`.
|
|
18
|
-
- `src/lib/auth.ts` — memory-only `accessToken`, `restoreSession()` from an
|
|
19
|
-
HttpOnly refresh cookie, and the `requireAuth` guard.
|
|
20
|
-
- `src/components/` — tiny `x-button` and `x-input` Web Components.
|
|
21
|
-
- `mado.config.json` — one config file. Includes a `dev.proxy` for `/api`.
|
|
22
|
-
|
|
23
|
-
## Commands
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
npm run dev # tsc -w + dev server on http://localhost:5173, HMR on
|
|
27
|
-
npm run build # tsc → dist/
|
|
28
|
-
npm run typecheck # tsc --noEmit
|
|
29
|
-
npm run bundle # esbuild → out/assets/
|
|
30
|
-
npm run bake # prerender baked routes → out/baked/
|
|
31
|
-
npm run release # typecheck + build + bundle + bake + promote baked HTML + copy public/ → out/
|
|
32
|
-
npm run preview # serve out/ locally (production rehearsal)
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
To deploy, run `npm run release` and upload the entire `out/` directory
|
|
36
|
-
anywhere static (nginx, Cloudflare Pages, S3, Netlify, GitHub Pages, …).
|
|
37
|
-
|
|
38
|
-
## Backend expectations
|
|
39
|
-
|
|
40
|
-
The blessed `api` client speaks JSON. The auth recipe expects:
|
|
41
|
-
|
|
42
|
-
- `POST /api/auth/login` → `{ accessToken: string }` (sets refresh cookie)
|
|
43
|
-
- `POST /api/auth/refresh` → `{ accessToken: string }` (reads refresh cookie)
|
|
44
|
-
- `POST /api/auth/logout` → 204 (clears refresh cookie)
|
|
45
|
-
|
|
46
|
-
Change `mado.config.json#dev.proxy` to point at your backend in development.
|
|
47
|
-
|
|
48
|
-
## Where things live
|
|
49
|
-
|
|
50
|
-
| What | Where |
|
|
51
|
-
|---------------------|--------------------------------------|
|
|
52
|
-
| New URL | `src/pages/*.ts` + add to `routes.ts`|
|
|
53
|
-
| New protected URL | inside the `/admin` layout block |
|
|
54
|
-
| New layout | `src/layouts/*.ts` |
|
|
55
|
-
| New reusable widget | `src/components/x-*.ts` |
|
|
56
|
-
| New API call | `src/lib/api.ts` (add a method) |
|
|
57
|
-
| New global signal | `src/lib/<name>.ts` |
|
|
58
|
-
| Static image | `public/<file>` |
|
|
59
|
-
|
|
60
|
-
See the framework docs:
|
|
61
|
-
[`docs/en/11-layouts.md`](https://github.com/madojs/mado/blob/main/docs/en/11-layouts.md),
|
|
62
|
-
[`docs/en/12-auth-and-api.md`](https://github.com/madojs/mado/blob/main/docs/en/12-auth-and-api.md),
|
|
63
|
-
[`docs/en/13-deployment.md`](https://github.com/madojs/mado/blob/main/docs/en/13-deployment.md).
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
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 /admin/orders/42 still serves this same
|
|
11
|
-
index.html, and the browser resolves "./dist/main.js" against the
|
|
12
|
-
current URL → /admin/orders/dist/main.js → 404 → blank page.
|
|
13
|
-
Root-absolute paths always resolve to /dist/main.js regardless of route.
|
|
14
|
-
-->
|
|
15
|
-
<script type="importmap">
|
|
16
|
-
{
|
|
17
|
-
"imports": {
|
|
18
|
-
"@madojs/mado": "/node_modules/@madojs/mado/dist/src/index.js",
|
|
19
|
-
"@madojs/mado/": "/node_modules/@madojs/mado/dist/src/"
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
</script>
|
|
23
|
-
</head>
|
|
24
|
-
<body>
|
|
25
|
-
<div id="app"></div>
|
|
26
|
-
<script type="module" src="/dist/main.js"></script>
|
|
27
|
-
</body>
|
|
28
|
-
</html>
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"dev": {
|
|
3
|
-
"port": 5173,
|
|
4
|
-
"proxy": {
|
|
5
|
-
"/api": "http://localhost:3000"
|
|
6
|
-
}
|
|
7
|
-
},
|
|
8
|
-
"build": {
|
|
9
|
-
"out": "out",
|
|
10
|
-
"dist": "dist",
|
|
11
|
-
"publicDir": "public"
|
|
12
|
-
},
|
|
13
|
-
"bake": {
|
|
14
|
-
"entry": "src/routes.ts",
|
|
15
|
-
"template": "index.html",
|
|
16
|
-
"baseUrl": "https://example.com"
|
|
17
|
-
},
|
|
18
|
-
"bundle": {
|
|
19
|
-
"splitting": true,
|
|
20
|
-
"compress": ["gz", "br"]
|
|
21
|
-
}
|
|
22
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "__PACKAGE_NAME__",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"build": "mado build",
|
|
8
|
-
"typecheck": "mado typecheck",
|
|
9
|
-
"dev": "mado dev",
|
|
10
|
-
"serve": "mado serve",
|
|
11
|
-
"bundle": "mado bundle",
|
|
12
|
-
"bake": "mado bake",
|
|
13
|
-
"release": "mado release",
|
|
14
|
-
"preview": "mado preview"
|
|
15
|
-
},
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"@madojs/mado": "__MADOJS_VERSION__"
|
|
18
|
-
},
|
|
19
|
-
"devDependencies": {
|
|
20
|
-
"esbuild": "^0.28.0",
|
|
21
|
-
"linkedom": "^0.18.12",
|
|
22
|
-
"typescript": "^6.0.3"
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// <x-button variant="primary|ghost|danger" ?disabled>
|
|
2
|
-
//
|
|
3
|
-
// Wraps a native <button> so it can be slotted with text/icon and styled
|
|
4
|
-
// consistently across the app.
|
|
5
|
-
//
|
|
6
|
-
// Handles two Shadow DOM gotchas out of the box:
|
|
7
|
-
// 1. Reactive attributes via ctx.attr() — external ?disabled changes
|
|
8
|
-
// re-render the inner button automatically.
|
|
9
|
-
// 2. Form submit — a <button type="submit"> inside Shadow DOM cannot
|
|
10
|
-
// trigger <form> submit in Light DOM (spec limitation). We call
|
|
11
|
-
// form.requestSubmit() from a click handler to bridge this gap.
|
|
12
|
-
|
|
13
|
-
import { component, css, html } from "@madojs/mado";
|
|
14
|
-
|
|
15
|
-
component(
|
|
16
|
-
"x-button",
|
|
17
|
-
({ host, attr }) => {
|
|
18
|
-
const variant = attr("variant", "primary");
|
|
19
|
-
const disabled = attr("disabled");
|
|
20
|
-
|
|
21
|
-
const handleClick = () => {
|
|
22
|
-
const typeAttr = host.getAttribute("type");
|
|
23
|
-
if (typeAttr === "button" || typeAttr === "reset") return;
|
|
24
|
-
const form = host.closest("form");
|
|
25
|
-
if (form && !host.hasAttribute("disabled")) form.requestSubmit();
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
return () => html`
|
|
29
|
-
<button
|
|
30
|
-
data-variant=${variant()}
|
|
31
|
-
?disabled=${() => disabled() !== ""}
|
|
32
|
-
@click=${handleClick}
|
|
33
|
-
>
|
|
34
|
-
<slot></slot>
|
|
35
|
-
</button>
|
|
36
|
-
`;
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
styles: css`
|
|
40
|
-
:host {
|
|
41
|
-
display: inline-flex;
|
|
42
|
-
}
|
|
43
|
-
button {
|
|
44
|
-
display: inline-flex;
|
|
45
|
-
align-items: center;
|
|
46
|
-
gap: var(--space-2);
|
|
47
|
-
padding: 8px 14px;
|
|
48
|
-
border-radius: var(--radius-sm);
|
|
49
|
-
border: 1px solid transparent;
|
|
50
|
-
font: inherit;
|
|
51
|
-
cursor: pointer;
|
|
52
|
-
background: var(--accent);
|
|
53
|
-
color: var(--accent-fg);
|
|
54
|
-
transition: filter 0.12s ease;
|
|
55
|
-
}
|
|
56
|
-
button:hover:not(:disabled) {
|
|
57
|
-
filter: brightness(1.07);
|
|
58
|
-
}
|
|
59
|
-
button:active:not(:disabled) {
|
|
60
|
-
filter: brightness(0.95);
|
|
61
|
-
}
|
|
62
|
-
button:disabled {
|
|
63
|
-
opacity: 0.55;
|
|
64
|
-
cursor: not-allowed;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
button[data-variant="ghost"] {
|
|
68
|
-
background: transparent;
|
|
69
|
-
color: var(--fg);
|
|
70
|
-
border-color: var(--border);
|
|
71
|
-
}
|
|
72
|
-
button[data-variant="ghost"]:hover:not(:disabled) {
|
|
73
|
-
background: var(--bg-elevated);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
button[data-variant="danger"] {
|
|
77
|
-
background: var(--danger);
|
|
78
|
-
color: white;
|
|
79
|
-
}
|
|
80
|
-
`,
|
|
81
|
-
},
|
|
82
|
-
);
|