@voltro/cli 0.52.0 → 0.54.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 +424 -0
- package/THIRD-PARTY-NOTICES.md +8311 -3318
- package/dist/{agentsMd-0l980yhL.js → agentsMd-BaLC10Na.js} +110 -82
- package/dist/agentsMd-DCY1RSs8.js +2 -0
- package/dist/apiBuild-CeUN55uk.js +2 -0
- package/dist/{apiBuild-CSFI8QGq.js → apiBuild-DTWp0S_q.js} +11 -5
- package/dist/bin.js +1 -1
- package/dist/build-D4ygSbnV.js +843 -0
- package/dist/{checkCommand-COmqc2cB.js → checkCommand-Dg1G7Gwd.js} +6 -6
- package/dist/{checkCommand-2SbqzukH.js → checkCommand-L7DTlpIF.js} +1 -1
- package/dist/{cloudCmd-QUXh-b5w.js → cloudCmd-DzKcSYuy.js} +1 -1
- package/dist/{codegen-VF479Cnb.js → codegen-DSLM8Su9.js} +1 -1
- package/dist/codegen-DjgxEOnD.js +2 -0
- package/dist/codegenCommand-CG_Vx4lc.js +41 -0
- package/dist/{codemodRunner-r7J9lIa7.js → codemodRunner-Cd4xkC6u.js} +109 -11
- package/dist/{commands-Cc_nV8WI.js → commands-6Kzi92Np.js} +96 -73
- package/dist/{dashboardCommand-C-gKvwqh.js → dashboardCommand-Cq1PWvI1.js} +5 -5
- package/dist/{dataCommand-BgpBHnlB.js → dataCommand-DYzW8vkv.js} +3 -3
- package/dist/{dbCommand-sHedr-NJ.js → dbCommand-B4NWZtGL.js} +278 -237
- package/dist/dbCommand-CSFWs9ev.js +2 -0
- package/dist/{dev-CRHoCEiy.js → dev-CmuvUKRq.js} +2903 -2306
- package/dist/{dev--A3nsxA3.js → dev-cKUiZZsB.js} +1 -1
- package/dist/{doctorCommand-DtfJ3FA6.js → doctorCommand-DCiFVMtZ.js} +101 -69
- package/dist/doctorCommand-J3qu4E0Y.js +2 -0
- package/dist/{dormancyCommand-Drn7o0No.js → dormancyCommand-w1TrmgYP.js} +1 -1
- package/dist/{embeddingsCommand-Z-jO1fWN.js → embeddingsCommand-CMgPyRTr.js} +1 -1
- package/dist/{envCommand-D4gCrrTZ.js → envCommand-Cyynmcfa.js} +8 -8
- package/dist/{evolveCommand-CMROeKeA.js → evolveCommand-BwvQ8dVH.js} +2 -2
- package/dist/fileConventions-l-RIXbx8.js +36 -0
- package/dist/{fileTaxonomy-DvDUV9wq.js → fileTaxonomy-CbyMQYx_.js} +42 -42
- package/dist/fontPipeline-LxIHa1vo.js +2 -0
- package/dist/fontPipeline-Tsh8kZfA.js +152 -0
- package/dist/{frameworkTableAssembly-w-XnLa3q.js → frameworkTableAssembly-D7LJuALW.js} +5 -5
- package/dist/frameworkTableAssembly-IPD1pUnZ.js +2 -0
- package/dist/imagePipeline-B_GVJgm6.js +2 -0
- package/dist/imagePipeline-CBZmjT4i.js +127 -0
- package/dist/index.js +2 -2
- package/dist/{infoCommand-DXM868o_.js → infoCommand-DlYlUPqs.js} +1 -1
- package/dist/{inspect-CtL_xTbu.js → inspect-Bd8-9wsi.js} +1 -1
- package/dist/inspect-CuoDInfZ.js +2 -0
- package/dist/{inspectGateHint-BjnFubmH.js → inspectGateHint-4LxkNtrz.js} +1 -1
- package/dist/manifestBuild-C4-J1-m_.js +2 -0
- package/dist/{manifestBuild-CuU1VrSm.js → manifestBuild-Cqgsx2bM.js} +1 -1
- package/dist/{metaCommands-C6RFmF1r.js → metaCommands-x7RCi2AF.js} +9 -3
- package/dist/{migrate-D0F-eTlK.js → migrate-BK_Bbx-_.js} +2 -2
- package/dist/mobileCommand-D9O6iq3D.js +428 -0
- package/dist/mobileCommand-DAum7tsG.js +2 -0
- package/dist/{pageConvention-CzUiSbtU.js → pageConvention-CMpfDN6r.js} +1 -1
- package/dist/{privacyCommand-DGdopOI6.js → privacyCommand-BCa2OoZG.js} +1 -1
- package/dist/{probeCommand-C9gazU0H.js → probeCommand-_C0YU207.js} +83 -24
- package/dist/{projectScaffold-EzlErR4E.js → projectScaffold-CJfP-xbT.js} +1 -1
- package/dist/{projectScaffold-B4dmTlwT.js → projectScaffold-CSN0OzBV.js} +2 -2
- package/dist/renderModeScan-43yQ2opo.js +147 -0
- package/dist/{renderProfile-Ck32Fzxr.js → renderProfile-DvrhVJHa.js} +2 -2
- package/dist/{runtimeTrace-BPQyCmC5.js → runtimeTrace-CGWx1Q6l.js} +1 -1
- package/dist/{sdkgen-Se88ifTd.js → sdkgen-CDGHQUFj.js} +1 -1
- package/dist/serveCommand-BiPe8BJm.js +2 -0
- package/dist/{serveCommand-DkP3OT0W.js → serveCommand-Bje09q1v.js} +889 -825
- package/dist/serveEntry.js +1 -1
- package/dist/start-B0bnJgxI.js +3 -0
- package/dist/{start-jw89Xbqy.js → start-Clz-1BHB.js} +633 -455
- package/dist/startEntry.js +1 -1
- package/dist/{staticCommand-BwNEDlSU.js → staticCommand-ey0kYmOT.js} +1 -1
- package/dist/{subcommandNames-DpYs3DXr.js → subcommandNames-CDzfEtKV.js} +3 -3
- package/dist/{templates-BR-fb4SP.js → templates-BTWZkJJT.js} +41 -9
- package/dist/{test-f3amja6a.js → test-D_kW4KMj.js} +1 -1
- package/dist/updateCommand-CIoVDKnj.js +2 -0
- package/dist/{updateCommand-BMk2e4ky.js → updateCommand-CRJlAOaM.js} +139 -115
- package/dist/{webDev-BgWL9gKV.js → webDev-DSI9SOhs.js} +1598 -1028
- package/dist/webDev-DlvZO30c.js +2 -0
- package/dist/{webhooksCommand-CoIO3jbj.js → webhooksCommand-BvzXNHji.js} +1 -1
- package/dist/workspaceDeps-RKEkX92S.js +45 -0
- package/package.json +60 -19
- package/templates/AGENTS.core.md +2 -0
- package/templates/AGENTS.md +8 -4
- package/templates/agent-docs/_index.md +6 -4
- package/templates/agent-docs/_manifest.json +21 -5
- package/templates/agent-docs/ai.md +6 -6
- package/templates/agent-docs/authentication.md +73 -1
- package/templates/agent-docs/cli.md +6 -3
- package/templates/agent-docs/configuration.md +17 -0
- package/templates/agent-docs/data.md +522 -29
- package/templates/agent-docs/database/advancedqueries.md +8 -8
- package/templates/agent-docs/database/columntypes.md +2 -2
- package/templates/agent-docs/database/migrations.md +1 -1
- package/templates/agent-docs/database/querying.md +1 -1
- package/templates/agent-docs/database/schema.md +1 -1
- package/templates/agent-docs/database/seedsdialects.md +65 -3
- package/templates/agent-docs/database/transactions.md +3 -3
- package/templates/agent-docs/deployment.md +8 -0
- package/templates/agent-docs/internationalization.md +4 -2
- package/templates/agent-docs/introduction.md +32 -1
- package/templates/agent-docs/local-first-mobile.md +226 -30
- package/templates/agent-docs/observability.md +5 -1
- package/templates/agent-docs/plugins/atlassian.md +2 -2
- package/templates/agent-docs/plugins/audit.md +2 -2
- package/templates/agent-docs/plugins/auth.md +1 -1
- package/templates/agent-docs/plugins/billing.md +1 -1
- package/templates/agent-docs/plugins/cdc-out.md +8 -3
- package/templates/agent-docs/plugins/comments.md +164 -0
- package/templates/agent-docs/plugins/notifications.md +47 -4
- package/templates/agent-docs/plugins/presence.md +45 -3
- package/templates/agent-docs/plugins/prometheus.md +3 -1
- package/templates/agent-docs/plugins/queue.md +172 -0
- package/templates/agent-docs/plugins.md +17 -13
- package/templates/agent-docs/reference.md +35 -4
- package/templates/agent-docs/routing.md +585 -7
- package/templates/agent-docs/scheduling.md +1 -1
- package/templates/agent-docs/schema-driven-ui.md +226 -4
- package/templates/agent-docs/security.md +3 -3
- package/templates/agent-docs/templates/appshells.md +36 -4
- package/templates/agent-docs/whats-new.md +134 -66
- package/templates/apps/api-ai/package.json +6 -6
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +9 -9
- package/templates/apps/api-collab/README.md +3 -3
- package/templates/apps/api-collab/app.config.ts +1 -1
- package/templates/apps/api-collab/database/schema.ts +12 -8
- package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +1 -1
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +1 -1
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +1 -1
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-collab/template.json +1 -1
- package/templates/apps/api-collab/tests/documents.setBody.test.ts +10 -2
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-row-history/package.json +8 -8
- package/templates/apps/api-saas/package.json +11 -10
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/app.config.ts +26 -2
- package/templates/apps/changelog/content/releases/{0.1.0.mdx → v0-1-0.mdx} +0 -1
- package/templates/apps/changelog/content/releases/{0.2.0.mdx → v0-2-0.mdx} +0 -1
- package/templates/apps/changelog/package.json +9 -8
- package/templates/apps/changelog/src/collections/releases.collection.ts +48 -0
- package/templates/apps/changelog/src/globals.d.ts +1 -1
- package/templates/apps/changelog/src/locales/de.ts +1 -1
- package/templates/apps/changelog/src/locales/en.ts +1 -1
- package/templates/apps/changelog/src/pages/[locale]/[slug]/page.tsx +7 -4
- package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +13 -41
- package/templates/apps/changelog/src/pages/[slug]/page.test.tsx +26 -51
- package/templates/apps/changelog/src/pages/[slug]/page.tsx +21 -19
- package/templates/apps/changelog/src/pages/page.test.tsx +14 -37
- package/templates/apps/changelog/src/pages/page.tsx +18 -12
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +9 -9
- package/templates/apps/frontend-auth/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-cms/package.json +9 -9
- package/templates/apps/frontend-collab/README.md +43 -24
- package/templates/apps/frontend-collab/app.config.ts +3 -3
- package/templates/apps/frontend-collab/package.json +14 -10
- package/templates/apps/frontend-collab/src/locales/de.ts +1 -2
- package/templates/apps/frontend-collab/src/locales/en.ts +1 -2
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +72 -76
- package/templates/apps/frontend-collab/src/pages/page.tsx +45 -22
- package/templates/apps/frontend-collab/template.json +2 -2
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/content/docs/de/guides/first-page.md +4 -0
- package/templates/apps/frontend-docs/content/docs/de/intro/getting-started.md +4 -0
- package/templates/apps/frontend-docs/content/docs/en/guides/first-page.md +4 -0
- package/templates/apps/frontend-docs/content/docs/en/intro/getting-started.md +4 -0
- package/templates/apps/frontend-docs/package.json +9 -6
- package/templates/apps/frontend-docs/src/collections/docs.collection.ts +21 -0
- package/templates/apps/frontend-docs/src/locales/de.ts +0 -5
- package/templates/apps/frontend-docs/src/locales/en.ts +0 -5
- package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug]/page.tsx +17 -9
- package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +10 -5
- package/templates/apps/frontend-docs/src/pages/[locale]/page.tsx +8 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.test.tsx +53 -15
- package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.tsx +27 -33
- package/templates/apps/frontend-docs/src/pages/page.test.tsx +17 -3
- package/templates/apps/frontend-docs/src/pages/page.tsx +16 -12
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/README.md +48 -0
- package/templates/apps/frontend-landing/app.config.ts +28 -0
- package/templates/apps/frontend-landing/package.json +7 -6
- package/templates/apps/frontend-landing/src/assets/hero.jpg +0 -0
- package/templates/apps/frontend-landing/src/fonts/Geist-Variable.woff2 +0 -0
- package/templates/apps/frontend-landing/src/fonts/LICENSE-Geist.txt +92 -0
- package/templates/apps/frontend-landing/src/globals.css +15 -0
- package/templates/apps/frontend-landing/src/globals.d.ts +17 -0
- package/templates/apps/frontend-landing/src/locales/de.ts +3 -2
- package/templates/apps/frontend-landing/src/locales/en.ts +5 -2
- package/templates/apps/frontend-landing/src/pages/page.test.tsx +79 -0
- package/templates/apps/frontend-landing/src/pages/page.tsx +26 -3
- package/templates/apps/frontend-landing/template.json +2 -2
- package/templates/apps/frontend-portal/package.json +8 -8
- package/templates/apps/frontend-saas/package.json +8 -8
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/content/posts/cms-to-ssg.md +12 -0
- package/templates/apps/frontend-static-blog/content/posts/hello-static.md +12 -0
- package/templates/apps/frontend-static-blog/content/posts/islands-not-hydration.md +14 -0
- package/templates/apps/frontend-static-blog/package.json +9 -6
- package/templates/apps/frontend-static-blog/src/collections/posts.collection.ts +27 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug]/page.tsx +7 -4
- package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +7 -1
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.test.tsx +19 -9
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.tsx +19 -21
- package/templates/apps/frontend-static-blog/src/pages/page.test.tsx +18 -5
- package/templates/apps/frontend-static-blog/src/pages/page.tsx +21 -14
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/mobile-app/package.json +12 -11
- package/templates/apps/mobile-app/src/lib/deeplinks.ts +29 -17
- package/templates/apps/mobile-app/tests/deeplinks.test.ts +16 -0
- package/dist/agentsMd-SDDSkyl4.js +0 -2
- package/dist/apiBuild-BYBpL7Pz.js +0 -2
- package/dist/build-CPgcMQug.js +0 -793
- package/dist/codegen-CctkDO-1.js +0 -2
- package/dist/codegenCommand-DCdG2JN-.js +0 -137
- package/dist/dbCommand-DNb6yeOG.js +0 -2
- package/dist/doctorCommand-CqoWA2p5.js +0 -2
- package/dist/fileConventions-DOqD3lPS.js +0 -34
- package/dist/frameworkTableAssembly-C6ETawPR.js +0 -2
- package/dist/inspect-CuGDYES0.js +0 -2
- package/dist/manifestBuild-CPjhvM62.js +0 -2
- package/dist/renderModeScan-CcH2X1_D.js +0 -120
- package/dist/serveCommand-DLc-BznW.js +0 -2
- package/dist/start-DfL3fOiN.js +0 -3
- package/dist/updateCommand-5gFVfK5q.js +0 -2
- package/dist/webDev-CZbTsDcH.js +0 -2
- package/templates/apps/changelog/scripts/generate-rss.mjs +0 -38
- package/templates/apps/changelog/src/lib/releases.ts +0 -21
- package/templates/apps/frontend-static-blog/src/content/posts.ts +0 -64
|
@@ -173,8 +173,10 @@ A combined create-or-edit screen is a three-line wrapper:
|
|
|
173
173
|
### The customization ladder
|
|
174
174
|
|
|
175
175
|
- **Rung 0 — zero config.** Fields render from the schema: a `Schema.Literal`
|
|
176
|
-
union → `select`; string → text; boolean → checkbox; `Date` → date;
|
|
177
|
-
|
|
176
|
+
union → `select`; string → text; boolean → checkbox; `Schema.Date` → date;
|
|
177
|
+
a nested struct → a real `<fieldset>` SECTION with its legend and dotted
|
|
178
|
+
fields; widgets receive `onBlur`, so errors reveal on leave-field exactly
|
|
179
|
+
like the headless binding.
|
|
178
180
|
- **Rung 1 — one custom widget** via a `<Field>` render-prop:
|
|
179
181
|
```tsx
|
|
180
182
|
import { AutoForm, Field, AsyncSelect } from '@voltro/ui'
|
|
@@ -214,6 +216,189 @@ Schema.Struct({
|
|
|
214
216
|
})
|
|
215
217
|
```
|
|
216
218
|
|
|
219
|
+
### Validation — one schema, translated messages, server field errors
|
|
220
|
+
|
|
221
|
+
Client and server validate the SAME input schema, and the messages a user
|
|
222
|
+
sees are structured, not developer text. Every failed check maps to a stable
|
|
223
|
+
message id with params — `validation.required`, `validation.minLength {min}`,
|
|
224
|
+
`validation.minValue {min}` — rendered through a built-in en/de catalog. The
|
|
225
|
+
locale follows `<html lang>` (the framework's locale contract); override it
|
|
226
|
+
per form with `locale:`, or wire your own catalog in one line:
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
// ONCE, at the app root — every form under it resolves ids through your catalog
|
|
230
|
+
<ValidationMessagesProvider messages={(id, params) => t(id, params)}>
|
|
231
|
+
<App />
|
|
232
|
+
</ValidationMessagesProvider>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
A per-form `messages:` option still exists and wins over the provider; a
|
|
236
|
+
resolver returning `undefined` falls through to the built-ins per id, so a
|
|
237
|
+
partial catalog costs nothing.
|
|
238
|
+
|
|
239
|
+
A widget kit that resolves ids itself reads the same resolver with
|
|
240
|
+
`useValidationMessages()`. It returns the provider's function, or
|
|
241
|
+
`undefined` when no provider is mounted — so a kit can fall through to the
|
|
242
|
+
built-in catalog instead of shipping its own.
|
|
243
|
+
|
|
244
|
+
Errors key the FULL field path (`address.city`, `entries.0.startsAt`), and a
|
|
245
|
+
`message` annotation on a schema may BE an id with params — as may the issues
|
|
246
|
+
a struct-level `filter` returns, which land at THEIR field:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
const Input = Schema.Struct({
|
|
250
|
+
name: Schema.String.pipe(Schema.minLength(2)),
|
|
251
|
+
startsAt: Schema.Number,
|
|
252
|
+
endsAt: Schema.Number,
|
|
253
|
+
}).pipe(
|
|
254
|
+
Schema.filter((v) =>
|
|
255
|
+
v.startsAt < v.endsAt ? undefined : [{ path: ['endsAt'], message: 'validation.beforeStart' }],
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Server-side rules route to their field too.** An executor raises a typed
|
|
261
|
+
field error through the always-present `ctx.validation` — no declaration
|
|
262
|
+
needed, `ValidationError` is auto-merged into every mutation's and action's
|
|
263
|
+
wire error union exactly like `ScopeError`:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
// executor
|
|
267
|
+
if (await emailTaken(input.email)) {
|
|
268
|
+
return yield* ctx.validation.fail('email', 'validation.emailTaken')
|
|
269
|
+
}
|
|
270
|
+
yield* ctx.validation.require(input.startsAt < input.endsAt, 'endsAt', 'validation.beforeStart')
|
|
271
|
+
// several at once: ctx.validation.failAll([{ field, message }, …])
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The binding routes it: the message lands in `errors.email` (translated
|
|
275
|
+
through the same catalog), the form stays editable, and `submitError` only
|
|
276
|
+
carries what NO field can — a `BusinessRuleViolation` whose rule pinpointed
|
|
277
|
+
a `field` routes the same way. Custom widget kits get the same judgement via
|
|
278
|
+
`fieldIssuesOf(error)` from `@voltro/protocol`.
|
|
279
|
+
|
|
280
|
+
**Async checks gate the submit.** Bind a uniqueness probe to its field and
|
|
281
|
+
`submit` waits for it — an `invalid` verdict blocks with the message on that
|
|
282
|
+
field, an unsettled check fails closed after 5s:
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
const email = useAsyncValidation('app', 'users.emailAvailable', values.email ?? '', {
|
|
286
|
+
interpret: (r) => ({ valid: (r as { available: boolean }).available, message: 'Email taken' }),
|
|
287
|
+
})
|
|
288
|
+
const form = useFormBinding('app', 'users.create', { asyncFields: { email } })
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**The binding is typed — `toInput` included.** `createHooks<AppProcedures>('app')`
|
|
292
|
+
returns `useFormBinding` beside the other hooks — tag as a literal type,
|
|
293
|
+
`values`/`defaults` and the submit output inferred from the descriptor. With
|
|
294
|
+
`toInput`, the form gets its OWN `Values` shape and the mapper's return is
|
|
295
|
+
checked against the mutation's input by the compiler — a mapping that stops
|
|
296
|
+
producing the wire shape is a type error, not a runtime refusal.
|
|
297
|
+
|
|
298
|
+
### The full binding — nested values, arrays, timing, one state
|
|
299
|
+
|
|
300
|
+
`useFormBinding` carries a complete form, not just flat fields. Nested structs
|
|
301
|
+
flatten into SECTIONS (`address.city`, grouped under `section('address')`),
|
|
302
|
+
arrays of structs become field arrays, and every field is reachable as a
|
|
303
|
+
bound handle a widget kit spreads onto its input:
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
const form = useFormBinding('app', 'employees.update', {
|
|
307
|
+
defaults: fromRow(employee),
|
|
308
|
+
toInput: (values) => ({ id: employee.id, ...employeePatch(values) }),
|
|
309
|
+
errorPath: { fullName: 'name' },
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
const city = form.field('address.city') // { value, setValue, onBlur, error, required, label, a11y, … }
|
|
313
|
+
form.array('entries').push({ startsAt: '' })
|
|
314
|
+
form.section('address') // the section's descriptors
|
|
315
|
+
form.state // { isDirty, canSubmit, isSubmitting, isSubmitSuccessful, submissionAttempts, errorCount, pending, … }
|
|
316
|
+
form.reset(nextDefaults) // switch the edited record without a remount
|
|
317
|
+
form.focusFirstInvalid()
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Error timing has defaults a form can trust:** a form NEVER opens with
|
|
321
|
+
errors — a field reveals its error after ITS blur or after the first submit
|
|
322
|
+
attempt, then live (`validate: { onChange: 'afterTouched' }`; `'always'` and
|
|
323
|
+
`'never'` exist). `isValid`/`canSubmit` always tell the truth underneath, so
|
|
324
|
+
the save button disables correctly while the user is not yet being scolded.
|
|
325
|
+
|
|
326
|
+
**Values ≠ mutation input:** `toInput` maps form values to the wire input
|
|
327
|
+
BEFORE validation; input-schema issues route back to form fields via
|
|
328
|
+
`errorPath` (same-name fields map automatically). For composed saves,
|
|
329
|
+
`onSubmit: async ({ input, values, mutate }) => …` owns the write and keeps
|
|
330
|
+
optimistic + error routing.
|
|
331
|
+
|
|
332
|
+
**Per-field rendering:** with `subscribe: 'fields'` the binding only
|
|
333
|
+
re-renders on submission-level changes, and each field component subscribes
|
|
334
|
+
narrowly:
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
const BoundField = ({ form, path }: { form: FormBinding<Record<string, unknown>, unknown>; path: string }) => {
|
|
338
|
+
const f = useFormField(form, path)
|
|
339
|
+
return <input id={f.a11y.id} value={String(f.value ?? '')} onChange={(e) => f.setValue(e.target.value)} onBlur={f.onBlur} aria-invalid={f.a11y['aria-invalid']} />
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Annotate structure on the schema itself: `formField({ section, order, label,
|
|
344
|
+
widget })` rides an annotation, `description` becomes help text, and
|
|
345
|
+
`Schema.Date` / `Schema.DateTimeUtc` map to date/datetime widgets. The engine
|
|
346
|
+
underneath is an implementation detail — no engine type appears in the public
|
|
347
|
+
API, and production builds stub its devtools channel automatically.
|
|
348
|
+
|
|
349
|
+
### Reference fields, uploads, and testing the form
|
|
350
|
+
|
|
351
|
+
**Reference fields.** Mark a schema field as a table reference and it renders
|
|
352
|
+
as a picker-shaped field whose VALUE stays the id (or id list — which is
|
|
353
|
+
exactly what a target's declared `relations:` consumes):
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
const EmployeesUpdateInput = Schema.Struct({
|
|
357
|
+
id: Schema.String,
|
|
358
|
+
storeId: Schema.String.annotations(formField({ reference: 'stores' })),
|
|
359
|
+
assignedStores: Schema.Array(Schema.String).annotations(formField({ reference: 'stores' })),
|
|
360
|
+
})
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
The descriptor carries `widget: 'reference'` + `reference: 'stores'`; bind
|
|
364
|
+
the shipped `<AsyncSelect>` (or your own picker) to it via a render-prop —
|
|
365
|
+
the default registry deliberately renders the render-prop note, because a
|
|
366
|
+
live picker needs a query binding only the app can name.
|
|
367
|
+
|
|
368
|
+
**Uploads as field values.** `useUpload` returns a `fileId`; hold it in a
|
|
369
|
+
field and LINK it in the submit — the composed save is what `onSubmit` is
|
|
370
|
+
for, so the upload that never got attached cannot happen silently:
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
const form = useFormBinding('app', 'tasks.create', {
|
|
374
|
+
onSubmit: async ({ input, values, mutate }) => {
|
|
375
|
+
const row = await mutate(input)
|
|
376
|
+
// attachments: [...existing, ...uploaded fileIds] — linked HERE, not forgotten
|
|
377
|
+
return row
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Leaving a dirty form** is guarded by the router: `useBlocker(form.state.isDirty)`
|
|
383
|
+
holds SPA navigations (Back button included) and arms the native
|
|
384
|
+
`beforeunload` prompt — see the routing docs.
|
|
385
|
+
|
|
386
|
+
**Testing.** `renderFormBinding` (from `@voltro/testing/client`) drives the
|
|
387
|
+
REAL binding against a fake api — fill, blur, submit, read the visible
|
|
388
|
+
errors; a mutation handler that throws `ValidationError({ field })`
|
|
389
|
+
exercises the same routing path a server refusal takes:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
const form = await renderFormBinding('users.create', {
|
|
393
|
+
binding: { schema: UsersCreateInput },
|
|
394
|
+
mutation: () => { throw new ValidationError({ field: 'email', message: 'validation.emailTaken' }) },
|
|
395
|
+
})
|
|
396
|
+
await form.submit()
|
|
397
|
+
expect(form.errors()['email']).toBe('validation.emailTaken')
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Runs under jsdom (`// @vitest-environment jsdom`).
|
|
401
|
+
|
|
217
402
|
### Forms without JavaScript
|
|
218
403
|
|
|
219
404
|
On a server-rendered page, `<AutoForm>` works with JavaScript disabled — or not
|
|
@@ -234,7 +419,18 @@ the SAME input schema the RPC path decodes:
|
|
|
234
419
|
- unknown keys are dropped
|
|
235
420
|
|
|
236
421
|
Validation runs through the same `validateFields` as the client-side
|
|
237
|
-
validation, so the error texts are identical
|
|
422
|
+
validation, so the error texts are identical — in the request's language, not
|
|
423
|
+
in English. The handler resolves the locale from THIS request through the same
|
|
424
|
+
resolver that decided the surrounding page's language ([`voltro:locale` cookie
|
|
425
|
+
› `Accept-Language` › the app's
|
|
426
|
+
`defaultLocale`](/docs/i18n/overview#locale-resolution-order)); an app that
|
|
427
|
+
configures no `locales` gets `en`. Then:
|
|
428
|
+
|
|
429
|
+
Under URL-prefix i18n the referring URL wins over the cookie chain: a form on
|
|
430
|
+
`/de/todos` renders its errors in German even if the `voltro:locale` cookie says
|
|
431
|
+
otherwise, because there the prefix is which page you are on rather than a
|
|
432
|
+
preference. A first segment that is not a declared locale falls through to the
|
|
433
|
+
cookie chain.
|
|
238
434
|
|
|
239
435
|
- **Success → `303 See Other`** (POST-redirect-GET): back to the submitting
|
|
240
436
|
page, or to `redirectTo` (same-origin relative paths only; anything else is
|
|
@@ -720,7 +916,33 @@ multi-device CRDT (single-user/device, honest about its limits).
|
|
|
720
916
|
|
|
721
917
|
```tsx
|
|
722
918
|
const outbox = useOutbox({ send })
|
|
723
|
-
// → { queue, enqueue, replay, online, pending, conflicts }
|
|
919
|
+
// → { queue, enqueue, replay, online, pending, conflicts, resolveConflict }
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
**Durable across reloads.** Pass `persistence` and the queue survives:
|
|
923
|
+
entries rehydrate on mount, every transition persists (delivered entries are
|
|
924
|
+
compacted away), and a failing backing degrades to the in-memory behaviour
|
|
925
|
+
instead of breaking the outbox. The one real implementation is
|
|
926
|
+
`outboxPersistence()` from `@voltro/local-first`, backed by the same
|
|
927
|
+
`PersistenceAdapter` the sync engine drains — one durable queue per device:
|
|
928
|
+
|
|
929
|
+
```tsx
|
|
930
|
+
import { createIndexedDbPersistence, outboxPersistence } from '@voltro/local-first'
|
|
931
|
+
|
|
932
|
+
const adapter = await createIndexedDbPersistence()
|
|
933
|
+
const outbox = useOutbox({ send, persistence: outboxPersistence(adapter) })
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
**Resolving a conflict.** A conflicted entry blocks everything behind it
|
|
937
|
+
(causality). `resolveConflict(id, input)` replaces its input with the RESOLVED
|
|
938
|
+
value, returns it to pending and replays — typically computed with
|
|
939
|
+
`resolveWithPolicy()` from `@voltro/local-first`, where `crdtText()` columns
|
|
940
|
+
merge and scalars follow the declared `conflictPolicy()`:
|
|
941
|
+
|
|
942
|
+
```tsx
|
|
943
|
+
outbox.resolveConflict(entry.id, resolveWithPolicy(policy, local, remote, {
|
|
944
|
+
crdtColumns: ['body'],
|
|
945
|
+
}))
|
|
724
946
|
```
|
|
725
947
|
|
|
726
948
|
|
|
@@ -195,7 +195,7 @@ would break every link into your api.
|
|
|
195
195
|
|
|
196
196
|
### Raw WebSocket gateways are guarded before the upgrade
|
|
197
197
|
|
|
198
|
-
A [`defineWebSocket` gateway](/docs/data/subscriptions#raw-websocket-gateways
|
|
198
|
+
A [`defineWebSocket` gateway](/docs/data/subscriptions#raw-websocket-gateways-definewebsocket)
|
|
199
199
|
mounts its own upgrade path beside the rpc socket, and the listener treats it
|
|
200
200
|
exactly like the rpc upgrade:
|
|
201
201
|
|
|
@@ -479,7 +479,7 @@ deferred registry, the shell's bundle tags, and React's own bootstrap/settle
|
|
|
479
479
|
scripts — while the policy header itself stays the middleware's to set via
|
|
480
480
|
`responseHeaders`, carrying the same nonce. An `isr` page combined with
|
|
481
481
|
`cspNonce` refuses the render: a cached nonce is a lie the browser enforces.
|
|
482
|
-
Mechanics + examples: [Middleware → cspNonce](/docs/routing/middleware#a-per-request-csp-nonce
|
|
482
|
+
Mechanics + examples: [Middleware → cspNonce](/docs/routing/middleware#a-per-request-csp-nonce-cspnonce).
|
|
483
483
|
|
|
484
484
|
## Incoming webhooks must verify their caller
|
|
485
485
|
|
|
@@ -582,7 +582,7 @@ dependency, like a rate-limit counter in Redis — catches internally and
|
|
|
582
582
|
degrades **loudly**. `@voltro/plugin-ratelimit`'s `httpShield` does exactly
|
|
583
583
|
that: a Redis outage means unlimited-with-a-warning, never a self-inflicted API
|
|
584
584
|
outage. The full authoring guidance is in the
|
|
585
|
-
[plugin contract](/docs/plugins/contract#onhttprequest-httprequestinterceptor
|
|
585
|
+
[plugin contract](/docs/plugins/contract#onhttprequest-httprequestinterceptor-pre-auth-http-pipeline-hook).
|
|
586
586
|
|
|
587
587
|
## Response compression — and where BREACH sits
|
|
588
588
|
|
|
@@ -13,7 +13,7 @@ _A marketing landing page — hero, features, CTA. Static-rendered with zero JS
|
|
|
13
13
|
|
|
14
14
|
A marketing landing page. The page exports `renderMode = 'static'` + `interactive = 'none'`, so `voltro build` pre-renders it to HTML and `voltro start` serves the file directly — zero framework JS on the wire. Template id: **`frontend-landing`**.
|
|
15
15
|
|
|
16
|
-
It ships plain JSX (a hero, a features list, a CTA) you replace with your own copy
|
|
16
|
+
It ships plain JSX (a hero, a features list, a CTA) you replace with your own copy, bilingual out of the box (URL-prefix i18n: `/` + `/de`), plus the two asset pipelines a marketing page actually needs: a **local hero image** through `?image` + `<Image>` and a **self-hosted woff2** declared under `fonts:`. When you need a contact form or sign-up flow, switch `interactive: 'islands'` on the page and mark the interactive component with a `*.island.tsx` suffix so only that bundle ships.
|
|
17
17
|
|
|
18
18
|
## Scaffold
|
|
19
19
|
|
|
@@ -27,14 +27,21 @@ voltro add-app marketing --template=frontend-landing --to acme
|
|
|
27
27
|
|
|
28
28
|
```text
|
|
29
29
|
apps/acme/web/ # dir named by the app, not the template
|
|
30
|
-
├── app.config.ts # type:web, port:<allocated
|
|
30
|
+
├── app.config.ts # type:web, port:<allocated>, locales, fonts:
|
|
31
31
|
├── package.json
|
|
32
32
|
├── tsconfig.json
|
|
33
33
|
└── src/
|
|
34
34
|
├── globals.css
|
|
35
|
+
├── globals.d.ts # ambient '*.css' + '*?image'
|
|
36
|
+
├── assets/hero.jpg # imported with ?image (build-time pipeline)
|
|
37
|
+
├── fonts/Geist-Variable.woff2 # self-hosted, declared in app.config.ts
|
|
38
|
+
├── fonts/LICENSE-Geist.txt # the face's licence, shipped beside it
|
|
39
|
+
├── lib/locale.ts # URL-prefix i18n helpers
|
|
40
|
+
├── locales/{en,de}.ts # the two catalogs
|
|
35
41
|
└── pages/
|
|
36
42
|
├── layout.tsx # imports globals.css, renders {children}
|
|
37
|
-
|
|
43
|
+
├── page.tsx # the landing page (hero · features · CTA)
|
|
44
|
+
└── [locale]/page.tsx # the /de mirror
|
|
38
45
|
```
|
|
39
46
|
|
|
40
47
|
## The page
|
|
@@ -88,9 +95,34 @@ import SignupForm from '../components/SignupForm.island'
|
|
|
88
95
|
|
|
89
96
|
The surrounding HTML stays static; only the island hydrates.
|
|
90
97
|
|
|
98
|
+
## The hero image — `?image` + `<Image>`
|
|
99
|
+
|
|
100
|
+
`src/assets/hero.jpg` is imported with the **`?image` suffix**, the explicit opt-in to the [build-time image pipeline](/docs/routing/assets): the import resolves to an optimized-asset object instead of vite's plain hashed URL, and `<Image>` renders it as a `<picture>` with one `<source>` per modern format.
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
import { Image } from '@voltro/web'
|
|
104
|
+
import hero from '../assets/hero.jpg?image'
|
|
105
|
+
|
|
106
|
+
<Image src={hero} alt="Abstract gradient artwork" priority sizes="(max-width: 900px) 100vw, 900px" />
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`width`, `height` and the blur placeholder are **not props** — they come off the asset, which is what reserves the box (CLS ≈ 0) without hand-written numbers. `priority` marks it the LCP image (eager + high fetch priority). Nothing here needs JS, so it survives `interactive: 'none'`.
|
|
110
|
+
|
|
111
|
+
The `*?image` ambient type is declared once in `src/globals.d.ts`. A **dynamic** `src` (a URL from a loader or CMS frontmatter) cannot be seen at build time — use the loader seam (`<Image src={url} loader={cdn} />`) instead.
|
|
112
|
+
|
|
113
|
+
> **Under `voltro test`** the image plugin is not wired (it is a dev/build transform), so a `?image` specifier resolves to a plain URL string. The shipped `page.test.tsx` `vi.mock`s the import with the asset object the pipeline produces — copy that pattern rather than hand-writing `width`/`height` on the page.
|
|
114
|
+
|
|
115
|
+
## The font — self-hosted, no CDN
|
|
116
|
+
|
|
117
|
+
`app.config.ts` declares one family under [`fonts:`](/docs/routing/fonts), pointing at the woff2 committed in `src/fonts/`. The build content-hashes it and serves it from **your origin**, emits `@font-face`, computes a size-adjusted fallback face from the file's real metrics so the swap moves no text, and puts a `<link rel="preload">` in the shell.
|
|
118
|
+
|
|
119
|
+
Reference it from CSS through the `--font-geist` variable the shell defines (`globals.css` points the kit's `--font-sans` at it), or from TSX with `localFont('Geist')`.
|
|
120
|
+
|
|
121
|
+
**Swapping in your own face:** drop the `woff2` **and its licence file** into `src/fonts/`, then change `family` + `path`. The framework ships no font downloader on purpose — licence terms differ per family. The bundled Geist is SIL OFL 1.1 (`src/fonts/LICENSE-Geist.txt`).
|
|
122
|
+
|
|
91
123
|
## Styling
|
|
92
124
|
|
|
93
|
-
`globals.css`
|
|
125
|
+
`globals.css` imports `@voltro/ui-shadcn/tokens.css` (the design tokens) plus the mandatory `@source "./**/*.{tsx,ts,jsx,js}"` glob. Drop the kit import and use `@import "tailwindcss"` directly if you would rather start from nothing — but keep the `@source` line either way, and keep the `--font-sans` mapping if you keep the font declaration.
|
|
94
126
|
|
|
95
127
|
## What it doesn't ship
|
|
96
128
|
|