@voltro/cli 0.28.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 +655 -0
- package/THIRD-PARTY-NOTICES.md +90 -4
- package/dist/{apiBuild-eUM32r1u.js → apiBuild-BceikpQf.js} +3 -3
- package/dist/apiBuild-CxvR1Dwt.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/checkCommand-BMr-X48w.js +2427 -0
- package/dist/checkCommand-BOk9y264.js +2 -0
- package/dist/cliRuntime-Oh517vCV.js +96 -0
- package/dist/{commands-CSkrUI1h.js → commands-D4Y1-Nlu.js} +8575 -9366
- package/dist/{dbCommand-CpYgmSw4.js → dbCommand-C7hbl5pk.js} +345 -266
- package/dist/dbCommand-D8nhyppR.js +2 -0
- package/dist/{dev-DlBWWnJQ.js → dev-Cr-teYsz.js} +1 -1
- package/dist/{dev-CEZwJhmb.js → dev-DmnkH-wr.js} +2939 -1832
- package/dist/fileConventions-Cof68_BL.js +33 -0
- package/dist/{frameworkTableAssembly-BwHU9Euq.js → frameworkTableAssembly-BzAajuUv.js} +115 -99
- package/dist/frameworkTableAssembly-DU0aOxjL.js +2 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +1 -1
- package/dist/{inspect-CUCCzw2I.js → inspect-BUUjt773.js} +349 -192
- package/dist/inspect-mmBuRXmy.js +2 -0
- package/dist/{inspectMetrics-BU90mvJN.js → inspectMetrics-BeJdP_3b.js} +1068 -978
- package/dist/{manifestBuild-BnzAxp2O.js → manifestBuild-BLrVuSlM.js} +1 -1
- package/dist/manifestBuild-Dj8Jjoto.js +2 -0
- package/dist/sdkgen-CqpOq_fy.js +582 -0
- package/dist/serveCommand-Ciq_wIY8.js +1577 -0
- package/dist/serveEntry.js +2 -2
- package/dist/{start-BGXIf6zT.js → start-ocqB1seX.js} +283 -270
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.core.md +50 -0
- package/templates/AGENTS.md +54 -2
- package/templates/agent-docs/_index.md +4 -2
- package/templates/agent-docs/_manifest.json +26 -5
- package/templates/agent-docs/ai.md +191 -0
- package/templates/agent-docs/cli.md +143 -6
- package/templates/agent-docs/configuration.md +32 -0
- package/templates/agent-docs/data.md +282 -0
- package/templates/agent-docs/database/migrations.md +47 -0
- package/templates/agent-docs/database/misc.md +57 -0
- package/templates/agent-docs/database/schema.md +3 -1
- package/templates/agent-docs/internationalization.md +169 -8
- package/templates/agent-docs/local-first-mobile.md +426 -0
- package/templates/agent-docs/routing.md +94 -3
- package/templates/agent-docs/schema-driven-ui.md +12 -0
- package/templates/agent-docs/security.md +17 -0
- package/templates/agent-docs/templates/apibackends.md +210 -1
- package/templates/agent-docs/templates/appshells.md +144 -1
- package/templates/agent-docs/templates/mobile.md +60 -0
- package/templates/agent-docs/templates/overview.md +17 -7
- package/templates/agent-docs/whats-new.md +238 -158
- package/templates/agent-docs/workflows.md +376 -0
- package/templates/apps/api-ai/package.json +7 -7
- 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/.env.example +19 -0
- package/templates/apps/api-backend-sqlite/README.md +38 -0
- package/templates/apps/api-backend-sqlite/app.config.ts +35 -0
- package/templates/apps/api-backend-sqlite/database/schema.ts +54 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-sqlite/package.json +30 -0
- package/templates/apps/api-backend-sqlite/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-sqlite/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-sqlite/template.json +6 -0
- package/templates/apps/api-backend-sqlite/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-sqlite/tsconfig.json +5 -0
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/README.md +104 -0
- package/templates/apps/api-cms/actions/content.get.action.server.ts +27 -0
- package/templates/apps/api-cms/actions/content.get.action.ts +19 -0
- package/templates/apps/api-cms/actions/content.types.action.server.ts +26 -0
- package/templates/apps/api-cms/actions/content.types.action.ts +40 -0
- package/templates/apps/api-cms/actions/me.action.server.ts +18 -0
- package/templates/apps/api-cms/actions/me.action.ts +16 -0
- package/templates/apps/api-cms/app.config.ts +61 -0
- package/templates/apps/api-cms/content/blogPost.contentType.ts +39 -0
- package/templates/apps/api-cms/content/index.ts +18 -0
- package/templates/apps/api-cms/content/page.contentType.ts +24 -0
- package/templates/apps/api-cms/database/schema.ts +64 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.server.ts +19 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.ts +15 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.server.ts +36 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +32 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.server.ts +19 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +11 -0
- package/templates/apps/api-cms/package.json +32 -0
- package/templates/apps/api-cms/queries/content.list.query.server.ts +0 -0
- package/templates/apps/api-cms/queries/content.list.query.ts +27 -0
- package/templates/apps/api-cms/template.json +6 -0
- package/templates/apps/api-cms/tests/content.descriptors.test.ts +64 -0
- package/templates/apps/api-cms/tests/content.write.test.ts +85 -0
- package/templates/apps/api-cms/tsconfig.json +5 -0
- package/templates/apps/api-collab/README.md +70 -0
- package/templates/apps/api-collab/app.config.ts +28 -0
- package/templates/apps/api-collab/database/schema.ts +63 -0
- package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +19 -0
- package/templates/apps/api-collab/mutations/documents.create.mutation.ts +36 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +20 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +30 -0
- package/templates/apps/api-collab/package.json +30 -0
- package/templates/apps/api-collab/queries/documents.query.server.ts +16 -0
- package/templates/apps/api-collab/queries/documents.query.ts +23 -0
- package/templates/apps/api-collab/template.json +6 -0
- package/templates/apps/api-collab/tests/documents.setBody.test.ts +89 -0
- package/templates/apps/api-collab/tsconfig.json +5 -0
- 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-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/README.md +103 -0
- package/templates/apps/api-saas-starter/actions/me.action.server.ts +18 -0
- package/templates/apps/api-saas-starter/actions/me.action.ts +20 -0
- package/templates/apps/api-saas-starter/app.config.ts +87 -0
- package/templates/apps/api-saas-starter/database/schema.ts +57 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.server.ts +26 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +18 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.server.ts +29 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +18 -0
- package/templates/apps/api-saas-starter/package.json +32 -0
- package/templates/apps/api-saas-starter/queries/invites.list.query.server.ts +14 -0
- package/templates/apps/api-saas-starter/queries/invites.list.query.ts +17 -0
- package/templates/apps/api-saas-starter/queries/projects.list.query.server.ts +14 -0
- package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -0
- package/templates/apps/api-saas-starter/template.json +6 -0
- package/templates/apps/api-saas-starter/tests/projects.create.test.ts +62 -0
- package/templates/apps/api-saas-starter/tests/session.test.ts +33 -0
- package/templates/apps/api-saas-starter/tsconfig.json +5 -0
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/README.md +88 -0
- package/templates/apps/api-status/app.config.ts +36 -0
- package/templates/apps/api-status/authz.ts +33 -0
- package/templates/apps/api-status/database/schema.ts +70 -0
- package/templates/apps/api-status/mutations/components.create.mutation.server.ts +19 -0
- package/templates/apps/api-status/mutations/components.create.mutation.ts +16 -0
- package/templates/apps/api-status/mutations/incidents.create.mutation.server.ts +25 -0
- package/templates/apps/api-status/mutations/incidents.create.mutation.ts +22 -0
- package/templates/apps/api-status/mutations/incidents.resolve.mutation.server.ts +24 -0
- package/templates/apps/api-status/mutations/incidents.resolve.mutation.ts +19 -0
- package/templates/apps/api-status/mutations/incidents.update.mutation.server.ts +26 -0
- package/templates/apps/api-status/mutations/incidents.update.mutation.ts +21 -0
- package/templates/apps/api-status/package.json +30 -0
- package/templates/apps/api-status/queries/components.list.query.server.ts +14 -0
- package/templates/apps/api-status/queries/components.list.query.ts +13 -0
- package/templates/apps/api-status/queries/incidents.live.query.server.ts +14 -0
- package/templates/apps/api-status/queries/incidents.live.query.ts +20 -0
- package/templates/apps/api-status/queries/updates.list.query.server.ts +14 -0
- package/templates/apps/api-status/queries/updates.list.query.ts +17 -0
- package/templates/apps/api-status/template.json +6 -0
- package/templates/apps/api-status/tests/status.test.ts +70 -0
- package/templates/apps/api-status/tsconfig.json +5 -0
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/app.config.ts +1 -1
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-admin/src/locales/index.ts +1 -1
- package/templates/apps/frontend-app/app.config.ts +1 -1
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-app/src/locales/index.ts +1 -1
- package/templates/apps/frontend-app/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-app/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-auth/README.md +78 -0
- package/templates/apps/frontend-auth/app.config.ts +34 -0
- package/templates/apps/frontend-auth/package.json +32 -0
- package/templates/apps/frontend-auth/src/components/AuthShell.tsx +35 -0
- package/templates/apps/frontend-auth/src/components/PasswordStrength.tsx +33 -0
- package/templates/apps/frontend-auth/src/config.ts +11 -0
- package/templates/apps/frontend-auth/src/globals.css +105 -0
- package/templates/apps/frontend-auth/src/globals.d.ts +6 -0
- package/templates/apps/frontend-auth/src/lib/auth.ts +34 -0
- package/templates/apps/frontend-auth/src/lib/redirect.test.ts +24 -0
- package/templates/apps/frontend-auth/src/lib/redirect.ts +29 -0
- package/templates/apps/frontend-auth/src/locales/de.ts +66 -0
- package/templates/apps/frontend-auth/src/locales/en.ts +76 -0
- package/templates/apps/frontend-auth/src/locales/index.ts +14 -0
- package/templates/apps/frontend-auth/src/pages/forgot/page.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-auth/src/pages/login/page.test.tsx +53 -0
- package/templates/apps/frontend-auth/src/pages/login/page.tsx +67 -0
- package/templates/apps/frontend-auth/src/pages/logout/page.tsx +28 -0
- package/templates/apps/frontend-auth/src/pages/magic/page.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/page.tsx +26 -0
- package/templates/apps/frontend-auth/src/pages/reset/page.test.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/reset/page.tsx +63 -0
- package/templates/apps/frontend-auth/src/pages/signup/page.tsx +60 -0
- package/templates/apps/frontend-auth/src/pages/verify/page.tsx +50 -0
- package/templates/apps/frontend-auth/template.json +6 -0
- package/templates/apps/frontend-auth/tsconfig.json +5 -0
- package/templates/apps/frontend-blank/app.config.ts +1 -1
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-blank/src/locales/index.ts +1 -1
- package/templates/apps/frontend-blank/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-blank/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-cms/README.md +47 -0
- package/templates/apps/frontend-cms/app.config.ts +37 -0
- package/templates/apps/frontend-cms/package.json +33 -0
- package/templates/apps/frontend-cms/src/config.ts +8 -0
- package/templates/apps/frontend-cms/src/globals.css +105 -0
- package/templates/apps/frontend-cms/src/globals.d.ts +6 -0
- package/templates/apps/frontend-cms/src/lib/api.ts +58 -0
- package/templates/apps/frontend-cms/src/locales/de.ts +50 -0
- package/templates/apps/frontend-cms/src/locales/en.ts +55 -0
- package/templates/apps/frontend-cms/src/locales/index.ts +14 -0
- package/templates/apps/frontend-cms/src/pages/(app)/error.tsx +18 -0
- package/templates/apps/frontend-cms/src/pages/(app)/layout.test.tsx +31 -0
- package/templates/apps/frontend-cms/src/pages/(app)/layout.tsx +62 -0
- package/templates/apps/frontend-cms/src/pages/(app)/not-found.tsx +14 -0
- package/templates/apps/frontend-cms/src/pages/(app)/page.test.tsx +110 -0
- package/templates/apps/frontend-cms/src/pages/(app)/page.tsx +159 -0
- package/templates/apps/frontend-cms/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-cms/src/pages/login/page.test.tsx +58 -0
- package/templates/apps/frontend-cms/src/pages/login/page.tsx +93 -0
- package/templates/apps/frontend-cms/template.json +6 -0
- package/templates/apps/frontend-cms/tsconfig.json +5 -0
- package/templates/apps/frontend-collab/README.md +80 -0
- package/templates/apps/frontend-collab/app.config.ts +38 -0
- package/templates/apps/frontend-collab/package.json +33 -0
- package/templates/apps/frontend-collab/src/locales/de.ts +29 -0
- package/templates/apps/frontend-collab/src/locales/en.ts +31 -0
- package/templates/apps/frontend-collab/src/locales/index.ts +15 -0
- package/templates/apps/frontend-collab/src/pages/layout.tsx +39 -0
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +150 -0
- package/templates/apps/frontend-collab/src/pages/page.tsx +186 -0
- package/templates/apps/frontend-collab/template.json +6 -0
- package/templates/apps/frontend-collab/tsconfig.json +11 -0
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/app.config.ts +1 -1
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-dashboard/src/locales/index.ts +1 -1
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/README.md +71 -0
- package/templates/apps/frontend-portal/app.config.ts +37 -0
- package/templates/apps/frontend-portal/package.json +32 -0
- package/templates/apps/frontend-portal/src/config.ts +8 -0
- package/templates/apps/frontend-portal/src/globals.css +93 -0
- package/templates/apps/frontend-portal/src/globals.d.ts +6 -0
- package/templates/apps/frontend-portal/src/lib/api.ts +62 -0
- package/templates/apps/frontend-portal/src/locales/de.ts +86 -0
- package/templates/apps/frontend-portal/src/locales/en.ts +93 -0
- package/templates/apps/frontend-portal/src/locales/index.ts +14 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.test.tsx +71 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.tsx +124 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.test.tsx +86 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.tsx +97 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/error.tsx +19 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/invoices/page.tsx +45 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/layout.test.tsx +31 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/layout.tsx +63 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/not-found.tsx +15 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/page.tsx +39 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/profile/page.tsx +79 -0
- package/templates/apps/frontend-portal/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-portal/src/pages/login/page.test.tsx +58 -0
- package/templates/apps/frontend-portal/src/pages/login/page.tsx +96 -0
- package/templates/apps/frontend-portal/template.json +6 -0
- package/templates/apps/frontend-portal/tsconfig.json +5 -0
- package/templates/apps/frontend-saas/README.md +73 -0
- package/templates/apps/frontend-saas/app.config.ts +46 -0
- package/templates/apps/frontend-saas/package.json +32 -0
- package/templates/apps/frontend-saas/src/config.ts +8 -0
- package/templates/apps/frontend-saas/src/globals.css +85 -0
- package/templates/apps/frontend-saas/src/globals.d.ts +6 -0
- package/templates/apps/frontend-saas/src/lib/api.ts +47 -0
- package/templates/apps/frontend-saas/src/locales/de.ts +72 -0
- package/templates/apps/frontend-saas/src/locales/en.ts +81 -0
- package/templates/apps/frontend-saas/src/locales/index.ts +14 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/layout.tsx +30 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.test.tsx +58 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.tsx +96 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/page.tsx +27 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +72 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/error.tsx +20 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/layout.test.tsx +32 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/layout.tsx +69 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/not-found.tsx +17 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +113 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +76 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +73 -0
- package/templates/apps/frontend-saas/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-saas/template.json +6 -0
- package/templates/apps/frontend-saas/tsconfig.json +5 -0
- package/templates/apps/frontend-spa/app.config.ts +1 -1
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-spa/src/locales/index.ts +1 -1
- package/templates/apps/frontend-spa/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-spa/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-ssr/app.config.ts +1 -1
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr/src/locales/index.ts +1 -1
- package/templates/apps/frontend-ssr/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-ssr/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-ssr-api/app.config.ts +1 -1
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-ssr-api/src/locales/index.ts +1 -1
- package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-status/README.md +51 -0
- package/templates/apps/frontend-status/app.config.ts +39 -0
- package/templates/apps/frontend-status/package.json +32 -0
- package/templates/apps/frontend-status/src/config.ts +7 -0
- package/templates/apps/frontend-status/src/globals.css +70 -0
- package/templates/apps/frontend-status/src/globals.d.ts +6 -0
- package/templates/apps/frontend-status/src/lib/status.ts +81 -0
- package/templates/apps/frontend-status/src/locales/de.ts +41 -0
- package/templates/apps/frontend-status/src/locales/en.ts +45 -0
- package/templates/apps/frontend-status/src/locales/index.ts +13 -0
- package/templates/apps/frontend-status/src/pages/layout.tsx +27 -0
- package/templates/apps/frontend-status/src/pages/page.test.tsx +123 -0
- package/templates/apps/frontend-status/src/pages/page.tsx +136 -0
- package/templates/apps/frontend-status/template.json +6 -0
- package/templates/apps/frontend-status/tsconfig.json +11 -0
- package/templates/apps/mobile-app/README.md +58 -0
- package/templates/apps/mobile-app/app.config.ts +33 -0
- package/templates/apps/mobile-app/babel.config.js +5 -0
- package/templates/apps/mobile-app/metro.config.js +20 -0
- package/templates/apps/mobile-app/package.json +33 -0
- package/templates/apps/mobile-app/src/app/_layout.tsx +61 -0
- package/templates/apps/mobile-app/src/app/index.tsx +62 -0
- package/templates/apps/mobile-app/src/app/orders/[id].tsx +19 -0
- package/templates/apps/mobile-app/src/app/settings.tsx +51 -0
- package/templates/apps/mobile-app/src/client.ts +74 -0
- package/templates/apps/mobile-app/src/lib/deeplinks.ts +39 -0
- package/templates/apps/mobile-app/src/lib/notifications.ts +31 -0
- package/templates/apps/mobile-app/src/lib/sync.ts +29 -0
- package/templates/apps/mobile-app/src/persistence.ts +38 -0
- package/templates/apps/mobile-app/template.json +6 -0
- package/templates/apps/mobile-app/tests/deeplinks.test.ts +32 -0
- package/templates/apps/mobile-app/tests/notifications.test.ts +26 -0
- package/templates/apps/mobile-app/tests/sync.test.ts +34 -0
- package/templates/apps/mobile-app/tsconfig.json +10 -0
- package/templates/apps/mobile-app/voltro.mobile.ts +11 -0
- package/templates/baselines/compose/docker-compose.prod.yml +15 -0
- package/templates/baselines/compose-mariadb/docker-compose.prod.yml +15 -0
- package/templates/patches/@effect__cluster@0.60.0.patch +6 -6
- package/dist/apiBuild-DgBS9ayv.js +0 -2
- package/dist/dbCommand-DvguqlzF.js +0 -2
- package/dist/fileConventions-3bffWssN.js +0 -30
- package/dist/frameworkTableAssembly-lrjZtk0G.js +0 -2
- package/dist/inspect-gt8bq-Tz.js +0 -2
- package/dist/manifestBuild-ifczArzr.js +0 -2
- package/dist/serveCommand-ZTn-dPFa.js +0 -1425
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Verify — URL `/verify?token=…`. The landing the magic-link / email-verification
|
|
2
|
+
// mail points at. On mount it consumes the token via POST
|
|
3
|
+
// /auth/magic-link/callback: a valid token signs the user in (session cookie
|
|
4
|
+
// set) and we redirect home; an invalid/expired one shows a retry path. The same
|
|
5
|
+
// page doubles as the email-verification confirmation.
|
|
6
|
+
import type { ReactNode } from 'react'
|
|
7
|
+
import { useEffect, useState } from 'react'
|
|
8
|
+
import type { PageMeta } from '@voltro/web'
|
|
9
|
+
import { T } from '@voltro/i18n'
|
|
10
|
+
import { AuthShell } from '../../components/AuthShell'
|
|
11
|
+
import { AUTH_HOME } from '../../config'
|
|
12
|
+
import { postAuth } from '../../lib/auth'
|
|
13
|
+
import { nextFromLocation, tokenFromLocation } from '../../lib/redirect'
|
|
14
|
+
import { getCatalog } from '../../locales'
|
|
15
|
+
|
|
16
|
+
export const renderMode = 'static' as const
|
|
17
|
+
|
|
18
|
+
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
19
|
+
title: getCatalog(locale)['meta.verify.title'],
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
type State = 'pending' | 'success' | 'failed' | 'noToken'
|
|
23
|
+
|
|
24
|
+
export default function Verify(): ReactNode {
|
|
25
|
+
const [state, setState] = useState<State>('pending')
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const token = tokenFromLocation()
|
|
29
|
+
if (!token) { setState('noToken'); return }
|
|
30
|
+
void postAuth('/auth/magic-link/callback', { token })
|
|
31
|
+
.then((res) => {
|
|
32
|
+
if (res.ok) {
|
|
33
|
+
setState('success')
|
|
34
|
+
window.location.assign(nextFromLocation(AUTH_HOME))
|
|
35
|
+
} else {
|
|
36
|
+
setState('failed')
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
.catch(() => setState('failed'))
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<AuthShell titleId="verify.title" footer={state !== 'pending' ? <a href="/login"><T id="verify.toLogin" /></a> : null}>
|
|
44
|
+
{state === 'pending' ? <p className="muted" role="status"><T id="verify.pending" /></p> : null}
|
|
45
|
+
{state === 'success' ? <p className="ok" role="status"><T id="verify.success" /></p> : null}
|
|
46
|
+
{state === 'failed' ? <p className="error-text" role="alert"><T id="verify.failed" /></p> : null}
|
|
47
|
+
{state === 'noToken' ? <p className="error-text" role="alert"><T id="verify.noToken" /></p> : null}
|
|
48
|
+
</AuthShell>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "frontend-auth",
|
|
3
|
+
"kind": "web",
|
|
4
|
+
"summary": "A drop-in auth surface that mounts in ANY project: sign-in, sign-up, forgot/reset-password, magic-link request + consume (email verification). Every page is a real form POSTing to the sibling api's /auth/* routes (@voltro/plugin-auth) with CSRF, plus an open-redirect-guarded `next=`. Bilingual (en/de). Pair with the api-auth backend; the standalone-subdomain-SSO layout is an opt-in note.",
|
|
5
|
+
"tags": ["web", "frontend", "auth", "login", "signup", "password-reset", "magic-link", "ssr", "csrf"]
|
|
6
|
+
}
|
|
@@ -22,7 +22,7 @@ export default {
|
|
|
22
22
|
// <I18nProvider> from these codes and loads the matching
|
|
23
23
|
// `src/locales/<code>.ts` catalog — so `<T id="…" />` / `useT()` /
|
|
24
24
|
// `useLocale()` work app-wide with no manual setup. The <LocaleSwitcher>
|
|
25
|
-
// in the layout writes the `voltro:
|
|
25
|
+
// in the layout writes the `voltro:locale` cookie + reloads. Grow the app to
|
|
26
26
|
// more languages by adding a code here + a `src/locales/<code>.ts`.
|
|
27
27
|
locales: ['en', 'de'] as const,
|
|
28
28
|
defaultLocale: 'en' as const,
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/client": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/i18n": "0.
|
|
19
|
-
"@voltro/ui-shadcn": "0.
|
|
20
|
-
"@voltro/web": "0.
|
|
15
|
+
"@voltro/cli": "0.30.0",
|
|
16
|
+
"@voltro/client": "0.30.0",
|
|
17
|
+
"@voltro/env": "0.30.0",
|
|
18
|
+
"@voltro/i18n": "0.30.0",
|
|
19
|
+
"@voltro/ui-shadcn": "0.30.0",
|
|
20
|
+
"@voltro/web": "0.30.0",
|
|
21
21
|
"react": "^19.0.0",
|
|
22
22
|
"react-dom": "^19.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/devtools": "0.
|
|
25
|
+
"@voltro/devtools": "0.30.0",
|
|
26
26
|
"@types/react": "^19.0.0",
|
|
27
27
|
"@types/react-dom": "^19.0.0",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Catalog lookup OUTSIDE React — for a page's `meta({ locale })` locale-aware
|
|
2
2
|
// <title>. This app is cookie-i18n (no `/<locale>` URL prefix), so @voltro/web
|
|
3
|
-
// hands `meta` the active locale from the `voltro:
|
|
3
|
+
// hands `meta` the active locale from the `voltro:locale` cookie. Inside React,
|
|
4
4
|
// use `<T>` / `useT()` via the framework's auto-wired <I18nProvider> instead.
|
|
5
5
|
|
|
6
6
|
import en from './en'
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// to control the dark class.
|
|
8
8
|
//
|
|
9
9
|
// The app is bilingual (see `locales` in app.config.ts): <LocaleSwitcher>
|
|
10
|
-
// writes the `voltro:
|
|
10
|
+
// writes the `voltro:locale` cookie + reloads so the framework re-renders in
|
|
11
11
|
// the chosen language.
|
|
12
12
|
|
|
13
13
|
import type { ReactNode } from 'react'
|
|
@@ -10,7 +10,7 @@ import { T } from '@voltro/i18n'
|
|
|
10
10
|
import { getCatalog } from '../locales'
|
|
11
11
|
|
|
12
12
|
// Locale-aware tab title: @voltro/web resolves meta({ locale }) from the
|
|
13
|
-
// active locale (the `voltro:
|
|
13
|
+
// active locale (the `voltro:locale` cookie in this cookie-i18n app).
|
|
14
14
|
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
15
15
|
title: getCatalog(locale)['meta.home.title'],
|
|
16
16
|
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# {{capProjectName}} {{capAppName}} — CMS editor
|
|
2
|
+
|
|
3
|
+
The editor UI for the **api-cms** backend. It signs editors in, gates the app
|
|
4
|
+
behind that session, and generates a content form per type by INTROSPECTING the
|
|
5
|
+
api — no content-type shape is hardcoded in the browser. Bilingual (en/de).
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
voltro init my-cms --api=api-cms --web=frontend-cms
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## How it works
|
|
12
|
+
|
|
13
|
+
- **`/login`** — a static sign-in page that POSTs to the api's `/auth/*` routes
|
|
14
|
+
(CSRF token first), which set the HttpOnly session cookie.
|
|
15
|
+
- **`(app)/layout.tsx`** — the SSR auth gate. Its loader asks the api's
|
|
16
|
+
`session.me` server-side and RedirectErrors an anonymous visitor to `/login`.
|
|
17
|
+
- **`(app)/page.tsx`** — the editor. It fetches the registered content types
|
|
18
|
+
(`content.types`) and renders `<ContentForm>` (from `@voltro/cms/web`) for the
|
|
19
|
+
selected type — the widget per field is driven entirely by the field's
|
|
20
|
+
`editorHint`. It lists a type's drafts live (`content.list`), saves them
|
|
21
|
+
(`content.saveDraft` — validation comes back as `{ ok: false, violations }` and
|
|
22
|
+
is shown inline), and publishes / unpublishes.
|
|
23
|
+
|
|
24
|
+
Adding a content type on the api needs **zero** changes here — it appears as a
|
|
25
|
+
new tab automatically.
|
|
26
|
+
|
|
27
|
+
## Files
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
app.config.ts web app; apis.cms → @{{projectName}}/api
|
|
31
|
+
src/pages/
|
|
32
|
+
layout.tsx root layout (stylesheet)
|
|
33
|
+
login/page.tsx(.test) sign-in / sign-up form (POST /auth/*)
|
|
34
|
+
(app)/layout.tsx(.test) editor shell + SSR auth gate (session.me)
|
|
35
|
+
(app)/page.tsx(.test) the introspecting editor (<ContentForm>)
|
|
36
|
+
(app)/error.tsx · not-found.tsx scoped boundaries
|
|
37
|
+
src/lib/api.ts wire shapes + toContentFormType()
|
|
38
|
+
src/locales/{en,de}.ts bilingual catalogs (parity-enforced)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Customising
|
|
42
|
+
|
|
43
|
+
- **Rich text** — the default `richText` widget is a textarea placeholder. Swap a
|
|
44
|
+
real editor (e.g. TipTap) via `<ContentForm widgets={{ richText: MyEditor }} />`
|
|
45
|
+
without forking the renderer.
|
|
46
|
+
- **Media** — a `Schema.Media()` field renders a placeholder; wire an upload with
|
|
47
|
+
`@voltro/plugin-storage` + a custom `media` widget.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Web app for the {{projectName}} project — the CMS EDITOR that drives the
|
|
2
|
+
// sibling `api-cms` backend.
|
|
3
|
+
//
|
|
4
|
+
// Two areas:
|
|
5
|
+
// - `/login` — a static sign-in page that POSTs to the api's `/auth/*` routes.
|
|
6
|
+
// - The authed editor in the SILENT route group `src/pages/(app)/`. Its
|
|
7
|
+
// `layout.tsx` loader is the auth gate: server-side it asks the api's
|
|
8
|
+
// `session.me` and RedirectErrors an anonymous visitor to /login. Its pages
|
|
9
|
+
// are `renderMode: 'ssr'` so the gate runs per request.
|
|
10
|
+
//
|
|
11
|
+
// The editor fetches the registered content types (`content.types`) and renders
|
|
12
|
+
// a `<ContentForm>` per type from @voltro/cms — no content-type file is imported
|
|
13
|
+
// into the browser bundle.
|
|
14
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
15
|
+
|
|
16
|
+
// Public, build-time env (baked into the bundle). Never a secret in the browser.
|
|
17
|
+
export const env = defineEnv({
|
|
18
|
+
VOLTRO_PUBLIC_APP_NAME: envVar.string({ access: 'public', default: '{{capProjectName}} CMS' }),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
type: 'web' as const,
|
|
23
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
24
|
+
port: {{port}},
|
|
25
|
+
theme: 'system' as const,
|
|
26
|
+
env,
|
|
27
|
+
// Bilingual by default (cookie-driven <I18nProvider>, SSR too). Add a code +
|
|
28
|
+
// `src/locales/<code>.ts` to grow the app.
|
|
29
|
+
locales: ['en', 'de'] as const,
|
|
30
|
+
defaultLocale: 'en' as const,
|
|
31
|
+
// The api this editor consumes. The lookup key ('cms') is what the hooks pass:
|
|
32
|
+
// `useAction('cms', 'content.types')`, `useSubscription('cms', 'content.list')`.
|
|
33
|
+
// Codegen pulls its typed rpc surface (content.* + the plugin's /auth).
|
|
34
|
+
apis: {
|
|
35
|
+
cms: { package: '@{{projectName}}/api' },
|
|
36
|
+
},
|
|
37
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{projectName}}/{{appName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "voltro dev .",
|
|
8
|
+
"lint": "voltro doctor .",
|
|
9
|
+
"build": "voltro build .",
|
|
10
|
+
"start": "voltro start .",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"test": "voltro test"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@{{projectName}}/api": "0.30.0",
|
|
16
|
+
"@voltro/cli": "0.30.0",
|
|
17
|
+
"@voltro/client": "0.30.0",
|
|
18
|
+
"@voltro/cms": "0.30.0",
|
|
19
|
+
"@voltro/env": "0.30.0",
|
|
20
|
+
"@voltro/i18n": "0.30.0",
|
|
21
|
+
"@voltro/ui-shadcn": "0.30.0",
|
|
22
|
+
"@voltro/web": "0.30.0",
|
|
23
|
+
"react": "^19.0.0",
|
|
24
|
+
"react-dom": "^19.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@voltro/devtools": "0.30.0",
|
|
28
|
+
"@types/react": "^19.0.0",
|
|
29
|
+
"@types/react-dom": "^19.0.0",
|
|
30
|
+
"typescript": "^6.0.3",
|
|
31
|
+
"vitest": "^4.1.10"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The brand name shown in the marketing header + dashboard sidebar.
|
|
2
|
+
//
|
|
3
|
+
// A plain constant — not `publicEnv` — so the template typechecks BEFORE
|
|
4
|
+
// `voltro dev` generates the env types. `VOLTRO_PUBLIC_APP_NAME` is still
|
|
5
|
+
// declared in app.config.ts (validated + in the env manifest); to read it typed
|
|
6
|
+
// instead, after one `voltro dev` swap this for
|
|
7
|
+
// `publicEnv.VOLTRO_PUBLIC_APP_NAME` from '@voltro/env/public'.
|
|
8
|
+
export const APP_NAME = '{{capProjectName}}'
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/* Plain CSS — no Tailwind, so there's no @source scanner to configure. */
|
|
2
|
+
:root {
|
|
3
|
+
--bg: #ffffff;
|
|
4
|
+
--fg: #0a0a0a;
|
|
5
|
+
--muted: #6b7280;
|
|
6
|
+
--accent: #4f46e5;
|
|
7
|
+
--danger: #dc2626;
|
|
8
|
+
--border: #e5e7eb;
|
|
9
|
+
--panel: #f9fafb;
|
|
10
|
+
}
|
|
11
|
+
:root.dark {
|
|
12
|
+
--bg: #0a0a0a;
|
|
13
|
+
--fg: #fafafa;
|
|
14
|
+
--muted: #9ca3af;
|
|
15
|
+
--accent: #818cf8;
|
|
16
|
+
--danger: #f87171;
|
|
17
|
+
--border: #1f2937;
|
|
18
|
+
--panel: #111827;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
* { box-sizing: border-box; }
|
|
22
|
+
body {
|
|
23
|
+
margin: 0;
|
|
24
|
+
background: var(--bg);
|
|
25
|
+
color: var(--fg);
|
|
26
|
+
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
27
|
+
line-height: 1.55;
|
|
28
|
+
}
|
|
29
|
+
a { color: inherit; }
|
|
30
|
+
.muted { color: var(--muted); }
|
|
31
|
+
.spacer { flex: 1; }
|
|
32
|
+
|
|
33
|
+
/* Buttons + inputs */
|
|
34
|
+
.btn {
|
|
35
|
+
display: inline-block; cursor: pointer; border: 0; border-radius: 8px;
|
|
36
|
+
background: var(--accent); color: #fff; padding: 0.55rem 1rem;
|
|
37
|
+
font: inherit; font-weight: 600; text-decoration: none;
|
|
38
|
+
}
|
|
39
|
+
.btn:disabled { opacity: 0.6; cursor: default; }
|
|
40
|
+
.btn.ghost { background: transparent; color: var(--accent); border: 1px solid var(--border); }
|
|
41
|
+
.link { background: none; border: 0; color: var(--accent); cursor: pointer; font: inherit; padding: 0; }
|
|
42
|
+
input {
|
|
43
|
+
font: inherit; padding: 0.5rem 0.65rem; border: 1px solid var(--border);
|
|
44
|
+
border-radius: 8px; background: var(--bg); color: var(--fg); min-width: 0;
|
|
45
|
+
}
|
|
46
|
+
.error-text { color: var(--danger); }
|
|
47
|
+
|
|
48
|
+
/* Marketing */
|
|
49
|
+
.marketing header {
|
|
50
|
+
display: flex; align-items: center; gap: 1rem;
|
|
51
|
+
padding: 1rem 1.5rem; border-bottom: 1px solid var(--border);
|
|
52
|
+
}
|
|
53
|
+
.marketing .brand { font-weight: 700; text-decoration: none; }
|
|
54
|
+
.hero { max-width: 40rem; margin: 0 auto; padding: 3rem 1.5rem; }
|
|
55
|
+
.hero h1 { margin: 0 0 0.75rem; }
|
|
56
|
+
.auth-form { display: flex; flex-direction: column; gap: 0.85rem; max-width: 22rem; margin-top: 1.25rem; }
|
|
57
|
+
.auth-form label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.9rem; }
|
|
58
|
+
|
|
59
|
+
/* Dashboard shell */
|
|
60
|
+
.dash { display: grid; grid-template-columns: 15rem 1fr; min-height: 100vh; }
|
|
61
|
+
.dash aside { border-right: 1px solid var(--border); padding: 1.25rem; background: var(--panel); }
|
|
62
|
+
.dash aside .brand { font-weight: 700; margin-bottom: 1rem; }
|
|
63
|
+
.dash nav { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
64
|
+
.dash nav a { text-decoration: none; color: var(--muted); padding: 0.35rem 0; }
|
|
65
|
+
.dash nav a[aria-current="page"] { color: var(--fg); font-weight: 600; }
|
|
66
|
+
.dash main { padding: 1.5rem 2rem; }
|
|
67
|
+
.topbar { display: flex; align-items: center; margin-bottom: 1rem; }
|
|
68
|
+
@media (max-width: 40rem) { .dash { grid-template-columns: 1fr; } .dash aside { border-right: 0; border-bottom: 1px solid var(--border); } }
|
|
69
|
+
|
|
70
|
+
/* Bits */
|
|
71
|
+
.row { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; margin-top: 0.5rem; }
|
|
72
|
+
.empty { color: var(--muted); margin-top: 1.5rem; }
|
|
73
|
+
.cards { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); gap: 0.75rem; }
|
|
74
|
+
.card { border: 1px solid var(--border); border-radius: 10px; padding: 0.9rem 1rem; background: var(--panel); }
|
|
75
|
+
.card .value { font-weight: 600; }
|
|
76
|
+
.card .label { color: var(--muted); font-size: 0.85rem; margin-top: 0.2rem; }
|
|
77
|
+
.paywall { border: 1px solid var(--accent); border-radius: 12px; padding: 1rem 1.25rem; margin-top: 1.25rem; background: color-mix(in oklab, var(--accent) 8%, transparent); }
|
|
78
|
+
.paywall p { margin: 0.35rem 0 0.75rem; }
|
|
79
|
+
.facts { display: grid; grid-template-columns: max-content 1fr; gap: 0.4rem 1.5rem; margin-top: 1rem; }
|
|
80
|
+
.facts dt { color: var(--muted); }
|
|
81
|
+
.facts dd { margin: 0; font-weight: 600; }
|
|
82
|
+
.rows { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.4rem; }
|
|
83
|
+
.rows li { display: flex; justify-content: space-between; align-items: center; border: 1px solid var(--border); border-radius: 8px; padding: 0.6rem 0.9rem; background: var(--panel); }
|
|
84
|
+
.tag { font-size: 0.8rem; color: var(--muted); border: 1px solid var(--border); border-radius: 999px; padding: 0.05rem 0.55rem; }
|
|
85
|
+
code { background: color-mix(in oklab, var(--fg) 8%, transparent); padding: 0.1em 0.35em; border-radius: 4px; }
|
|
86
|
+
|
|
87
|
+
/* CMS editor shell */
|
|
88
|
+
.cms-top { display: flex; align-items: center; gap: 1rem; padding: 0.9rem 1.5rem; border-bottom: 1px solid var(--border); }
|
|
89
|
+
.cms-top .brand { font-weight: 700; }
|
|
90
|
+
.cms-grid { display: grid; grid-template-columns: 22rem 1fr; gap: 1.5rem; padding: 1.5rem; align-items: start; }
|
|
91
|
+
.cms-main { padding: 2rem 1.5rem; }
|
|
92
|
+
.cms-types { display: flex; flex-wrap: wrap; gap: 0.4rem; }
|
|
93
|
+
.chip { cursor: pointer; border: 1px solid var(--border); background: var(--panel); color: var(--fg); border-radius: 999px; padding: 0.25rem 0.7rem; font: inherit; }
|
|
94
|
+
.chip.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
95
|
+
.label { color: var(--muted); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.03em; margin: 0 0 0.5rem; }
|
|
96
|
+
.cms-list .rows li { gap: 0.5rem; }
|
|
97
|
+
.cms-list .rows .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
98
|
+
.cms-editor form [data-cms-form] { display: flex; flex-direction: column; gap: 0.75rem; }
|
|
99
|
+
.cms-editor [data-cms-field] { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
100
|
+
.cms-editor [data-cms-field] label { color: var(--muted); font-size: 0.85rem; }
|
|
101
|
+
.cms-editor input, .cms-editor textarea, .cms-editor select { font: inherit; padding: 0.5rem 0.65rem; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); color: var(--fg); width: 100%; }
|
|
102
|
+
.cms-editor textarea { min-height: 6rem; }
|
|
103
|
+
.violations { color: var(--danger); list-style: none; padding: 0; margin: 0.75rem 0 0; font-size: 0.9rem; }
|
|
104
|
+
.ok { color: var(--accent); margin-top: 0.75rem; }
|
|
105
|
+
@media (max-width: 48rem) { .cms-grid { grid-template-columns: 1fr; } }
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Ambient declarations.
|
|
2
|
+
//
|
|
3
|
+
// `declare module '*.css'`: TypeScript with `moduleResolution: Bundler` rejects
|
|
4
|
+
// a bare `import './globals.css'` unless the asset module is declared. Vite
|
|
5
|
+
// resolves it at build time; the declaration is type-only.
|
|
6
|
+
declare module '*.css'
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The shapes this editor reads off the api-cms backend, kept LOCAL so the pages
|
|
2
|
+
// are self-contained. They mirror the api's descriptor outputs
|
|
3
|
+
// (`content.types`, `content.list`, `content.saveDraft`).
|
|
4
|
+
import type { FieldSchema } from '@voltro/cms/web'
|
|
5
|
+
|
|
6
|
+
// The caller's resolved identity, from `session.me`. The SSR gate reads it.
|
|
7
|
+
export interface Identity {
|
|
8
|
+
readonly type: string
|
|
9
|
+
readonly id: string | null
|
|
10
|
+
readonly tenantId: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// One field of a content type, as `content.types` sends it (the wire subset of
|
|
14
|
+
// @voltro/cms's FieldSchema — enough for <ContentForm> to pick a widget).
|
|
15
|
+
export interface FieldDescriptor {
|
|
16
|
+
readonly name: string
|
|
17
|
+
readonly kind: string
|
|
18
|
+
readonly editorHint: string
|
|
19
|
+
readonly isOptional: boolean
|
|
20
|
+
readonly maxLength?: number
|
|
21
|
+
readonly literals?: ReadonlyArray<string>
|
|
22
|
+
readonly referenceType?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// One registered content type.
|
|
26
|
+
export interface ContentTypeDescriptor {
|
|
27
|
+
readonly name: string
|
|
28
|
+
readonly displayName: string
|
|
29
|
+
readonly pluralName: string
|
|
30
|
+
readonly fields: ReadonlyArray<FieldDescriptor>
|
|
31
|
+
readonly columns: ReadonlyArray<string>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A row in the editor's list (the `content.list` projection).
|
|
35
|
+
export interface ContentRow {
|
|
36
|
+
readonly id: string
|
|
37
|
+
readonly status: string
|
|
38
|
+
readonly title?: string
|
|
39
|
+
readonly slug?: string
|
|
40
|
+
readonly updatedAt: string | Date
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// `content.saveDraft` returns a typed RESULT union — success or per-field
|
|
44
|
+
// violations the form annotates. NOT a thrown error.
|
|
45
|
+
export type SaveResult =
|
|
46
|
+
| { readonly ok: true; readonly id: string; readonly status: string }
|
|
47
|
+
| { readonly ok: false; readonly violations: ReadonlyArray<{ field: string; rule: string; message: string }> }
|
|
48
|
+
|
|
49
|
+
// Turn the wire field list into the `{ fields }` shape <ContentForm> reads. The
|
|
50
|
+
// wire carries `kind`/`editorHint` as strings; ContentForm only reads
|
|
51
|
+
// `editorHint` + `maxLength`/`literals`, so the cast at this boundary is safe.
|
|
52
|
+
export const toContentFormType = (
|
|
53
|
+
type: ContentTypeDescriptor,
|
|
54
|
+
): { fields: Record<string, FieldSchema> } => {
|
|
55
|
+
const fields: Record<string, FieldSchema> = {}
|
|
56
|
+
for (const f of type.fields) fields[f.name] = f as unknown as FieldSchema
|
|
57
|
+
return { fields }
|
|
58
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// German catalog. Mirrors every key in `en.ts` — `defineLocale<typeof en>()`
|
|
2
|
+
// fails the build if a key is missing. Translate the prose only; keep ICU
|
|
3
|
+
// placeholders (`{type}`) identical to en.ts.
|
|
4
|
+
import { defineLocale } from '@voltro/i18n'
|
|
5
|
+
import en from './en'
|
|
6
|
+
|
|
7
|
+
export default defineLocale<typeof en>()({
|
|
8
|
+
'meta.login.title': 'Anmelden',
|
|
9
|
+
'meta.editor.title': 'Inhalte',
|
|
10
|
+
|
|
11
|
+
'auth.signIn.heading': 'Beim CMS anmelden',
|
|
12
|
+
'auth.signUp.heading': 'Redakteurskonto erstellen',
|
|
13
|
+
'auth.email': 'E-Mail',
|
|
14
|
+
'auth.password': 'Passwort',
|
|
15
|
+
'auth.signIn.submit': 'Anmelden',
|
|
16
|
+
'auth.signUp.submit': 'Konto erstellen',
|
|
17
|
+
'auth.toggle.toSignUp': 'Kein Konto? Erstelle eins',
|
|
18
|
+
'auth.toggle.toSignIn': 'Konto vorhanden? Anmelden',
|
|
19
|
+
'auth.pending': 'Bitte warten…',
|
|
20
|
+
'auth.error': 'Das hat nicht geklappt. Prüfe deine Angaben und versuche es erneut.',
|
|
21
|
+
|
|
22
|
+
'editor.brand': 'Inhalte',
|
|
23
|
+
'editor.signOut': 'Abmelden',
|
|
24
|
+
'lang.label': 'Sprache',
|
|
25
|
+
|
|
26
|
+
'editor.types.heading': 'Inhaltstypen',
|
|
27
|
+
'editor.list.heading': '{type}',
|
|
28
|
+
'editor.list.empty': 'Noch keine Entwürfe — erstelle einen mit dem Formular rechts.',
|
|
29
|
+
'editor.row.edit': 'Bearbeiten',
|
|
30
|
+
'editor.row.publish': 'Veröffentlichen',
|
|
31
|
+
'editor.row.unpublish': 'Zurückziehen',
|
|
32
|
+
'editor.status.draft': 'Entwurf',
|
|
33
|
+
'editor.status.published': 'veröffentlicht',
|
|
34
|
+
'editor.status.archived': 'archiviert',
|
|
35
|
+
|
|
36
|
+
'editor.form.new': 'Neu: {type}',
|
|
37
|
+
'editor.form.editing': 'Bearbeiten',
|
|
38
|
+
'editor.form.save': 'Entwurf speichern',
|
|
39
|
+
'editor.form.saving': 'Wird gespeichert…',
|
|
40
|
+
'editor.form.cancel': 'Abbrechen',
|
|
41
|
+
'editor.form.saved': 'Entwurf gespeichert.',
|
|
42
|
+
'editor.form.invalid': 'Bitte korrigiere die markierten Felder.',
|
|
43
|
+
'editor.form.loading': 'Typen werden geladen…',
|
|
44
|
+
|
|
45
|
+
'error.heading': 'Etwas ist schiefgelaufen',
|
|
46
|
+
'error.retry': 'Erneut versuchen',
|
|
47
|
+
'notFound.heading': 'Nicht gefunden',
|
|
48
|
+
'notFound.body': 'Diese Seite existiert nicht.',
|
|
49
|
+
'notFound.back': '← Zurück zu den Inhalten',
|
|
50
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Base catalog — the source of truth for this app's UI strings. Every key here
|
|
2
|
+
// MUST be mirrored in `de.ts`; `defineLocale<typeof en>()` enforces that parity
|
|
3
|
+
// at build time. Messages use ICU: `{name}` placeholders + `<code>…</code>` tags.
|
|
4
|
+
import { defineCatalog } from '@voltro/i18n'
|
|
5
|
+
|
|
6
|
+
export default defineCatalog({
|
|
7
|
+
// Browser tab titles — locale-aware via each page's `meta({ locale })`.
|
|
8
|
+
'meta.login.title': 'Sign in',
|
|
9
|
+
'meta.editor.title': 'Content',
|
|
10
|
+
|
|
11
|
+
// ---- Auth form (/login) ----
|
|
12
|
+
'auth.signIn.heading': 'Sign in to the CMS',
|
|
13
|
+
'auth.signUp.heading': 'Create an editor account',
|
|
14
|
+
'auth.email': 'Email',
|
|
15
|
+
'auth.password': 'Password',
|
|
16
|
+
'auth.signIn.submit': 'Sign in',
|
|
17
|
+
'auth.signUp.submit': 'Create account',
|
|
18
|
+
'auth.toggle.toSignUp': 'No account? Create one',
|
|
19
|
+
'auth.toggle.toSignIn': 'Have an account? Sign in',
|
|
20
|
+
'auth.pending': 'Please wait…',
|
|
21
|
+
'auth.error': 'That didn’t work. Check your details and try again.',
|
|
22
|
+
|
|
23
|
+
// ---- Editor shell ----
|
|
24
|
+
'editor.brand': 'Content',
|
|
25
|
+
'editor.signOut': 'Sign out',
|
|
26
|
+
'lang.label': 'Language',
|
|
27
|
+
|
|
28
|
+
// ---- Type list + rows ----
|
|
29
|
+
'editor.types.heading': 'Content types',
|
|
30
|
+
'editor.list.heading': '{type}',
|
|
31
|
+
'editor.list.empty': 'No drafts yet — create one with the form on the right.',
|
|
32
|
+
'editor.row.edit': 'Edit',
|
|
33
|
+
'editor.row.publish': 'Publish',
|
|
34
|
+
'editor.row.unpublish': 'Unpublish',
|
|
35
|
+
'editor.status.draft': 'draft',
|
|
36
|
+
'editor.status.published': 'published',
|
|
37
|
+
'editor.status.archived': 'archived',
|
|
38
|
+
|
|
39
|
+
// ---- Editing form ----
|
|
40
|
+
'editor.form.new': 'New {type}',
|
|
41
|
+
'editor.form.editing': 'Editing',
|
|
42
|
+
'editor.form.save': 'Save draft',
|
|
43
|
+
'editor.form.saving': 'Saving…',
|
|
44
|
+
'editor.form.cancel': 'Cancel',
|
|
45
|
+
'editor.form.saved': 'Draft saved.',
|
|
46
|
+
'editor.form.invalid': 'Please fix the highlighted fields.',
|
|
47
|
+
'editor.form.loading': 'Loading types…',
|
|
48
|
+
|
|
49
|
+
// ---- Scoped fallbacks ----
|
|
50
|
+
'error.heading': 'Something went wrong',
|
|
51
|
+
'error.retry': 'Try again',
|
|
52
|
+
'notFound.heading': 'Not found',
|
|
53
|
+
'notFound.body': 'That page doesn’t exist.',
|
|
54
|
+
'notFound.back': '← Back to content',
|
|
55
|
+
} as const)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Catalog lookup OUTSIDE React — for a page's `meta({ locale })` locale-aware
|
|
2
|
+
// <title>. This app is cookie-i18n (no `/<locale>` URL prefix), so @voltro/web
|
|
3
|
+
// hands `meta` the active locale from the `voltro:locale` cookie. Inside React use
|
|
4
|
+
// `<T>` / `useT()` via the framework's auto-wired <I18nProvider> instead.
|
|
5
|
+
import en from './en'
|
|
6
|
+
import de from './de'
|
|
7
|
+
|
|
8
|
+
// Both catalogs share `en`'s keys (defineLocale enforces the mirror), so a
|
|
9
|
+
// known-key lookup returns `string` — which is what `PageMeta.title` requires
|
|
10
|
+
// under exactOptionalPropertyTypes.
|
|
11
|
+
export type Messages = Record<keyof typeof en, string>
|
|
12
|
+
|
|
13
|
+
export const getCatalog = (locale?: string): Messages =>
|
|
14
|
+
(locale === 'de' ? de : en) as Messages
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Error boundary scoped to the editor subtree. A render error (or a loader
|
|
2
|
+
// rejection that ISN'T a RedirectError / NotFoundError) bubbles here. `reset`
|
|
3
|
+
// re-runs the failed segment.
|
|
4
|
+
import type { ReactNode } from 'react'
|
|
5
|
+
import type { ErrorBoundaryProps } from '@voltro/web'
|
|
6
|
+
import { T } from '@voltro/i18n'
|
|
7
|
+
|
|
8
|
+
export default function EditorError({ error, reset }: ErrorBoundaryProps): ReactNode {
|
|
9
|
+
return (
|
|
10
|
+
<main style={{ padding: '2rem' }}>
|
|
11
|
+
<h1><T id="error.heading" /></h1>
|
|
12
|
+
<p className="muted">{String(error)}</p>
|
|
13
|
+
<p style={{ marginTop: '1rem' }}>
|
|
14
|
+
<button type="button" className="btn" onClick={reset}><T id="error.retry" /></button>
|
|
15
|
+
</p>
|
|
16
|
+
</main>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The editor auth GATE, tested at the loader. Server-side (a `query` fn is
|
|
2
|
+
// present) it asks the api's `session.me`: an anonymous identity must throw
|
|
3
|
+
// RedirectError to /login; a `user` identity resolves with the tenant. On a
|
|
4
|
+
// client navigation (`query` undefined) it resolves permissively.
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
7
|
+
import { RedirectError } from '@voltro/web'
|
|
8
|
+
import { loader } from './layout'
|
|
9
|
+
import type { Identity } from '../../lib/api'
|
|
10
|
+
|
|
11
|
+
const queryReturning = (identity: Identity) =>
|
|
12
|
+
vi.fn(async (_tag: string, _input: unknown) => identity as unknown)
|
|
13
|
+
|
|
14
|
+
describe('editor gate — loader', () => {
|
|
15
|
+
test('redirects an anonymous caller to /login', async () => {
|
|
16
|
+
const query = queryReturning({ type: 'anonymous', id: null, tenantId: null })
|
|
17
|
+
await expect(loader({ query } as never)).rejects.toBeInstanceOf(RedirectError)
|
|
18
|
+
expect(query).toHaveBeenCalledWith('session.me', {})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('lets a signed-in user through and returns the tenant', async () => {
|
|
22
|
+
const query = queryReturning({ type: 'user', id: 'u_1', tenantId: 'acme' })
|
|
23
|
+
const data = await loader({ query } as never)
|
|
24
|
+
expect(data).toEqual({ tenantId: 'acme' })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('client navigation (no server query fn) resolves without re-gating', async () => {
|
|
28
|
+
const data = await loader({ query: undefined } as never)
|
|
29
|
+
expect(data).toEqual({ tenantId: null })
|
|
30
|
+
})
|
|
31
|
+
})
|