@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,58 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// The auth form. We assert config + that it renders email/password fields and
|
|
4
|
+
// toggles between sign-in and sign-up. The network submit (CSRF + POST /auth/*)
|
|
5
|
+
// is exercised against a running api by `voltro dev`, not unit-mocked here.
|
|
6
|
+
|
|
7
|
+
import { afterEach, describe, expect, test } from 'vitest'
|
|
8
|
+
import { act, createElement, type ReactNode } from 'react'
|
|
9
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
10
|
+
import { I18nProvider } from '@voltro/i18n'
|
|
11
|
+
import enCatalog from '../../locales/en'
|
|
12
|
+
|
|
13
|
+
const { default: Login, meta, renderMode } = await import('./page')
|
|
14
|
+
|
|
15
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
16
|
+
|
|
17
|
+
let container: HTMLDivElement
|
|
18
|
+
let root: Root
|
|
19
|
+
|
|
20
|
+
const render = (node: ReactNode): void => {
|
|
21
|
+
container = document.createElement('div')
|
|
22
|
+
document.body.appendChild(container)
|
|
23
|
+
act(() => {
|
|
24
|
+
root = createRoot(container)
|
|
25
|
+
root.render(
|
|
26
|
+
createElement(I18nProvider, { locale: 'en', messages: enCatalog, defaultLocale: 'en', children: node }),
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
if (root) act(() => root.unmount())
|
|
33
|
+
container?.remove()
|
|
34
|
+
document.body.innerHTML = ''
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('login — config', () => {
|
|
38
|
+
test('is a static page titled Sign in', () => {
|
|
39
|
+
expect(renderMode).toBe('static')
|
|
40
|
+
expect(meta({ locale: 'en' }).title).toBe('Sign in')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('login — form', () => {
|
|
45
|
+
test('renders email + password inputs and defaults to sign-in', () => {
|
|
46
|
+
render(createElement(Login))
|
|
47
|
+
expect(container.querySelector('input[type="email"]')).not.toBeNull()
|
|
48
|
+
expect(container.querySelector('input[type="password"]')).not.toBeNull()
|
|
49
|
+
expect(container.querySelector('h1')?.textContent).toBe('Sign in')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('toggling switches to the create-account heading', () => {
|
|
53
|
+
render(createElement(Login))
|
|
54
|
+
const toggle = container.querySelector('.link')!
|
|
55
|
+
act(() => { toggle.dispatchEvent(new MouseEvent('click', { bubbles: true })) })
|
|
56
|
+
expect(container.querySelector('h1')?.textContent).toBe('Create your account')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Sign in / sign up — URL `/login`. A REAL auth form: it POSTs to the api's
|
|
2
|
+
// `/auth/*` routes (contributed by @voltro/plugin-auth on the sibling api),
|
|
3
|
+
// which set the HttpOnly session cookie. State-changing auth routes are
|
|
4
|
+
// CSRF-protected, so we fetch a token first. On success we do a FULL navigation
|
|
5
|
+
// (`window.location.assign`) rather than a client one, so the dashboard's SSR
|
|
6
|
+
// auth gate re-runs with the freshly-set cookie.
|
|
7
|
+
import type { ReactNode } from 'react'
|
|
8
|
+
import { useState, type FormEvent } from 'react'
|
|
9
|
+
import type { PageMeta } from '@voltro/web'
|
|
10
|
+
import { T, useT } from '@voltro/i18n'
|
|
11
|
+
import { getCatalog } from '../../locales/index'
|
|
12
|
+
|
|
13
|
+
export const renderMode = 'static' as const
|
|
14
|
+
|
|
15
|
+
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
16
|
+
title: getCatalog(locale)['meta.login.title'],
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
type Mode = 'signIn' | 'signUp'
|
|
20
|
+
|
|
21
|
+
// One round-trip: get a CSRF token, then POST the credentials with it. Returns
|
|
22
|
+
// true on a 2xx (cookie set), false otherwise.
|
|
23
|
+
async function submitAuth(mode: Mode, email: string, password: string): Promise<boolean> {
|
|
24
|
+
const csrf = (await fetch('/auth/csrf').then((r) => r.json())) as { csrfToken: string }
|
|
25
|
+
const path = mode === 'signUp' ? '/auth/sign-up' : '/auth/sign-in'
|
|
26
|
+
const res = await fetch(path, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'content-type': 'application/json', 'x-csrf-token': csrf.csrfToken },
|
|
29
|
+
body: JSON.stringify({ email, password }),
|
|
30
|
+
})
|
|
31
|
+
return res.ok
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function Login(): ReactNode {
|
|
35
|
+
const [mode, setMode] = useState<Mode>('signIn')
|
|
36
|
+
const [email, setEmail] = useState('')
|
|
37
|
+
const [password, setPassword] = useState('')
|
|
38
|
+
const [pending, setPending] = useState(false)
|
|
39
|
+
const [failed, setFailed] = useState(false)
|
|
40
|
+
const emailLabel = useT('auth.email')
|
|
41
|
+
const passwordLabel = useT('auth.password')
|
|
42
|
+
|
|
43
|
+
const onSubmit = (e: FormEvent): void => {
|
|
44
|
+
e.preventDefault()
|
|
45
|
+
setPending(true)
|
|
46
|
+
setFailed(false)
|
|
47
|
+
void submitAuth(mode, email, password)
|
|
48
|
+
.then((ok) => {
|
|
49
|
+
if (ok) {
|
|
50
|
+
const from = new URLSearchParams(window.location.search).get('from') ?? '/'
|
|
51
|
+
window.location.assign(from)
|
|
52
|
+
} else {
|
|
53
|
+
setFailed(true)
|
|
54
|
+
setPending(false)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
.catch(() => {
|
|
58
|
+
setFailed(true)
|
|
59
|
+
setPending(false)
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<section className="hero">
|
|
65
|
+
<h1><T id={mode === 'signUp' ? 'auth.signUp.heading' : 'auth.signIn.heading'} /></h1>
|
|
66
|
+
<form className="auth-form" onSubmit={onSubmit}>
|
|
67
|
+
<label>
|
|
68
|
+
<span><T id="auth.email" /></span>
|
|
69
|
+
<input
|
|
70
|
+
type="email" required aria-label={emailLabel} value={email}
|
|
71
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
72
|
+
/>
|
|
73
|
+
</label>
|
|
74
|
+
<label>
|
|
75
|
+
<span><T id="auth.password" /></span>
|
|
76
|
+
<input
|
|
77
|
+
type="password" required aria-label={passwordLabel} value={password}
|
|
78
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
79
|
+
/>
|
|
80
|
+
</label>
|
|
81
|
+
{failed ? <p className="error-text"><T id="auth.error" /></p> : null}
|
|
82
|
+
<button type="submit" className="btn" disabled={pending}>
|
|
83
|
+
<T id={pending ? 'auth.pending' : mode === 'signUp' ? 'auth.signUp.submit' : 'auth.signIn.submit'} />
|
|
84
|
+
</button>
|
|
85
|
+
</form>
|
|
86
|
+
<p style={{ marginTop: '1rem' }}>
|
|
87
|
+
<button
|
|
88
|
+
type="button" className="link"
|
|
89
|
+
onClick={() => { setMode((m) => (m === 'signIn' ? 'signUp' : 'signIn')); setFailed(false) }}
|
|
90
|
+
>
|
|
91
|
+
<T id={mode === 'signIn' ? 'auth.toggle.toSignUp' : 'auth.toggle.toSignIn'} />
|
|
92
|
+
</button>
|
|
93
|
+
</p>
|
|
94
|
+
</section>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "frontend-portal",
|
|
3
|
+
"kind": "web",
|
|
4
|
+
"summary": "A lean customer billing + account portal over the api-saas-starter backend: an SSR auth gate (session.me), an overview, a billing page (plan + payment method via billing.portalUrl + plan change via billing.previewChange/changePlan), a paginated invoice list (billing.invoices → provider hostedUrl/pdfUrl), an API-keys page over the framework's /v1/api-keys REST surface (degrades cleanly when not enabled), and a profile page. Bilingual (en/de).",
|
|
5
|
+
"tags": ["web", "frontend", "portal", "billing", "invoices", "api-keys", "account", "ssr"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# {{capProjectName}} {{capAppName}} — the SaaS dashboard
|
|
2
|
+
|
|
3
|
+
The frontend that closes the **signup → paywall → app** loop, consuming the
|
|
4
|
+
`api-saas-starter` api. Bilingual (en/de), auth-gated, live.
|
|
5
|
+
|
|
6
|
+
Scaffold both halves together:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
voltro init my-saas --api=api-saas-starter --web=frontend-saas
|
|
10
|
+
voltro dev # api on :4000, this app on its configured port
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## The loop, as pages
|
|
14
|
+
|
|
15
|
+
| URL | What it does |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `/` | Marketing landing (static, zero-JS) |
|
|
18
|
+
| `/login` | Sign-in / sign-up form → POST `/auth/*` (CSRF handled), sets the session cookie |
|
|
19
|
+
| `/dashboard` | **Gate** (SSR loader asks `session.me`, redirects anonymous → /login) + live projects list; creating the 4th trips the **paywall** |
|
|
20
|
+
| `/dashboard/billing` | Live `billing.subscription`; **upgrade** via `billing.startCheckout`, **add seats** via `billing.changeSeats` |
|
|
21
|
+
| `/dashboard/team` | Live `invites.list`; **invite** via `invites.create` (spends a seat) |
|
|
22
|
+
|
|
23
|
+
## How the pieces connect
|
|
24
|
+
|
|
25
|
+
- **Auth gate** — `dashboard/layout.tsx` exports a `loader`. Server-side it calls
|
|
26
|
+
the api's `session.me` (forwarding the session cookie) and throws
|
|
27
|
+
`RedirectError('/login')` for an anonymous caller. The pages under it are
|
|
28
|
+
`renderMode: 'ssr'` so the gate runs per request.
|
|
29
|
+
- **Live reads** — `useSubscription('app', 'projects.list' | 'invites.list' |
|
|
30
|
+
'billing.subscription')`. First paint is server-rendered; the WebSocket
|
|
31
|
+
upgrades it live.
|
|
32
|
+
- **The paywall** — `projects.create` / `invites.create` are entitlement-gated
|
|
33
|
+
on the api. When quota runs out the mutation rejects with a typed
|
|
34
|
+
`EntitlementExceeded`; the page branches on `isEntitlementExceeded(err)` (see
|
|
35
|
+
`src/lib/api.ts`) and shows the upgrade CTA — never a raw error string.
|
|
36
|
+
- **Checkout + seats** — `useAction('app', 'billing.startCheckout' |
|
|
37
|
+
'billing.changeSeats')`. Under `voltro dev` the `mock` billing provider returns
|
|
38
|
+
a dev checkout URL; swap the api to `provider: 'stripe'` for real Stripe.
|
|
39
|
+
|
|
40
|
+
## i18n
|
|
41
|
+
|
|
42
|
+
Cookie-driven (no `/<locale>` URL prefix): `<LocaleSwitcher>` writes `voltro:locale`
|
|
43
|
+
and reloads. Every string is a key in `src/locales/en.ts`, mirrored in `de.ts`
|
|
44
|
+
(`defineLocale<typeof en>()` fails the build on a missing key). Add a language
|
|
45
|
+
with a new code in `app.config.ts` + a `src/locales/<code>.ts`.
|
|
46
|
+
|
|
47
|
+
## Files
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
src/pages/
|
|
51
|
+
(marketing)/ silent route group → `/` and `/login`
|
|
52
|
+
page.tsx landing (static)
|
|
53
|
+
login/page.tsx real auth form → /auth/*
|
|
54
|
+
dashboard/
|
|
55
|
+
layout.tsx SSR auth gate (session.me) + sidebar shell
|
|
56
|
+
page.tsx projects list + create + PAYWALL branch
|
|
57
|
+
billing/page.tsx subscription + upgrade (checkout) + seats
|
|
58
|
+
team/page.tsx invites list + create
|
|
59
|
+
error.tsx / not-found.tsx
|
|
60
|
+
src/lib/api.ts local row types + isEntitlementExceeded()
|
|
61
|
+
src/locales/{en,de}.ts bilingual catalogs
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Anti-patterns
|
|
65
|
+
|
|
66
|
+
- **String-matching the paywall.** Branch on the typed `_tag ===
|
|
67
|
+
'EntitlementExceeded'` (`isEntitlementExceeded`), not on a message — messages
|
|
68
|
+
are localized and change.
|
|
69
|
+
- **A `static` gated page.** The auth gate must run per request; a `static` page
|
|
70
|
+
can't redirect per-visitor and the build rejects a `RedirectError` there. Keep
|
|
71
|
+
the dashboard pages `renderMode: 'ssr'`.
|
|
72
|
+
- **Putting a secret in this app's env.** The browser bundle is public. Secrets
|
|
73
|
+
live on the api.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Web app for the {{projectName}} project — the SaaS DASHBOARD that closes the
|
|
2
|
+
// signup → paywall → app loop against the sibling `api-saas-starter` api.
|
|
3
|
+
//
|
|
4
|
+
// Two areas:
|
|
5
|
+
// - Public pages in a SILENT route group `src/pages/(marketing)/` — `/` and
|
|
6
|
+
// `/login` (the parens organise files + scope a layout WITHOUT adding a URL
|
|
7
|
+
// segment).
|
|
8
|
+
// - The authed app under `src/pages/dashboard/`. Its `layout.tsx` loader is
|
|
9
|
+
// the auth gate: server-side it asks the api's `session.me` and throws
|
|
10
|
+
// RedirectError to /login for an anonymous visitor. Those pages are
|
|
11
|
+
// `renderMode: 'ssr'` so the gate runs per request.
|
|
12
|
+
//
|
|
13
|
+
// The pages read the api live with `useSubscription` and write with
|
|
14
|
+
// `useMutation` / `useAction`; the auth forms POST to the api's `/auth/*`.
|
|
15
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
16
|
+
|
|
17
|
+
// Public, build-time env (baked into the bundle; read via `publicEnv`).
|
|
18
|
+
// Never a secret here — a secret belongs on the api, not the browser bundle.
|
|
19
|
+
export const env = defineEnv({
|
|
20
|
+
VOLTRO_PUBLIC_APP_NAME: envVar.string({ access: 'public', default: '{{capProjectName}}' }),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
type: 'web' as const,
|
|
25
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
26
|
+
port: {{port}},
|
|
27
|
+
// Follow the visitor's OS theme, set before first paint (no flash).
|
|
28
|
+
theme: 'system' as const,
|
|
29
|
+
env,
|
|
30
|
+
// Bilingual by default. The framework auto-wires a cookie-driven
|
|
31
|
+
// <I18nProvider> from these codes and loads the matching
|
|
32
|
+
// `src/locales/<code>.ts` catalog — so `<T id="…" />` / `useT()` /
|
|
33
|
+
// `useLocale()` work app-wide (SSR too). The <LocaleSwitcher> writes the
|
|
34
|
+
// `voltro:locale` cookie + reloads. Add a code + a `src/locales/<code>.ts` to
|
|
35
|
+
// grow the app to more languages.
|
|
36
|
+
locales: ['en', 'de'] as const,
|
|
37
|
+
defaultLocale: 'en' as const,
|
|
38
|
+
// The api this dashboard consumes. The NAME ('app') is the lookup key the
|
|
39
|
+
// hooks + the loader's `query` use: `useSubscription('app', 'projects.list')`,
|
|
40
|
+
// `useMutation('app', 'projects.create')`, `query('session.me', {})`. Its
|
|
41
|
+
// port is auto-discovered from the api's own app.config.ts, and codegen pulls
|
|
42
|
+
// its typed rpc surface (including the plugins' `billing.*` / `/auth`).
|
|
43
|
+
apis: {
|
|
44
|
+
app: { package: '@{{projectName}}/api' },
|
|
45
|
+
},
|
|
46
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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/env": "0.30.0",
|
|
19
|
+
"@voltro/i18n": "0.30.0",
|
|
20
|
+
"@voltro/ui-shadcn": "0.30.0",
|
|
21
|
+
"@voltro/web": "0.30.0",
|
|
22
|
+
"react": "^19.0.0",
|
|
23
|
+
"react-dom": "^19.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@voltro/devtools": "0.30.0",
|
|
27
|
+
"@types/react": "^19.0.0",
|
|
28
|
+
"@types/react-dom": "^19.0.0",
|
|
29
|
+
"typescript": "^6.0.3",
|
|
30
|
+
"vitest": "^4.1.10"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -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,85 @@
|
|
|
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; }
|
|
@@ -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,47 @@
|
|
|
1
|
+
// The row + payload shapes this dashboard reads off the api, kept LOCAL so the
|
|
2
|
+
// pages are self-contained. They mirror the api's descriptor outputs
|
|
3
|
+
// (`projects.list`, `invites.list`, `billing.subscription`); you can also
|
|
4
|
+
// `import type` them from the api package — a type-only cross-app import is
|
|
5
|
+
// browser-safe.
|
|
6
|
+
|
|
7
|
+
export interface Project {
|
|
8
|
+
readonly id: string
|
|
9
|
+
readonly name: string
|
|
10
|
+
readonly tenantId: string
|
|
11
|
+
readonly createdAt: string | Date
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Invite {
|
|
15
|
+
readonly id: string
|
|
16
|
+
readonly email: string
|
|
17
|
+
readonly status: string
|
|
18
|
+
readonly tenantId: string
|
|
19
|
+
readonly createdAt: string | Date
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// `billing.subscription` streams `{ subscription: … | null }` — null before the
|
|
23
|
+
// tenant has any subscription row (they're on the implicit free plan).
|
|
24
|
+
export interface SubscriptionState {
|
|
25
|
+
readonly subscription: {
|
|
26
|
+
readonly plan: string
|
|
27
|
+
readonly status: string
|
|
28
|
+
readonly quantity: number
|
|
29
|
+
readonly currentPeriodEnd: string | null
|
|
30
|
+
readonly cancelAt: string | null
|
|
31
|
+
} | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The caller's resolved identity, from `session.me`. The SSR gate reads it.
|
|
35
|
+
export interface Identity {
|
|
36
|
+
readonly type: string
|
|
37
|
+
readonly id: string | null
|
|
38
|
+
readonly tenantId: string | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The billing plugin merges a typed `EntitlementExceeded` into the wire-error
|
|
42
|
+
// union of any entitlement-gated mutation (projects.create / invites.create).
|
|
43
|
+
// This is how the paywall is detected — NOT by string-matching a message.
|
|
44
|
+
export const isEntitlementExceeded = (error: unknown): boolean =>
|
|
45
|
+
typeof error === 'object' &&
|
|
46
|
+
error !== null &&
|
|
47
|
+
(error as { _tag?: unknown })._tag === 'EntitlementExceeded'
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// German catalog. Mirrors every key in `en.ts` — `defineLocale<typeof en>()`
|
|
2
|
+
// fails the build if a key is missing or misspelled. Translate the prose only;
|
|
3
|
+
// keep ICU placeholders (`{when}`) and rich tags (`<code>`) identical to en.ts.
|
|
4
|
+
import { defineLocale } from '@voltro/i18n'
|
|
5
|
+
import en from './en'
|
|
6
|
+
|
|
7
|
+
export default defineLocale<typeof en>()({
|
|
8
|
+
'meta.landing.title': 'Willkommen',
|
|
9
|
+
'meta.landing.description': 'Ein vollständiger Voltro-SaaS-Starter — Auth, Plan-Paywall, Checkout, Sitze und Einladungen, durchgängig verdrahtet.',
|
|
10
|
+
'meta.login.title': 'Anmelden',
|
|
11
|
+
'meta.overview.title': 'Projekte',
|
|
12
|
+
'meta.billing.title': 'Abrechnung',
|
|
13
|
+
'meta.team.title': 'Team',
|
|
14
|
+
|
|
15
|
+
'nav.dashboard': 'Dashboard',
|
|
16
|
+
'nav.signIn': 'Anmelden',
|
|
17
|
+
'lang.label': 'Sprache',
|
|
18
|
+
'landing.heading': 'Liefere dein SaaS, nicht seine Rohrleitungen.',
|
|
19
|
+
'landing.body': 'Registrieren, in der App landen, an die Plan-Paywall stoßen, upgraden, Teammitglied einladen — der ganze Ablauf funktioniert bereits. Auth, Billing-Entitlements und Sitze sind mit dem <code>api-saas-starter</code>-Backend verdrahtet.',
|
|
20
|
+
'landing.cta': 'Loslegen →',
|
|
21
|
+
|
|
22
|
+
'auth.signIn.heading': 'Anmelden',
|
|
23
|
+
'auth.signUp.heading': 'Konto erstellen',
|
|
24
|
+
'auth.email': 'E-Mail',
|
|
25
|
+
'auth.password': 'Passwort',
|
|
26
|
+
'auth.signIn.submit': 'Anmelden',
|
|
27
|
+
'auth.signUp.submit': 'Konto erstellen',
|
|
28
|
+
'auth.toggle.toSignUp': 'Kein Konto? Erstelle eins',
|
|
29
|
+
'auth.toggle.toSignIn': 'Konto vorhanden? Anmelden',
|
|
30
|
+
'auth.pending': 'Bitte warten…',
|
|
31
|
+
'auth.error': 'Das hat nicht geklappt. Prüfe deine Angaben und versuche es erneut.',
|
|
32
|
+
|
|
33
|
+
'dash.nav.projects': 'Projekte',
|
|
34
|
+
'dash.nav.billing': 'Abrechnung',
|
|
35
|
+
'dash.nav.team': 'Team',
|
|
36
|
+
'dash.nav.backToSite': '← Zurück zur Seite',
|
|
37
|
+
|
|
38
|
+
'projects.heading': 'Projekte',
|
|
39
|
+
'projects.new.placeholder': 'Name des neuen Projekts',
|
|
40
|
+
'projects.new.submit': 'Projekt erstellen',
|
|
41
|
+
'projects.empty': 'Noch keine Projekte — erstelle oben dein erstes.',
|
|
42
|
+
'projects.created': 'Erstellt {when}',
|
|
43
|
+
'projects.paywall.title': 'Du hast dein Planlimit erreicht',
|
|
44
|
+
'projects.paywall.body': 'Der kostenlose Plan umfasst 3 Projekte. Upgrade auf Pro für unbegrenzte Projekte.',
|
|
45
|
+
'projects.paywall.cta': 'Pläne ansehen →',
|
|
46
|
+
|
|
47
|
+
'billing.heading': 'Abrechnung',
|
|
48
|
+
'billing.plan.label': 'Aktueller Plan',
|
|
49
|
+
'billing.plan.free': 'Kostenlos',
|
|
50
|
+
'billing.status.label': 'Status',
|
|
51
|
+
'billing.seats.label': 'Sitze',
|
|
52
|
+
'billing.upgrade.title': 'Upgrade auf Pro',
|
|
53
|
+
'billing.upgrade.body': 'Unbegrenzte Projekte und bis zu 10 Sitze.',
|
|
54
|
+
'billing.upgrade.cta': 'Upgraden →',
|
|
55
|
+
'billing.seats.add': 'Sitz hinzufügen',
|
|
56
|
+
'billing.pending': 'Wird ausgeführt…',
|
|
57
|
+
|
|
58
|
+
'team.heading': 'Team',
|
|
59
|
+
'team.invite.placeholder': 'kollege@example.com',
|
|
60
|
+
'team.invite.submit': 'Einladung senden',
|
|
61
|
+
'team.empty': 'Noch keine Einladungen.',
|
|
62
|
+
'team.paywall': 'Keine Sitze mehr frei. Füge auf der Abrechnungsseite einen Sitz hinzu und lade erneut ein.',
|
|
63
|
+
'team.status.pending': 'ausstehend',
|
|
64
|
+
'team.status.accepted': 'angenommen',
|
|
65
|
+
'team.status.revoked': 'widerrufen',
|
|
66
|
+
|
|
67
|
+
'error.heading': 'Etwas ist schiefgelaufen',
|
|
68
|
+
'error.retry': 'Erneut versuchen',
|
|
69
|
+
'notFound.heading': 'Nicht gefunden',
|
|
70
|
+
'notFound.body': 'Diese Seite existiert nicht.',
|
|
71
|
+
'notFound.back': '← Zurück zu den Projekten',
|
|
72
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
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, so a missing translation fails typecheck rather than silently
|
|
4
|
+
// falling back. Messages use ICU: `{name}` placeholders + rich `<code>…</code>`
|
|
5
|
+
// tags rendered by passing a matching function to `<T values={{ … }} />`.
|
|
6
|
+
import { defineCatalog } from '@voltro/i18n'
|
|
7
|
+
|
|
8
|
+
export default defineCatalog({
|
|
9
|
+
// Browser tab titles — locale-aware via each page's `meta({ locale })`.
|
|
10
|
+
'meta.landing.title': 'Welcome',
|
|
11
|
+
'meta.landing.description': 'A full Voltro SaaS starter — auth, a plan paywall, checkout, seats and invites, wired end to end.',
|
|
12
|
+
'meta.login.title': 'Sign in',
|
|
13
|
+
'meta.overview.title': 'Projects',
|
|
14
|
+
'meta.billing.title': 'Billing',
|
|
15
|
+
'meta.team.title': 'Team',
|
|
16
|
+
|
|
17
|
+
// ---- Marketing chrome + landing ----
|
|
18
|
+
'nav.dashboard': 'Dashboard',
|
|
19
|
+
'nav.signIn': 'Sign in',
|
|
20
|
+
'lang.label': 'Language',
|
|
21
|
+
'landing.heading': 'Ship your SaaS, not its plumbing.',
|
|
22
|
+
'landing.body': 'Sign up, land in the app, hit the plan paywall, upgrade, invite a teammate — the whole loop already works. Auth, billing entitlements and seats are wired to the <code>api-saas-starter</code> backend.',
|
|
23
|
+
'landing.cta': 'Get started →',
|
|
24
|
+
|
|
25
|
+
// ---- Auth form ((marketing)/login) ----
|
|
26
|
+
'auth.signIn.heading': 'Sign in',
|
|
27
|
+
'auth.signUp.heading': 'Create your account',
|
|
28
|
+
'auth.email': 'Email',
|
|
29
|
+
'auth.password': 'Password',
|
|
30
|
+
'auth.signIn.submit': 'Sign in',
|
|
31
|
+
'auth.signUp.submit': 'Create account',
|
|
32
|
+
'auth.toggle.toSignUp': 'No account? Create one',
|
|
33
|
+
'auth.toggle.toSignIn': 'Have an account? Sign in',
|
|
34
|
+
'auth.pending': 'Please wait…',
|
|
35
|
+
'auth.error': 'That didn’t work. Check your details and try again.',
|
|
36
|
+
|
|
37
|
+
// ---- Dashboard shell ----
|
|
38
|
+
'dash.nav.projects': 'Projects',
|
|
39
|
+
'dash.nav.billing': 'Billing',
|
|
40
|
+
'dash.nav.team': 'Team',
|
|
41
|
+
'dash.nav.backToSite': '← Back to site',
|
|
42
|
+
|
|
43
|
+
// ---- Projects page ----
|
|
44
|
+
'projects.heading': 'Projects',
|
|
45
|
+
'projects.new.placeholder': 'New project name',
|
|
46
|
+
'projects.new.submit': 'Create project',
|
|
47
|
+
'projects.empty': 'No projects yet — create your first one above.',
|
|
48
|
+
'projects.created': 'Created {when}',
|
|
49
|
+
'projects.paywall.title': 'You’ve hit your plan limit',
|
|
50
|
+
'projects.paywall.body': 'The free plan includes 3 projects. Upgrade to Pro for unlimited projects.',
|
|
51
|
+
'projects.paywall.cta': 'See plans →',
|
|
52
|
+
|
|
53
|
+
// ---- Billing page ----
|
|
54
|
+
'billing.heading': 'Billing',
|
|
55
|
+
'billing.plan.label': 'Current plan',
|
|
56
|
+
'billing.plan.free': 'Free',
|
|
57
|
+
'billing.status.label': 'Status',
|
|
58
|
+
'billing.seats.label': 'Seats',
|
|
59
|
+
'billing.upgrade.title': 'Upgrade to Pro',
|
|
60
|
+
'billing.upgrade.body': 'Unlimited projects and up to 10 seats.',
|
|
61
|
+
'billing.upgrade.cta': 'Upgrade →',
|
|
62
|
+
'billing.seats.add': 'Add a seat',
|
|
63
|
+
'billing.pending': 'Working…',
|
|
64
|
+
|
|
65
|
+
// ---- Team page ----
|
|
66
|
+
'team.heading': 'Team',
|
|
67
|
+
'team.invite.placeholder': 'teammate@example.com',
|
|
68
|
+
'team.invite.submit': 'Send invite',
|
|
69
|
+
'team.empty': 'No invites yet.',
|
|
70
|
+
'team.paywall': 'You’re out of seats. Add a seat on the Billing page, then invite again.',
|
|
71
|
+
'team.status.pending': 'pending',
|
|
72
|
+
'team.status.accepted': 'accepted',
|
|
73
|
+
'team.status.revoked': 'revoked',
|
|
74
|
+
|
|
75
|
+
// ---- Scoped fallbacks ----
|
|
76
|
+
'error.heading': 'Something went wrong',
|
|
77
|
+
'error.retry': 'Try again',
|
|
78
|
+
'notFound.heading': 'Not found',
|
|
79
|
+
'notFound.body': 'That page doesn’t exist.',
|
|
80
|
+
'notFound.back': '← Back to projects',
|
|
81
|
+
} 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
|