march-control-cli 0.1.3

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.
Files changed (53) hide show
  1. package/README.md +220 -0
  2. package/core/apply.js +152 -0
  3. package/core/backup.js +53 -0
  4. package/core/constants.js +55 -0
  5. package/core/desktop-service.js +219 -0
  6. package/core/desktop-state.js +511 -0
  7. package/core/index.js +1293 -0
  8. package/core/paths.js +71 -0
  9. package/core/presets.js +171 -0
  10. package/core/probe.js +70 -0
  11. package/core/store.js +218 -0
  12. package/core/utils.js +178 -0
  13. package/core/writers/codex.js +102 -0
  14. package/core/writers/index.js +16 -0
  15. package/core/writers/openclaw.js +93 -0
  16. package/core/writers/opencode.js +91 -0
  17. package/desktop/assets/march-mark.svg +21 -0
  18. package/desktop/main.js +192 -0
  19. package/desktop/preload.js +49 -0
  20. package/desktop/renderer/app.js +327 -0
  21. package/desktop/renderer/index.html +130 -0
  22. package/desktop/renderer/styles.css +413 -0
  23. package/package.json +106 -0
  24. package/scripts/desktop-dev.mjs +90 -0
  25. package/scripts/postinstall.mjs +28 -0
  26. package/scripts/serve-site.mjs +51 -0
  27. package/site/app.js +10 -0
  28. package/site/assets/march-mark.svg +22 -0
  29. package/site/index.html +286 -0
  30. package/site/styles.css +566 -0
  31. package/src/App.tsx +1186 -0
  32. package/src/components/layout/app-sidebar.tsx +103 -0
  33. package/src/components/layout/top-toolbar.tsx +44 -0
  34. package/src/components/layout/workspace-tabs.tsx +32 -0
  35. package/src/components/providers/inspector-panel.tsx +84 -0
  36. package/src/components/providers/metric-strip.tsx +26 -0
  37. package/src/components/providers/provider-editor.tsx +87 -0
  38. package/src/components/providers/provider-table.tsx +85 -0
  39. package/src/components/ui/logo-mark.tsx +16 -0
  40. package/src/features/mcp/mcp-view.tsx +45 -0
  41. package/src/features/prompts/prompts-view.tsx +40 -0
  42. package/src/features/providers/providers-view.tsx +40 -0
  43. package/src/features/providers/types.ts +8 -0
  44. package/src/features/skills/skills-view.tsx +44 -0
  45. package/src/hooks/use-control-workspace.ts +184 -0
  46. package/src/index.css +22 -0
  47. package/src/lib/client.ts +944 -0
  48. package/src/lib/query-client.ts +3 -0
  49. package/src/lib/workspace-sections.ts +34 -0
  50. package/src/main.tsx +14 -0
  51. package/src/types.ts +76 -0
  52. package/src/vite-env.d.ts +56 -0
  53. package/src-tauri/README.md +11 -0
@@ -0,0 +1,413 @@
1
+ :root {
2
+ --bg: #f4efe6;
3
+ --panel: rgba(255, 251, 245, 0.84);
4
+ --ink: #162531;
5
+ --muted: #5a6a74;
6
+ --line: rgba(22, 37, 49, 0.1);
7
+ --accent: #0e8f88;
8
+ --accent-strong: #0f6970;
9
+ --accent-soft: #ddf4f2;
10
+ --shadow: 0 22px 60px rgba(22, 37, 49, 0.14);
11
+ }
12
+
13
+ * {
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ html,
18
+ body {
19
+ margin: 0;
20
+ height: 100%;
21
+ }
22
+
23
+ body {
24
+ font-family: "Manrope", "Segoe UI Variable", "PingFang SC", sans-serif;
25
+ color: var(--ink);
26
+ background:
27
+ radial-gradient(circle at top left, rgba(201, 141, 47, 0.14), transparent 28%),
28
+ radial-gradient(circle at right, rgba(14, 143, 136, 0.18), transparent 32%),
29
+ linear-gradient(180deg, #fbf7ef 0%, #f2ebdf 100%);
30
+ }
31
+
32
+ button,
33
+ input,
34
+ select {
35
+ font: inherit;
36
+ }
37
+
38
+ .shell {
39
+ display: grid;
40
+ grid-template-columns: 276px minmax(0, 1fr);
41
+ min-height: 100vh;
42
+ }
43
+
44
+ .sidebar {
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 18px;
48
+ padding: 28px 18px;
49
+ background:
50
+ radial-gradient(circle at top, rgba(14, 143, 136, 0.16), transparent 36%),
51
+ linear-gradient(180deg, rgba(12, 20, 28, 0.98), rgba(18, 37, 49, 0.94));
52
+ color: #eef6f4;
53
+ }
54
+
55
+ .brand {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 12px;
59
+ padding: 6px 4px 16px;
60
+ }
61
+
62
+ .brand img {
63
+ width: 46px;
64
+ height: 46px;
65
+ }
66
+
67
+ .brand strong,
68
+ .hero h1,
69
+ .panel h2,
70
+ .stat strong {
71
+ font-family: "Space Grotesk", "Segoe UI Variable", sans-serif;
72
+ }
73
+
74
+ .brand strong {
75
+ font-size: 1.05rem;
76
+ }
77
+
78
+ .brand span,
79
+ .eyebrow,
80
+ .sidebar-card span {
81
+ color: rgba(238, 246, 244, 0.72);
82
+ letter-spacing: 0.08em;
83
+ text-transform: uppercase;
84
+ font-size: 0.75rem;
85
+ }
86
+
87
+ .platforms,
88
+ .provider-list,
89
+ .file-list,
90
+ .probe-list,
91
+ .form {
92
+ display: grid;
93
+ gap: 12px;
94
+ }
95
+
96
+ .platform-tab,
97
+ .sidebar-card,
98
+ .panel,
99
+ .stat,
100
+ .provider-card,
101
+ .file-item,
102
+ .probe-row {
103
+ border: 1px solid rgba(255, 255, 255, 0.08);
104
+ border-radius: 22px;
105
+ }
106
+
107
+ .platform-tab {
108
+ display: grid;
109
+ gap: 4px;
110
+ width: 100%;
111
+ padding: 14px 16px;
112
+ color: inherit;
113
+ text-align: left;
114
+ background: rgba(255, 255, 255, 0.04);
115
+ cursor: pointer;
116
+ transition: transform 0.18s ease, background 0.18s ease, border-color 0.18s ease;
117
+ }
118
+
119
+ .platform-tab strong {
120
+ font-size: 1rem;
121
+ }
122
+
123
+ .platform-tab small,
124
+ .provider-card small,
125
+ .probe-row small,
126
+ .empty,
127
+ .inline-status {
128
+ color: rgba(238, 246, 244, 0.72);
129
+ font-size: 0.8rem;
130
+ }
131
+
132
+ .platform-tab.active {
133
+ background: linear-gradient(135deg, rgba(14, 143, 136, 0.3), rgba(255, 255, 255, 0.08));
134
+ border-color: rgba(117, 232, 224, 0.18);
135
+ }
136
+
137
+ .sidebar-card {
138
+ margin-top: auto;
139
+ display: grid;
140
+ gap: 10px;
141
+ padding: 18px;
142
+ background: rgba(255, 255, 255, 0.05);
143
+ }
144
+
145
+ .sidebar-card strong {
146
+ font-size: 1.08rem;
147
+ line-height: 1.5;
148
+ }
149
+
150
+ .sidebar-card small {
151
+ color: rgba(238, 246, 244, 0.68);
152
+ line-height: 1.7;
153
+ }
154
+
155
+ .workspace {
156
+ padding: 28px;
157
+ }
158
+
159
+ .hero,
160
+ .panel-head,
161
+ .form-actions,
162
+ .hero-actions,
163
+ .provider-card header,
164
+ .file-item header,
165
+ .probe-row header {
166
+ display: flex;
167
+ align-items: center;
168
+ justify-content: space-between;
169
+ gap: 12px;
170
+ }
171
+
172
+ .hero {
173
+ margin-bottom: 22px;
174
+ }
175
+
176
+ .hero h1 {
177
+ margin: 8px 0 10px;
178
+ font-size: 3.1rem;
179
+ letter-spacing: -0.05em;
180
+ }
181
+
182
+ .hero p {
183
+ max-width: 760px;
184
+ margin: 0;
185
+ color: var(--muted);
186
+ font-size: 1rem;
187
+ line-height: 1.7;
188
+ }
189
+
190
+ .hero-actions button,
191
+ .form-actions button,
192
+ .file-item button,
193
+ .provider-card button {
194
+ padding: 12px 16px;
195
+ border: 1px solid var(--line);
196
+ border-radius: 999px;
197
+ background: rgba(255, 255, 255, 0.68);
198
+ cursor: pointer;
199
+ transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
200
+ }
201
+
202
+ .hero-actions button:hover,
203
+ .form-actions button:hover,
204
+ .file-item button:hover,
205
+ .provider-card button:hover,
206
+ .platform-tab:hover {
207
+ transform: translateY(-1px);
208
+ }
209
+
210
+ .solid {
211
+ color: #fffaf2;
212
+ border-color: transparent;
213
+ background: linear-gradient(135deg, var(--ink), var(--accent));
214
+ box-shadow: 0 12px 28px rgba(22, 37, 49, 0.18);
215
+ }
216
+
217
+ .stats {
218
+ display: grid;
219
+ grid-template-columns: repeat(4, minmax(0, 1fr));
220
+ gap: 16px;
221
+ margin-bottom: 18px;
222
+ }
223
+
224
+ .stat,
225
+ .panel {
226
+ border-color: rgba(22, 37, 49, 0.08);
227
+ background: rgba(255, 251, 245, 0.78);
228
+ box-shadow: var(--shadow);
229
+ }
230
+
231
+ .stat {
232
+ display: grid;
233
+ gap: 8px;
234
+ padding: 18px;
235
+ }
236
+
237
+ .stat strong {
238
+ font-size: 1.4rem;
239
+ }
240
+
241
+ .stat span,
242
+ .panel label span,
243
+ .provider-card p,
244
+ .provider-card span,
245
+ .file-item code,
246
+ .probe-row span,
247
+ .empty,
248
+ .inline-status {
249
+ color: var(--muted);
250
+ }
251
+
252
+ .grid {
253
+ display: grid;
254
+ grid-template-columns: 1.15fr 0.85fr;
255
+ gap: 18px;
256
+ }
257
+
258
+ .panel {
259
+ padding: 22px;
260
+ }
261
+
262
+ .panel h2 {
263
+ margin: 6px 0 0;
264
+ font-size: 1.5rem;
265
+ letter-spacing: -0.04em;
266
+ }
267
+
268
+ .provider-list,
269
+ .file-list,
270
+ .probe-list,
271
+ .form {
272
+ margin-top: 18px;
273
+ }
274
+
275
+ .provider-card,
276
+ .file-item,
277
+ .probe-row {
278
+ padding: 16px 18px;
279
+ background: rgba(255, 255, 255, 0.62);
280
+ }
281
+
282
+ .provider-card.active,
283
+ .probe-row.best {
284
+ border-color: rgba(14, 143, 136, 0.24);
285
+ background: linear-gradient(180deg, rgba(236, 251, 249, 0.96), rgba(255, 255, 255, 0.82));
286
+ }
287
+
288
+ .provider-card strong,
289
+ .file-item strong,
290
+ .probe-row strong {
291
+ font-size: 1rem;
292
+ }
293
+
294
+ .provider-card p,
295
+ .provider-card span,
296
+ .provider-card small,
297
+ .file-item code,
298
+ .probe-row span,
299
+ .probe-row small {
300
+ margin: 4px 0 0;
301
+ display: block;
302
+ }
303
+
304
+ .badge {
305
+ padding: 8px 10px;
306
+ border-radius: 999px;
307
+ background: var(--accent-soft);
308
+ color: var(--accent-strong);
309
+ font-size: 0.74rem;
310
+ font-weight: 700;
311
+ }
312
+
313
+ .form label {
314
+ display: grid;
315
+ gap: 8px;
316
+ }
317
+
318
+ .form input,
319
+ .form select {
320
+ width: 100%;
321
+ padding: 14px 16px;
322
+ color: var(--ink);
323
+ border: 1px solid var(--line);
324
+ border-radius: 18px;
325
+ background: rgba(255, 255, 255, 0.78);
326
+ }
327
+
328
+ .inline-status {
329
+ margin-top: 14px;
330
+ padding: 14px 16px;
331
+ border-radius: 16px;
332
+ background: rgba(22, 37, 49, 0.05);
333
+ font-size: 0.9rem;
334
+ }
335
+
336
+ .file-item code {
337
+ font-family: "Cascadia Code", "Consolas", monospace;
338
+ font-size: 0.86rem;
339
+ word-break: break-all;
340
+ }
341
+
342
+ .file-item button,
343
+ .provider-card button {
344
+ align-self: flex-start;
345
+ padding: 10px 14px;
346
+ }
347
+
348
+ .empty {
349
+ padding: 16px;
350
+ font-size: 0.95rem;
351
+ }
352
+
353
+ .toast {
354
+ position: fixed;
355
+ right: 24px;
356
+ bottom: 24px;
357
+ min-width: 240px;
358
+ max-width: 440px;
359
+ padding: 14px 16px;
360
+ border-radius: 16px;
361
+ color: #eef6f4;
362
+ background: rgba(18, 37, 49, 0.94);
363
+ box-shadow: 0 16px 40px rgba(18, 37, 49, 0.24);
364
+ opacity: 0;
365
+ transform: translateY(8px);
366
+ pointer-events: none;
367
+ transition: opacity 0.18s ease, transform 0.18s ease;
368
+ font-size: 0.92rem;
369
+ }
370
+
371
+ .toast.visible {
372
+ opacity: 1;
373
+ transform: translateY(0);
374
+ }
375
+
376
+ @media (max-width: 1220px) {
377
+ .shell {
378
+ grid-template-columns: 1fr;
379
+ }
380
+
381
+ .platforms {
382
+ grid-template-columns: repeat(3, minmax(0, 1fr));
383
+ }
384
+
385
+ .stats,
386
+ .grid {
387
+ grid-template-columns: repeat(2, minmax(0, 1fr));
388
+ }
389
+ }
390
+
391
+ @media (max-width: 860px) {
392
+ .workspace {
393
+ padding: 20px;
394
+ }
395
+
396
+ .hero,
397
+ .panel-head,
398
+ .hero-actions,
399
+ .form-actions {
400
+ flex-direction: column;
401
+ align-items: stretch;
402
+ }
403
+
404
+ .stats,
405
+ .grid,
406
+ .platforms {
407
+ grid-template-columns: 1fr;
408
+ }
409
+
410
+ .hero h1 {
411
+ font-size: 2.3rem;
412
+ }
413
+ }
package/package.json ADDED
@@ -0,0 +1,106 @@
1
+ {
2
+ "name": "march-control-cli",
3
+ "version": "0.1.3",
4
+ "description": "March Control desktop-style manager for Codex, OpenCode, and OpenClaw.",
5
+ "type": "module",
6
+ "main": "desktop/main.js",
7
+ "bin": {
8
+ "march": "core/index.js",
9
+ "mch": "core/index.js"
10
+ },
11
+ "files": [
12
+ "core",
13
+ "desktop",
14
+ "scripts",
15
+ "site",
16
+ "src",
17
+ "src-tauri",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "dev": "vite --configLoader native",
22
+ "build:web": "vite build --configLoader native",
23
+ "preview": "vite preview",
24
+ "typecheck": "tsc --noEmit",
25
+ "start": "node ./core/index.js",
26
+ "check": "node --check ./core/index.js",
27
+ "desktop:dev": "node ./scripts/desktop-dev.mjs",
28
+ "desktop:check": "node --check ./desktop/main.js && node --check ./desktop/preload.js && node --check ./desktop/renderer/app.js && node --check ./core/desktop-service.js",
29
+ "desktop:install-runtime": "node ./node_modules/electron/install.js",
30
+ "desktop:pack": "npm run build:web && electron-builder --win nsis portable",
31
+ "site:dev": "node ./scripts/serve-site.mjs",
32
+ "site:check": "node --check ./scripts/serve-site.mjs",
33
+ "postinstall": "node ./scripts/postinstall.mjs",
34
+ "prepublishOnly": "npm run check && npm run typecheck && npm run build:web",
35
+ "pack:dry": "npm pack"
36
+ },
37
+ "publishConfig": {
38
+ "registry": "https://registry.npmjs.org/"
39
+ },
40
+ "keywords": [
41
+ "codex",
42
+ "openclaw",
43
+ "opencode",
44
+ "relay",
45
+ "config",
46
+ "cli"
47
+ ],
48
+ "author": "March Team",
49
+ "license": "MIT",
50
+ "engines": {
51
+ "node": ">=18.0.0"
52
+ },
53
+ "dependencies": {
54
+ "@iarna/toml": "^2.2.5",
55
+ "@tanstack/react-query": "^5.95.2",
56
+ "chalk": "^5.4.1",
57
+ "clsx": "^2.1.1",
58
+ "commander": "^12.1.0",
59
+ "inquirer": "^12.6.3",
60
+ "lucide-react": "^1.7.0",
61
+ "react": "^19.2.4",
62
+ "react-dom": "^19.2.4"
63
+ },
64
+ "devDependencies": {
65
+ "@types/react": "^19.2.2",
66
+ "@types/react-dom": "^19.2.2",
67
+ "@vitejs/plugin-react": "^5.1.0",
68
+ "@tailwindcss/postcss": "^4.1.15",
69
+ "autoprefixer": "^10.4.21",
70
+ "electron": "^41.1.0",
71
+ "electron-builder": "^26.8.1",
72
+ "postcss": "^8.5.6",
73
+ "tailwindcss": "^4.1.15",
74
+ "typescript": "^5.9.3",
75
+ "vite": "^7.1.12"
76
+ },
77
+ "build": {
78
+ "appId": "cn.gmn.march.desktop",
79
+ "productName": "March-Control",
80
+ "npmRebuild": false,
81
+ "directories": {
82
+ "output": "release"
83
+ },
84
+ "files": [
85
+ "desktop/**/*",
86
+ "dist/**/*",
87
+ "src/**/*",
88
+ "site/**/*",
89
+ "package.json",
90
+ "README.md",
91
+ "core/**/*"
92
+ ],
93
+ "win": {
94
+ "target": [
95
+ "nsis",
96
+ "portable"
97
+ ],
98
+ "artifactName": "March-Control-${version}-${arch}.${ext}"
99
+ },
100
+ "nsis": {
101
+ "oneClick": false,
102
+ "allowToChangeInstallationDirectory": true,
103
+ "perMachine": false
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,90 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import { setTimeout as delay } from "node:timers/promises";
4
+
5
+ const rootDir = process.cwd();
6
+ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
7
+ const electronBin =
8
+ process.platform === "win32"
9
+ ? path.join(rootDir, "node_modules", ".bin", "electron.cmd")
10
+ : path.join(rootDir, "node_modules", ".bin", "electron");
11
+
12
+ function stopProcess(child) {
13
+ if (!child || child.killed) {
14
+ return;
15
+ }
16
+
17
+ try {
18
+ child.kill();
19
+ } catch {
20
+ // ignore
21
+ }
22
+ }
23
+
24
+ async function waitForServer(url, timeoutMs = 60000) {
25
+ const start = Date.now();
26
+
27
+ while (Date.now() - start < timeoutMs) {
28
+ try {
29
+ const response = await fetch(url, { cache: "no-store" });
30
+ if (response.ok || response.status < 500) {
31
+ return;
32
+ }
33
+ } catch {
34
+ // server not ready yet
35
+ }
36
+
37
+ await delay(500);
38
+ }
39
+
40
+ throw new Error(`Vite dev server did not start within ${timeoutMs}ms: ${url}`);
41
+ }
42
+
43
+ const viteProc = spawn(npmCmd, ["run", "dev", "--", "--host", "127.0.0.1", "--port", "3001", "--strictPort"], {
44
+ cwd: rootDir,
45
+ stdio: "inherit",
46
+ env: process.env
47
+ });
48
+
49
+ let electronProc = null;
50
+ let shuttingDown = false;
51
+
52
+ function shutdown(code = 0) {
53
+ if (shuttingDown) {
54
+ return;
55
+ }
56
+ shuttingDown = true;
57
+
58
+ stopProcess(electronProc);
59
+ stopProcess(viteProc);
60
+ process.exit(code);
61
+ }
62
+
63
+ process.on("SIGINT", () => shutdown(0));
64
+ process.on("SIGTERM", () => shutdown(0));
65
+
66
+ viteProc.on("exit", (code) => {
67
+ if (!electronProc && !shuttingDown) {
68
+ shutdown(code ?? 1);
69
+ }
70
+ });
71
+
72
+ try {
73
+ await waitForServer("http://127.0.0.1:3001", 90000);
74
+
75
+ electronProc = spawn(electronBin, ["."], {
76
+ cwd: rootDir,
77
+ stdio: "inherit",
78
+ env: {
79
+ ...process.env,
80
+ MARCH_DESKTOP_DEV_URL: "http://127.0.0.1:3001"
81
+ }
82
+ });
83
+
84
+ electronProc.on("exit", (code) => {
85
+ shutdown(code ?? 0);
86
+ });
87
+ } catch (error) {
88
+ console.error(error instanceof Error ? error.message : error);
89
+ shutdown(1);
90
+ }
@@ -0,0 +1,28 @@
1
+ const lines = [
2
+ "",
3
+ "============================================================",
4
+ " MARCH 安装完成",
5
+ "============================================================",
6
+ "",
7
+ " __ __ _ ____ ____ _ _ ",
8
+ "| \\/ | / \\ | _ \\ / ___| | | |",
9
+ "| |\\/| | / _ \\ | |_) | | | |_| |",
10
+ "| | | |/ ___ \\| _ <| |___| _ |",
11
+ "|_| |_/_/ \\_\\_| \\_\\\\____|_| |_|",
12
+ "",
13
+ "欢迎使用 March 配置管理工具",
14
+ "",
15
+ "快速开始:",
16
+ " march",
17
+ " march setup --preset march -k sk-xxxx",
18
+ "",
19
+ "常用命令:",
20
+ " march --help",
21
+ " march menu",
22
+ " march preset --help",
23
+ ""
24
+ ];
25
+
26
+ for (const line of lines) {
27
+ console.log(line);
28
+ }
@@ -0,0 +1,51 @@
1
+ import fs from "node:fs";
2
+ import http from "node:http";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const siteRoot = path.resolve(__dirname, "..", "site");
9
+ const port = Number(process.env.PORT || 4173);
10
+
11
+ const mimeTypes = {
12
+ ".css": "text/css; charset=utf-8",
13
+ ".html": "text/html; charset=utf-8",
14
+ ".js": "application/javascript; charset=utf-8",
15
+ ".json": "application/json; charset=utf-8",
16
+ ".svg": "image/svg+xml",
17
+ ".txt": "text/plain; charset=utf-8"
18
+ };
19
+
20
+ function resolveFile(requestUrl) {
21
+ const parsed = new URL(requestUrl, `http://localhost:${port}`);
22
+ const requestedPath = decodeURIComponent(parsed.pathname === "/" ? "/index.html" : parsed.pathname);
23
+ const absolutePath = path.resolve(siteRoot, `.${requestedPath}`);
24
+
25
+ if (!absolutePath.startsWith(siteRoot)) {
26
+ return null;
27
+ }
28
+
29
+ return absolutePath;
30
+ }
31
+
32
+ const server = http.createServer((req, res) => {
33
+ const target = resolveFile(req.url || "/");
34
+
35
+ if (!target || !fs.existsSync(target) || fs.statSync(target).isDirectory()) {
36
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
37
+ res.end("Not found");
38
+ return;
39
+ }
40
+
41
+ const ext = path.extname(target).toLowerCase();
42
+ res.writeHead(200, {
43
+ "content-type": mimeTypes[ext] || "application/octet-stream",
44
+ "cache-control": "no-store"
45
+ });
46
+ fs.createReadStream(target).pipe(res);
47
+ });
48
+
49
+ server.listen(port, () => {
50
+ console.log(`March site preview: http://localhost:${port}`);
51
+ });
package/site/app.js ADDED
@@ -0,0 +1,10 @@
1
+ const downloadCards = [...document.querySelectorAll(".download-card")];
2
+
3
+ for (const card of downloadCards) {
4
+ card.addEventListener("mouseenter", () => {
5
+ for (const item of downloadCards) {
6
+ item.classList.remove("active");
7
+ }
8
+ card.classList.add("active");
9
+ });
10
+ }
@@ -0,0 +1,22 @@
1
+ <svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <defs>
3
+ <linearGradient id="bg" x1="18" y1="12" x2="107" y2="118" gradientUnits="userSpaceOnUse">
4
+ <stop stop-color="#102532"/>
5
+ <stop offset="1" stop-color="#0E8F88"/>
6
+ </linearGradient>
7
+ <linearGradient id="stroke" x1="30" y1="32" x2="94" y2="86" gradientUnits="userSpaceOnUse">
8
+ <stop stop-color="#FFF8EE"/>
9
+ <stop offset="1" stop-color="#D7F2F0"/>
10
+ </linearGradient>
11
+ </defs>
12
+
13
+ <rect x="8" y="8" width="112" height="112" rx="32" fill="url(#bg)"/>
14
+ <path
15
+ d="M30 88V42C30 38.6863 32.6863 36 36 36H37.2C39.0512 36 40.7999 36.8538 41.9392 38.3138L63.5 65.9307L85.0608 38.3138C86.2001 36.8538 87.9488 36 89.8 36H91C94.3137 36 97 38.6863 97 42V88"
16
+ stroke="url(#stroke)"
17
+ stroke-width="10"
18
+ stroke-linecap="round"
19
+ stroke-linejoin="round"
20
+ />
21
+ <path d="M85 90H102" stroke="#F2BD67" stroke-width="10" stroke-linecap="round"/>
22
+ </svg>