create-volt 0.31.0 → 0.33.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 +24 -0
- package/addons/posts/files/lib/posts.js +4 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/templates/blog/Dockerfile +20 -0
- package/templates/blog/Procfile +1 -0
- package/templates/blog/README.md +38 -0
- package/templates/blog/dockerignore +6 -0
- package/templates/blog/env +3 -0
- package/templates/blog/fly.toml +15 -0
- package/templates/blog/gitignore +5 -0
- package/templates/blog/package.json +17 -0
- package/templates/blog/pages/_theme.js +35 -0
- package/templates/blog/pages/about.md +8 -0
- package/templates/blog/posts/2026-06-10-themes.md +11 -0
- package/templates/blog/posts/2026-06-20-markdown-and-seo.md +14 -0
- package/templates/blog/posts/2026-06-28-hello-volt.md +17 -0
- package/templates/blog/public/favicon.webp +0 -0
- package/templates/blog/public/logo.webp +0 -0
- package/templates/blog/public/volt-ssr.js +63 -0
- package/templates/blog/public/volt.js +273 -0
- package/templates/blog/render.yaml +15 -0
- package/templates/blog/server.js +464 -0
- package/templates/blog/setup/index.html +28 -0
- package/templates/blog/setup/setup.js +200 -0
- package/templates/blog/setup/studio.html +29 -0
- package/templates/blog/views/index.html +25 -0
- package/templates/default/public/favicon.webp +0 -0
- package/templates/default/public/logo.webp +0 -0
- package/templates/default/server.js +4 -0
- package/templates/default/setup/index.html +2 -1
- package/templates/default/setup/studio.html +2 -1
- package/templates/default/views/index.html +4 -5
- package/templates/docs/Dockerfile +20 -0
- package/templates/docs/Procfile +1 -0
- package/templates/docs/README.md +24 -0
- package/templates/docs/dockerignore +6 -0
- package/templates/docs/env +2 -0
- package/templates/docs/fly.toml +15 -0
- package/templates/docs/gitignore +5 -0
- package/templates/docs/package.json +17 -0
- package/templates/docs/pages/_theme.js +32 -0
- package/templates/docs/pages/configuration.md +13 -0
- package/templates/docs/pages/deployment.md +9 -0
- package/templates/docs/pages/getting-started.md +14 -0
- package/templates/docs/public/favicon.webp +0 -0
- package/templates/docs/public/logo.webp +0 -0
- package/templates/docs/public/volt-ssr.js +63 -0
- package/templates/docs/public/volt.js +273 -0
- package/templates/docs/render.yaml +15 -0
- package/templates/docs/server.js +464 -0
- package/templates/docs/setup/index.html +28 -0
- package/templates/docs/setup/setup.js +200 -0
- package/templates/docs/setup/studio.html +29 -0
- package/templates/docs/views/index.html +15 -0
- package/templates/starter/public/app.js +1 -1
- package/templates/starter/public/favicon.webp +0 -0
- package/templates/starter/public/logo.webp +0 -0
- package/templates/starter/server.js +4 -0
- package/templates/starter/setup/index.html +2 -1
- package/templates/starter/setup/studio.html +2 -1
- package/templates/starter/views/index.html +2 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// setup.js — first-run / --edit wizard, built with Volt. Tick add-ons + fill
|
|
2
|
+
// settings → writes .env (a VOLT_ADDONS list + settings), adds any needed
|
|
3
|
+
// packages, installs, and starts the app. Add-on code is bundled; enabling is
|
|
4
|
+
// just config.
|
|
5
|
+
import { signal, computed, html, mount } from "/volt.js";
|
|
6
|
+
|
|
7
|
+
const { available, current, defaultPort } = await (await fetch("/setup/state")).json();
|
|
8
|
+
const depsOf = Object.fromEntries(available.map((a) => [a.name, a.dependsOn || []]));
|
|
9
|
+
const order = available.map((a) => a.name);
|
|
10
|
+
const enabledNow = new Set(String(current.VOLT_ADDONS || "").split(",").map((s) => s.trim()).filter(Boolean));
|
|
11
|
+
|
|
12
|
+
const state = signal({
|
|
13
|
+
addons: Object.fromEntries(available.map((a) => [a.name, enabledNow.has(a.name)])),
|
|
14
|
+
dbDriver: current.DB_DRIVER || "memory",
|
|
15
|
+
mongoUri: current.MONGODB_URI || "",
|
|
16
|
+
mongoDb: current.MONGODB_DATABASE || "",
|
|
17
|
+
dbUrl: current.DATABASE_URL || "",
|
|
18
|
+
smtpUrl: current.SMTP_URL || "",
|
|
19
|
+
mailFrom: current.MAIL_FROM || "",
|
|
20
|
+
mediaDriver: current.MEDIA_DRIVER || "local",
|
|
21
|
+
s3Endpoint: current.S3_ENDPOINT || "",
|
|
22
|
+
s3Region: current.S3_REGION || "",
|
|
23
|
+
s3Bucket: current.S3_BUCKET || "",
|
|
24
|
+
s3Key: current.S3_KEY || "",
|
|
25
|
+
s3Secret: current.S3_SECRET || "",
|
|
26
|
+
s3PublicBase: current.S3_PUBLIC_BASE || "",
|
|
27
|
+
port: current.PORT || String(defaultPort),
|
|
28
|
+
});
|
|
29
|
+
const set = (patch) => state({ ...state(), ...patch });
|
|
30
|
+
const toggle = (n) => state({ ...state(), addons: { ...state().addons, [n]: !state().addons[n] } });
|
|
31
|
+
const status = signal("");
|
|
32
|
+
|
|
33
|
+
// selected add-ons, dependencies expanded, in display order
|
|
34
|
+
function effective(s) {
|
|
35
|
+
const want = new Set();
|
|
36
|
+
const visit = (n) => {
|
|
37
|
+
if (want.has(n)) return;
|
|
38
|
+
want.add(n);
|
|
39
|
+
(depsOf[n] || []).forEach(visit);
|
|
40
|
+
};
|
|
41
|
+
for (const n of order) if (s.addons[n]) visit(n);
|
|
42
|
+
return order.filter((n) => want.has(n));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const clean = (v) => String(v).replace(/[\r\n]/g, "").trim(); // one value per line; no injection
|
|
46
|
+
function genEnv(s) {
|
|
47
|
+
const eff = effective(s);
|
|
48
|
+
const out = [`VOLT_ADDONS=${eff.join(",")}`, `PORT=${clean(s.port)}`];
|
|
49
|
+
if (eff.includes("db")) {
|
|
50
|
+
out.push(`DB_DRIVER=${clean(s.dbDriver)}`);
|
|
51
|
+
if (s.dbDriver === "mongodb") {
|
|
52
|
+
out.push(`MONGODB_URI=${clean(s.mongoUri)}`);
|
|
53
|
+
if (s.mongoDb) out.push(`MONGODB_DATABASE=${clean(s.mongoDb)}`);
|
|
54
|
+
} else if (s.dbDriver === "mysql" || s.dbDriver === "postgres") {
|
|
55
|
+
out.push(`DATABASE_URL=${clean(s.dbUrl)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (eff.includes("mailer")) {
|
|
59
|
+
if (s.smtpUrl) out.push(`SMTP_URL=${clean(s.smtpUrl)}`);
|
|
60
|
+
else out.push("# SMTP_URL= # unset → emails print to the console");
|
|
61
|
+
if (s.mailFrom) out.push(`MAIL_FROM=${clean(s.mailFrom)}`);
|
|
62
|
+
}
|
|
63
|
+
if (eff.includes("media")) {
|
|
64
|
+
out.push(`MEDIA_DRIVER=${clean(s.mediaDriver)}`);
|
|
65
|
+
if (s.mediaDriver === "s3") {
|
|
66
|
+
out.push(`S3_ENDPOINT=${clean(s.s3Endpoint)}`);
|
|
67
|
+
out.push(`S3_REGION=${clean(s.s3Region)}`);
|
|
68
|
+
out.push(`S3_BUCKET=${clean(s.s3Bucket)}`);
|
|
69
|
+
out.push(`S3_KEY=${clean(s.s3Key)}`);
|
|
70
|
+
out.push(`S3_SECRET=${clean(s.s3Secret)}`);
|
|
71
|
+
if (s.s3PublicBase) out.push(`S3_PUBLIC_BASE=${clean(s.s3PublicBase)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out.join("\n") + "\n";
|
|
75
|
+
}
|
|
76
|
+
const env = computed(() => genEnv(state()));
|
|
77
|
+
const eff = computed(() => effective(state()));
|
|
78
|
+
// Memoized, primitive-valued derivations: a conditional section keyed on these
|
|
79
|
+
// only re-renders when the *discriminant* changes — not on every keystroke in a
|
|
80
|
+
// field it contains (which would recreate the input and drop focus).
|
|
81
|
+
const dbDriver = computed(() => state().dbDriver);
|
|
82
|
+
const mediaDriver = computed(() => state().mediaDriver);
|
|
83
|
+
const hasDb = computed(() => eff().includes("db"));
|
|
84
|
+
const hasMailer = computed(() => eff().includes("mailer"));
|
|
85
|
+
const hasMedia = computed(() => eff().includes("media"));
|
|
86
|
+
|
|
87
|
+
async function testDb() {
|
|
88
|
+
const s = state();
|
|
89
|
+
const e = { DB_DRIVER: s.dbDriver };
|
|
90
|
+
if (s.dbDriver === "mongodb") {
|
|
91
|
+
e.MONGODB_URI = s.mongoUri;
|
|
92
|
+
e.MONGODB_DATABASE = s.mongoDb;
|
|
93
|
+
} else if (s.dbDriver === "mysql" || s.dbDriver === "postgres") {
|
|
94
|
+
e.DATABASE_URL = s.dbUrl;
|
|
95
|
+
}
|
|
96
|
+
status("Testing connection…");
|
|
97
|
+
try {
|
|
98
|
+
const r = await (await fetch("/setup/test-db", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ env: e }) })).json();
|
|
99
|
+
status(r.ok ? `✓ Connected (${r.driver}).` : `✗ ${r.error}`);
|
|
100
|
+
} catch {
|
|
101
|
+
status("Network error testing connection.");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function apply() {
|
|
106
|
+
status("Saving…");
|
|
107
|
+
let d;
|
|
108
|
+
try {
|
|
109
|
+
d = await (await fetch("/setup/apply", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ addons: eff(), env: env() }) })).json();
|
|
110
|
+
} catch {
|
|
111
|
+
return status("Network error.");
|
|
112
|
+
}
|
|
113
|
+
if (!d.ok) return status("Error: " + d.error);
|
|
114
|
+
status(d.installing?.length ? `Installing ${d.installing.join(", ")}, then starting…` : "Starting the app…");
|
|
115
|
+
const target = `http://localhost:${d.port}/`;
|
|
116
|
+
const tries = d.installing?.length ? 90 : 20; // npm install can take a while
|
|
117
|
+
const go = async (n) => {
|
|
118
|
+
try {
|
|
119
|
+
await fetch(target, { mode: "no-cors" });
|
|
120
|
+
location.href = target;
|
|
121
|
+
} catch {
|
|
122
|
+
if (n > 0) setTimeout(() => go(n - 1), 500);
|
|
123
|
+
else location.href = target;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
setTimeout(() => go(tries), 600);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- views ---
|
|
130
|
+
const field = (label, key, placeholder = "") =>
|
|
131
|
+
html`<div class="mb-2">
|
|
132
|
+
<label class="form-label small mb-1">${label}</label>
|
|
133
|
+
<input class="form-control" placeholder=${placeholder} value=${() => state()[key]} oninput=${(e) => set({ [key]: e.target.value })} />
|
|
134
|
+
</div>`;
|
|
135
|
+
|
|
136
|
+
const addonRow = (a) =>
|
|
137
|
+
html`<div class="form-check mb-2">
|
|
138
|
+
<input class="form-check-input" type="checkbox" id=${"x-" + a.name} checked=${() => state().addons[a.name]} onchange=${() => toggle(a.name)} />
|
|
139
|
+
<label class="form-check-label" for=${"x-" + a.name}>
|
|
140
|
+
<span class="accent">${a.name}</span>${a.dependsOn?.length ? html` <span class="text-muted small">(needs ${a.dependsOn.join(", ")})</span>` : ""}
|
|
141
|
+
<div class="small text-muted">${a.description}</div>
|
|
142
|
+
</label>
|
|
143
|
+
</div>`;
|
|
144
|
+
|
|
145
|
+
const dbSettings = () =>
|
|
146
|
+
html`<div class="mb-2">
|
|
147
|
+
<label class="form-label small mb-1">Database (DB_DRIVER)</label>
|
|
148
|
+
<select class="form-select" value=${() => dbDriver()} onchange=${(e) => set({ dbDriver: e.target.value })}>
|
|
149
|
+
<option value="memory">memory (no setup)</option>
|
|
150
|
+
<option value="mongodb">mongodb</option>
|
|
151
|
+
<option value="mysql">mysql</option>
|
|
152
|
+
<option value="postgres">postgres</option>
|
|
153
|
+
</select>
|
|
154
|
+
</div>
|
|
155
|
+
${() =>
|
|
156
|
+
dbDriver() === "mongodb"
|
|
157
|
+
? html`${field("MONGODB_URI", "mongoUri", "mongodb://user:pass@host:27017/db")}${field("MONGODB_DATABASE", "mongoDb", "db")}`
|
|
158
|
+
: dbDriver() === "mysql" || dbDriver() === "postgres"
|
|
159
|
+
? field("DATABASE_URL", "dbUrl", dbDriver() + "://user:pass@host/db")
|
|
160
|
+
: null}
|
|
161
|
+
${() => (dbDriver() !== "memory" ? html`<button class="btn btn-sm btn-outline-secondary mb-2" onclick=${testDb}>Test connection</button>` : null)}`;
|
|
162
|
+
|
|
163
|
+
const mediaSettings = () =>
|
|
164
|
+
html`<div class="mb-2">
|
|
165
|
+
<label class="form-label small mb-1">Media storage (MEDIA_DRIVER)</label>
|
|
166
|
+
<select class="form-select" value=${() => mediaDriver()} onchange=${(e) => set({ mediaDriver: e.target.value })}>
|
|
167
|
+
<option value="local">local (disk)</option>
|
|
168
|
+
<option value="s3">s3 — AWS S3 / DigitalOcean Spaces</option>
|
|
169
|
+
</select>
|
|
170
|
+
</div>
|
|
171
|
+
${() =>
|
|
172
|
+
mediaDriver() === "s3"
|
|
173
|
+
? html`${field("S3_ENDPOINT", "s3Endpoint", "https://nyc3.digitaloceanspaces.com")}${field("S3_REGION", "s3Region", "us-east-1")}${field("S3_BUCKET", "s3Bucket", "my-space")}${field("S3_KEY", "s3Key", "access key")}${field("S3_SECRET", "s3Secret", "secret key")}${field("S3_PUBLIC_BASE (optional CDN base)", "s3PublicBase", "https://cdn.example.com")}`
|
|
174
|
+
: null}`;
|
|
175
|
+
|
|
176
|
+
mount(
|
|
177
|
+
"#app",
|
|
178
|
+
available.length
|
|
179
|
+
? html`<div class="card-x p-4 mb-3">
|
|
180
|
+
<h2 class="h6 mb-3">Features</h2>
|
|
181
|
+
${available.map(addonRow)}
|
|
182
|
+
<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>
|
|
183
|
+
</div>`
|
|
184
|
+
: null,
|
|
185
|
+
html`<div class="card-x p-4 mb-3">
|
|
186
|
+
<h2 class="h6 mb-3">Settings</h2>
|
|
187
|
+
${field("PORT", "port", String(defaultPort))}
|
|
188
|
+
${() => (hasDb() ? dbSettings() : null)}
|
|
189
|
+
${() => (hasMailer() ? html`${field("SMTP_URL (optional)", "smtpUrl", "smtp://user:pass@smtp.host:587")}${field("MAIL_FROM", "mailFrom", "App <no-reply@you.com>")}` : null)}
|
|
190
|
+
${() => (hasMedia() ? mediaSettings() : null)}
|
|
191
|
+
</div>`,
|
|
192
|
+
html`<div class="card-x p-4 mb-3">
|
|
193
|
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
194
|
+
<h2 class="h6 mb-0">.env</h2>
|
|
195
|
+
<button class="btn btn-primary btn-sm" onclick=${apply}>Apply & start →</button>
|
|
196
|
+
</div>
|
|
197
|
+
<pre class="mb-0" style="background:#0b0d11;border:1px solid #232a36;border-radius:10px;padding:12px;color:#cfe3ff;white-space:pre-wrap">${env}</pre>
|
|
198
|
+
</div>`,
|
|
199
|
+
() => (status() ? html`<p class="small accent">${status}</p>` : null),
|
|
200
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
<link rel="icon" href="/favicon.webp" />
|
|
7
|
+
<title>Volt Studio</title>
|
|
8
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
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
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<main class="container py-5" style="max-width: 820px;">
|
|
17
|
+
<header class="mb-4">
|
|
18
|
+
<h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Volt Studio</span> <small class="text-muted">data browser</small></h1>
|
|
19
|
+
<p class="text-muted mb-0">Browse the database in your <code>.env</code>. Disposable & localhost-only — close the terminal when done.</p>
|
|
20
|
+
</header>
|
|
21
|
+
<div id="app"></div>
|
|
22
|
+
</main>
|
|
23
|
+
<script type="module">
|
|
24
|
+
import { mount } from "/volt.js";
|
|
25
|
+
import { dbAdminPanel } from "/db-admin-ui.js";
|
|
26
|
+
mount("#app", dbAdminPanel());
|
|
27
|
+
</script>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
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>Docs</title>
|
|
7
|
+
<link rel="icon" href="/favicon.webp" />
|
|
8
|
+
<!-- the docs home redirects to the first page; edit pages/ + the theme's NAV -->
|
|
9
|
+
<meta http-equiv="refresh" content="0; url=/getting-started" />
|
|
10
|
+
<link rel="canonical" href="/getting-started" />
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<p>Redirecting to <a href="/getting-started">the docs →</a></p>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -147,7 +147,7 @@ const TABS = [
|
|
|
147
147
|
const nav = () =>
|
|
148
148
|
html`<nav class="navx py-2 mb-4">
|
|
149
149
|
<div class="container d-flex align-items-center" style="max-width:760px">
|
|
150
|
-
<span class="brand me-3"><span class="accent"
|
|
150
|
+
<span class="brand me-3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Volt</span></span>
|
|
151
151
|
${TABS.map(([key, label]) => html`<button class=${() => "btn btn-link btn-sm " + (section() === key ? "active" : "")} onclick=${() => section(key)}>${label}</button>`)}
|
|
152
152
|
<span class="ms-auto small text-muted">${() => (me() ? shortName(me()) : "guest")}</span>
|
|
153
153
|
</div>
|
|
Binary file
|
|
Binary file
|
|
@@ -251,6 +251,8 @@ function startSetup() {
|
|
|
251
251
|
const assets = {
|
|
252
252
|
"/setup.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "setup", "setup.js"))],
|
|
253
253
|
"/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
|
|
254
|
+
"/logo.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "logo.webp"))],
|
|
255
|
+
"/favicon.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "favicon.webp"))],
|
|
254
256
|
};
|
|
255
257
|
const indexHtml = fs.readFileSync(path.join(__dirname, "setup", "index.html"));
|
|
256
258
|
|
|
@@ -407,6 +409,8 @@ async function startStudio() {
|
|
|
407
409
|
const visible = (n) => n && !HIDDEN_COLLECTIONS.has(n);
|
|
408
410
|
const assets = {
|
|
409
411
|
"/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
|
|
412
|
+
"/logo.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "logo.webp"))],
|
|
413
|
+
"/favicon.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "favicon.webp"))],
|
|
410
414
|
"/db-admin-ui.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(ADDONS_DIR, "db", "files", "public", "db-admin-ui.js"))],
|
|
411
415
|
};
|
|
412
416
|
const studioHtml = fs.readFileSync(path.join(__dirname, "setup", "studio.html"));
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<link rel="icon" href="/favicon.webp" />
|
|
6
7
|
<title>Set up your Volt app</title>
|
|
7
8
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
8
9
|
<style>
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
<body>
|
|
18
19
|
<main class="container py-5" style="max-width: 640px;">
|
|
19
20
|
<header class="mb-4">
|
|
20
|
-
<h1 class="h3"><span class="accent"
|
|
21
|
+
<h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Set up your Volt app</span></h1>
|
|
21
22
|
<p class="text-muted mb-0">Fill these in and the app starts. This page is disposable — it disappears once you click Apply. Re-open it anytime with <code>npm run dev -- --edit</code>.</p>
|
|
22
23
|
</header>
|
|
23
24
|
<div id="app"></div>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<link rel="icon" href="/favicon.webp" />
|
|
6
7
|
<title>Volt Studio</title>
|
|
7
8
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
8
9
|
<style>
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
<body>
|
|
15
16
|
<main class="container py-5" style="max-width: 820px;">
|
|
16
17
|
<header class="mb-4">
|
|
17
|
-
<h1 class="h3"><span class="accent"
|
|
18
|
+
<h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Volt Studio</span> <small class="text-muted">data browser</small></h1>
|
|
18
19
|
<p class="text-muted mb-0">Browse the database in your <code>.env</code>. Disposable & localhost-only — close the terminal when done.</p>
|
|
19
20
|
</header>
|
|
20
21
|
<div id="app"></div>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<link rel="icon" href="/favicon.webp" />
|
|
6
7
|
<title>Volt starter</title>
|
|
7
8
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
8
9
|
<style>
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
.navx .btn-link { color: #9aa4b2; text-decoration: none; }
|
|
17
18
|
.navx .btn-link.active { color: #ffd24a; }
|
|
18
19
|
a { color: #ffd24a; }
|
|
20
|
+
.text-muted { color: #9aa4b2 !important; } /* readable muted on the dark bg */
|
|
19
21
|
</style>
|
|
20
22
|
</head>
|
|
21
23
|
<body>
|