create-volt 0.29.0 → 0.30.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,20 @@ 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.30.0] - 2026-06-29
8
+
9
+ ### Fixed
10
+ - **Setup wizard: typing in a settings field no longer drops focus.** The
11
+ conditional sections (DB-driver fields, S3 fields) read the whole `state()`
12
+ signal, so every keystroke re-rendered the section and recreated the `<input>`,
13
+ losing focus. They now key on memoized primitive derivations (`dbDriver`,
14
+ `mediaDriver`, `hasDb`/`hasMailer`/`hasMedia`) so a section re-renders only when
15
+ its discriminant changes.
16
+ - **`volt.js`: reactive `value` bindings skip redundant DOM writes.** `setAttr`
17
+ no longer reassigns `node.value` when it already equals the new value — those
18
+ writes were resetting the caret to the end while typing. Benefits every Volt
19
+ app, not just the wizard.
20
+
7
21
  ## [0.29.0] - 2026-06-29
8
22
 
9
23
  ### Fixed
@@ -399,6 +413,7 @@ All notable changes to `create-volt` are documented here. The format follows
399
413
  watching and full-page hot reload. Supports `--skip-install` and `--force`,
400
414
  and auto-detects npm / pnpm / yarn / bun for the install step.
401
415
 
416
+ [0.30.0]: https://github.com/MIR-2025/volt/releases/tag/v0.30.0
402
417
  [0.29.0]: https://github.com/MIR-2025/volt/releases/tag/v0.29.0
403
418
  [0.28.0]: https://github.com/MIR-2025/volt/releases/tag/v0.28.0
404
419
  [0.27.0]: https://github.com/MIR-2025/volt/releases/tag/v0.27.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-volt",
3
- "version": "0.29.0",
3
+ "version": "0.30.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": {
@@ -127,7 +127,8 @@ export function mount(target, ...children) {
127
127
 
128
128
  function setAttr(node, name, value) {
129
129
  if (name === "value") {
130
- node.value = value ?? "";
130
+ const v = value ?? "";
131
+ if (node.value !== v) node.value = v; // skip redundant writes — they reset the caret while typing
131
132
  return;
132
133
  }
133
134
  if (name === "checked" || name === "disabled" || name === "selected") {
@@ -75,6 +75,14 @@ function genEnv(s) {
75
75
  }
76
76
  const env = computed(() => genEnv(state()));
77
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"));
78
86
 
79
87
  async function testDb() {
80
88
  const s = state();
@@ -137,7 +145,7 @@ const addonRow = (a) =>
137
145
  const dbSettings = () =>
138
146
  html`<div class="mb-2">
139
147
  <label class="form-label small mb-1">Database (DB_DRIVER)</label>
140
- <select class="form-select" value=${() => state().dbDriver} onchange=${(e) => set({ dbDriver: e.target.value })}>
148
+ <select class="form-select" value=${() => dbDriver()} onchange=${(e) => set({ dbDriver: e.target.value })}>
141
149
  <option value="memory">memory (no setup)</option>
142
150
  <option value="mongodb">mongodb</option>
143
151
  <option value="mysql">mysql</option>
@@ -145,23 +153,23 @@ const dbSettings = () =>
145
153
  </select>
146
154
  </div>
147
155
  ${() =>
148
- state().dbDriver === "mongodb"
156
+ dbDriver() === "mongodb"
149
157
  ? html`${field("MONGODB_URI", "mongoUri", "mongodb://user:pass@host:27017/db")}${field("MONGODB_DATABASE", "mongoDb", "db")}`
150
- : state().dbDriver === "mysql" || state().dbDriver === "postgres"
151
- ? field("DATABASE_URL", "dbUrl", state().dbDriver + "://user:pass@host/db")
158
+ : dbDriver() === "mysql" || dbDriver() === "postgres"
159
+ ? field("DATABASE_URL", "dbUrl", dbDriver() + "://user:pass@host/db")
152
160
  : null}
153
- ${() => (state().dbDriver !== "memory" ? html`<button class="btn btn-sm btn-outline-secondary mb-2" onclick=${testDb}>Test connection</button>` : null)}`;
161
+ ${() => (dbDriver() !== "memory" ? html`<button class="btn btn-sm btn-outline-secondary mb-2" onclick=${testDb}>Test connection</button>` : null)}`;
154
162
 
155
163
  const mediaSettings = () =>
156
164
  html`<div class="mb-2">
157
165
  <label class="form-label small mb-1">Media storage (MEDIA_DRIVER)</label>
158
- <select class="form-select" value=${() => state().mediaDriver} onchange=${(e) => set({ mediaDriver: e.target.value })}>
166
+ <select class="form-select" value=${() => mediaDriver()} onchange=${(e) => set({ mediaDriver: e.target.value })}>
159
167
  <option value="local">local (disk)</option>
160
168
  <option value="s3">s3 — AWS S3 / DigitalOcean Spaces</option>
161
169
  </select>
162
170
  </div>
163
171
  ${() =>
164
- state().mediaDriver === "s3"
172
+ mediaDriver() === "s3"
165
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")}`
166
174
  : null}`;
167
175
 
@@ -177,9 +185,9 @@ mount(
177
185
  html`<div class="card-x p-4 mb-3">
178
186
  <h2 class="h6 mb-3">Settings</h2>
179
187
  ${field("PORT", "port", String(defaultPort))}
180
- ${() => (eff().includes("db") ? dbSettings() : null)}
181
- ${() => (eff().includes("mailer") ? html`${field("SMTP_URL (optional)", "smtpUrl", "smtp://user:pass@smtp.host:587")}${field("MAIL_FROM", "mailFrom", "App <no-reply@you.com>")}` : null)}
182
- ${() => (eff().includes("media") ? mediaSettings() : null)}
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)}
183
191
  </div>`,
184
192
  html`<div class="card-x p-4 mb-3">
185
193
  <div class="d-flex justify-content-between align-items-center mb-2">
@@ -127,7 +127,8 @@ export function mount(target, ...children) {
127
127
 
128
128
  function setAttr(node, name, value) {
129
129
  if (name === "value") {
130
- node.value = value ?? "";
130
+ const v = value ?? "";
131
+ if (node.value !== v) node.value = v; // skip redundant writes — they reset the caret while typing
131
132
  return;
132
133
  }
133
134
  if (name === "checked" || name === "disabled" || name === "selected") {
@@ -127,7 +127,8 @@ export function mount(target, ...children) {
127
127
 
128
128
  function setAttr(node, name, value) {
129
129
  if (name === "value") {
130
- node.value = value ?? "";
130
+ const v = value ?? "";
131
+ if (node.value !== v) node.value = v; // skip redundant writes — they reset the caret while typing
131
132
  return;
132
133
  }
133
134
  if (name === "checked" || name === "disabled" || name === "selected") {
@@ -75,6 +75,14 @@ function genEnv(s) {
75
75
  }
76
76
  const env = computed(() => genEnv(state()));
77
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"));
78
86
 
79
87
  async function testDb() {
80
88
  const s = state();
@@ -137,7 +145,7 @@ const addonRow = (a) =>
137
145
  const dbSettings = () =>
138
146
  html`<div class="mb-2">
139
147
  <label class="form-label small mb-1">Database (DB_DRIVER)</label>
140
- <select class="form-select" value=${() => state().dbDriver} onchange=${(e) => set({ dbDriver: e.target.value })}>
148
+ <select class="form-select" value=${() => dbDriver()} onchange=${(e) => set({ dbDriver: e.target.value })}>
141
149
  <option value="memory">memory (no setup)</option>
142
150
  <option value="mongodb">mongodb</option>
143
151
  <option value="mysql">mysql</option>
@@ -145,23 +153,23 @@ const dbSettings = () =>
145
153
  </select>
146
154
  </div>
147
155
  ${() =>
148
- state().dbDriver === "mongodb"
156
+ dbDriver() === "mongodb"
149
157
  ? html`${field("MONGODB_URI", "mongoUri", "mongodb://user:pass@host:27017/db")}${field("MONGODB_DATABASE", "mongoDb", "db")}`
150
- : state().dbDriver === "mysql" || state().dbDriver === "postgres"
151
- ? field("DATABASE_URL", "dbUrl", state().dbDriver + "://user:pass@host/db")
158
+ : dbDriver() === "mysql" || dbDriver() === "postgres"
159
+ ? field("DATABASE_URL", "dbUrl", dbDriver() + "://user:pass@host/db")
152
160
  : null}
153
- ${() => (state().dbDriver !== "memory" ? html`<button class="btn btn-sm btn-outline-secondary mb-2" onclick=${testDb}>Test connection</button>` : null)}`;
161
+ ${() => (dbDriver() !== "memory" ? html`<button class="btn btn-sm btn-outline-secondary mb-2" onclick=${testDb}>Test connection</button>` : null)}`;
154
162
 
155
163
  const mediaSettings = () =>
156
164
  html`<div class="mb-2">
157
165
  <label class="form-label small mb-1">Media storage (MEDIA_DRIVER)</label>
158
- <select class="form-select" value=${() => state().mediaDriver} onchange=${(e) => set({ mediaDriver: e.target.value })}>
166
+ <select class="form-select" value=${() => mediaDriver()} onchange=${(e) => set({ mediaDriver: e.target.value })}>
159
167
  <option value="local">local (disk)</option>
160
168
  <option value="s3">s3 — AWS S3 / DigitalOcean Spaces</option>
161
169
  </select>
162
170
  </div>
163
171
  ${() =>
164
- state().mediaDriver === "s3"
172
+ mediaDriver() === "s3"
165
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")}`
166
174
  : null}`;
167
175
 
@@ -177,9 +185,9 @@ mount(
177
185
  html`<div class="card-x p-4 mb-3">
178
186
  <h2 class="h6 mb-3">Settings</h2>
179
187
  ${field("PORT", "port", String(defaultPort))}
180
- ${() => (eff().includes("db") ? dbSettings() : null)}
181
- ${() => (eff().includes("mailer") ? html`${field("SMTP_URL (optional)", "smtpUrl", "smtp://user:pass@smtp.host:587")}${field("MAIL_FROM", "mailFrom", "App <no-reply@you.com>")}` : null)}
182
- ${() => (eff().includes("media") ? mediaSettings() : null)}
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)}
183
191
  </div>`,
184
192
  html`<div class="card-x p-4 mb-3">
185
193
  <div class="d-flex justify-content-between align-items-center mb-2">