convex-cms 0.0.3 → 0.0.5-alpha.2

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 (375) hide show
  1. package/README.md +107 -60
  2. package/admin/README.md +99 -0
  3. package/admin/src/components/AdminLayout.tsx +22 -0
  4. package/admin/src/components/BreakingChangesWarningDialog.tsx +81 -0
  5. package/admin/src/components/BulkActionBar.tsx +190 -0
  6. package/admin/src/components/BulkOperationModal.tsx +177 -0
  7. package/admin/src/components/ContentEntryEditor.tsx +1104 -0
  8. package/admin/src/components/ContentTypeFormModal.tsx +1012 -0
  9. package/admin/src/components/ErrorBoundary.tsx +83 -0
  10. package/admin/src/components/ErrorState.tsx +147 -0
  11. package/admin/src/components/Header.tsx +294 -0
  12. package/admin/src/components/RouteGuard.tsx +264 -0
  13. package/admin/src/components/Sidebar.tsx +90 -0
  14. package/admin/src/components/TaxonomyEditor.tsx +348 -0
  15. package/admin/src/components/TermTree.tsx +533 -0
  16. package/admin/src/components/UploadDropzone.tsx +383 -0
  17. package/admin/src/components/VersionCompare.tsx +250 -0
  18. package/admin/src/components/VersionHistory.tsx +279 -0
  19. package/admin/src/components/VersionRollbackModal.tsx +79 -0
  20. package/admin/src/components/cmsds/CmsButton.tsx +101 -0
  21. package/admin/src/components/cmsds/CmsDialog.tsx +139 -0
  22. package/admin/src/components/cmsds/CmsDropdown.tsx +62 -0
  23. package/admin/src/components/cmsds/CmsEmptyState.tsx +54 -0
  24. package/admin/src/components/cmsds/CmsField.tsx +47 -0
  25. package/admin/src/components/cmsds/CmsPageHeader.tsx +35 -0
  26. package/admin/src/components/cmsds/CmsStatusBadge.tsx +153 -0
  27. package/admin/src/components/cmsds/CmsSurface.tsx +52 -0
  28. package/admin/src/components/cmsds/CmsTable.tsx +164 -0
  29. package/admin/src/components/cmsds/CmsToolbar.tsx +58 -0
  30. package/admin/src/components/cmsds/index.ts +10 -0
  31. package/admin/src/components/fields/BooleanField.tsx +74 -0
  32. package/admin/src/components/fields/CategoryField.tsx +394 -0
  33. package/admin/src/components/fields/DateField.tsx +173 -0
  34. package/admin/src/components/fields/DefaultFieldRenderer.tsx +74 -0
  35. package/admin/src/components/fields/FieldRenderer.tsx +180 -0
  36. package/admin/src/components/fields/FieldWrapper.tsx +57 -0
  37. package/admin/src/components/fields/JsonField.tsx +172 -0
  38. package/admin/src/components/fields/MediaField.tsx +367 -0
  39. package/admin/src/components/fields/MultiSelectField.tsx +118 -0
  40. package/admin/src/components/fields/NumberField.tsx +77 -0
  41. package/admin/src/components/fields/ReferenceField.tsx +386 -0
  42. package/admin/src/components/fields/RichTextField.tsx +171 -0
  43. package/admin/src/components/fields/SelectField.tsx +62 -0
  44. package/admin/src/components/fields/TagField.tsx +325 -0
  45. package/admin/src/components/fields/TextAreaField.tsx +68 -0
  46. package/admin/src/components/fields/TextField.tsx +56 -0
  47. package/admin/src/components/fields/index.ts +54 -0
  48. package/admin/src/components/fields/registry.ts +64 -0
  49. package/admin/src/components/fields/types.ts +217 -0
  50. package/admin/src/components/filters/TaxonomyFilter.tsx +254 -0
  51. package/admin/src/components/filters/index.ts +1 -0
  52. package/admin/src/components/index.ts +8 -0
  53. package/admin/src/components/media/MediaAssetActions.tsx +115 -0
  54. package/admin/src/components/media/MediaAssetEditDialog.tsx +217 -0
  55. package/admin/src/components/media/MediaBulkActionBar.tsx +51 -0
  56. package/admin/src/components/media/MediaFolderActions.tsx +69 -0
  57. package/admin/src/components/media/MediaFolderEditDialog.tsx +126 -0
  58. package/admin/src/components/media/MediaMoveModal.tsx +179 -0
  59. package/admin/src/components/media/MediaPreviewModal.tsx +370 -0
  60. package/admin/src/components/media/MediaTaxonomyPicker.tsx +304 -0
  61. package/admin/src/components/media/MediaTrashBulkActionBar.tsx +59 -0
  62. package/admin/src/components/ui/accordion.tsx +64 -0
  63. package/admin/src/components/ui/alert-dialog.tsx +155 -0
  64. package/admin/src/components/ui/alert.tsx +66 -0
  65. package/admin/src/components/ui/avatar.tsx +53 -0
  66. package/admin/src/components/ui/badge.tsx +46 -0
  67. package/admin/src/components/ui/breadcrumb.tsx +109 -0
  68. package/admin/src/components/ui/button.tsx +62 -0
  69. package/admin/src/components/ui/calendar.tsx +220 -0
  70. package/admin/src/components/ui/card.tsx +92 -0
  71. package/admin/src/components/ui/checkbox.tsx +30 -0
  72. package/admin/src/components/ui/command.tsx +182 -0
  73. package/admin/src/components/ui/dialog.tsx +143 -0
  74. package/admin/src/components/ui/dropdown-menu.tsx +257 -0
  75. package/admin/src/components/ui/form.tsx +167 -0
  76. package/admin/src/components/ui/input.tsx +21 -0
  77. package/admin/src/components/ui/label.tsx +24 -0
  78. package/admin/src/components/ui/popover.tsx +46 -0
  79. package/admin/src/components/ui/scroll-area.tsx +56 -0
  80. package/admin/src/components/ui/select.tsx +190 -0
  81. package/admin/src/components/ui/separator.tsx +26 -0
  82. package/admin/src/components/ui/sheet.tsx +137 -0
  83. package/admin/src/components/ui/sidebar.tsx +724 -0
  84. package/admin/src/components/ui/skeleton.tsx +13 -0
  85. package/admin/src/components/ui/sonner.tsx +38 -0
  86. package/admin/src/components/ui/switch.tsx +31 -0
  87. package/admin/src/components/ui/table.tsx +114 -0
  88. package/admin/src/components/ui/tabs.tsx +66 -0
  89. package/admin/src/components/ui/textarea.tsx +18 -0
  90. package/admin/src/components/ui/tooltip.tsx +61 -0
  91. package/admin/src/contexts/AdminConfigContext.tsx +30 -0
  92. package/admin/src/contexts/AuthContext.tsx +330 -0
  93. package/admin/src/contexts/BreadcrumbContext.tsx +49 -0
  94. package/admin/src/contexts/SettingsConfigContext.tsx +57 -0
  95. package/admin/src/contexts/ThemeContext.tsx +91 -0
  96. package/admin/src/contexts/index.ts +20 -0
  97. package/admin/src/embed/components/EmbedHeader.tsx +103 -0
  98. package/admin/src/embed/components/EmbedLayout.tsx +29 -0
  99. package/admin/src/embed/components/EmbedSidebar.tsx +119 -0
  100. package/admin/src/embed/components/index.ts +3 -0
  101. package/admin/src/embed/contexts/ApiContext.tsx +32 -0
  102. package/admin/src/embed/index.tsx +184 -0
  103. package/admin/src/embed/navigation.tsx +202 -0
  104. package/admin/src/embed/pages/Content.tsx +19 -0
  105. package/admin/src/embed/pages/ContentTypes.tsx +19 -0
  106. package/admin/src/embed/pages/Dashboard.tsx +19 -0
  107. package/admin/src/embed/pages/Media.tsx +19 -0
  108. package/admin/src/embed/pages/Settings.tsx +22 -0
  109. package/admin/src/embed/pages/Taxonomies.tsx +22 -0
  110. package/admin/src/embed/pages/Trash.tsx +22 -0
  111. package/admin/src/embed/pages/index.ts +7 -0
  112. package/admin/src/embed/types.ts +24 -0
  113. package/admin/src/hooks/index.ts +2 -0
  114. package/admin/src/hooks/use-mobile.ts +19 -0
  115. package/admin/src/hooks/useBreadcrumbLabel.ts +15 -0
  116. package/admin/src/hooks/usePermissions.ts +211 -0
  117. package/admin/src/lib/admin-config.ts +111 -0
  118. package/admin/src/lib/cn.ts +6 -0
  119. package/admin/src/lib/config.server.ts +56 -0
  120. package/admin/src/lib/convex.ts +26 -0
  121. package/admin/src/lib/embed-adapter.ts +80 -0
  122. package/admin/src/lib/icons.tsx +96 -0
  123. package/admin/src/lib/loadAdminConfig.ts +92 -0
  124. package/admin/src/lib/motion.ts +29 -0
  125. package/admin/src/lib/navigation.ts +43 -0
  126. package/admin/src/lib/tanstack-adapter.ts +82 -0
  127. package/admin/src/pages/ContentPage.tsx +337 -0
  128. package/admin/src/pages/ContentTypesPage.tsx +457 -0
  129. package/admin/src/pages/DashboardPage.tsx +163 -0
  130. package/admin/src/pages/MediaPage.tsx +34 -0
  131. package/admin/src/pages/SettingsPage.tsx +486 -0
  132. package/admin/src/pages/TaxonomiesPage.tsx +289 -0
  133. package/admin/src/pages/TrashPage.tsx +421 -0
  134. package/admin/src/pages/index.ts +14 -0
  135. package/admin/src/routeTree.gen.ts +262 -0
  136. package/admin/src/router.tsx +22 -0
  137. package/admin/src/routes/__root.tsx +250 -0
  138. package/admin/src/routes/content-types.tsx +20 -0
  139. package/admin/src/routes/content.tsx +20 -0
  140. package/admin/src/routes/entries/$entryId.tsx +107 -0
  141. package/admin/src/routes/entries/new.$contentTypeId.tsx +69 -0
  142. package/admin/src/routes/entries/type/$contentTypeId.tsx +503 -0
  143. package/admin/src/routes/index.tsx +20 -0
  144. package/admin/src/routes/media.tsx +1095 -0
  145. package/admin/src/routes/settings.tsx +20 -0
  146. package/admin/src/routes/taxonomies.tsx +20 -0
  147. package/admin/src/routes/trash.tsx +20 -0
  148. package/admin/src/styles/globals.css +69 -0
  149. package/admin/src/styles/tailwind-config.css +74 -0
  150. package/admin/src/styles/theme.css +73 -0
  151. package/admin/src/types/index.ts +221 -0
  152. package/admin/src/utils/errorParsing.ts +163 -0
  153. package/admin/src/utils/index.ts +5 -0
  154. package/admin/src/vite-env.d.ts +14 -0
  155. package/admin/tailwind.preset.cjs +102 -0
  156. package/admin-dist/nitro.json +1 -1
  157. package/admin-dist/public/assets/{CmsEmptyState-CRswfTzk.js → CmsEmptyState-CkqBIab3.js} +2 -2
  158. package/admin-dist/public/assets/{CmsPageHeader-CirpXndm.js → CmsPageHeader-CUtl5MMG.js} +1 -1
  159. package/admin-dist/public/assets/{CmsStatusBadge-CbEUpQu-.js → CmsStatusBadge-CUYFgEe-.js} +1 -1
  160. package/admin-dist/public/assets/CmsSurface-CsJfAVa3.js +1 -0
  161. package/admin-dist/public/assets/{CmsToolbar-BI2nZOXp.js → CmsToolbar-CnfbcxeP.js} +1 -1
  162. package/admin-dist/public/assets/{ContentEntryEditor-CBeCyK_m.js → ContentEntryEditor-BU220CCy.js} +1 -1
  163. package/admin-dist/public/assets/TaxonomyFilter-CWCxC5HZ.js +1 -0
  164. package/admin-dist/public/assets/_contentTypeId-DK8cskRt.js +1 -0
  165. package/admin-dist/public/assets/{_entryId-CKU_glsK.js → _entryId-CuVMExbb.js} +1 -1
  166. package/admin-dist/public/assets/alert-CF1BSzGR.js +1 -0
  167. package/admin-dist/public/assets/{badge-hvUOzpVZ.js → badge-CmuOIVKp.js} +1 -1
  168. package/admin-dist/public/assets/{circle-check-big-CF_pR17r.js → circle-check-big-BKDVG6DU.js} +1 -1
  169. package/admin-dist/public/assets/{command-DU82cJlt.js → command-XJxnF2Sd.js} +1 -1
  170. package/admin-dist/public/assets/content-QBUxdxbS.js +1 -0
  171. package/admin-dist/public/assets/content-types-CrNEm8Hf.js +2 -0
  172. package/admin-dist/public/assets/globals-B7Wsfh_v.css +1 -0
  173. package/admin-dist/public/assets/index-C7xOwudI.js +1 -0
  174. package/admin-dist/public/assets/{label-KNtpL71g.js → label-CHCnXeBk.js} +1 -1
  175. package/admin-dist/public/assets/{link-2-Bw2aI4V4.js → link-2-Bb34judH.js} +1 -1
  176. package/admin-dist/public/assets/{list-sYepHjt_.js → list-9Pzt48ld.js} +1 -1
  177. package/admin-dist/public/assets/{main-CKj5yfEi.js → main-CjQ2VI9L.js} +3 -3
  178. package/admin-dist/public/assets/media-Dc5PWt2Q.js +1 -0
  179. package/admin-dist/public/assets/{new._contentTypeId-C3LstjNs.js → new._contentTypeId-C_I4YxIa.js} +1 -1
  180. package/admin-dist/public/assets/{plus-DUn8v_Xf.js → plus-Ceef7DHk.js} +1 -1
  181. package/admin-dist/public/assets/{rotate-ccw-DJEoHcRI.js → rotate-ccw-7k7-4VUq.js} +1 -1
  182. package/admin-dist/public/assets/scroll-area-CC6wujnp.js +1 -0
  183. package/admin-dist/public/assets/{search-MuAUDJKR.js → search-DwoUV2pv.js} +1 -1
  184. package/admin-dist/public/assets/select-hOZTp8aC.js +1 -0
  185. package/admin-dist/public/assets/settings-t2PbCZh4.js +1 -0
  186. package/admin-dist/public/assets/switch-jX2pDaNU.js +1 -0
  187. package/admin-dist/public/assets/tabs-q4EbZk7c.js +1 -0
  188. package/admin-dist/public/assets/tanstack-adapter-B-Glm4kH.js +1 -0
  189. package/admin-dist/public/assets/taxonomies-kyk5P4ZW.js +1 -0
  190. package/admin-dist/public/assets/{textarea-BTy7nwzR.js → textarea-B6SfBmr0.js} +1 -1
  191. package/admin-dist/public/assets/trash-BOCnIznD.js +1 -0
  192. package/admin-dist/public/assets/{triangle-alert-E52Vfeuh.js → triangle-alert-CXFIO_Gu.js} +1 -1
  193. package/admin-dist/public/assets/useBreadcrumbLabel-_6qBagc3.js +1 -0
  194. package/admin-dist/public/assets/{usePermissions-Basjs9BT.js → usePermissions-M1ijZ7a6.js} +1 -1
  195. package/admin-dist/server/_chunks/_libs/@tanstack/react-router.mjs +7 -0
  196. package/admin-dist/server/_ssr/{badge-6BsP37vG.mjs → CmsButton-DOiTVKQq.mjs} +33 -33
  197. package/admin-dist/server/_ssr/{CmsEmptyState-DU7-7-mV.mjs → CmsEmptyState-fbnGt3LD.mjs} +2 -2
  198. package/admin-dist/server/_ssr/{CmsPageHeader-CseW0AHm.mjs → CmsPageHeader-DHRrdOZa.mjs} +1 -1
  199. package/admin-dist/server/_ssr/{CmsStatusBadge-B_pi4KCp.mjs → CmsStatusBadge-s7obWbKZ.mjs} +2 -2
  200. package/admin-dist/server/_ssr/CmsSurface-rFoYjb62.mjs +44 -0
  201. package/admin-dist/server/_ssr/{CmsToolbar-X75ex6ek.mjs → CmsToolbar-zTE45z2q.mjs} +2 -2
  202. package/admin-dist/server/_ssr/{ContentEntryEditor-CepusRsA.mjs → ContentEntryEditor-BLoEjT_m.mjs} +12 -12
  203. package/admin-dist/server/_ssr/{TaxonomyFilter-Bwrq0-cz.mjs → TaxonomyFilter-XAtaJC2z.mjs} +5 -5
  204. package/admin-dist/server/_ssr/{_contentTypeId-BqYKEcLr.mjs → _contentTypeId-Csl4822C.mjs} +13 -13
  205. package/admin-dist/server/_ssr/{_entryId-CRfnqeDf.mjs → _entryId-D8alLFBx.mjs} +15 -15
  206. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BffZedId.mjs +4 -0
  207. package/admin-dist/server/_ssr/{command-fy8epIKf.mjs → command-C0Di14--.mjs} +1 -1
  208. package/admin-dist/server/_ssr/{content-B5RhL7uW.mjs → content-CT-FPsmV.mjs} +170 -98
  209. package/admin-dist/server/_ssr/{content-types-BIOqCQYN.mjs → content-types-C8cBFdzE.mjs} +260 -115
  210. package/admin-dist/server/_ssr/{index-DHSHDPt1.mjs → index-BJtcrEc-.mjs} +88 -17
  211. package/admin-dist/server/_ssr/index.mjs +2 -2
  212. package/admin-dist/server/_ssr/{label-C8Dko1j7.mjs → label-qn2Afwl4.mjs} +1 -1
  213. package/admin-dist/server/_ssr/{media-CSx3XttC.mjs → media-qv5IAsMZ.mjs} +43 -43
  214. package/admin-dist/server/_ssr/{new._contentTypeId-DzanEZQM.mjs → new._contentTypeId-DdGyrhqs.mjs} +13 -13
  215. package/admin-dist/server/_ssr/{router-DDWcF-kt.mjs → router-nSVkxb6Y.mjs} +11 -11
  216. package/admin-dist/server/_ssr/{scroll-area-bjPYwhXN.mjs → scroll-area-BCinP455.mjs} +1 -1
  217. package/admin-dist/server/_ssr/{select-BUhDDf4T.mjs → select-BKQlQScw.mjs} +1 -1
  218. package/admin-dist/server/_ssr/{settings-DAsxnw2q.mjs → settings-BCr2KQlk.mjs} +236 -139
  219. package/admin-dist/server/_ssr/{switch-BgyRtQ1Z.mjs → switch-BaOi42fE.mjs} +1 -1
  220. package/admin-dist/server/_ssr/{tabs-DzMdRB1A.mjs → tabs-DYXEi9kq.mjs} +5 -3
  221. package/admin-dist/server/_ssr/tanstack-adapter-Bsz8kha-.mjs +119 -0
  222. package/admin-dist/server/_ssr/{taxonomies-C8j8g5Q5.mjs → taxonomies-CueMHTbE.mjs} +184 -73
  223. package/admin-dist/server/_ssr/{textarea-9jNeYJSc.mjs → textarea-CI0Jqx2x.mjs} +1 -1
  224. package/admin-dist/server/_ssr/{trash-DYMxwhZB.mjs → trash-DE6W8GoX.mjs} +211 -88
  225. package/admin-dist/server/_ssr/{useBreadcrumbLabel-FNSAr2Ha.mjs → useBreadcrumbLabel-B5Yi72lM.mjs} +1 -1
  226. package/admin-dist/server/_ssr/{usePermissions-BJGGahrJ.mjs → usePermissions-C3nZ-Izm.mjs} +1 -1
  227. package/admin-dist/server/index.mjs +189 -182
  228. package/dist/cli/commands/init.d.ts +6 -0
  229. package/dist/cli/commands/init.d.ts.map +1 -0
  230. package/dist/cli/commands/init.js +156 -0
  231. package/dist/cli/commands/init.js.map +1 -0
  232. package/dist/cli/index.js +6 -0
  233. package/dist/cli/index.js.map +1 -1
  234. package/dist/client/admin/bulk.d.ts +79 -0
  235. package/dist/client/admin/bulk.d.ts.map +1 -0
  236. package/dist/client/admin/bulk.js +72 -0
  237. package/dist/client/admin/bulk.js.map +1 -0
  238. package/dist/client/admin/contentLock.d.ts +118 -0
  239. package/dist/client/admin/contentLock.d.ts.map +1 -0
  240. package/dist/client/admin/contentLock.js +81 -0
  241. package/dist/client/admin/contentLock.js.map +1 -0
  242. package/dist/client/admin/contentTypes.d.ts +1204 -0
  243. package/dist/client/admin/contentTypes.d.ts.map +1 -0
  244. package/dist/client/admin/contentTypes.js +122 -0
  245. package/dist/client/admin/contentTypes.js.map +1 -0
  246. package/dist/client/admin/dashboard.d.ts +16 -0
  247. package/dist/client/admin/dashboard.d.ts.map +1 -0
  248. package/dist/client/admin/dashboard.js +33 -0
  249. package/dist/client/admin/dashboard.js.map +1 -0
  250. package/dist/client/admin/entries.d.ts +358 -0
  251. package/dist/client/admin/entries.d.ts.map +1 -0
  252. package/dist/client/admin/entries.js +220 -0
  253. package/dist/client/admin/entries.js.map +1 -0
  254. package/dist/client/admin/index.d.ts +6568 -0
  255. package/dist/client/admin/index.d.ts.map +1 -0
  256. package/dist/client/admin/index.js +305 -0
  257. package/dist/client/admin/index.js.map +1 -0
  258. package/dist/client/admin/media.d.ts +1038 -0
  259. package/dist/client/admin/media.d.ts.map +1 -0
  260. package/dist/client/admin/media.js +489 -0
  261. package/dist/client/admin/media.js.map +1 -0
  262. package/dist/client/admin/taxonomies.d.ts +339 -0
  263. package/dist/client/admin/taxonomies.d.ts.map +1 -0
  264. package/dist/client/admin/taxonomies.js +364 -0
  265. package/dist/client/admin/taxonomies.js.map +1 -0
  266. package/dist/client/admin/trash.d.ts +91 -0
  267. package/dist/client/admin/trash.d.ts.map +1 -0
  268. package/dist/client/admin/trash.js +71 -0
  269. package/dist/client/admin/trash.js.map +1 -0
  270. package/dist/client/admin/types.d.ts +320 -0
  271. package/dist/client/admin/types.d.ts.map +1 -0
  272. package/dist/client/admin/types.js +7 -0
  273. package/dist/client/admin/types.js.map +1 -0
  274. package/dist/client/admin/validators.d.ts +3886 -0
  275. package/dist/client/admin/validators.d.ts.map +1 -0
  276. package/dist/client/admin/validators.js +322 -0
  277. package/dist/client/admin/validators.js.map +1 -0
  278. package/dist/client/admin/versions.d.ts +106 -0
  279. package/dist/client/admin/versions.d.ts.map +1 -0
  280. package/dist/client/admin/versions.js +57 -0
  281. package/dist/client/admin/versions.js.map +1 -0
  282. package/dist/client/adminApiTypes.d.ts +27 -0
  283. package/dist/client/adminApiTypes.d.ts.map +1 -0
  284. package/dist/client/adminApiTypes.js +12 -0
  285. package/dist/client/adminApiTypes.js.map +1 -0
  286. package/dist/client/{admin-config.d.ts → adminConfig.d.ts} +4 -4
  287. package/dist/client/adminConfig.d.ts.map +1 -0
  288. package/dist/client/{admin-config.js → adminConfig.js} +3 -3
  289. package/dist/client/adminConfig.js.map +1 -0
  290. package/dist/client/agentTools.d.ts +11 -21
  291. package/dist/client/agentTools.d.ts.map +1 -1
  292. package/dist/client/agentTools.js +4 -4
  293. package/dist/client/index.d.ts +6 -6
  294. package/dist/client/index.d.ts.map +1 -1
  295. package/dist/client/index.js +19 -6
  296. package/dist/client/index.js.map +1 -1
  297. package/dist/client/schema/codegen.d.ts +2 -2
  298. package/dist/client/schema/codegen.d.ts.map +1 -1
  299. package/dist/client/schema/codegen.js +3 -3
  300. package/dist/client/schema/codegen.js.map +1 -1
  301. package/dist/client/schema/defineContentType.d.ts +3 -3
  302. package/dist/client/schema/defineContentType.js +3 -3
  303. package/dist/client/schema/index.d.ts +7 -7
  304. package/dist/client/schema/index.d.ts.map +1 -1
  305. package/dist/client/schema/index.js +5 -5
  306. package/dist/client/schema/index.js.map +1 -1
  307. package/dist/client/schema/schemaDrift.d.ts +1 -1
  308. package/dist/client/schema/schemaDrift.js +1 -1
  309. package/dist/client/schema/typedClient.d.ts +2 -2
  310. package/dist/client/schema/typedClient.js +2 -2
  311. package/dist/client/schema/types.d.ts +1 -1
  312. package/dist/client/schema/types.js +1 -1
  313. package/dist/client/wrapper.d.ts +108 -65
  314. package/dist/client/wrapper.d.ts.map +1 -1
  315. package/dist/client/wrapper.js +22 -22
  316. package/dist/client/wrapper.js.map +1 -1
  317. package/dist/component/contentEntries.d.ts +4 -4
  318. package/dist/component/contentEntryMutations.d.ts +46 -0
  319. package/dist/component/contentEntryMutations.d.ts.map +1 -1
  320. package/dist/component/contentEntryMutations.js +1 -1
  321. package/dist/component/contentEntryMutations.js.map +1 -1
  322. package/dist/component/contentTypeMigration.d.ts +1 -1
  323. package/dist/component/contentTypeMutations.d.ts +22 -0
  324. package/dist/component/contentTypeMutations.d.ts.map +1 -1
  325. package/dist/component/contentTypeMutations.js +1 -1
  326. package/dist/component/contentTypeMutations.js.map +1 -1
  327. package/dist/component/convex.config.d.ts +2 -2
  328. package/dist/component/convex.config.js +2 -2
  329. package/dist/component/index.d.ts +1 -1
  330. package/dist/component/index.js +1 -1
  331. package/dist/component/lib/ragContentChunker.d.ts +1 -1
  332. package/dist/component/lib/ragContentChunker.js +1 -1
  333. package/dist/component/mediaAssetMutations.d.ts +47 -0
  334. package/dist/component/mediaAssetMutations.d.ts.map +1 -1
  335. package/dist/component/mediaAssetMutations.js +1 -1
  336. package/dist/component/mediaAssetMutations.js.map +1 -1
  337. package/dist/component/roles.d.ts +1 -1
  338. package/dist/component/roles.js +1 -1
  339. package/dist/component/schema.d.ts +9 -0
  340. package/dist/component/schema.d.ts.map +1 -1
  341. package/dist/component/schema.js +1 -1
  342. package/dist/component/schema.js.map +1 -1
  343. package/dist/react/index.d.ts +2 -2
  344. package/dist/react/index.d.ts.map +1 -1
  345. package/dist/react/index.js +13 -7
  346. package/dist/react/index.js.map +1 -1
  347. package/dist/test.d.ts +2 -2
  348. package/dist/test.js +2 -2
  349. package/package.json +115 -13
  350. package/admin-dist/public/assets/ErrorState-BIVaWmom.js +0 -1
  351. package/admin-dist/public/assets/TaxonomyFilter-ChaY6Y_x.js +0 -1
  352. package/admin-dist/public/assets/_contentTypeId-DQ8k_Rvw.js +0 -1
  353. package/admin-dist/public/assets/alert-BXjTqrwQ.js +0 -1
  354. package/admin-dist/public/assets/content-_LXl3pp7.js +0 -1
  355. package/admin-dist/public/assets/content-types-KjxaXGxY.js +0 -2
  356. package/admin-dist/public/assets/globals-CS6BZ0zp.css +0 -1
  357. package/admin-dist/public/assets/index-DNGIZHL-.js +0 -1
  358. package/admin-dist/public/assets/media-Bkrkffm7.js +0 -1
  359. package/admin-dist/public/assets/scroll-area-DfIlT0in.js +0 -1
  360. package/admin-dist/public/assets/select-BD29IXCI.js +0 -1
  361. package/admin-dist/public/assets/settings-DmMyn_6A.js +0 -1
  362. package/admin-dist/public/assets/switch-h3Rrnl5i.js +0 -1
  363. package/admin-dist/public/assets/tabs-imc8h-Dp.js +0 -1
  364. package/admin-dist/public/assets/taxonomies-dAsrT65H.js +0 -1
  365. package/admin-dist/public/assets/trash-SAWKZZHv.js +0 -1
  366. package/admin-dist/public/assets/useBreadcrumbLabel-BECBMCzM.js +0 -1
  367. package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +0 -89
  368. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BwDlABVk.mjs +0 -4
  369. package/admin-dist/server/_ssr/alert-CVt45UUP.mjs +0 -92
  370. package/dist/client/admin-config.d.ts.map +0 -1
  371. package/dist/client/admin-config.js.map +0 -1
  372. package/dist/client/adminApi.d.ts +0 -2273
  373. package/dist/client/adminApi.d.ts.map +0 -1
  374. package/dist/client/adminApi.js +0 -716
  375. package/dist/client/adminApi.js.map +0 -1
@@ -0,0 +1,383 @@
1
+ import { useState, useRef, useCallback } from 'react'
2
+ import {
3
+ useMediaUploadQueue,
4
+ type UploadQueueFile,
5
+ type UploadQueueFileStatus,
6
+ } from 'convex-cms/react'
7
+ import type { FunctionReference } from 'convex/server'
8
+ import { CmsButton } from '~/components/cmsds/CmsButton'
9
+ import { cn } from '~/lib/cn'
10
+ import { Upload, Check, X, RefreshCw, AlertCircle } from 'lucide-react'
11
+
12
+ export interface UploadDropzoneProps {
13
+ onUploadComplete: (files: UploadedFile[]) => void
14
+ currentFolderId?: string
15
+ generateUploadUrl: FunctionReference<'mutation'>
16
+ createAsset: FunctionReference<'mutation'>
17
+ maxFileSize?: number
18
+ allowedMimeTypes?: string[]
19
+ maxConcurrentUploads?: number
20
+ onClose?: () => void
21
+ onError?: (error: string, filename: string) => void
22
+ }
23
+
24
+ export interface UploadedFile {
25
+ filename: string
26
+ storageId: string
27
+ success: boolean
28
+ error?: string
29
+ }
30
+
31
+ function formatFileSize(bytes: number): string {
32
+ if (bytes === 0) return '0 B'
33
+ const k = 1024
34
+ const sizes = ['B', 'KB', 'MB', 'GB']
35
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
36
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
37
+ }
38
+
39
+ function isMimeTypeAllowed(mimeType: string, allowedTypes: string[]): boolean {
40
+ if (allowedTypes.length === 0) return true
41
+ return allowedTypes.some((allowed) => {
42
+ if (allowed.endsWith('/*')) {
43
+ const category = allowed.slice(0, -2)
44
+ return mimeType.startsWith(category + '/')
45
+ }
46
+ return mimeType === allowed
47
+ })
48
+ }
49
+
50
+ export function UploadDropzone({
51
+ onUploadComplete,
52
+ currentFolderId,
53
+ generateUploadUrl,
54
+ createAsset,
55
+ maxFileSize,
56
+ allowedMimeTypes = [],
57
+ maxConcurrentUploads = 3,
58
+ onError,
59
+ }: UploadDropzoneProps) {
60
+ const [isDragActive, setIsDragActive] = useState(false)
61
+ const [validationErrors, setValidationErrors] = useState<Map<string, string>>(new Map())
62
+ const fileInputRef = useRef<HTMLInputElement>(null)
63
+
64
+ const queue = useMediaUploadQueue({
65
+ getUploadUrl: generateUploadUrl,
66
+ createAsset: createAsset,
67
+ maxConcurrent: maxConcurrentUploads,
68
+ metadata: currentFolderId ? { parentId: currentFolderId } : undefined,
69
+ onComplete: (results) => {
70
+ const uploadedFiles: UploadedFile[] = results.map((f) => ({
71
+ filename: f.file.name,
72
+ storageId: f.result ? String((f.result as { _id?: string })._id || '') : '',
73
+ success: f.status === 'complete',
74
+ error: f.error,
75
+ }))
76
+ onUploadComplete(uploadedFiles)
77
+ },
78
+ onError: (file) => {
79
+ onError?.(file.error || 'Upload failed', file.file.name)
80
+ },
81
+ })
82
+
83
+ const validateFile = useCallback(
84
+ (file: File): string | null => {
85
+ if (maxFileSize && file.size > maxFileSize) {
86
+ return `File exceeds maximum size of ${formatFileSize(maxFileSize)}`
87
+ }
88
+ if (allowedMimeTypes.length > 0 && !isMimeTypeAllowed(file.type, allowedMimeTypes)) {
89
+ return `File type ${file.type || 'unknown'} is not allowed`
90
+ }
91
+ return null
92
+ },
93
+ [maxFileSize, allowedMimeTypes]
94
+ )
95
+
96
+ const addFiles = useCallback(
97
+ (files: FileList | File[]) => {
98
+ const fileArray = Array.from(files)
99
+ const errors = new Map<string, string>()
100
+ const validFiles: File[] = []
101
+
102
+ for (const file of fileArray) {
103
+ const error = validateFile(file)
104
+ if (error) {
105
+ errors.set(file.name, error)
106
+ } else {
107
+ validFiles.push(file)
108
+ }
109
+ }
110
+
111
+ setValidationErrors(errors)
112
+ if (validFiles.length > 0) {
113
+ queue.addFiles(validFiles)
114
+ }
115
+ },
116
+ [validateFile, queue]
117
+ )
118
+
119
+ const handleDragEnter = useCallback((e: React.DragEvent) => {
120
+ e.preventDefault()
121
+ e.stopPropagation()
122
+ setIsDragActive(true)
123
+ }, [])
124
+
125
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
126
+ e.preventDefault()
127
+ e.stopPropagation()
128
+ setIsDragActive(false)
129
+ }, [])
130
+
131
+ const handleDragOver = useCallback((e: React.DragEvent) => {
132
+ e.preventDefault()
133
+ e.stopPropagation()
134
+ }, [])
135
+
136
+ const handleDrop = useCallback(
137
+ (e: React.DragEvent) => {
138
+ e.preventDefault()
139
+ e.stopPropagation()
140
+ setIsDragActive(false)
141
+ if (e.dataTransfer.files?.length) {
142
+ addFiles(e.dataTransfer.files)
143
+ }
144
+ },
145
+ [addFiles]
146
+ )
147
+
148
+ const handleFileChange = useCallback(
149
+ (e: React.ChangeEvent<HTMLInputElement>) => {
150
+ if (e.target.files?.length) {
151
+ addFiles(e.target.files)
152
+ e.target.value = ''
153
+ }
154
+ },
155
+ [addFiles]
156
+ )
157
+
158
+ const getStatusIcon = (status: UploadQueueFileStatus) => {
159
+ switch (status) {
160
+ case 'complete':
161
+ return <Check className="size-4 text-emerald-500" />
162
+ case 'error':
163
+ case 'cancelled':
164
+ return <X className="size-4 text-red-500" />
165
+ case 'uploading':
166
+ return (
167
+ <div className="size-4 animate-spin rounded-full border-2 border-muted border-t-primary" />
168
+ )
169
+ default:
170
+ return <div className="size-4 rounded-full border-2 border-muted" />
171
+ }
172
+ }
173
+
174
+ const completedCount = queue.files.filter((f) => f.status === 'complete').length
175
+ const errorCount = queue.files.filter(
176
+ (f) => f.status === 'error' || f.status === 'cancelled'
177
+ ).length
178
+ const hasCompletedOrFailed = completedCount > 0 || errorCount > 0
179
+
180
+ return (
181
+ <div className="space-y-4">
182
+ {validationErrors.size > 0 && (
183
+ <div className="rounded-md border border-destructive/50 bg-destructive/10 p-3">
184
+ <div className="flex items-start gap-2">
185
+ <AlertCircle className="mt-0.5 size-4 shrink-0 text-destructive" />
186
+ <div className="flex-1 space-y-1">
187
+ {Array.from(validationErrors.entries()).map(([filename, error]) => (
188
+ <p key={filename} className="text-sm text-destructive">
189
+ <span className="font-medium">{filename}:</span> {error}
190
+ </p>
191
+ ))}
192
+ </div>
193
+ </div>
194
+ <CmsButton
195
+ variant="ghost"
196
+ size="sm"
197
+ className="mt-2"
198
+ onClick={() => setValidationErrors(new Map())}
199
+ >
200
+ Dismiss
201
+ </CmsButton>
202
+ </div>
203
+ )}
204
+
205
+ {!queue.isUploading && queue.files.length === 0 && (
206
+ <div
207
+ className={cn(
208
+ 'flex cursor-pointer flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed p-8 text-center transition-colors',
209
+ isDragActive
210
+ ? 'border-primary bg-primary/5'
211
+ : 'border-muted-foreground/25 hover:border-primary/50 hover:bg-muted/50'
212
+ )}
213
+ onClick={() => fileInputRef.current?.click()}
214
+ onDragEnter={handleDragEnter}
215
+ onDragLeave={handleDragLeave}
216
+ onDragOver={handleDragOver}
217
+ onDrop={handleDrop}
218
+ >
219
+ <div className="flex size-12 items-center justify-center rounded-full bg-muted">
220
+ <Upload className="size-6 text-muted-foreground" />
221
+ </div>
222
+ <div>
223
+ <p className="text-sm font-medium text-foreground">
224
+ {isDragActive ? 'Drop files here' : 'Drag and drop files here'}
225
+ </p>
226
+ <p className="mt-1 text-xs text-muted-foreground">or click to browse</p>
227
+ </div>
228
+ <p className="text-xs text-muted-foreground">
229
+ Supports images, videos, documents, and more
230
+ {maxFileSize && ` (max ${formatFileSize(maxFileSize)} per file)`}
231
+ </p>
232
+ </div>
233
+ )}
234
+
235
+ {queue.files.length > 0 && (
236
+ <div className="space-y-3">
237
+ <div className="flex items-center justify-between">
238
+ <div className="flex items-center gap-2 text-sm">
239
+ <span className="font-medium">
240
+ {queue.files.length} file{queue.files.length !== 1 ? 's' : ''}
241
+ </span>
242
+ {queue.isUploading && (
243
+ <span className="text-muted-foreground">
244
+ {queue.overallProgress}% complete
245
+ </span>
246
+ )}
247
+ {!queue.isUploading && hasCompletedOrFailed && (
248
+ <div className="flex items-center gap-2">
249
+ {completedCount > 0 && (
250
+ <span className="text-emerald-600">{completedCount} completed</span>
251
+ )}
252
+ {errorCount > 0 && (
253
+ <span className="text-red-500">{errorCount} failed</span>
254
+ )}
255
+ </div>
256
+ )}
257
+ </div>
258
+ <div className="flex items-center gap-2">
259
+ {!queue.isUploading && (
260
+ <CmsButton
261
+ variant="outline"
262
+ size="sm"
263
+ onClick={() => fileInputRef.current?.click()}
264
+ >
265
+ Add More
266
+ </CmsButton>
267
+ )}
268
+ {queue.isUploading && (
269
+ <CmsButton variant="outline" size="sm" onClick={queue.cancelAll}>
270
+ Cancel All
271
+ </CmsButton>
272
+ )}
273
+ {hasCompletedOrFailed && !queue.isUploading && (
274
+ <CmsButton variant="ghost" size="sm" onClick={queue.clearCompleted}>
275
+ Clear Done
276
+ </CmsButton>
277
+ )}
278
+ </div>
279
+ </div>
280
+
281
+ {queue.isUploading && (
282
+ <div className="h-1.5 overflow-hidden rounded-full bg-muted">
283
+ <div
284
+ className="h-full bg-primary transition-all duration-300"
285
+ style={{ width: `${queue.overallProgress}%` }}
286
+ />
287
+ </div>
288
+ )}
289
+
290
+ <div className="space-y-2">
291
+ {queue.files.map((uploadFile: UploadQueueFile) => (
292
+ <div
293
+ key={uploadFile.id}
294
+ className={cn(
295
+ 'flex items-center gap-3 rounded-lg border bg-card p-3',
296
+ uploadFile.status === 'error' && 'border-red-200 bg-red-50 dark:border-red-900 dark:bg-red-950/20',
297
+ uploadFile.status === 'complete' && 'border-emerald-200 bg-emerald-50 dark:border-emerald-900 dark:bg-emerald-950/20'
298
+ )}
299
+ >
300
+ <div className="shrink-0">{getStatusIcon(uploadFile.status)}</div>
301
+
302
+ <div className="min-w-0 flex-1">
303
+ <p
304
+ className="truncate text-sm font-medium"
305
+ title={uploadFile.file.name}
306
+ >
307
+ {uploadFile.file.name}
308
+ </p>
309
+ <div className="flex items-center gap-2">
310
+ <span className="text-xs text-muted-foreground">
311
+ {formatFileSize(uploadFile.file.size)}
312
+ </span>
313
+ {uploadFile.error && (
314
+ <span className="text-xs text-red-500">{uploadFile.error}</span>
315
+ )}
316
+ </div>
317
+ </div>
318
+
319
+ {uploadFile.status === 'uploading' && (
320
+ <div className="flex w-20 items-center gap-2">
321
+ <div className="h-1.5 flex-1 overflow-hidden rounded-full bg-muted">
322
+ <div
323
+ className="h-full bg-primary transition-all"
324
+ style={{ width: `${uploadFile.progress}%` }}
325
+ />
326
+ </div>
327
+ <span className="text-xs text-muted-foreground">
328
+ {uploadFile.progress}%
329
+ </span>
330
+ </div>
331
+ )}
332
+
333
+ <div className="shrink-0">
334
+ {uploadFile.status === 'pending' && !queue.isUploading && (
335
+ <CmsButton
336
+ variant="ghost"
337
+ size="icon-sm"
338
+ onClick={() => queue.cancelFile(uploadFile.id)}
339
+ title="Remove"
340
+ >
341
+ <X className="size-4" />
342
+ </CmsButton>
343
+ )}
344
+ {(uploadFile.status === 'uploading' || uploadFile.status === 'pending') &&
345
+ queue.isUploading && (
346
+ <CmsButton
347
+ variant="ghost"
348
+ size="icon-sm"
349
+ onClick={() => queue.cancelFile(uploadFile.id)}
350
+ title="Cancel"
351
+ >
352
+ <X className="size-4" />
353
+ </CmsButton>
354
+ )}
355
+ {(uploadFile.status === 'error' || uploadFile.status === 'cancelled') &&
356
+ !queue.isUploading && (
357
+ <CmsButton
358
+ variant="ghost"
359
+ size="icon-sm"
360
+ onClick={() => queue.retryFile(uploadFile.id)}
361
+ title="Retry"
362
+ >
363
+ <RefreshCw className="size-4" />
364
+ </CmsButton>
365
+ )}
366
+ </div>
367
+ </div>
368
+ ))}
369
+ </div>
370
+ </div>
371
+ )}
372
+
373
+ <input
374
+ ref={fileInputRef}
375
+ type="file"
376
+ multiple
377
+ onChange={handleFileChange}
378
+ className="hidden"
379
+ accept={allowedMimeTypes.length > 0 ? allowedMimeTypes.join(',') : undefined}
380
+ />
381
+ </div>
382
+ )
383
+ }
@@ -0,0 +1,250 @@
1
+ import { useQuery } from 'convex/react'
2
+ import { api } from '../../convex/_generated/api'
3
+ import { CmsButton } from '~/components/cmsds/CmsButton'
4
+ import { Badge } from '~/components/ui/badge'
5
+ import { ScrollArea } from '~/components/ui/scroll-area'
6
+ import { X, ArrowRight, Plus, Minus, RefreshCw, GitCompare } from 'lucide-react'
7
+ import { cn } from '~/lib/cn'
8
+
9
+ interface VersionCompareProps {
10
+ entryId: string
11
+ fromVersion: number
12
+ toVersion: number
13
+ onClose: () => void
14
+ onRollback: (version: number) => void
15
+ }
16
+
17
+ interface FieldDiff {
18
+ field: string
19
+ fromValue: unknown
20
+ toValue: unknown
21
+ changeType: 'added' | 'removed' | 'modified'
22
+ }
23
+
24
+ interface VersionInfo {
25
+ versionNumber: number
26
+ status: string
27
+ slug: string
28
+ wasPublished: boolean
29
+ createdAt: number
30
+ }
31
+
32
+ interface ComparisonResult {
33
+ hasChanges: boolean
34
+ fromVersion: VersionInfo
35
+ toVersion: VersionInfo
36
+ changedFields: string[]
37
+ fieldDiffs: FieldDiff[]
38
+ slugChanged: boolean
39
+ statusChanged: boolean
40
+ changeSummary: string
41
+ }
42
+
43
+ export function VersionCompare({
44
+ entryId,
45
+ fromVersion,
46
+ toVersion,
47
+ onClose,
48
+ onRollback,
49
+ }: VersionCompareProps) {
50
+ const comparisonQuery = useQuery(api.versions.compare, {
51
+ entryId,
52
+ fromVersionNumber: fromVersion,
53
+ toVersionNumber: toVersion,
54
+ })
55
+
56
+ const isLoading = comparisonQuery === undefined
57
+ const comparison = comparisonQuery as ComparisonResult | null
58
+
59
+ const formatValue = (value: unknown): string => {
60
+ if (value === null || value === undefined) {
61
+ return '(empty)'
62
+ }
63
+ if (typeof value === 'object') {
64
+ return JSON.stringify(value, null, 2)
65
+ }
66
+ return String(value)
67
+ }
68
+
69
+ const getChangeIcon = (changeType: string) => {
70
+ switch (changeType) {
71
+ case 'added':
72
+ return <Plus className="size-3" />
73
+ case 'removed':
74
+ return <Minus className="size-3" />
75
+ case 'modified':
76
+ return <RefreshCw className="size-3" />
77
+ default:
78
+ return null
79
+ }
80
+ }
81
+
82
+ const getChangeStyles = (changeType: string) => {
83
+ switch (changeType) {
84
+ case 'added':
85
+ return 'border-emerald-200 bg-emerald-50'
86
+ case 'removed':
87
+ return 'border-red-200 bg-red-50'
88
+ case 'modified':
89
+ return 'border-amber-200 bg-amber-50'
90
+ default:
91
+ return 'border-border bg-card'
92
+ }
93
+ }
94
+
95
+ const getChangeIconStyles = (changeType: string) => {
96
+ switch (changeType) {
97
+ case 'added':
98
+ return 'bg-emerald-100 text-emerald-700'
99
+ case 'removed':
100
+ return 'bg-red-100 text-red-700'
101
+ case 'modified':
102
+ return 'bg-amber-100 text-amber-700'
103
+ default:
104
+ return 'bg-muted text-muted-foreground'
105
+ }
106
+ }
107
+
108
+ const formatDate = (timestamp: number) => {
109
+ return new Date(timestamp).toLocaleString(undefined, {
110
+ year: 'numeric',
111
+ month: 'short',
112
+ day: 'numeric',
113
+ hour: '2-digit',
114
+ minute: '2-digit',
115
+ })
116
+ }
117
+
118
+ return (
119
+ <div className="flex h-full flex-col bg-background">
120
+ <div className="flex items-center justify-between border-b px-4 py-3">
121
+ <div className="flex items-center gap-2">
122
+ <GitCompare className="size-4 text-muted-foreground" />
123
+ <h3 className="font-semibold">
124
+ Comparing v{fromVersion} → v{toVersion}
125
+ </h3>
126
+ </div>
127
+ <button
128
+ type="button"
129
+ onClick={onClose}
130
+ className="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
131
+ aria-label="Close comparison"
132
+ >
133
+ <X className="size-4" />
134
+ </button>
135
+ </div>
136
+
137
+ <ScrollArea className="min-h-0 flex-1">
138
+ <div className="p-4">
139
+ {isLoading ? (
140
+ <div className="flex flex-col items-center justify-center py-8">
141
+ <div className="size-6 animate-spin rounded-full border-2 border-muted border-t-primary" />
142
+ <p className="mt-2 text-sm text-muted-foreground">
143
+ Loading comparison...
144
+ </p>
145
+ </div>
146
+ ) : !comparison ? (
147
+ <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
148
+ Could not load version comparison
149
+ </div>
150
+ ) : !comparison.hasChanges ? (
151
+ <div className="py-8 text-center">
152
+ <p className="text-sm text-muted-foreground">
153
+ No differences found between these versions
154
+ </p>
155
+ </div>
156
+ ) : (
157
+ <>
158
+ <div className="mb-4 flex items-center justify-center gap-3 rounded-lg border bg-muted/30 p-3">
159
+ <div className="text-center">
160
+ <Badge variant="secondary" className="font-mono">
161
+ v{comparison.fromVersion.versionNumber}
162
+ </Badge>
163
+ <p className="mt-1 text-xs text-muted-foreground">
164
+ {formatDate(comparison.fromVersion.createdAt)}
165
+ </p>
166
+ </div>
167
+ <ArrowRight className="size-4 text-muted-foreground" />
168
+ <div className="text-center">
169
+ <Badge variant="secondary" className="font-mono">
170
+ v{comparison.toVersion.versionNumber}
171
+ </Badge>
172
+ <p className="mt-1 text-xs text-muted-foreground">
173
+ {formatDate(comparison.toVersion.createdAt)}
174
+ </p>
175
+ </div>
176
+ </div>
177
+
178
+ {comparison.changeSummary && (
179
+ <p className="mb-4 text-sm text-muted-foreground">
180
+ {comparison.changeSummary}
181
+ </p>
182
+ )}
183
+
184
+ <div className="space-y-3">
185
+ {comparison.fieldDiffs.map((change, index) => (
186
+ <div
187
+ key={index}
188
+ className={cn(
189
+ 'rounded-lg border p-3',
190
+ getChangeStyles(change.changeType)
191
+ )}
192
+ >
193
+ <div className="flex items-center gap-2">
194
+ <span
195
+ className={cn(
196
+ 'flex size-5 items-center justify-center rounded',
197
+ getChangeIconStyles(change.changeType)
198
+ )}
199
+ >
200
+ {getChangeIcon(change.changeType)}
201
+ </span>
202
+ <span className="font-medium text-foreground">
203
+ {change.field}
204
+ </span>
205
+ <Badge variant="outline" className="text-xs capitalize">
206
+ {change.changeType}
207
+ </Badge>
208
+ </div>
209
+
210
+ <div className="mt-2 space-y-2">
211
+ {change.changeType !== 'added' && (
212
+ <div className="rounded border border-red-200 bg-white p-2">
213
+ <p className="mb-1 text-xs font-medium text-red-700">
214
+ Before:
215
+ </p>
216
+ <pre className="overflow-x-auto whitespace-pre-wrap break-words font-mono text-xs text-red-900">
217
+ {formatValue(change.fromValue)}
218
+ </pre>
219
+ </div>
220
+ )}
221
+ {change.changeType !== 'removed' && (
222
+ <div className="rounded border border-emerald-200 bg-white p-2">
223
+ <p className="mb-1 text-xs font-medium text-emerald-700">
224
+ After:
225
+ </p>
226
+ <pre className="overflow-x-auto whitespace-pre-wrap break-words font-mono text-xs text-emerald-900">
227
+ {formatValue(change.toValue)}
228
+ </pre>
229
+ </div>
230
+ )}
231
+ </div>
232
+ </div>
233
+ ))}
234
+ </div>
235
+ </>
236
+ )}
237
+ </div>
238
+ </ScrollArea>
239
+
240
+ <div className="flex items-center justify-between border-t px-4 py-3">
241
+ <CmsButton variant="outline" onClick={onClose}>
242
+ Back to History
243
+ </CmsButton>
244
+ <CmsButton variant="ghost" onClick={() => onRollback(fromVersion)}>
245
+ Rollback to v{fromVersion}
246
+ </CmsButton>
247
+ </div>
248
+ </div>
249
+ )
250
+ }