@voltro/cli 0.28.0 → 0.29.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.
Files changed (280) hide show
  1. package/CHANGELOG.md +278 -0
  2. package/THIRD-PARTY-NOTICES.md +88 -2
  3. package/dist/{apiBuild-eUM32r1u.js → apiBuild-BESZbTjA.js} +3 -3
  4. package/dist/apiBuild-C8d74T6g.js +2 -0
  5. package/dist/bin.js +3 -3
  6. package/dist/checkCommand-BPAtyWs8.js +2420 -0
  7. package/dist/checkCommand-DBvZvCh1.js +2 -0
  8. package/dist/cliRuntime-Oh517vCV.js +96 -0
  9. package/dist/{commands-CSkrUI1h.js → commands-82BDEktg.js} +6803 -8191
  10. package/dist/dbCommand-CaIVTp-a.js +2 -0
  11. package/dist/{dbCommand-CpYgmSw4.js → dbCommand-bCBbk6tz.js} +2 -2
  12. package/dist/{dev-CEZwJhmb.js → dev-Cg4BKLmi.js} +1801 -1612
  13. package/dist/{dev-DlBWWnJQ.js → dev-DeEJj5T4.js} +1 -1
  14. package/dist/fileConventions-Cof68_BL.js +33 -0
  15. package/dist/{frameworkTableAssembly-BwHU9Euq.js → frameworkTableAssembly-4Db40V23.js} +2 -2
  16. package/dist/frameworkTableAssembly-BTNirAAk.js +2 -0
  17. package/dist/index.js +1 -1
  18. package/dist/{inspect-CUCCzw2I.js → inspect-BMvQpwqy.js} +211 -190
  19. package/dist/inspect-DHeaMZak.js +2 -0
  20. package/dist/{inspectMetrics-BU90mvJN.js → inspectMetrics-DHYygE4s.js} +999 -961
  21. package/dist/{manifestBuild-BnzAxp2O.js → manifestBuild-C4R1EdHn.js} +1 -1
  22. package/dist/manifestBuild-Ck_90gZy.js +2 -0
  23. package/dist/sdkgen-CW5NbYF0.js +582 -0
  24. package/dist/serveCommand-BXl8mhZD.js +1489 -0
  25. package/dist/serveEntry.js +2 -2
  26. package/dist/{start-BGXIf6zT.js → start-CsCIaW4-.js} +282 -269
  27. package/dist/startEntry.js +2 -2
  28. package/package.json +17 -17
  29. package/templates/AGENTS.md +2 -1
  30. package/templates/agent-docs/_index.md +2 -1
  31. package/templates/agent-docs/_manifest.json +10 -1
  32. package/templates/agent-docs/ai.md +135 -0
  33. package/templates/agent-docs/cli.md +124 -6
  34. package/templates/agent-docs/configuration.md +32 -0
  35. package/templates/agent-docs/data.md +282 -0
  36. package/templates/agent-docs/database/migrations.md +47 -0
  37. package/templates/agent-docs/database/misc.md +57 -0
  38. package/templates/agent-docs/database/schema.md +3 -1
  39. package/templates/agent-docs/internationalization.md +161 -1
  40. package/templates/agent-docs/local-first-mobile.md +414 -0
  41. package/templates/agent-docs/routing.md +93 -2
  42. package/templates/agent-docs/schema-driven-ui.md +12 -0
  43. package/templates/agent-docs/templates/apibackends.md +1 -1
  44. package/templates/agent-docs/whats-new.md +162 -181
  45. package/templates/agent-docs/workflows.md +11 -0
  46. package/templates/apps/api-ai/package.json +7 -7
  47. package/templates/apps/api-auth/package.json +8 -8
  48. package/templates/apps/api-backend/package.json +7 -7
  49. package/templates/apps/api-backend-deactivation/package.json +7 -7
  50. package/templates/apps/api-backend-mail/package.json +8 -8
  51. package/templates/apps/api-backend-mariadb/package.json +9 -9
  52. package/templates/apps/api-backend-sqlite/.env.example +19 -0
  53. package/templates/apps/api-backend-sqlite/README.md +38 -0
  54. package/templates/apps/api-backend-sqlite/app.config.ts +35 -0
  55. package/templates/apps/api-backend-sqlite/database/schema.ts +54 -0
  56. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.server.ts +19 -0
  57. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +37 -0
  58. package/templates/apps/api-backend-sqlite/package.json +30 -0
  59. package/templates/apps/api-backend-sqlite/queries/notes.query.server.ts +14 -0
  60. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +20 -0
  61. package/templates/apps/api-backend-sqlite/template.json +6 -0
  62. package/templates/apps/api-backend-sqlite/tests/notes.create.test.ts +50 -0
  63. package/templates/apps/api-backend-sqlite/tsconfig.json +5 -0
  64. package/templates/apps/api-backend-storage/package.json +8 -8
  65. package/templates/apps/api-cms/README.md +104 -0
  66. package/templates/apps/api-cms/actions/content.get.action.server.ts +27 -0
  67. package/templates/apps/api-cms/actions/content.get.action.ts +19 -0
  68. package/templates/apps/api-cms/actions/content.types.action.server.ts +26 -0
  69. package/templates/apps/api-cms/actions/content.types.action.ts +40 -0
  70. package/templates/apps/api-cms/actions/me.action.server.ts +18 -0
  71. package/templates/apps/api-cms/actions/me.action.ts +16 -0
  72. package/templates/apps/api-cms/app.config.ts +61 -0
  73. package/templates/apps/api-cms/content/blogPost.contentType.ts +39 -0
  74. package/templates/apps/api-cms/content/index.ts +18 -0
  75. package/templates/apps/api-cms/content/page.contentType.ts +24 -0
  76. package/templates/apps/api-cms/database/schema.ts +64 -0
  77. package/templates/apps/api-cms/mutations/content.publish.mutation.server.ts +19 -0
  78. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +15 -0
  79. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.server.ts +36 -0
  80. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +32 -0
  81. package/templates/apps/api-cms/mutations/content.unpublish.mutation.server.ts +19 -0
  82. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +11 -0
  83. package/templates/apps/api-cms/package.json +32 -0
  84. package/templates/apps/api-cms/queries/content.list.query.server.ts +0 -0
  85. package/templates/apps/api-cms/queries/content.list.query.ts +27 -0
  86. package/templates/apps/api-cms/template.json +6 -0
  87. package/templates/apps/api-cms/tests/content.descriptors.test.ts +64 -0
  88. package/templates/apps/api-cms/tests/content.write.test.ts +85 -0
  89. package/templates/apps/api-cms/tsconfig.json +5 -0
  90. package/templates/apps/api-data-advanced/package.json +8 -8
  91. package/templates/apps/api-durable/package.json +8 -8
  92. package/templates/apps/api-feature-flags/package.json +9 -9
  93. package/templates/apps/api-governance/package.json +8 -8
  94. package/templates/apps/api-kv/package.json +8 -8
  95. package/templates/apps/api-moderation/package.json +8 -8
  96. package/templates/apps/api-observability/package.json +8 -8
  97. package/templates/apps/api-ratelimit/package.json +8 -8
  98. package/templates/apps/api-rbac/package.json +8 -8
  99. package/templates/apps/api-rest/package.json +7 -7
  100. package/templates/apps/api-saas/package.json +11 -11
  101. package/templates/apps/api-saas-starter/README.md +103 -0
  102. package/templates/apps/api-saas-starter/actions/me.action.server.ts +18 -0
  103. package/templates/apps/api-saas-starter/actions/me.action.ts +20 -0
  104. package/templates/apps/api-saas-starter/app.config.ts +87 -0
  105. package/templates/apps/api-saas-starter/database/schema.ts +57 -0
  106. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.server.ts +26 -0
  107. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +18 -0
  108. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.server.ts +29 -0
  109. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +18 -0
  110. package/templates/apps/api-saas-starter/package.json +32 -0
  111. package/templates/apps/api-saas-starter/queries/invites.list.query.server.ts +14 -0
  112. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +17 -0
  113. package/templates/apps/api-saas-starter/queries/projects.list.query.server.ts +14 -0
  114. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -0
  115. package/templates/apps/api-saas-starter/template.json +6 -0
  116. package/templates/apps/api-saas-starter/tests/projects.create.test.ts +62 -0
  117. package/templates/apps/api-saas-starter/tests/session.test.ts +33 -0
  118. package/templates/apps/api-saas-starter/tsconfig.json +5 -0
  119. package/templates/apps/api-search/package.json +8 -8
  120. package/templates/apps/api-status/README.md +88 -0
  121. package/templates/apps/api-status/app.config.ts +36 -0
  122. package/templates/apps/api-status/authz.ts +33 -0
  123. package/templates/apps/api-status/database/schema.ts +70 -0
  124. package/templates/apps/api-status/mutations/components.create.mutation.server.ts +19 -0
  125. package/templates/apps/api-status/mutations/components.create.mutation.ts +16 -0
  126. package/templates/apps/api-status/mutations/incidents.create.mutation.server.ts +25 -0
  127. package/templates/apps/api-status/mutations/incidents.create.mutation.ts +22 -0
  128. package/templates/apps/api-status/mutations/incidents.resolve.mutation.server.ts +24 -0
  129. package/templates/apps/api-status/mutations/incidents.resolve.mutation.ts +19 -0
  130. package/templates/apps/api-status/mutations/incidents.update.mutation.server.ts +26 -0
  131. package/templates/apps/api-status/mutations/incidents.update.mutation.ts +21 -0
  132. package/templates/apps/api-status/package.json +30 -0
  133. package/templates/apps/api-status/queries/components.list.query.server.ts +14 -0
  134. package/templates/apps/api-status/queries/components.list.query.ts +13 -0
  135. package/templates/apps/api-status/queries/incidents.live.query.server.ts +14 -0
  136. package/templates/apps/api-status/queries/incidents.live.query.ts +20 -0
  137. package/templates/apps/api-status/queries/updates.list.query.server.ts +14 -0
  138. package/templates/apps/api-status/queries/updates.list.query.ts +17 -0
  139. package/templates/apps/api-status/template.json +6 -0
  140. package/templates/apps/api-status/tests/status.test.ts +70 -0
  141. package/templates/apps/api-status/tsconfig.json +5 -0
  142. package/templates/apps/api-versioning/package.json +8 -8
  143. package/templates/apps/api-webhooks/package.json +9 -9
  144. package/templates/apps/changelog/package.json +6 -6
  145. package/templates/apps/edge-functions/package.json +2 -2
  146. package/templates/apps/frontend-admin/package.json +8 -8
  147. package/templates/apps/frontend-app/package.json +8 -8
  148. package/templates/apps/frontend-auth/README.md +78 -0
  149. package/templates/apps/frontend-auth/app.config.ts +34 -0
  150. package/templates/apps/frontend-auth/package.json +32 -0
  151. package/templates/apps/frontend-auth/src/components/AuthShell.tsx +35 -0
  152. package/templates/apps/frontend-auth/src/components/PasswordStrength.tsx +33 -0
  153. package/templates/apps/frontend-auth/src/config.ts +11 -0
  154. package/templates/apps/frontend-auth/src/globals.css +105 -0
  155. package/templates/apps/frontend-auth/src/globals.d.ts +6 -0
  156. package/templates/apps/frontend-auth/src/lib/auth.ts +34 -0
  157. package/templates/apps/frontend-auth/src/lib/redirect.test.ts +24 -0
  158. package/templates/apps/frontend-auth/src/lib/redirect.ts +29 -0
  159. package/templates/apps/frontend-auth/src/locales/de.ts +66 -0
  160. package/templates/apps/frontend-auth/src/locales/en.ts +76 -0
  161. package/templates/apps/frontend-auth/src/locales/index.ts +14 -0
  162. package/templates/apps/frontend-auth/src/pages/forgot/page.tsx +51 -0
  163. package/templates/apps/frontend-auth/src/pages/layout.tsx +12 -0
  164. package/templates/apps/frontend-auth/src/pages/login/page.test.tsx +53 -0
  165. package/templates/apps/frontend-auth/src/pages/login/page.tsx +67 -0
  166. package/templates/apps/frontend-auth/src/pages/logout/page.tsx +28 -0
  167. package/templates/apps/frontend-auth/src/pages/magic/page.tsx +51 -0
  168. package/templates/apps/frontend-auth/src/pages/page.tsx +26 -0
  169. package/templates/apps/frontend-auth/src/pages/reset/page.test.tsx +51 -0
  170. package/templates/apps/frontend-auth/src/pages/reset/page.tsx +63 -0
  171. package/templates/apps/frontend-auth/src/pages/signup/page.tsx +60 -0
  172. package/templates/apps/frontend-auth/src/pages/verify/page.tsx +50 -0
  173. package/templates/apps/frontend-auth/template.json +6 -0
  174. package/templates/apps/frontend-auth/tsconfig.json +5 -0
  175. package/templates/apps/frontend-blank/package.json +7 -7
  176. package/templates/apps/frontend-cms/README.md +47 -0
  177. package/templates/apps/frontend-cms/app.config.ts +37 -0
  178. package/templates/apps/frontend-cms/package.json +33 -0
  179. package/templates/apps/frontend-cms/src/config.ts +8 -0
  180. package/templates/apps/frontend-cms/src/globals.css +105 -0
  181. package/templates/apps/frontend-cms/src/globals.d.ts +6 -0
  182. package/templates/apps/frontend-cms/src/lib/api.ts +58 -0
  183. package/templates/apps/frontend-cms/src/locales/de.ts +50 -0
  184. package/templates/apps/frontend-cms/src/locales/en.ts +55 -0
  185. package/templates/apps/frontend-cms/src/locales/index.ts +14 -0
  186. package/templates/apps/frontend-cms/src/pages/(app)/error.tsx +18 -0
  187. package/templates/apps/frontend-cms/src/pages/(app)/layout.test.tsx +31 -0
  188. package/templates/apps/frontend-cms/src/pages/(app)/layout.tsx +62 -0
  189. package/templates/apps/frontend-cms/src/pages/(app)/not-found.tsx +14 -0
  190. package/templates/apps/frontend-cms/src/pages/(app)/page.test.tsx +110 -0
  191. package/templates/apps/frontend-cms/src/pages/(app)/page.tsx +159 -0
  192. package/templates/apps/frontend-cms/src/pages/layout.tsx +12 -0
  193. package/templates/apps/frontend-cms/src/pages/login/page.test.tsx +58 -0
  194. package/templates/apps/frontend-cms/src/pages/login/page.tsx +93 -0
  195. package/templates/apps/frontend-cms/template.json +6 -0
  196. package/templates/apps/frontend-cms/tsconfig.json +5 -0
  197. package/templates/apps/frontend-contact/package.json +7 -7
  198. package/templates/apps/frontend-dashboard/package.json +7 -7
  199. package/templates/apps/frontend-docs/package.json +7 -7
  200. package/templates/apps/frontend-i18n/package.json +6 -6
  201. package/templates/apps/frontend-landing/package.json +7 -7
  202. package/templates/apps/frontend-portal/README.md +71 -0
  203. package/templates/apps/frontend-portal/app.config.ts +37 -0
  204. package/templates/apps/frontend-portal/package.json +32 -0
  205. package/templates/apps/frontend-portal/src/config.ts +8 -0
  206. package/templates/apps/frontend-portal/src/globals.css +93 -0
  207. package/templates/apps/frontend-portal/src/globals.d.ts +6 -0
  208. package/templates/apps/frontend-portal/src/lib/api.ts +62 -0
  209. package/templates/apps/frontend-portal/src/locales/de.ts +86 -0
  210. package/templates/apps/frontend-portal/src/locales/en.ts +93 -0
  211. package/templates/apps/frontend-portal/src/locales/index.ts +14 -0
  212. package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.test.tsx +71 -0
  213. package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.tsx +124 -0
  214. package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.test.tsx +86 -0
  215. package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.tsx +97 -0
  216. package/templates/apps/frontend-portal/src/pages/(portal)/error.tsx +19 -0
  217. package/templates/apps/frontend-portal/src/pages/(portal)/invoices/page.tsx +45 -0
  218. package/templates/apps/frontend-portal/src/pages/(portal)/layout.test.tsx +31 -0
  219. package/templates/apps/frontend-portal/src/pages/(portal)/layout.tsx +63 -0
  220. package/templates/apps/frontend-portal/src/pages/(portal)/not-found.tsx +15 -0
  221. package/templates/apps/frontend-portal/src/pages/(portal)/page.tsx +39 -0
  222. package/templates/apps/frontend-portal/src/pages/(portal)/profile/page.tsx +79 -0
  223. package/templates/apps/frontend-portal/src/pages/layout.tsx +12 -0
  224. package/templates/apps/frontend-portal/src/pages/login/page.test.tsx +58 -0
  225. package/templates/apps/frontend-portal/src/pages/login/page.tsx +96 -0
  226. package/templates/apps/frontend-portal/template.json +6 -0
  227. package/templates/apps/frontend-portal/tsconfig.json +5 -0
  228. package/templates/apps/frontend-saas/README.md +73 -0
  229. package/templates/apps/frontend-saas/app.config.ts +46 -0
  230. package/templates/apps/frontend-saas/package.json +32 -0
  231. package/templates/apps/frontend-saas/src/config.ts +8 -0
  232. package/templates/apps/frontend-saas/src/globals.css +85 -0
  233. package/templates/apps/frontend-saas/src/globals.d.ts +6 -0
  234. package/templates/apps/frontend-saas/src/lib/api.ts +47 -0
  235. package/templates/apps/frontend-saas/src/locales/de.ts +72 -0
  236. package/templates/apps/frontend-saas/src/locales/en.ts +81 -0
  237. package/templates/apps/frontend-saas/src/locales/index.ts +14 -0
  238. package/templates/apps/frontend-saas/src/pages/(marketing)/layout.tsx +30 -0
  239. package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.test.tsx +58 -0
  240. package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.tsx +96 -0
  241. package/templates/apps/frontend-saas/src/pages/(marketing)/page.tsx +27 -0
  242. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +72 -0
  243. package/templates/apps/frontend-saas/src/pages/dashboard/error.tsx +20 -0
  244. package/templates/apps/frontend-saas/src/pages/dashboard/layout.test.tsx +32 -0
  245. package/templates/apps/frontend-saas/src/pages/dashboard/layout.tsx +69 -0
  246. package/templates/apps/frontend-saas/src/pages/dashboard/not-found.tsx +17 -0
  247. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +113 -0
  248. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +76 -0
  249. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +73 -0
  250. package/templates/apps/frontend-saas/src/pages/layout.tsx +12 -0
  251. package/templates/apps/frontend-saas/template.json +6 -0
  252. package/templates/apps/frontend-saas/tsconfig.json +5 -0
  253. package/templates/apps/frontend-spa/package.json +7 -7
  254. package/templates/apps/frontend-ssr/package.json +7 -7
  255. package/templates/apps/frontend-ssr-api/package.json +8 -8
  256. package/templates/apps/frontend-static-blog/package.json +6 -6
  257. package/templates/apps/frontend-status/README.md +51 -0
  258. package/templates/apps/frontend-status/app.config.ts +39 -0
  259. package/templates/apps/frontend-status/package.json +32 -0
  260. package/templates/apps/frontend-status/src/config.ts +7 -0
  261. package/templates/apps/frontend-status/src/globals.css +70 -0
  262. package/templates/apps/frontend-status/src/globals.d.ts +6 -0
  263. package/templates/apps/frontend-status/src/lib/status.ts +81 -0
  264. package/templates/apps/frontend-status/src/locales/de.ts +41 -0
  265. package/templates/apps/frontend-status/src/locales/en.ts +45 -0
  266. package/templates/apps/frontend-status/src/locales/index.ts +13 -0
  267. package/templates/apps/frontend-status/src/pages/layout.tsx +27 -0
  268. package/templates/apps/frontend-status/src/pages/page.test.tsx +123 -0
  269. package/templates/apps/frontend-status/src/pages/page.tsx +136 -0
  270. package/templates/apps/frontend-status/template.json +6 -0
  271. package/templates/apps/frontend-status/tsconfig.json +11 -0
  272. package/templates/baselines/compose/docker-compose.prod.yml +15 -0
  273. package/templates/baselines/compose-mariadb/docker-compose.prod.yml +15 -0
  274. package/dist/apiBuild-DgBS9ayv.js +0 -2
  275. package/dist/dbCommand-DvguqlzF.js +0 -2
  276. package/dist/fileConventions-3bffWssN.js +0 -30
  277. package/dist/frameworkTableAssembly-lrjZtk0G.js +0 -2
  278. package/dist/inspect-gt8bq-Tz.js +0 -2
  279. package/dist/manifestBuild-ifczArzr.js +0 -2
  280. package/dist/serveCommand-ZTn-dPFa.js +0 -1425
@@ -0,0 +1,53 @@
1
+ // @vitest-environment jsdom
2
+ //
3
+ // The sign-in page. Asserts config + that it renders the credential form and the
4
+ // footer navigation to the other flows. The network submit (CSRF + POST
5
+ // /auth/sign-in) is exercised against a running api by `voltro dev`, not 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 — render', () => {
45
+ test('renders email + password inputs and the flow links', () => {
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('a[href="/signup"]')).not.toBeNull()
50
+ expect(container.querySelector('a[href="/forgot"]')).not.toBeNull()
51
+ expect(container.querySelector('a[href="/magic"]')).not.toBeNull()
52
+ })
53
+ })
@@ -0,0 +1,67 @@
1
+ // Sign in — URL `/login`. Email + password → POST /auth/sign-in (CSRF-guarded).
2
+ // On success we do a FULL navigation to the safelisted `?next=` (default
3
+ // AUTH_HOME) so the destination app's SSR gate re-runs with the fresh cookie.
4
+ import type { ReactNode } from 'react'
5
+ import { useState, type FormEvent } from 'react'
6
+ import type { PageMeta } from '@voltro/web'
7
+ import { T, useT } from '@voltro/i18n'
8
+ import { AuthShell } from '../../components/AuthShell'
9
+ import { AUTH_HOME } from '../../config'
10
+ import { postAuth } from '../../lib/auth'
11
+ import { nextFromLocation } from '../../lib/redirect'
12
+ import { getCatalog } from '../../locales'
13
+
14
+ export const renderMode = 'static' as const
15
+
16
+ export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
17
+ title: getCatalog(locale)['meta.login.title'],
18
+ })
19
+
20
+ export default function Login(): ReactNode {
21
+ const [email, setEmail] = useState('')
22
+ const [password, setPassword] = useState('')
23
+ const [pending, setPending] = useState(false)
24
+ const [failed, setFailed] = useState(false)
25
+ const emailLabel = useT('auth.email')
26
+ const passwordLabel = useT('auth.password')
27
+
28
+ const onSubmit = (e: FormEvent): void => {
29
+ e.preventDefault()
30
+ setPending(true)
31
+ setFailed(false)
32
+ void postAuth('/auth/sign-in', { email, password })
33
+ .then((res) => {
34
+ if (res.ok) window.location.assign(nextFromLocation(AUTH_HOME))
35
+ else { setFailed(true); setPending(false) }
36
+ })
37
+ .catch(() => { setFailed(true); setPending(false) })
38
+ }
39
+
40
+ return (
41
+ <AuthShell
42
+ titleId="login.title"
43
+ footer={
44
+ <>
45
+ <a href="/forgot"><T id="login.forgot" /></a>
46
+ <a href="/magic"><T id="login.magic" /></a>
47
+ <a href="/signup"><T id="login.toSignup" /></a>
48
+ </>
49
+ }
50
+ >
51
+ <form className="auth-form" onSubmit={onSubmit}>
52
+ <label>
53
+ <span><T id="auth.email" /></span>
54
+ <input type="email" required aria-label={emailLabel} value={email} onChange={(e) => setEmail(e.target.value)} />
55
+ </label>
56
+ <label>
57
+ <span><T id="auth.password" /></span>
58
+ <input type="password" required aria-label={passwordLabel} value={password} onChange={(e) => setPassword(e.target.value)} />
59
+ </label>
60
+ {failed ? <p className="error-text"><T id="auth.error" /></p> : null}
61
+ <button type="submit" className="btn" disabled={pending}>
62
+ <T id={pending ? 'auth.pending' : 'login.submit'} />
63
+ </button>
64
+ </form>
65
+ </AuthShell>
66
+ )
67
+ }
@@ -0,0 +1,28 @@
1
+ // Logout — URL `/logout`. On mount it POSTs /auth/sign-out (revokes the session
2
+ // row + clears the cookie), then sends the browser to /login regardless of the
3
+ // result (a failed sign-out still shouldn't strand the user).
4
+ import type { ReactNode } from 'react'
5
+ import { useEffect } from 'react'
6
+ import type { PageMeta } from '@voltro/web'
7
+ import { T } from '@voltro/i18n'
8
+ import { AuthShell } from '../../components/AuthShell'
9
+ import { postAuth } from '../../lib/auth'
10
+ import { getCatalog } from '../../locales'
11
+
12
+ export const renderMode = 'static' as const
13
+
14
+ export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
15
+ title: getCatalog(locale)['meta.logout.title'],
16
+ })
17
+
18
+ export default function Logout(): ReactNode {
19
+ useEffect(() => {
20
+ void postAuth('/auth/sign-out', {}).finally(() => window.location.assign('/login'))
21
+ }, [])
22
+
23
+ return (
24
+ <AuthShell titleId="logout.title">
25
+ <p className="muted" role="status"><T id="logout.body" /></p>
26
+ </AuthShell>
27
+ )
28
+ }
@@ -0,0 +1,51 @@
1
+ // Magic link — URL `/magic`. Passwordless sign-in: POST /auth/magic-link
2
+ // { email } mints a single-use, short-lived token and (when `sendEmail` is wired
3
+ // on the api) emails a link to `/verify?token=…`. Always shows the neutral
4
+ // "check your email" so it can't be used to probe which addresses exist.
5
+ import type { ReactNode } from 'react'
6
+ import { useState, type FormEvent } from 'react'
7
+ import type { PageMeta } from '@voltro/web'
8
+ import { T, useT } from '@voltro/i18n'
9
+ import { AuthShell } from '../../components/AuthShell'
10
+ import { postAuth } from '../../lib/auth'
11
+ import { getCatalog } from '../../locales'
12
+
13
+ export const renderMode = 'static' as const
14
+
15
+ export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
16
+ title: getCatalog(locale)['meta.magic.title'],
17
+ })
18
+
19
+ export default function Magic(): ReactNode {
20
+ const [email, setEmail] = useState('')
21
+ const [pending, setPending] = useState(false)
22
+ const [sent, setSent] = useState(false)
23
+ const emailLabel = useT('auth.email')
24
+
25
+ const onSubmit = (e: FormEvent): void => {
26
+ e.preventDefault()
27
+ setPending(true)
28
+ void postAuth('/auth/magic-link', { email })
29
+ .then(() => { setSent(true); setPending(false) })
30
+ .catch(() => { setSent(true); setPending(false) })
31
+ }
32
+
33
+ return (
34
+ <AuthShell titleId="magic.title" footer={<a href="/login"><T id="magic.toLogin" /></a>}>
35
+ {sent ? (
36
+ <p className="ok" role="status"><T id="magic.sent" /></p>
37
+ ) : (
38
+ <form className="auth-form" onSubmit={onSubmit}>
39
+ <p className="muted"><T id="magic.body" /></p>
40
+ <label>
41
+ <span><T id="auth.email" /></span>
42
+ <input type="email" required aria-label={emailLabel} value={email} onChange={(e) => setEmail(e.target.value)} />
43
+ </label>
44
+ <button type="submit" className="btn" disabled={pending}>
45
+ <T id={pending ? 'auth.pending' : 'magic.submit'} />
46
+ </button>
47
+ </form>
48
+ )}
49
+ </AuthShell>
50
+ )
51
+ }
@@ -0,0 +1,26 @@
1
+ // Root — URL `/`. The auth app has no landing of its own; send visitors to
2
+ // /login (preserving any `?next=`/`?token=` query so a deep link survives).
3
+ import type { ReactNode } from 'react'
4
+ import { useEffect } from 'react'
5
+ import type { PageMeta } from '@voltro/web'
6
+ import { T } from '@voltro/i18n'
7
+ import { AuthShell } from '../components/AuthShell'
8
+ import { getCatalog } from '../locales'
9
+
10
+ export const renderMode = 'static' as const
11
+
12
+ export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
13
+ title: getCatalog(locale)['meta.login.title'],
14
+ })
15
+
16
+ export default function Index(): ReactNode {
17
+ useEffect(() => {
18
+ const search = window.location.search
19
+ window.location.replace(`/login${search}`)
20
+ }, [])
21
+ return (
22
+ <AuthShell titleId="login.title">
23
+ <p className="muted"><a href="/login"><T id="login.submit" /></a></p>
24
+ </AuthShell>
25
+ )
26
+ }
@@ -0,0 +1,51 @@
1
+ // @vitest-environment jsdom
2
+ //
3
+ // The reset page. Asserts config + that submitting with NO `?token=` in the URL
4
+ // (jsdom's default location) surfaces the invalid-link message without a network
5
+ // call — the guard that a reset can't proceed tokenless.
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: Reset, 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('reset — config', () => {
38
+ test('is a static page titled Set a new password', () => {
39
+ expect(renderMode).toBe('static')
40
+ expect(meta({ locale: 'en' }).title).toBe('Set a new password')
41
+ })
42
+ })
43
+
44
+ describe('reset — tokenless guard', () => {
45
+ test('submitting without a ?token= shows the invalid-link message', () => {
46
+ render(createElement(Reset))
47
+ const form = container.querySelector('form')!
48
+ act(() => { form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })) })
49
+ expect(container.querySelector('.error-text')?.textContent).toContain('invalid or has expired')
50
+ })
51
+ })
@@ -0,0 +1,63 @@
1
+ // Reset password — URL `/reset?token=…`. The link the reset email points at.
2
+ // Reads the single-use token from the URL and POSTs it with the new password to
3
+ // /auth/password-reset/confirm. On success the api invalidates the token and the
4
+ // user signs in with the new password.
5
+ import type { ReactNode } from 'react'
6
+ import { useState, type FormEvent } from 'react'
7
+ import type { PageMeta } from '@voltro/web'
8
+ import { T, useT } from '@voltro/i18n'
9
+ import { AuthShell } from '../../components/AuthShell'
10
+ import { PasswordStrength } from '../../components/PasswordStrength'
11
+ import { postAuth } from '../../lib/auth'
12
+ import { tokenFromLocation } from '../../lib/redirect'
13
+ import { getCatalog } from '../../locales'
14
+
15
+ export const renderMode = 'static' as const
16
+
17
+ export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
18
+ title: getCatalog(locale)['meta.reset.title'],
19
+ })
20
+
21
+ export default function Reset(): ReactNode {
22
+ const [password, setPassword] = useState('')
23
+ const [pending, setPending] = useState(false)
24
+ const [failed, setFailed] = useState(false)
25
+ const [done, setDone] = useState(false)
26
+ const passwordLabel = useT('auth.newPassword')
27
+
28
+ const onSubmit = (e: FormEvent): void => {
29
+ e.preventDefault()
30
+ const token = tokenFromLocation()
31
+ if (!token) { setFailed(true); return }
32
+ setPending(true)
33
+ setFailed(false)
34
+ void postAuth('/auth/password-reset/confirm', { token, password })
35
+ .then((res) => {
36
+ if (res.ok) setDone(true)
37
+ else setFailed(true)
38
+ setPending(false)
39
+ })
40
+ .catch(() => { setFailed(true); setPending(false) })
41
+ }
42
+
43
+ return (
44
+ <AuthShell titleId="reset.title" footer={<a href="/login"><T id="reset.toLogin" /></a>}>
45
+ {done ? (
46
+ <p className="ok" role="status"><T id="reset.done" /></p>
47
+ ) : (
48
+ <form className="auth-form" onSubmit={onSubmit}>
49
+ <p className="muted"><T id="reset.body" /></p>
50
+ <label>
51
+ <span><T id="auth.newPassword" /></span>
52
+ <input type="password" required minLength={8} aria-label={passwordLabel} value={password} onChange={(e) => setPassword(e.target.value)} />
53
+ </label>
54
+ <PasswordStrength value={password} />
55
+ {failed ? <p className="error-text"><T id="reset.failed" /></p> : null}
56
+ <button type="submit" className="btn" disabled={pending}>
57
+ <T id={pending ? 'auth.pending' : 'reset.submit'} />
58
+ </button>
59
+ </form>
60
+ )}
61
+ </AuthShell>
62
+ )
63
+ }
@@ -0,0 +1,60 @@
1
+ // Sign up — URL `/signup`. Email + password → POST /auth/sign-up (registers +
2
+ // auto signs in, setting the session cookie). A live password-strength meter is
3
+ // advisory; the api is the authority on policy.
4
+ import type { ReactNode } from 'react'
5
+ import { useState, type FormEvent } from 'react'
6
+ import type { PageMeta } from '@voltro/web'
7
+ import { T, useT } from '@voltro/i18n'
8
+ import { AuthShell } from '../../components/AuthShell'
9
+ import { PasswordStrength } from '../../components/PasswordStrength'
10
+ import { AUTH_HOME } from '../../config'
11
+ import { postAuth } from '../../lib/auth'
12
+ import { nextFromLocation } from '../../lib/redirect'
13
+ import { getCatalog } from '../../locales'
14
+
15
+ export const renderMode = 'static' as const
16
+
17
+ export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
18
+ title: getCatalog(locale)['meta.signup.title'],
19
+ })
20
+
21
+ export default function Signup(): ReactNode {
22
+ const [email, setEmail] = useState('')
23
+ const [password, setPassword] = useState('')
24
+ const [pending, setPending] = useState(false)
25
+ const [failed, setFailed] = useState(false)
26
+ const emailLabel = useT('auth.email')
27
+ const passwordLabel = useT('auth.password')
28
+
29
+ const onSubmit = (e: FormEvent): void => {
30
+ e.preventDefault()
31
+ setPending(true)
32
+ setFailed(false)
33
+ void postAuth('/auth/sign-up', { email, password })
34
+ .then((res) => {
35
+ if (res.ok) window.location.assign(nextFromLocation(AUTH_HOME))
36
+ else { setFailed(true); setPending(false) }
37
+ })
38
+ .catch(() => { setFailed(true); setPending(false) })
39
+ }
40
+
41
+ return (
42
+ <AuthShell titleId="signup.title" footer={<a href="/login"><T id="signup.toLogin" /></a>}>
43
+ <form className="auth-form" onSubmit={onSubmit}>
44
+ <label>
45
+ <span><T id="auth.email" /></span>
46
+ <input type="email" required aria-label={emailLabel} value={email} onChange={(e) => setEmail(e.target.value)} />
47
+ </label>
48
+ <label>
49
+ <span><T id="auth.password" /></span>
50
+ <input type="password" required minLength={8} aria-label={passwordLabel} value={password} onChange={(e) => setPassword(e.target.value)} />
51
+ </label>
52
+ <PasswordStrength value={password} />
53
+ {failed ? <p className="error-text"><T id="auth.error" /></p> : null}
54
+ <button type="submit" className="btn" disabled={pending}>
55
+ <T id={pending ? 'auth.pending' : 'signup.submit'} />
56
+ </button>
57
+ </form>
58
+ </AuthShell>
59
+ )
60
+ }
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "include": ["**/*.ts", "**/*.tsx"],
4
+ "exclude": ["node_modules", "dist"]
5
+ }
@@ -12,17 +12,17 @@
12
12
  "typecheck": "tsc --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/cli": "0.28.0",
16
- "@voltro/client": "0.28.0",
17
- "@voltro/env": "0.28.0",
18
- "@voltro/i18n": "0.28.0",
19
- "@voltro/ui-shadcn": "0.28.0",
20
- "@voltro/web": "0.28.0",
15
+ "@voltro/cli": "0.29.0",
16
+ "@voltro/client": "0.29.0",
17
+ "@voltro/env": "0.29.0",
18
+ "@voltro/i18n": "0.29.0",
19
+ "@voltro/ui-shadcn": "0.29.0",
20
+ "@voltro/web": "0.29.0",
21
21
  "react": "^19.0.0",
22
22
  "react-dom": "^19.0.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/devtools": "0.28.0",
25
+ "@voltro/devtools": "0.29.0",
26
26
  "@types/react": "^19.0.0",
27
27
  "@types/react-dom": "^19.0.0",
28
28
  "typescript": "^6.0.3",
@@ -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.29.0",
16
+ "@voltro/cli": "0.29.0",
17
+ "@voltro/client": "0.29.0",
18
+ "@voltro/cms": "0.29.0",
19
+ "@voltro/env": "0.29.0",
20
+ "@voltro/i18n": "0.29.0",
21
+ "@voltro/ui-shadcn": "0.29.0",
22
+ "@voltro/web": "0.29.0",
23
+ "react": "^19.0.0",
24
+ "react-dom": "^19.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@voltro/devtools": "0.29.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}}'