create-volt 0.45.1 → 0.47.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/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ All notable changes to `create-volt` are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/), and this project adheres to
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.47.0] - 2026-06-29
8
+
9
+ ### Added
10
+ - **Config shows the create-volt version** at the top, always; with `(X available)`
11
+ + an Upgrade button when behind, `(up to date)` otherwise.
12
+ - **Light-mode switcher** in the config wizard (top-right; persists; defaults to dark).
13
+
14
+ ### Fixed
15
+ - **Content-editor textarea** showed a template placeholder instead of the page
16
+ body — now binds the body via the value property.
17
+
18
+ ## [0.46.0] - 2026-06-29
19
+
20
+ ### Added
21
+ - **`update` self-heals `server.js` startup-log encoding.** Older scaffolds had
22
+ byte-corrupted bolt/arrow/ellipsis/dash characters in their console logs;
23
+ `create-volt update` (and the config wizard Upgrade button, which runs it) now
24
+ surgically repairs them — rewriting the brand log lines to plain ASCII and
25
+ swapping the corrupted byte-runs — with no change to your logic.
26
+
7
27
  ## [0.45.1] - 2026-06-29
8
28
 
9
29
  ### Fixed
@@ -600,6 +620,8 @@ All notable changes to `create-volt` are documented here. The format follows
600
620
  watching and full-page hot reload. Supports `--skip-install` and `--force`,
601
621
  and auto-detects npm / pnpm / yarn / bun for the install step.
602
622
 
623
+ [0.47.0]: https://github.com/MIR-2025/volt/releases/tag/v0.47.0
624
+ [0.46.0]: https://github.com/MIR-2025/volt/releases/tag/v0.46.0
603
625
  [0.45.1]: https://github.com/MIR-2025/volt/releases/tag/v0.45.1
604
626
  [0.45.0]: https://github.com/MIR-2025/volt/releases/tag/v0.45.0
605
627
  [0.44.0]: https://github.com/MIR-2025/volt/releases/tag/v0.44.0
package/index.js CHANGED
@@ -270,8 +270,35 @@ if (positionals[0] === "update") {
270
270
  }
271
271
  fs.mkdirSync(path.join(cwd, ".volt"), { recursive: true });
272
272
  fs.writeFileSync(path.join(cwd, ".volt", "version"), pkg.version + "\n");
273
+
274
+ // Self-heal: older scaffolds have byte-corrupted ⚡/→/…/— in server.js's startup
275
+ // logs (a tooling bug). Repair is SURGICAL — it only swaps the corrupted byte-runs
276
+ // and rewrites the brand console.log lines to plain ASCII; no logic is touched.
277
+ const srv = path.join(cwd, "server.js");
278
+ if (fs.existsSync(srv)) {
279
+ let code = fs.readFileSync(srv, "utf8");
280
+ const orig = code;
281
+ code = code.replace(/`[^`]*Volt[^`]*http:\/\/localhost/g, "`Volt at http://localhost");
282
+ code = code.replace(/`\\n[^`]*Volt setup[^`]*\$\{url\}/g, "`\\nVolt setup at ${url}");
283
+ code = code.replace(/`\\n[^`]*Volt Studio[^`]*\$\{url\}/g, "`\\nVolt Studio at ${url}");
284
+ const cc = (...a) => String.fromCharCode(...a);
285
+ const moji = [
286
+ [cc(195, 162, 194, 128, 194, 148), "—"], // em-dash
287
+ [cc(195, 162, 194, 134, 194, 144), "→"], // arrow
288
+ [cc(195, 162, 194, 134, 194, 146), "→"], // arrow
289
+ [cc(195, 162, 194, 128, 194, 166), "…"], // ellipsis
290
+ [cc(195, 131, 194, 160), "—"],
291
+ [cc(226, 128, 148), "—"], // single-encoded em-dash
292
+ ];
293
+ for (const [from, to] of moji) code = code.split(from).join(to);
294
+ if (code !== orig) {
295
+ fs.writeFileSync(srv, code);
296
+ done.push("server.js (repaired startup-log encoding)");
297
+ }
298
+ }
299
+
273
300
  console.log(`\n${green("✔")} Updated to create-volt ${pkg.version}: ${done.join(", ")}.`);
274
- console.log(dim(` Your server.js + content are untouched (re-scaffold to adopt entry-point changes). Restart the app.`));
301
+ console.log(dim(` server.js logic is untouched (re-scaffold to adopt entry-point changes). Restart the app.`));
275
302
  process.exit(0);
276
303
  }
277
304
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-volt",
3
- "version": "0.45.1",
3
+ "version": "0.47.0",
4
4
  "description": "Scaffold a new Volt app — no-build, signals-based UI with Socket.io hot reload.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,15 +7,19 @@
7
7
  <title>Set up your Volt app</title>
8
8
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
9
9
  <style>
10
- body { background: #0f1115; color: #e7e9ee; }
11
- .accent { color: #ffd24a; }
12
- .card-x { background: #161a22; border: 1px solid #232a36; border-radius: 14px; }
13
- .form-control, .form-select { background: #0f1115; color: #e7e9ee; border-color: #232a36; }
14
- .form-control:focus, .form-select:focus { background: #0f1115; color: #e7e9ee; border-color: #ffd24a; box-shadow: none; }
15
- code { color: #ffd24a; }
10
+ :root { --bg: #0f1115; --fg: #e7e9ee; --card: #161a22; --border: #232a36; --accent: #ffd24a; --field: #0f1115; }
11
+ [data-theme="light"] { --bg: #f6f7f9; --fg: #1b1f27; --card: #ffffff; --border: #d8dce3; --accent: #b07d00; --field: #ffffff; }
12
+ body { background: var(--bg); color: var(--fg); }
13
+ .accent { color: var(--accent); }
14
+ .card-x { background: var(--card); border: 1px solid var(--border); border-radius: 14px; }
15
+ .form-control, .form-select { background: var(--field); color: var(--fg); border-color: var(--border); }
16
+ .form-control:focus, .form-select:focus { background: var(--field); color: var(--fg); border-color: var(--accent); box-shadow: none; }
17
+ code { color: var(--accent); }
18
+ #theme-toggle { position: fixed; top: 12px; right: 14px; z-index: 10; }
16
19
  </style>
17
20
  </head>
18
21
  <body>
22
+ <button id="theme-toggle" class="btn btn-sm btn-outline-secondary">Light mode</button>
19
23
  <main class="container py-5" style="max-width: 640px;">
20
24
  <header class="mb-4">
21
25
  <h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Set up your Volt app</span></h1>
@@ -23,6 +27,19 @@
23
27
  </header>
24
28
  <div id="app"></div>
25
29
  </main>
30
+ <script>
31
+ (function () {
32
+ const root = document.documentElement;
33
+ const btn = document.getElementById("theme-toggle");
34
+ const apply = (t) => { root.setAttribute("data-theme", t); btn.textContent = t === "light" ? "Dark mode" : "Light mode"; };
35
+ apply(localStorage.getItem("volt-setup-theme") || "dark");
36
+ btn.addEventListener("click", () => {
37
+ const next = root.getAttribute("data-theme") === "light" ? "dark" : "light";
38
+ apply(next);
39
+ localStorage.setItem("volt-setup-theme", next);
40
+ });
41
+ })();
42
+ </script>
26
43
  <script type="module" src="/setup.js"></script>
27
44
  </body>
28
45
  </html>
@@ -342,7 +342,7 @@ const editorPanel = () => {
342
342
  const e = editing(); // inputs are uncontrolled (read on Save) so typing never re-renders
343
343
  return html`<div class="p-3 mb-2" style="border:1px solid #232a36;border-radius:10px">
344
344
  <div class="d-flex gap-2 mb-2"><input id="mg-slug" class="form-control" placeholder="slug" value=${e.slug} readonly=${!e.isNew} /><span class="align-self-center small text-muted">${e.type === "post" ? "posts/" : "pages/"}</span></div>
345
- <textarea id="mg-body" class="form-control" rows="16" style="font-family:ui-monospace,monospace;font-size:13px">${e.body}</textarea>
345
+ <textarea id="mg-body" class="form-control" rows="16" style="font-family:ui-monospace,monospace;font-size:13px" value=${e.body}></textarea>
346
346
  <div class="mt-2 d-flex gap-2"><button class="btn btn-primary btn-sm" onclick=${saveItem}>Save</button><button class="btn btn-outline-secondary btn-sm" onclick=${() => editing(null)}>Cancel</button></div>
347
347
  </div>`;
348
348
  };
@@ -353,7 +353,11 @@ const manageView = () =>
353
353
  </div>`;
354
354
 
355
355
  const configView = () =>
356
- html`${() => (upgrade()?.available ? html`<div class="card-x p-3 mb-3 d-flex justify-content-between align-items-center"><span class="small">⬆ <strong>create-volt ${upgrade().latest}</strong> is available — you have ${upgrade().current}.</span><button class="btn btn-sm btn-primary" onclick=${doUpgrade}>Upgrade</button></div>` : "")}
356
+ html`${() => {
357
+ const u = upgrade();
358
+ if (!u || !u.current || u.current === "?") return "";
359
+ return html`<div class="card-x p-3 mb-3 d-flex justify-content-between align-items-center"><span class="small">create-volt <strong>${u.current}</strong> ${u.available ? html`<span class="accent">(${u.latest} available)</span>` : u.latest && u.latest !== "?" ? html`<span class="text-muted">(up to date)</span>` : ""}</span>${u.available ? html`<button class="btn btn-sm btn-primary" onclick=${doUpgrade}>Upgrade</button>` : ""}</div>`;
360
+ }}
357
361
  ${() => (aiCredits()?.ok ? html`<div class="card-x p-3 mb-3"><div class="d-flex justify-content-between align-items-center mb-2"><strong>AI credits</strong><span class="small text-muted">${aiCredits().tier}${typeof aiCredits().creditBalanceUsd === "number" ? ` · $${aiCredits().creditBalanceUsd.toFixed(2)} left` : ""}</span></div>${aiCredits().payments ? html`<div class="d-flex gap-2 align-items-center"><span class="small text-muted me-1">Top up:</span>${[10, 25, 50].map((a) => html`<button class="btn btn-sm btn-outline-primary" onclick=${() => buyCredits(a)}>$${a}</button>`)}</div>` : html`<div class="small text-muted">Pay-as-you-go isn't enabled on the gateway yet — using the free tier.</div>`}</div>` : "")}
358
362
  ${available.length ? html`<div class="card-x p-4 mb-3"><h2 class="h6 mb-3">Features</h2>${available.map(addonRow)}<p class="small text-muted mb-0">Enabling a feature wires its backend automatically. Frontend UI (login form, chat) is yours to build — or start from <code>--template guestbook</code>.</p></div>` : ""}
359
363
  <div class="card-x p-4 mb-3">
@@ -7,15 +7,19 @@
7
7
  <title>Set up your Volt app</title>
8
8
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
9
9
  <style>
10
- body { background: #0f1115; color: #e7e9ee; }
11
- .accent { color: #ffd24a; }
12
- .card-x { background: #161a22; border: 1px solid #232a36; border-radius: 14px; }
13
- .form-control, .form-select { background: #0f1115; color: #e7e9ee; border-color: #232a36; }
14
- .form-control:focus, .form-select:focus { background: #0f1115; color: #e7e9ee; border-color: #ffd24a; box-shadow: none; }
15
- code { color: #ffd24a; }
10
+ :root { --bg: #0f1115; --fg: #e7e9ee; --card: #161a22; --border: #232a36; --accent: #ffd24a; --field: #0f1115; }
11
+ [data-theme="light"] { --bg: #f6f7f9; --fg: #1b1f27; --card: #ffffff; --border: #d8dce3; --accent: #b07d00; --field: #ffffff; }
12
+ body { background: var(--bg); color: var(--fg); }
13
+ .accent { color: var(--accent); }
14
+ .card-x { background: var(--card); border: 1px solid var(--border); border-radius: 14px; }
15
+ .form-control, .form-select { background: var(--field); color: var(--fg); border-color: var(--border); }
16
+ .form-control:focus, .form-select:focus { background: var(--field); color: var(--fg); border-color: var(--accent); box-shadow: none; }
17
+ code { color: var(--accent); }
18
+ #theme-toggle { position: fixed; top: 12px; right: 14px; z-index: 10; }
16
19
  </style>
17
20
  </head>
18
21
  <body>
22
+ <button id="theme-toggle" class="btn btn-sm btn-outline-secondary">Light mode</button>
19
23
  <main class="container py-5" style="max-width: 640px;">
20
24
  <header class="mb-4">
21
25
  <h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Set up your Volt app</span></h1>
@@ -23,6 +27,19 @@
23
27
  </header>
24
28
  <div id="app"></div>
25
29
  </main>
30
+ <script>
31
+ (function () {
32
+ const root = document.documentElement;
33
+ const btn = document.getElementById("theme-toggle");
34
+ const apply = (t) => { root.setAttribute("data-theme", t); btn.textContent = t === "light" ? "Dark mode" : "Light mode"; };
35
+ apply(localStorage.getItem("volt-setup-theme") || "dark");
36
+ btn.addEventListener("click", () => {
37
+ const next = root.getAttribute("data-theme") === "light" ? "dark" : "light";
38
+ apply(next);
39
+ localStorage.setItem("volt-setup-theme", next);
40
+ });
41
+ })();
42
+ </script>
26
43
  <script type="module" src="/setup.js"></script>
27
44
  </body>
28
45
  </html>
@@ -342,7 +342,7 @@ const editorPanel = () => {
342
342
  const e = editing(); // inputs are uncontrolled (read on Save) so typing never re-renders
343
343
  return html`<div class="p-3 mb-2" style="border:1px solid #232a36;border-radius:10px">
344
344
  <div class="d-flex gap-2 mb-2"><input id="mg-slug" class="form-control" placeholder="slug" value=${e.slug} readonly=${!e.isNew} /><span class="align-self-center small text-muted">${e.type === "post" ? "posts/" : "pages/"}</span></div>
345
- <textarea id="mg-body" class="form-control" rows="16" style="font-family:ui-monospace,monospace;font-size:13px">${e.body}</textarea>
345
+ <textarea id="mg-body" class="form-control" rows="16" style="font-family:ui-monospace,monospace;font-size:13px" value=${e.body}></textarea>
346
346
  <div class="mt-2 d-flex gap-2"><button class="btn btn-primary btn-sm" onclick=${saveItem}>Save</button><button class="btn btn-outline-secondary btn-sm" onclick=${() => editing(null)}>Cancel</button></div>
347
347
  </div>`;
348
348
  };
@@ -353,7 +353,11 @@ const manageView = () =>
353
353
  </div>`;
354
354
 
355
355
  const configView = () =>
356
- html`${() => (upgrade()?.available ? html`<div class="card-x p-3 mb-3 d-flex justify-content-between align-items-center"><span class="small">⬆ <strong>create-volt ${upgrade().latest}</strong> is available — you have ${upgrade().current}.</span><button class="btn btn-sm btn-primary" onclick=${doUpgrade}>Upgrade</button></div>` : "")}
356
+ html`${() => {
357
+ const u = upgrade();
358
+ if (!u || !u.current || u.current === "?") return "";
359
+ return html`<div class="card-x p-3 mb-3 d-flex justify-content-between align-items-center"><span class="small">create-volt <strong>${u.current}</strong> ${u.available ? html`<span class="accent">(${u.latest} available)</span>` : u.latest && u.latest !== "?" ? html`<span class="text-muted">(up to date)</span>` : ""}</span>${u.available ? html`<button class="btn btn-sm btn-primary" onclick=${doUpgrade}>Upgrade</button>` : ""}</div>`;
360
+ }}
357
361
  ${() => (aiCredits()?.ok ? html`<div class="card-x p-3 mb-3"><div class="d-flex justify-content-between align-items-center mb-2"><strong>AI credits</strong><span class="small text-muted">${aiCredits().tier}${typeof aiCredits().creditBalanceUsd === "number" ? ` · $${aiCredits().creditBalanceUsd.toFixed(2)} left` : ""}</span></div>${aiCredits().payments ? html`<div class="d-flex gap-2 align-items-center"><span class="small text-muted me-1">Top up:</span>${[10, 25, 50].map((a) => html`<button class="btn btn-sm btn-outline-primary" onclick=${() => buyCredits(a)}>$${a}</button>`)}</div>` : html`<div class="small text-muted">Pay-as-you-go isn't enabled on the gateway yet — using the free tier.</div>`}</div>` : "")}
358
362
  ${available.length ? html`<div class="card-x p-4 mb-3"><h2 class="h6 mb-3">Features</h2>${available.map(addonRow)}<p class="small text-muted mb-0">Enabling a feature wires its backend automatically. Frontend UI (login form, chat) is yours to build — or start from <code>--template guestbook</code>.</p></div>` : ""}
359
363
  <div class="card-x p-4 mb-3">
@@ -7,15 +7,19 @@
7
7
  <title>Set up your Volt app</title>
8
8
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
9
9
  <style>
10
- body { background: #0f1115; color: #e7e9ee; }
11
- .accent { color: #ffd24a; }
12
- .card-x { background: #161a22; border: 1px solid #232a36; border-radius: 14px; }
13
- .form-control, .form-select { background: #0f1115; color: #e7e9ee; border-color: #232a36; }
14
- .form-control:focus, .form-select:focus { background: #0f1115; color: #e7e9ee; border-color: #ffd24a; box-shadow: none; }
15
- code { color: #ffd24a; }
10
+ :root { --bg: #0f1115; --fg: #e7e9ee; --card: #161a22; --border: #232a36; --accent: #ffd24a; --field: #0f1115; }
11
+ [data-theme="light"] { --bg: #f6f7f9; --fg: #1b1f27; --card: #ffffff; --border: #d8dce3; --accent: #b07d00; --field: #ffffff; }
12
+ body { background: var(--bg); color: var(--fg); }
13
+ .accent { color: var(--accent); }
14
+ .card-x { background: var(--card); border: 1px solid var(--border); border-radius: 14px; }
15
+ .form-control, .form-select { background: var(--field); color: var(--fg); border-color: var(--border); }
16
+ .form-control:focus, .form-select:focus { background: var(--field); color: var(--fg); border-color: var(--accent); box-shadow: none; }
17
+ code { color: var(--accent); }
18
+ #theme-toggle { position: fixed; top: 12px; right: 14px; z-index: 10; }
16
19
  </style>
17
20
  </head>
18
21
  <body>
22
+ <button id="theme-toggle" class="btn btn-sm btn-outline-secondary">Light mode</button>
19
23
  <main class="container py-5" style="max-width: 640px;">
20
24
  <header class="mb-4">
21
25
  <h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Set up your Volt app</span></h1>
@@ -23,6 +27,19 @@
23
27
  </header>
24
28
  <div id="app"></div>
25
29
  </main>
30
+ <script>
31
+ (function () {
32
+ const root = document.documentElement;
33
+ const btn = document.getElementById("theme-toggle");
34
+ const apply = (t) => { root.setAttribute("data-theme", t); btn.textContent = t === "light" ? "Dark mode" : "Light mode"; };
35
+ apply(localStorage.getItem("volt-setup-theme") || "dark");
36
+ btn.addEventListener("click", () => {
37
+ const next = root.getAttribute("data-theme") === "light" ? "dark" : "light";
38
+ apply(next);
39
+ localStorage.setItem("volt-setup-theme", next);
40
+ });
41
+ })();
42
+ </script>
26
43
  <script type="module" src="/setup.js"></script>
27
44
  </body>
28
45
  </html>
@@ -342,7 +342,7 @@ const editorPanel = () => {
342
342
  const e = editing(); // inputs are uncontrolled (read on Save) so typing never re-renders
343
343
  return html`<div class="p-3 mb-2" style="border:1px solid #232a36;border-radius:10px">
344
344
  <div class="d-flex gap-2 mb-2"><input id="mg-slug" class="form-control" placeholder="slug" value=${e.slug} readonly=${!e.isNew} /><span class="align-self-center small text-muted">${e.type === "post" ? "posts/" : "pages/"}</span></div>
345
- <textarea id="mg-body" class="form-control" rows="16" style="font-family:ui-monospace,monospace;font-size:13px">${e.body}</textarea>
345
+ <textarea id="mg-body" class="form-control" rows="16" style="font-family:ui-monospace,monospace;font-size:13px" value=${e.body}></textarea>
346
346
  <div class="mt-2 d-flex gap-2"><button class="btn btn-primary btn-sm" onclick=${saveItem}>Save</button><button class="btn btn-outline-secondary btn-sm" onclick=${() => editing(null)}>Cancel</button></div>
347
347
  </div>`;
348
348
  };
@@ -353,7 +353,11 @@ const manageView = () =>
353
353
  </div>`;
354
354
 
355
355
  const configView = () =>
356
- html`${() => (upgrade()?.available ? html`<div class="card-x p-3 mb-3 d-flex justify-content-between align-items-center"><span class="small">⬆ <strong>create-volt ${upgrade().latest}</strong> is available — you have ${upgrade().current}.</span><button class="btn btn-sm btn-primary" onclick=${doUpgrade}>Upgrade</button></div>` : "")}
356
+ html`${() => {
357
+ const u = upgrade();
358
+ if (!u || !u.current || u.current === "?") return "";
359
+ return html`<div class="card-x p-3 mb-3 d-flex justify-content-between align-items-center"><span class="small">create-volt <strong>${u.current}</strong> ${u.available ? html`<span class="accent">(${u.latest} available)</span>` : u.latest && u.latest !== "?" ? html`<span class="text-muted">(up to date)</span>` : ""}</span>${u.available ? html`<button class="btn btn-sm btn-primary" onclick=${doUpgrade}>Upgrade</button>` : ""}</div>`;
360
+ }}
357
361
  ${() => (aiCredits()?.ok ? html`<div class="card-x p-3 mb-3"><div class="d-flex justify-content-between align-items-center mb-2"><strong>AI credits</strong><span class="small text-muted">${aiCredits().tier}${typeof aiCredits().creditBalanceUsd === "number" ? ` · $${aiCredits().creditBalanceUsd.toFixed(2)} left` : ""}</span></div>${aiCredits().payments ? html`<div class="d-flex gap-2 align-items-center"><span class="small text-muted me-1">Top up:</span>${[10, 25, 50].map((a) => html`<button class="btn btn-sm btn-outline-primary" onclick=${() => buyCredits(a)}>$${a}</button>`)}</div>` : html`<div class="small text-muted">Pay-as-you-go isn't enabled on the gateway yet — using the free tier.</div>`}</div>` : "")}
358
362
  ${available.length ? html`<div class="card-x p-4 mb-3"><h2 class="h6 mb-3">Features</h2>${available.map(addonRow)}<p class="small text-muted mb-0">Enabling a feature wires its backend automatically. Frontend UI (login form, chat) is yours to build — or start from <code>--template guestbook</code>.</p></div>` : ""}
359
363
  <div class="card-x p-4 mb-3">
@@ -7,15 +7,19 @@
7
7
  <title>Set up your Volt app</title>
8
8
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
9
9
  <style>
10
- body { background: #0f1115; color: #e7e9ee; }
11
- .accent { color: #ffd24a; }
12
- .card-x { background: #161a22; border: 1px solid #232a36; border-radius: 14px; }
13
- .form-control, .form-select { background: #0f1115; color: #e7e9ee; border-color: #232a36; }
14
- .form-control:focus, .form-select:focus { background: #0f1115; color: #e7e9ee; border-color: #ffd24a; box-shadow: none; }
15
- code { color: #ffd24a; }
10
+ :root { --bg: #0f1115; --fg: #e7e9ee; --card: #161a22; --border: #232a36; --accent: #ffd24a; --field: #0f1115; }
11
+ [data-theme="light"] { --bg: #f6f7f9; --fg: #1b1f27; --card: #ffffff; --border: #d8dce3; --accent: #b07d00; --field: #ffffff; }
12
+ body { background: var(--bg); color: var(--fg); }
13
+ .accent { color: var(--accent); }
14
+ .card-x { background: var(--card); border: 1px solid var(--border); border-radius: 14px; }
15
+ .form-control, .form-select { background: var(--field); color: var(--fg); border-color: var(--border); }
16
+ .form-control:focus, .form-select:focus { background: var(--field); color: var(--fg); border-color: var(--accent); box-shadow: none; }
17
+ code { color: var(--accent); }
18
+ #theme-toggle { position: fixed; top: 12px; right: 14px; z-index: 10; }
16
19
  </style>
17
20
  </head>
18
21
  <body>
22
+ <button id="theme-toggle" class="btn btn-sm btn-outline-secondary">Light mode</button>
19
23
  <main class="container py-5" style="max-width: 640px;">
20
24
  <header class="mb-4">
21
25
  <h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Set up your Volt app</span></h1>
@@ -23,6 +27,19 @@
23
27
  </header>
24
28
  <div id="app"></div>
25
29
  </main>
30
+ <script>
31
+ (function () {
32
+ const root = document.documentElement;
33
+ const btn = document.getElementById("theme-toggle");
34
+ const apply = (t) => { root.setAttribute("data-theme", t); btn.textContent = t === "light" ? "Dark mode" : "Light mode"; };
35
+ apply(localStorage.getItem("volt-setup-theme") || "dark");
36
+ btn.addEventListener("click", () => {
37
+ const next = root.getAttribute("data-theme") === "light" ? "dark" : "light";
38
+ apply(next);
39
+ localStorage.setItem("volt-setup-theme", next);
40
+ });
41
+ })();
42
+ </script>
26
43
  <script type="module" src="/setup.js"></script>
27
44
  </body>
28
45
  </html>
@@ -342,7 +342,7 @@ const editorPanel = () => {
342
342
  const e = editing(); // inputs are uncontrolled (read on Save) so typing never re-renders
343
343
  return html`<div class="p-3 mb-2" style="border:1px solid #232a36;border-radius:10px">
344
344
  <div class="d-flex gap-2 mb-2"><input id="mg-slug" class="form-control" placeholder="slug" value=${e.slug} readonly=${!e.isNew} /><span class="align-self-center small text-muted">${e.type === "post" ? "posts/" : "pages/"}</span></div>
345
- <textarea id="mg-body" class="form-control" rows="16" style="font-family:ui-monospace,monospace;font-size:13px">${e.body}</textarea>
345
+ <textarea id="mg-body" class="form-control" rows="16" style="font-family:ui-monospace,monospace;font-size:13px" value=${e.body}></textarea>
346
346
  <div class="mt-2 d-flex gap-2"><button class="btn btn-primary btn-sm" onclick=${saveItem}>Save</button><button class="btn btn-outline-secondary btn-sm" onclick=${() => editing(null)}>Cancel</button></div>
347
347
  </div>`;
348
348
  };
@@ -353,7 +353,11 @@ const manageView = () =>
353
353
  </div>`;
354
354
 
355
355
  const configView = () =>
356
- html`${() => (upgrade()?.available ? html`<div class="card-x p-3 mb-3 d-flex justify-content-between align-items-center"><span class="small">⬆ <strong>create-volt ${upgrade().latest}</strong> is available — you have ${upgrade().current}.</span><button class="btn btn-sm btn-primary" onclick=${doUpgrade}>Upgrade</button></div>` : "")}
356
+ html`${() => {
357
+ const u = upgrade();
358
+ if (!u || !u.current || u.current === "?") return "";
359
+ return html`<div class="card-x p-3 mb-3 d-flex justify-content-between align-items-center"><span class="small">create-volt <strong>${u.current}</strong> ${u.available ? html`<span class="accent">(${u.latest} available)</span>` : u.latest && u.latest !== "?" ? html`<span class="text-muted">(up to date)</span>` : ""}</span>${u.available ? html`<button class="btn btn-sm btn-primary" onclick=${doUpgrade}>Upgrade</button>` : ""}</div>`;
360
+ }}
357
361
  ${() => (aiCredits()?.ok ? html`<div class="card-x p-3 mb-3"><div class="d-flex justify-content-between align-items-center mb-2"><strong>AI credits</strong><span class="small text-muted">${aiCredits().tier}${typeof aiCredits().creditBalanceUsd === "number" ? ` · $${aiCredits().creditBalanceUsd.toFixed(2)} left` : ""}</span></div>${aiCredits().payments ? html`<div class="d-flex gap-2 align-items-center"><span class="small text-muted me-1">Top up:</span>${[10, 25, 50].map((a) => html`<button class="btn btn-sm btn-outline-primary" onclick=${() => buyCredits(a)}>$${a}</button>`)}</div>` : html`<div class="small text-muted">Pay-as-you-go isn't enabled on the gateway yet — using the free tier.</div>`}</div>` : "")}
358
362
  ${available.length ? html`<div class="card-x p-4 mb-3"><h2 class="h6 mb-3">Features</h2>${available.map(addonRow)}<p class="small text-muted mb-0">Enabling a feature wires its backend automatically. Frontend UI (login form, chat) is yours to build — or start from <code>--template guestbook</code>.</p></div>` : ""}
359
363
  <div class="card-x p-4 mb-3">