@voltro/cli 0.52.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/CHANGELOG.md +424 -0
  2. package/THIRD-PARTY-NOTICES.md +8311 -3318
  3. package/dist/{agentsMd-0l980yhL.js → agentsMd-BaLC10Na.js} +110 -82
  4. package/dist/agentsMd-DCY1RSs8.js +2 -0
  5. package/dist/apiBuild-CeUN55uk.js +2 -0
  6. package/dist/{apiBuild-CSFI8QGq.js → apiBuild-DTWp0S_q.js} +11 -5
  7. package/dist/bin.js +1 -1
  8. package/dist/build-D4ygSbnV.js +843 -0
  9. package/dist/{checkCommand-COmqc2cB.js → checkCommand-Dg1G7Gwd.js} +6 -6
  10. package/dist/{checkCommand-2SbqzukH.js → checkCommand-L7DTlpIF.js} +1 -1
  11. package/dist/{cloudCmd-QUXh-b5w.js → cloudCmd-DzKcSYuy.js} +1 -1
  12. package/dist/{codegen-VF479Cnb.js → codegen-DSLM8Su9.js} +1 -1
  13. package/dist/codegen-DjgxEOnD.js +2 -0
  14. package/dist/codegenCommand-CG_Vx4lc.js +41 -0
  15. package/dist/{codemodRunner-r7J9lIa7.js → codemodRunner-Cd4xkC6u.js} +109 -11
  16. package/dist/{commands-Cc_nV8WI.js → commands-6Kzi92Np.js} +96 -73
  17. package/dist/{dashboardCommand-C-gKvwqh.js → dashboardCommand-Cq1PWvI1.js} +5 -5
  18. package/dist/{dataCommand-BgpBHnlB.js → dataCommand-DYzW8vkv.js} +3 -3
  19. package/dist/{dbCommand-sHedr-NJ.js → dbCommand-B4NWZtGL.js} +278 -237
  20. package/dist/dbCommand-CSFWs9ev.js +2 -0
  21. package/dist/{dev-CRHoCEiy.js → dev-CmuvUKRq.js} +2903 -2306
  22. package/dist/{dev--A3nsxA3.js → dev-cKUiZZsB.js} +1 -1
  23. package/dist/{doctorCommand-DtfJ3FA6.js → doctorCommand-DCiFVMtZ.js} +101 -69
  24. package/dist/doctorCommand-J3qu4E0Y.js +2 -0
  25. package/dist/{dormancyCommand-Drn7o0No.js → dormancyCommand-w1TrmgYP.js} +1 -1
  26. package/dist/{embeddingsCommand-Z-jO1fWN.js → embeddingsCommand-CMgPyRTr.js} +1 -1
  27. package/dist/{envCommand-D4gCrrTZ.js → envCommand-Cyynmcfa.js} +8 -8
  28. package/dist/{evolveCommand-CMROeKeA.js → evolveCommand-BwvQ8dVH.js} +2 -2
  29. package/dist/fileConventions-l-RIXbx8.js +36 -0
  30. package/dist/{fileTaxonomy-DvDUV9wq.js → fileTaxonomy-CbyMQYx_.js} +42 -42
  31. package/dist/fontPipeline-LxIHa1vo.js +2 -0
  32. package/dist/fontPipeline-Tsh8kZfA.js +152 -0
  33. package/dist/{frameworkTableAssembly-w-XnLa3q.js → frameworkTableAssembly-D7LJuALW.js} +5 -5
  34. package/dist/frameworkTableAssembly-IPD1pUnZ.js +2 -0
  35. package/dist/imagePipeline-B_GVJgm6.js +2 -0
  36. package/dist/imagePipeline-CBZmjT4i.js +127 -0
  37. package/dist/index.js +2 -2
  38. package/dist/{infoCommand-DXM868o_.js → infoCommand-DlYlUPqs.js} +1 -1
  39. package/dist/{inspect-CtL_xTbu.js → inspect-Bd8-9wsi.js} +1 -1
  40. package/dist/inspect-CuoDInfZ.js +2 -0
  41. package/dist/{inspectGateHint-BjnFubmH.js → inspectGateHint-4LxkNtrz.js} +1 -1
  42. package/dist/manifestBuild-C4-J1-m_.js +2 -0
  43. package/dist/{manifestBuild-CuU1VrSm.js → manifestBuild-Cqgsx2bM.js} +1 -1
  44. package/dist/{metaCommands-C6RFmF1r.js → metaCommands-x7RCi2AF.js} +9 -3
  45. package/dist/{migrate-D0F-eTlK.js → migrate-BK_Bbx-_.js} +2 -2
  46. package/dist/mobileCommand-D9O6iq3D.js +428 -0
  47. package/dist/mobileCommand-DAum7tsG.js +2 -0
  48. package/dist/{pageConvention-CzUiSbtU.js → pageConvention-CMpfDN6r.js} +1 -1
  49. package/dist/{privacyCommand-DGdopOI6.js → privacyCommand-BCa2OoZG.js} +1 -1
  50. package/dist/{probeCommand-C9gazU0H.js → probeCommand-_C0YU207.js} +83 -24
  51. package/dist/{projectScaffold-EzlErR4E.js → projectScaffold-CJfP-xbT.js} +1 -1
  52. package/dist/{projectScaffold-B4dmTlwT.js → projectScaffold-CSN0OzBV.js} +2 -2
  53. package/dist/renderModeScan-43yQ2opo.js +147 -0
  54. package/dist/{renderProfile-Ck32Fzxr.js → renderProfile-DvrhVJHa.js} +2 -2
  55. package/dist/{runtimeTrace-BPQyCmC5.js → runtimeTrace-CGWx1Q6l.js} +1 -1
  56. package/dist/{sdkgen-Se88ifTd.js → sdkgen-CDGHQUFj.js} +1 -1
  57. package/dist/serveCommand-BiPe8BJm.js +2 -0
  58. package/dist/{serveCommand-DkP3OT0W.js → serveCommand-Bje09q1v.js} +889 -825
  59. package/dist/serveEntry.js +1 -1
  60. package/dist/start-B0bnJgxI.js +3 -0
  61. package/dist/{start-jw89Xbqy.js → start-Clz-1BHB.js} +633 -455
  62. package/dist/startEntry.js +1 -1
  63. package/dist/{staticCommand-BwNEDlSU.js → staticCommand-ey0kYmOT.js} +1 -1
  64. package/dist/{subcommandNames-DpYs3DXr.js → subcommandNames-CDzfEtKV.js} +3 -3
  65. package/dist/{templates-BR-fb4SP.js → templates-BTWZkJJT.js} +41 -9
  66. package/dist/{test-f3amja6a.js → test-D_kW4KMj.js} +1 -1
  67. package/dist/updateCommand-CIoVDKnj.js +2 -0
  68. package/dist/{updateCommand-BMk2e4ky.js → updateCommand-CRJlAOaM.js} +139 -115
  69. package/dist/{webDev-BgWL9gKV.js → webDev-DSI9SOhs.js} +1598 -1028
  70. package/dist/webDev-DlvZO30c.js +2 -0
  71. package/dist/{webhooksCommand-CoIO3jbj.js → webhooksCommand-BvzXNHji.js} +1 -1
  72. package/dist/workspaceDeps-RKEkX92S.js +45 -0
  73. package/package.json +60 -19
  74. package/templates/AGENTS.core.md +2 -0
  75. package/templates/AGENTS.md +8 -4
  76. package/templates/agent-docs/_index.md +6 -4
  77. package/templates/agent-docs/_manifest.json +21 -5
  78. package/templates/agent-docs/ai.md +6 -6
  79. package/templates/agent-docs/authentication.md +73 -1
  80. package/templates/agent-docs/cli.md +6 -3
  81. package/templates/agent-docs/configuration.md +17 -0
  82. package/templates/agent-docs/data.md +522 -29
  83. package/templates/agent-docs/database/advancedqueries.md +8 -8
  84. package/templates/agent-docs/database/columntypes.md +2 -2
  85. package/templates/agent-docs/database/migrations.md +1 -1
  86. package/templates/agent-docs/database/querying.md +1 -1
  87. package/templates/agent-docs/database/schema.md +1 -1
  88. package/templates/agent-docs/database/seedsdialects.md +65 -3
  89. package/templates/agent-docs/database/transactions.md +3 -3
  90. package/templates/agent-docs/deployment.md +8 -0
  91. package/templates/agent-docs/internationalization.md +4 -2
  92. package/templates/agent-docs/introduction.md +32 -1
  93. package/templates/agent-docs/local-first-mobile.md +226 -30
  94. package/templates/agent-docs/observability.md +5 -1
  95. package/templates/agent-docs/plugins/atlassian.md +2 -2
  96. package/templates/agent-docs/plugins/audit.md +2 -2
  97. package/templates/agent-docs/plugins/auth.md +1 -1
  98. package/templates/agent-docs/plugins/billing.md +1 -1
  99. package/templates/agent-docs/plugins/cdc-out.md +8 -3
  100. package/templates/agent-docs/plugins/comments.md +164 -0
  101. package/templates/agent-docs/plugins/notifications.md +47 -4
  102. package/templates/agent-docs/plugins/presence.md +45 -3
  103. package/templates/agent-docs/plugins/prometheus.md +3 -1
  104. package/templates/agent-docs/plugins/queue.md +172 -0
  105. package/templates/agent-docs/plugins.md +17 -13
  106. package/templates/agent-docs/reference.md +35 -4
  107. package/templates/agent-docs/routing.md +585 -7
  108. package/templates/agent-docs/scheduling.md +1 -1
  109. package/templates/agent-docs/schema-driven-ui.md +226 -4
  110. package/templates/agent-docs/security.md +3 -3
  111. package/templates/agent-docs/templates/appshells.md +36 -4
  112. package/templates/agent-docs/whats-new.md +134 -66
  113. package/templates/apps/api-ai/package.json +6 -6
  114. package/templates/apps/api-auth/package.json +8 -8
  115. package/templates/apps/api-backend/package.json +7 -7
  116. package/templates/apps/api-backend-deactivation/package.json +7 -7
  117. package/templates/apps/api-backend-mail/package.json +8 -8
  118. package/templates/apps/api-backend-mariadb/package.json +9 -9
  119. package/templates/apps/api-backend-sqlite/package.json +8 -8
  120. package/templates/apps/api-backend-storage/package.json +8 -8
  121. package/templates/apps/api-cms/package.json +9 -9
  122. package/templates/apps/api-collab/README.md +3 -3
  123. package/templates/apps/api-collab/app.config.ts +1 -1
  124. package/templates/apps/api-collab/database/schema.ts +12 -8
  125. package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +1 -1
  126. package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +1 -1
  127. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +1 -1
  128. package/templates/apps/api-collab/package.json +8 -8
  129. package/templates/apps/api-collab/template.json +1 -1
  130. package/templates/apps/api-collab/tests/documents.setBody.test.ts +10 -2
  131. package/templates/apps/api-data-advanced/package.json +8 -8
  132. package/templates/apps/api-durable/package.json +8 -8
  133. package/templates/apps/api-feature-flags/package.json +9 -9
  134. package/templates/apps/api-governance/package.json +8 -8
  135. package/templates/apps/api-kv/package.json +8 -8
  136. package/templates/apps/api-moderation/package.json +8 -8
  137. package/templates/apps/api-observability/package.json +8 -8
  138. package/templates/apps/api-ratelimit/package.json +8 -8
  139. package/templates/apps/api-rbac/package.json +8 -8
  140. package/templates/apps/api-rest/package.json +7 -7
  141. package/templates/apps/api-row-history/package.json +8 -8
  142. package/templates/apps/api-saas/package.json +11 -10
  143. package/templates/apps/api-saas-starter/package.json +10 -10
  144. package/templates/apps/api-search/package.json +8 -8
  145. package/templates/apps/api-status/package.json +8 -8
  146. package/templates/apps/api-webhooks/package.json +9 -9
  147. package/templates/apps/changelog/app.config.ts +26 -2
  148. package/templates/apps/changelog/content/releases/{0.1.0.mdx → v0-1-0.mdx} +0 -1
  149. package/templates/apps/changelog/content/releases/{0.2.0.mdx → v0-2-0.mdx} +0 -1
  150. package/templates/apps/changelog/package.json +9 -8
  151. package/templates/apps/changelog/src/collections/releases.collection.ts +48 -0
  152. package/templates/apps/changelog/src/globals.d.ts +1 -1
  153. package/templates/apps/changelog/src/locales/de.ts +1 -1
  154. package/templates/apps/changelog/src/locales/en.ts +1 -1
  155. package/templates/apps/changelog/src/pages/[locale]/[slug]/page.tsx +7 -4
  156. package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +13 -41
  157. package/templates/apps/changelog/src/pages/[slug]/page.test.tsx +26 -51
  158. package/templates/apps/changelog/src/pages/[slug]/page.tsx +21 -19
  159. package/templates/apps/changelog/src/pages/page.test.tsx +14 -37
  160. package/templates/apps/changelog/src/pages/page.tsx +18 -12
  161. package/templates/apps/edge-functions/package.json +2 -2
  162. package/templates/apps/frontend-admin/package.json +8 -8
  163. package/templates/apps/frontend-app/package.json +9 -9
  164. package/templates/apps/frontend-auth/package.json +8 -8
  165. package/templates/apps/frontend-blank/package.json +7 -7
  166. package/templates/apps/frontend-cms/package.json +9 -9
  167. package/templates/apps/frontend-collab/README.md +43 -24
  168. package/templates/apps/frontend-collab/app.config.ts +3 -3
  169. package/templates/apps/frontend-collab/package.json +14 -10
  170. package/templates/apps/frontend-collab/src/locales/de.ts +1 -2
  171. package/templates/apps/frontend-collab/src/locales/en.ts +1 -2
  172. package/templates/apps/frontend-collab/src/pages/page.test.tsx +72 -76
  173. package/templates/apps/frontend-collab/src/pages/page.tsx +45 -22
  174. package/templates/apps/frontend-collab/template.json +2 -2
  175. package/templates/apps/frontend-contact/package.json +7 -7
  176. package/templates/apps/frontend-dashboard/package.json +7 -7
  177. package/templates/apps/frontend-docs/content/docs/de/guides/first-page.md +4 -0
  178. package/templates/apps/frontend-docs/content/docs/de/intro/getting-started.md +4 -0
  179. package/templates/apps/frontend-docs/content/docs/en/guides/first-page.md +4 -0
  180. package/templates/apps/frontend-docs/content/docs/en/intro/getting-started.md +4 -0
  181. package/templates/apps/frontend-docs/package.json +9 -6
  182. package/templates/apps/frontend-docs/src/collections/docs.collection.ts +21 -0
  183. package/templates/apps/frontend-docs/src/locales/de.ts +0 -5
  184. package/templates/apps/frontend-docs/src/locales/en.ts +0 -5
  185. package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug]/page.tsx +17 -9
  186. package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +10 -5
  187. package/templates/apps/frontend-docs/src/pages/[locale]/page.tsx +8 -0
  188. package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.test.tsx +53 -15
  189. package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.tsx +27 -33
  190. package/templates/apps/frontend-docs/src/pages/page.test.tsx +17 -3
  191. package/templates/apps/frontend-docs/src/pages/page.tsx +16 -12
  192. package/templates/apps/frontend-i18n/package.json +6 -6
  193. package/templates/apps/frontend-landing/README.md +48 -0
  194. package/templates/apps/frontend-landing/app.config.ts +28 -0
  195. package/templates/apps/frontend-landing/package.json +7 -6
  196. package/templates/apps/frontend-landing/src/assets/hero.jpg +0 -0
  197. package/templates/apps/frontend-landing/src/fonts/Geist-Variable.woff2 +0 -0
  198. package/templates/apps/frontend-landing/src/fonts/LICENSE-Geist.txt +92 -0
  199. package/templates/apps/frontend-landing/src/globals.css +15 -0
  200. package/templates/apps/frontend-landing/src/globals.d.ts +17 -0
  201. package/templates/apps/frontend-landing/src/locales/de.ts +3 -2
  202. package/templates/apps/frontend-landing/src/locales/en.ts +5 -2
  203. package/templates/apps/frontend-landing/src/pages/page.test.tsx +79 -0
  204. package/templates/apps/frontend-landing/src/pages/page.tsx +26 -3
  205. package/templates/apps/frontend-landing/template.json +2 -2
  206. package/templates/apps/frontend-portal/package.json +8 -8
  207. package/templates/apps/frontend-saas/package.json +8 -8
  208. package/templates/apps/frontend-spa/package.json +7 -7
  209. package/templates/apps/frontend-ssr/package.json +7 -7
  210. package/templates/apps/frontend-ssr-api/package.json +8 -8
  211. package/templates/apps/frontend-static-blog/content/posts/cms-to-ssg.md +12 -0
  212. package/templates/apps/frontend-static-blog/content/posts/hello-static.md +12 -0
  213. package/templates/apps/frontend-static-blog/content/posts/islands-not-hydration.md +14 -0
  214. package/templates/apps/frontend-static-blog/package.json +9 -6
  215. package/templates/apps/frontend-static-blog/src/collections/posts.collection.ts +27 -0
  216. package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug]/page.tsx +7 -4
  217. package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +7 -1
  218. package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.test.tsx +19 -9
  219. package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.tsx +19 -21
  220. package/templates/apps/frontend-static-blog/src/pages/page.test.tsx +18 -5
  221. package/templates/apps/frontend-static-blog/src/pages/page.tsx +21 -14
  222. package/templates/apps/frontend-status/package.json +8 -8
  223. package/templates/apps/mobile-app/package.json +12 -11
  224. package/templates/apps/mobile-app/src/lib/deeplinks.ts +29 -17
  225. package/templates/apps/mobile-app/tests/deeplinks.test.ts +16 -0
  226. package/dist/agentsMd-SDDSkyl4.js +0 -2
  227. package/dist/apiBuild-BYBpL7Pz.js +0 -2
  228. package/dist/build-CPgcMQug.js +0 -793
  229. package/dist/codegen-CctkDO-1.js +0 -2
  230. package/dist/codegenCommand-DCdG2JN-.js +0 -137
  231. package/dist/dbCommand-DNb6yeOG.js +0 -2
  232. package/dist/doctorCommand-CqoWA2p5.js +0 -2
  233. package/dist/fileConventions-DOqD3lPS.js +0 -34
  234. package/dist/frameworkTableAssembly-C6ETawPR.js +0 -2
  235. package/dist/inspect-CuGDYES0.js +0 -2
  236. package/dist/manifestBuild-CPjhvM62.js +0 -2
  237. package/dist/renderModeScan-CcH2X1_D.js +0 -120
  238. package/dist/serveCommand-DLc-BznW.js +0 -2
  239. package/dist/start-DfL3fOiN.js +0 -3
  240. package/dist/updateCommand-5gFVfK5q.js +0 -2
  241. package/dist/webDev-CZbTsDcH.js +0 -2
  242. package/templates/apps/changelog/scripts/generate-rss.mjs +0 -38
  243. package/templates/apps/changelog/src/lib/releases.ts +0 -21
  244. package/templates/apps/frontend-static-blog/src/content/posts.ts +0 -64
@@ -1,49 +1,25 @@
1
1
  // @vitest-environment jsdom
2
2
  //
3
- // `/` changelog index — lists every release, newest first. The real release
4
- // source (`../lib/releases`) reads .mdx via `import.meta.glob` + node-gated
5
- // shiki, so we mock it with canned releases and assert the observable render:
6
- // config exports, meta, one entry per release, and its tags + link.
3
+ // `/` changelog index — lists every release, newest first, loaded through
4
+ // the REAL `releases` content collection (this app's content/releases/*.mdx)
5
+ // so the test exercises frontmatter decoding, draft/future filtering and
6
+ // ordering no canned copies to drift.
7
7
 
8
8
  import { afterEach, describe, expect, test, vi } from 'vitest'
9
9
  import { act, createElement, type ReactNode } from 'react'
10
10
  import { createRoot, type Root } from 'react-dom/client'
11
+ import { dirname, join } from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { setContentRoot } from '@voltro/content'
11
14
 
12
- interface Release {
13
- version: string
14
- releasedAt: string
15
- slug: string
16
- tags: ReadonlyArray<string>
17
- title: string
18
- summary: string
19
- html: string
20
- }
21
-
22
- const releases: ReadonlyArray<Release> = [
23
- {
24
- version: '0.2.0',
25
- releasedAt: '2026-06-21',
26
- slug: 'v0-2-0',
27
- tags: ['feature', 'performance'],
28
- title: 'Typed env + faster boot',
29
- summary: 'Declare your environment once.',
30
- html: '<p>body</p>',
31
- },
32
- {
33
- version: '0.1.0',
34
- releasedAt: '2026-01-15',
35
- slug: 'v0-1-0',
36
- tags: ['feature'],
37
- title: 'First release',
38
- summary: 'The initial cut.',
39
- html: '<p>body</p>',
40
- },
41
- ]
15
+ setContentRoot(join(dirname(fileURLToPath(import.meta.url)), '..', '..'))
42
16
 
43
- vi.mock('../lib/releases', () => ({ releases }))
44
17
  // `withLocalePrefix` (lib/locale) reads useLocation; the page pins the locale
45
18
  // via useLocale, so any bare path is fine here.
46
- vi.mock('@voltro/web', () => ({ useLocation: () => '/' }))
19
+ vi.mock('@voltro/web', () => ({
20
+ useLocation: () => '/',
21
+ useLoaderData: () => entries,
22
+ }))
47
23
  // Chrome (meta via getCatalog + the empty state) comes from @voltro/i18n;
48
24
  // stub it (identity catalog defs, useLocale → en, <T> passes children through).
49
25
  vi.mock('@voltro/i18n', () => ({
@@ -57,7 +33,8 @@ vi.mock('@voltro/i18n', () => ({
57
33
  T: ({ id }: { id: string }) => id,
58
34
  }))
59
35
 
60
- const { default: ChangelogIndex, meta, renderMode, interactive } = await import('./page')
36
+ const { default: ChangelogIndex, meta, renderMode, interactive, loader } = await import('./page')
37
+ const entries = await loader()
61
38
 
62
39
  ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
63
40
 
@@ -2,16 +2,18 @@
2
2
  // locale) and mirrored at `/de` by src/pages/[locale]/index.tsx. Pure static
3
3
  // render with no client JS (`interactive: 'none'`).
4
4
  //
5
- // `meta` is a FUNCTION of `{ locale }` so each variant's <title>/<description>
6
- // is localised in the pre-rendered HTML. The release list itself is CONTENT
7
- // (the .mdx files) and is NOT translated only the chrome (page meta, the
8
- // empty state) comes from the catalogs. Entry links are locale-prefixed via
9
- // `withLocalePrefix` so /de stays on /de.
5
+ // The list comes from the `releases` CONTENT COLLECTION: the loader runs at
6
+ // build time, decodes each entry's frontmatter against the schema, and
7
+ // filters drafts + future-dated entries (visibleReleases). The release list
8
+ // itself is CONTENT and is NOT translated only the chrome comes from the
9
+ // catalogs. Entry links are locale-prefixed via `withLocalePrefix`.
10
10
 
11
11
  import type { ReactNode } from 'react'
12
12
  import type { PageMeta } from '@voltro/web'
13
+ import { useLoaderData } from '@voltro/web'
14
+ import { getCollection } from '@voltro/content'
13
15
  import { T, useLocale } from '@voltro/i18n'
14
- import { releases } from '../lib/releases'
16
+ import { releases, tagsOf, visibleReleases, type ReleaseData, type ReleaseEntry } from '../collections/releases.collection'
15
17
  import { getCatalog, withLocalePrefix } from '../lib/locale'
16
18
 
17
19
  export const renderMode = 'static' as const
@@ -25,9 +27,13 @@ export const meta = ({ locale }: { locale: string }): PageMeta => {
25
27
  }
26
28
  }
27
29
 
30
+ export const loader = async (): Promise<ReadonlyArray<ReleaseEntry>> =>
31
+ visibleReleases(await getCollection<ReleaseData>(releases.name))
32
+
28
33
  export default function ChangelogIndex(): ReactNode {
34
+ const entries = useLoaderData<ReadonlyArray<ReleaseEntry>>()
29
35
  const locale = useLocale()
30
- if (releases.length === 0) {
36
+ if (entries.length === 0) {
31
37
  return (
32
38
  <p>
33
39
  <T id="empty.body" values={{ code: (chunks: ReactNode) => <code>{chunks}</code> }} />
@@ -36,18 +42,18 @@ export default function ChangelogIndex(): ReactNode {
36
42
  }
37
43
  return (
38
44
  <div>
39
- {releases.map((release) => (
45
+ {entries.map((release) => (
40
46
  <article key={release.slug} className="changelog-entry">
41
47
  <p className="changelog-meta">
42
- <time dateTime={release.releasedAt}>{release.releasedAt}</time> · v{release.version}
43
- {release.tags.map((tag) => (
48
+ <time dateTime={release.data.releasedAt}>{release.data.releasedAt}</time> · v{release.data.version}
49
+ {tagsOf(release.data).map((tag) => (
44
50
  <span key={tag} className="changelog-tag">{tag}</span>
45
51
  ))}
46
52
  </p>
47
53
  <h2>
48
- <a href={withLocalePrefix(`/${release.slug}`, locale)}>{release.title}</a>
54
+ <a href={withLocalePrefix(`/${release.slug}`, locale)}>{release.data.title}</a>
49
55
  </h2>
50
- <p>{release.summary}</p>
56
+ <p>{release.data.summary}</p>
51
57
  </article>
52
58
  ))}
53
59
  </div>
@@ -12,8 +12,8 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
- "@voltro/cli": "0.52.0",
16
- "@voltro/serverless": "0.52.0",
15
+ "@voltro/cli": "0.54.0",
16
+ "@voltro/serverless": "0.54.0",
17
17
  "effect": "^3.22.0"
18
18
  },
19
19
  "devDependencies": {
@@ -12,18 +12,18 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.52.0",
16
- "@voltro/cli": "0.52.0",
17
- "@voltro/client": "0.52.0",
18
- "@voltro/env": "0.52.0",
19
- "@voltro/i18n": "0.52.0",
20
- "@voltro/ui-shadcn": "0.52.0",
21
- "@voltro/web": "0.52.0",
15
+ "@{{projectName}}/api": "0.54.0",
16
+ "@voltro/cli": "0.54.0",
17
+ "@voltro/client": "0.54.0",
18
+ "@voltro/env": "0.54.0",
19
+ "@voltro/i18n": "0.54.0",
20
+ "@voltro/ui-shadcn": "0.54.0",
21
+ "@voltro/web": "0.54.0",
22
22
  "react": "^19.0.0",
23
23
  "react-dom": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/devtools": "0.52.0",
26
+ "@voltro/devtools": "0.54.0",
27
27
  "@types/react": "^19.0.0",
28
28
  "@types/react-dom": "^19.0.0",
29
29
  "jsdom": "^30.0.0",
@@ -12,19 +12,19 @@
12
12
  "typecheck": "tsc --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.52.0",
16
- "@voltro/cli": "0.52.0",
17
- "@voltro/client": "0.52.0",
18
- "@voltro/env": "0.52.0",
19
- "@voltro/i18n": "0.52.0",
20
- "@voltro/ui-shadcn": "0.52.0",
21
- "@voltro/web": "0.52.0",
15
+ "@{{projectName}}/api": "0.54.0",
16
+ "@voltro/cli": "0.54.0",
17
+ "@voltro/client": "0.54.0",
18
+ "@voltro/env": "0.54.0",
19
+ "@voltro/i18n": "0.54.0",
20
+ "@voltro/ui-shadcn": "0.54.0",
21
+ "@voltro/web": "0.54.0",
22
22
  "react": "^19.0.0",
23
23
  "react-dom": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/devtools": "0.52.0",
27
- "@voltro/testing": "0.52.0",
26
+ "@voltro/devtools": "0.54.0",
27
+ "@voltro/testing": "0.54.0",
28
28
  "@types/react": "^19.0.0",
29
29
  "@types/react-dom": "^19.0.0",
30
30
  "jsdom": "^30.0.0",
@@ -12,18 +12,18 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.52.0",
16
- "@voltro/cli": "0.52.0",
17
- "@voltro/client": "0.52.0",
18
- "@voltro/env": "0.52.0",
19
- "@voltro/i18n": "0.52.0",
20
- "@voltro/ui-shadcn": "0.52.0",
21
- "@voltro/web": "0.52.0",
15
+ "@{{projectName}}/api": "0.54.0",
16
+ "@voltro/cli": "0.54.0",
17
+ "@voltro/client": "0.54.0",
18
+ "@voltro/env": "0.54.0",
19
+ "@voltro/i18n": "0.54.0",
20
+ "@voltro/ui-shadcn": "0.54.0",
21
+ "@voltro/web": "0.54.0",
22
22
  "react": "^19.0.0",
23
23
  "react-dom": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/devtools": "0.52.0",
26
+ "@voltro/devtools": "0.54.0",
27
27
  "@types/react": "^19.0.0",
28
28
  "@types/react-dom": "^19.0.0",
29
29
  "jsdom": "^30.0.0",
@@ -12,17 +12,17 @@
12
12
  "typecheck": "tsc --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/cli": "0.52.0",
16
- "@voltro/client": "0.52.0",
17
- "@voltro/env": "0.52.0",
18
- "@voltro/i18n": "0.52.0",
19
- "@voltro/ui-shadcn": "0.52.0",
20
- "@voltro/web": "0.52.0",
15
+ "@voltro/cli": "0.54.0",
16
+ "@voltro/client": "0.54.0",
17
+ "@voltro/env": "0.54.0",
18
+ "@voltro/i18n": "0.54.0",
19
+ "@voltro/ui-shadcn": "0.54.0",
20
+ "@voltro/web": "0.54.0",
21
21
  "react": "^19.0.0",
22
22
  "react-dom": "^19.0.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/devtools": "0.52.0",
25
+ "@voltro/devtools": "0.54.0",
26
26
  "@types/react": "^19.0.0",
27
27
  "@types/react-dom": "^19.0.0",
28
28
  "jsdom": "^30.0.0",
@@ -12,19 +12,19 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.52.0",
16
- "@voltro/cli": "0.52.0",
17
- "@voltro/client": "0.52.0",
18
- "@voltro/cms": "0.52.0",
19
- "@voltro/env": "0.52.0",
20
- "@voltro/i18n": "0.52.0",
21
- "@voltro/ui-shadcn": "0.52.0",
22
- "@voltro/web": "0.52.0",
15
+ "@{{projectName}}/api": "0.54.0",
16
+ "@voltro/cli": "0.54.0",
17
+ "@voltro/client": "0.54.0",
18
+ "@voltro/cms": "0.54.0",
19
+ "@voltro/env": "0.54.0",
20
+ "@voltro/i18n": "0.54.0",
21
+ "@voltro/ui-shadcn": "0.54.0",
22
+ "@voltro/web": "0.54.0",
23
23
  "react": "^19.0.0",
24
24
  "react-dom": "^19.0.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@voltro/devtools": "0.52.0",
27
+ "@voltro/devtools": "0.54.0",
28
28
  "@types/react": "^19.0.0",
29
29
  "@types/react-dom": "^19.0.0",
30
30
  "jsdom": "^30.0.0",
@@ -2,7 +2,7 @@
2
2
 
3
3
  Voltro collaborative-editing frontend (template: **frontend-collab**).
4
4
 
5
- A document editor whose textarea is bound to a `crdtText()` body over the
5
+ A **rich-text** document editor Tiptap, bound to a `crdtDoc()` body over the
6
6
  framework's reactive loop. Type in **two browser tabs at once** and the edits
7
7
  **converge** — no last-write-wins loser — because the api merges every update
8
8
  into the shared document server-side, then broadcasts the merged result back
@@ -30,45 +30,64 @@ same URL and type in both. Every keystroke merges — both tabs' edits survive.
30
30
  ```
31
31
  Tab A api (api-collab)
32
32
  ───── ────────────────
33
- local crdtText() handle documents.setBody (crdtText 'body' column)
34
- keystrokeinsert/delete └─ runtime folds the update into the
35
- │ → handle.encode() ──rpc──► STORED state (mergeCrdtStates) → merged
33
+ useCrdtDoc → CrdtDocHandle documents.setBody (crdtDoc 'body' column)
34
+ editor edit doc.onUpdate └─ runtime folds the update into the
35
+ (local:true) push ──rpc──► STORED state (mergeCrdtStates) → merged
36
36
  ▼ └─► reactive delta ──WS──► every tab
37
37
  useSubscription('app',
38
- 'documents.list') ◄──WS── merged body ── handle.merge(body)textarea
38
+ 'documents.list') ◄──WS── merged body ── folded (local:falseno push back)
39
+
40
+ └─ useCrdtEditor({ doc }) → Tiptap <EditorContent>
39
41
  ```
40
42
 
41
43
  1. **`useSubscription('app', 'documents.list')`** streams the shared document,
42
- including its **merged** `body` (encoded CRDT bytes), on every write.
43
- 2. A **local `crdtText()` handle** (`@voltro/local-first`) is seeded from that
44
- body. Each keystroke applies a contiguous insert/delete to the handle and
45
- sends `handle.encode()` via **`useMutation('app', 'documents.setBody')`**.
46
- 3. The api's **authoritative server-side merge** folds the update into the
44
+ including its **merged** `body` (encoded CRDT bytes), on every write. That
45
+ value is `useCrdtDoc`'s `remote` `null` means *not loaded*, not *empty*.
46
+ 2. **`useCrdtDoc`** owns one sync client and one live CRDT document for the
47
+ cell. **`useCrdtEditor({ doc })`** mounts a Tiptap editor over it, so typing
48
+ mutates the shared document directly.
49
+ 3. Those edits surface as `doc.onUpdate(update, { local: true })` and are queued
50
+ as **incremental** updates through **`useMutation('app',
51
+ 'documents.setBody')`** — a keystroke is bytes, not the whole document.
52
+ 4. The api's **authoritative server-side merge** folds each update into the
47
53
  stored state before broadcasting — so concurrent edits converge.
48
- 4. Incoming server bodies are **folded back** into the local handle
49
- (idempotent), keeping every tab in sync.
54
+ 5. Incoming bodies are **folded back** into the same document. Those folds
55
+ report `local: false`, and the hook's **echo guard** drops them: without it,
56
+ one keystroke becomes one server write *per open tab*.
50
57
 
51
- This page is exactly the app-level binding the local-first docs describe as the
52
- `SyncClient` transport: `push` = the `setBody` mutation, `onRemoteState` = the
53
- `documents` subscription.
58
+ The two names the framework cannot derive are the app's to supply, and they are
59
+ the whole binding: `push` = the `setBody` mutation, `remote` = the `documents`
60
+ subscription. The sync-client lifecycle, offline queue, bounded retry,
61
+ `outstanding`/`synced` and the echo guard all live in the hook.
54
62
 
55
- ## Not wired here: presence cursors
63
+ ## `renderMode = 'client'`
56
64
 
57
- Live "who else is editing" cursors (`usePresence` from
58
- `@voltro/local-first/react`) need a `PresenceChannel` bound to the app's
59
- broadcast broker a runtime binding, not zero-infra in a single dev process
60
- across tabs so this template omits them. The CRDT text convergence above needs
61
- none of that; it rides the reactive engine you already have.
65
+ `page.tsx` sets it explicitly. A rich-text editor needs a browser: `renderMode`
66
+ defaults to `'static'`, which pre-renders at build time, and Tiptap's
67
+ `new Editor()` finds no `window` there the build fails. There is nothing
68
+ worth pre-rendering for a document that arrives over a live subscription
69
+ anyway.
70
+
71
+ ## Not wired here: presence carets
72
+
73
+ Live "who else is editing" carets need two things this template does not ship:
74
+ a `PresenceChannel` bound to the app's broadcast broker, and an
75
+ `AwarenessTransport` over a `delivery: 'latest'` event
76
+ (`useCrdtEditor({ doc, awareness, user })`). Both are runtime bindings, not
77
+ zero-infra in a single dev process across tabs. The convergence above needs
78
+ none of it — it rides the reactive engine you already have.
62
79
 
63
80
  ## Files
64
81
 
65
82
  ```
66
83
  app.config.ts type:web + the `apis: { app }` wiring
67
- package.json depends on @{{projectName}}/api (the sibling) + @voltro/local-first
84
+ package.json depends on @{{projectName}}/api (the sibling),
85
+ @voltro/local-first + the four @tiptap/* peers
68
86
  src/pages/
69
87
  layout.tsx root shell (header + <main>)
70
- page.tsx the collaborative editor — subscription + CRDT handle + setBody
71
- page.test.tsx render + edit-dispatch tests (jsdom)
88
+ page.tsx the collaborative editor — subscription + useCrdtDoc + useCrdtEditor
89
+ page.test.tsx page-contract tests (jsdom) — states, create, and a
90
+ server-side fold converging into the open editor
72
91
  src/locales/{en,de}.ts bilingual UI strings
73
92
  ```
74
93
 
@@ -1,8 +1,8 @@
1
1
  // Web app for the {{projectName}} project. Read by `voltro dev` / `voltro start`.
2
2
  //
3
- // This is the COLLABORATIVE-EDITING frontend — a textarea bound to a
4
- // `crdtText()` document body over the framework's reactive loop. Open it in two
5
- // browser tabs and type in both: the edits CONVERGE (no last-write-wins loser)
3
+ // This is the COLLABORATIVE-EDITING frontend — a Tiptap rich-text editor bound
4
+ // to a `crdtDoc()` document body over the framework's reactive loop. Open it in
5
+ // two browser tabs and type in both: the edits CONVERGE (no last-write-wins loser)
6
6
  // because the api's server-authoritative CRDT merge folds every update into the
7
7
  // stored state, then the reactive engine broadcasts the merged row back. Zero
8
8
  // infra — the merge + broadcast are in-process on a single `voltro dev`.
@@ -12,20 +12,24 @@
12
12
  "typecheck": "tsc --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.52.0",
16
- "@voltro/cli": "0.52.0",
17
- "@voltro/client": "0.52.0",
18
- "@voltro/env": "0.52.0",
19
- "@voltro/i18n": "0.52.0",
20
- "@voltro/local-first": "0.52.0",
21
- "@voltro/ui-shadcn": "0.52.0",
22
- "@voltro/web": "0.52.0",
15
+ "@{{projectName}}/api": "0.54.0",
16
+ "@tiptap/extension-collaboration": "^3.0.0",
17
+ "@tiptap/extension-collaboration-caret": "^3.0.0",
18
+ "@tiptap/react": "^3.0.0",
19
+ "@tiptap/starter-kit": "^3.0.0",
20
+ "@voltro/cli": "0.54.0",
21
+ "@voltro/client": "0.54.0",
22
+ "@voltro/env": "0.54.0",
23
+ "@voltro/i18n": "0.54.0",
24
+ "@voltro/local-first": "0.54.0",
25
+ "@voltro/ui-shadcn": "0.54.0",
26
+ "@voltro/web": "0.54.0",
23
27
  "react": "^19.0.0",
24
28
  "react-dom": "^19.0.0"
25
29
  },
26
30
  "devDependencies": {
27
- "@voltro/devtools": "0.52.0",
28
- "@voltro/testing": "0.52.0",
31
+ "@voltro/devtools": "0.54.0",
32
+ "@voltro/testing": "0.54.0",
29
33
  "@types/react": "^19.0.0",
30
34
  "@types/react-dom": "^19.0.0",
31
35
  "jsdom": "^30.0.0",
@@ -14,7 +14,7 @@ export default defineLocale<typeof en>()({
14
14
  'lang.label': 'Sprache',
15
15
 
16
16
  'editor.heading': 'Kollaborativer Editor',
17
- 'editor.intro': 'Öffne diese Seite in <strong>zwei Browser-Tabs</strong> und tippe in beiden. Die Änderungen <strong>konvergieren</strong> — kein Last-Write-Wins-Verlierer — weil die API jedes Update serverseitig in den gemeinsamen <code>crdtText()</code>-Body mergt und live zurücksendet.',
17
+ 'editor.intro': 'Öffne diese Seite in <strong>zwei Browser-Tabs</strong> und tippe in beiden. Die Änderungen <strong>konvergieren</strong> — kein Last-Write-Wins-Verlierer — weil die API jedes Update serverseitig in den gemeinsamen <code>crdtDoc()</code>-Body mergt und live zurücksendet.',
18
18
  'editor.status.tenant': 'Mandant <code>{tenant}</code> · Revision {revision}',
19
19
  'editor.status.saving': ' · synchronisiere…',
20
20
  'editor.status.loading': ' · lädt…',
@@ -24,6 +24,5 @@ export default defineLocale<typeof en>()({
24
24
  'editor.create': 'Dokument erstellen',
25
25
  'editor.creating': 'Wird erstellt…',
26
26
  'editor.docLabel': 'Bearbeite <strong>{title}</strong>',
27
- 'editor.placeholder': 'Fang an zu tippen — jeder Tastendruck synchronisiert und mergt.',
28
27
  'editor.hint': 'Tipp: Öffne einen zweiten Tab und tippe gleichzeitig. Beide Änderungen bleiben erhalten.',
29
28
  })
@@ -16,7 +16,7 @@ export default defineCatalog({
16
16
 
17
17
  // ---- Editor page (pages/page.tsx) ----
18
18
  'editor.heading': 'Collaborative editor',
19
- 'editor.intro': 'Open this page in <strong>two browser tabs</strong> and type in both. Edits <strong>converge</strong> — no last-write-wins loser — because the api merges every update into the shared <code>crdtText()</code> body server-side, then broadcasts it back live.',
19
+ 'editor.intro': 'Open this page in <strong>two browser tabs</strong> and type in both. Edits <strong>converge</strong> — no last-write-wins loser — because the api merges every update into the shared <code>crdtDoc()</code> body server-side, then broadcasts it back live.',
20
20
  'editor.status.tenant': 'Tenant <code>{tenant}</code> · revision {revision}',
21
21
  'editor.status.saving': ' · syncing…',
22
22
  'editor.status.loading': ' · loading…',
@@ -26,6 +26,5 @@ export default defineCatalog({
26
26
  'editor.create': 'Create document',
27
27
  'editor.creating': 'Creating…',
28
28
  'editor.docLabel': 'Editing <strong>{title}</strong>',
29
- 'editor.placeholder': 'Start typing — every keystroke syncs and merges.',
30
29
  'editor.hint': 'Tip: open a second tab and type at the same time. Both edits survive.',
31
30
  } as const)