convex-cms 0.0.5-alpha.0 → 0.0.5-alpha.3

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 (323) hide show
  1. package/README.md +95 -144
  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-CiMQwSQV.js → CmsEmptyState-CkqBIab3.js} +1 -1
  158. package/admin-dist/public/assets/{CmsPageHeader-ohOq0luT.js → CmsPageHeader-CUtl5MMG.js} +1 -1
  159. package/admin-dist/public/assets/{CmsStatusBadge-BdNf0V9v.js → CmsStatusBadge-CUYFgEe-.js} +1 -1
  160. package/admin-dist/public/assets/{CmsSurface-CWup6Jh7.js → CmsSurface-CsJfAVa3.js} +1 -1
  161. package/admin-dist/public/assets/{CmsToolbar-cEBlCHa3.js → CmsToolbar-CnfbcxeP.js} +1 -1
  162. package/admin-dist/public/assets/{ContentEntryEditor-BY5ypfUs.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-BpSmrfAm.js → _entryId-CuVMExbb.js} +1 -1
  166. package/admin-dist/public/assets/{alert-Bf2l8kxw.js → alert-CF1BSzGR.js} +1 -1
  167. package/admin-dist/public/assets/{badge-qPrc4AUM.js → badge-CmuOIVKp.js} +1 -1
  168. package/admin-dist/public/assets/{circle-check-big-Dgozy3vV.js → circle-check-big-BKDVG6DU.js} +1 -1
  169. package/admin-dist/public/assets/{command-QOmNhlb0.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-DCsUdvFh.js → label-CHCnXeBk.js} +1 -1
  175. package/admin-dist/public/assets/{link-2-Czw1N61H.js → link-2-Bb34judH.js} +1 -1
  176. package/admin-dist/public/assets/{list-DtCsXj8-.js → list-9Pzt48ld.js} +1 -1
  177. package/admin-dist/public/assets/{main-CXgkZMhe.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-CoTDxKzf.js → new._contentTypeId-C_I4YxIa.js} +1 -1
  180. package/admin-dist/public/assets/{plus-xCFJK0RC.js → plus-Ceef7DHk.js} +1 -1
  181. package/admin-dist/public/assets/{rotate-ccw-DIqK63wY.js → rotate-ccw-7k7-4VUq.js} +1 -1
  182. package/admin-dist/public/assets/{scroll-area-B-yrE66a.js → scroll-area-CC6wujnp.js} +1 -1
  183. package/admin-dist/public/assets/{search-CbCbboeU.js → search-DwoUV2pv.js} +1 -1
  184. package/admin-dist/public/assets/{select-Co3TZFJb.js → select-hOZTp8aC.js} +1 -1
  185. package/admin-dist/public/assets/{settings-BspTTv_o.js → settings-t2PbCZh4.js} +1 -1
  186. package/admin-dist/public/assets/{switch-CfavASmR.js → switch-jX2pDaNU.js} +1 -1
  187. package/admin-dist/public/assets/{tabs-CN5s5u2W.js → tabs-q4EbZk7c.js} +1 -1
  188. package/admin-dist/public/assets/{tanstack-adapter-npeE3RdY.js → tanstack-adapter-B-Glm4kH.js} +1 -1
  189. package/admin-dist/public/assets/taxonomies-kyk5P4ZW.js +1 -0
  190. package/admin-dist/public/assets/{textarea-BJ0XFZpT.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-BZRcqsUg.js → triangle-alert-CXFIO_Gu.js} +1 -1
  193. package/admin-dist/public/assets/{useBreadcrumbLabel-DwZlwvFF.js → useBreadcrumbLabel-_6qBagc3.js} +1 -1
  194. package/admin-dist/public/assets/{usePermissions-C1JQhfqb.js → usePermissions-M1ijZ7a6.js} +1 -1
  195. package/admin-dist/server/_ssr/{CmsButton-B45JAKR1.mjs → CmsButton-DOiTVKQq.mjs} +1 -1
  196. package/admin-dist/server/_ssr/{CmsEmptyState-D_BQFAVR.mjs → CmsEmptyState-fbnGt3LD.mjs} +2 -2
  197. package/admin-dist/server/_ssr/{CmsPageHeader-CrUZA59A.mjs → CmsPageHeader-DHRrdOZa.mjs} +1 -1
  198. package/admin-dist/server/_ssr/{CmsStatusBadge-B-sj6yaj.mjs → CmsStatusBadge-s7obWbKZ.mjs} +2 -2
  199. package/admin-dist/server/_ssr/{CmsSurface-DKJZhpjk.mjs → CmsSurface-rFoYjb62.mjs} +1 -1
  200. package/admin-dist/server/_ssr/{CmsToolbar-ByaW5iXf.mjs → CmsToolbar-zTE45z2q.mjs} +2 -2
  201. package/admin-dist/server/_ssr/{ContentEntryEditor-D3_Jb1dq.mjs → ContentEntryEditor-BLoEjT_m.mjs} +12 -12
  202. package/admin-dist/server/_ssr/{TaxonomyFilter-BRJkuCtA.mjs → TaxonomyFilter-XAtaJC2z.mjs} +5 -5
  203. package/admin-dist/server/_ssr/{_contentTypeId-B9kA6CaM.mjs → _contentTypeId-Csl4822C.mjs} +13 -13
  204. package/admin-dist/server/_ssr/{_entryId-BddcMkZN.mjs → _entryId-D8alLFBx.mjs} +15 -15
  205. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BffZedId.mjs +4 -0
  206. package/admin-dist/server/_ssr/{command-CGtVr8Gb.mjs → command-C0Di14--.mjs} +1 -1
  207. package/admin-dist/server/_ssr/{content-D1tbeOd0.mjs → content-CT-FPsmV.mjs} +12 -55
  208. package/admin-dist/server/_ssr/{content-types-BZqY_BER.mjs → content-types-C8cBFdzE.mjs} +15 -46
  209. package/admin-dist/server/_ssr/{index-BIdq4xaC.mjs → index-BJtcrEc-.mjs} +5 -5
  210. package/admin-dist/server/_ssr/index.mjs +2 -2
  211. package/admin-dist/server/_ssr/{label-T-QNKAr6.mjs → label-qn2Afwl4.mjs} +1 -1
  212. package/admin-dist/server/_ssr/{media-C-xqjBrl.mjs → media-qv5IAsMZ.mjs} +14 -14
  213. package/admin-dist/server/_ssr/{new._contentTypeId-DWic9cRq.mjs → new._contentTypeId-DdGyrhqs.mjs} +13 -13
  214. package/admin-dist/server/_ssr/{router-D1BMAMJT.mjs → router-nSVkxb6Y.mjs} +11 -11
  215. package/admin-dist/server/_ssr/{scroll-area-C0pic_WA.mjs → scroll-area-BCinP455.mjs} +1 -1
  216. package/admin-dist/server/_ssr/{select-CqmuN2F6.mjs → select-BKQlQScw.mjs} +1 -1
  217. package/admin-dist/server/_ssr/{settings-CAkncGGV.mjs → settings-BCr2KQlk.mjs} +55 -40
  218. package/admin-dist/server/_ssr/{switch-CgmuJkT9.mjs → switch-BaOi42fE.mjs} +1 -1
  219. package/admin-dist/server/_ssr/{tabs-CnMj0aRy.mjs → tabs-DYXEi9kq.mjs} +2 -2
  220. package/admin-dist/server/_ssr/{tanstack-adapter-BXZrMauE.mjs → tanstack-adapter-Bsz8kha-.mjs} +1 -1
  221. package/admin-dist/server/_ssr/{taxonomies-thl3BfVm.mjs → taxonomies-CueMHTbE.mjs} +30 -19
  222. package/admin-dist/server/_ssr/{textarea-4K5OJgeh.mjs → textarea-CI0Jqx2x.mjs} +1 -1
  223. package/admin-dist/server/_ssr/{trash-B40Gx5zP.mjs → trash-DE6W8GoX.mjs} +20 -17
  224. package/admin-dist/server/_ssr/{useBreadcrumbLabel-rn-fL4zV.mjs → useBreadcrumbLabel-B5Yi72lM.mjs} +1 -1
  225. package/admin-dist/server/_ssr/{usePermissions-CKeM6_Vw.mjs → usePermissions-C3nZ-Izm.mjs} +1 -1
  226. package/admin-dist/server/index.mjs +187 -194
  227. package/dist/client/admin/bulk.d.ts +79 -0
  228. package/dist/client/admin/bulk.d.ts.map +1 -0
  229. package/dist/client/admin/bulk.js +72 -0
  230. package/dist/client/admin/bulk.js.map +1 -0
  231. package/dist/client/admin/contentLock.d.ts +118 -0
  232. package/dist/client/admin/contentLock.d.ts.map +1 -0
  233. package/dist/client/admin/contentLock.js +81 -0
  234. package/dist/client/admin/contentLock.js.map +1 -0
  235. package/dist/client/{adminApi.d.ts → admin/contentTypes.d.ts} +39 -1134
  236. package/dist/client/admin/contentTypes.d.ts.map +1 -0
  237. package/dist/client/admin/contentTypes.js +122 -0
  238. package/dist/client/admin/contentTypes.js.map +1 -0
  239. package/dist/client/admin/dashboard.d.ts +16 -0
  240. package/dist/client/admin/dashboard.d.ts.map +1 -0
  241. package/dist/client/admin/dashboard.js +33 -0
  242. package/dist/client/admin/dashboard.js.map +1 -0
  243. package/dist/client/admin/entries.d.ts +358 -0
  244. package/dist/client/admin/entries.d.ts.map +1 -0
  245. package/dist/client/admin/entries.js +220 -0
  246. package/dist/client/admin/entries.js.map +1 -0
  247. package/dist/client/admin/index.d.ts +6568 -0
  248. package/dist/client/admin/index.d.ts.map +1 -0
  249. package/dist/client/admin/index.js +305 -0
  250. package/dist/client/admin/index.js.map +1 -0
  251. package/dist/client/admin/media.d.ts +1038 -0
  252. package/dist/client/admin/media.d.ts.map +1 -0
  253. package/dist/client/admin/media.js +489 -0
  254. package/dist/client/admin/media.js.map +1 -0
  255. package/dist/client/admin/taxonomies.d.ts +339 -0
  256. package/dist/client/admin/taxonomies.d.ts.map +1 -0
  257. package/dist/client/admin/taxonomies.js +364 -0
  258. package/dist/client/admin/taxonomies.js.map +1 -0
  259. package/dist/client/admin/trash.d.ts +91 -0
  260. package/dist/client/admin/trash.d.ts.map +1 -0
  261. package/dist/client/admin/trash.js +71 -0
  262. package/dist/client/admin/trash.js.map +1 -0
  263. package/dist/client/admin/types.d.ts +320 -0
  264. package/dist/client/admin/types.d.ts.map +1 -0
  265. package/dist/client/admin/types.js +7 -0
  266. package/dist/client/admin/types.js.map +1 -0
  267. package/dist/client/admin/validators.d.ts +3886 -0
  268. package/dist/client/admin/validators.d.ts.map +1 -0
  269. package/dist/client/admin/validators.js +322 -0
  270. package/dist/client/admin/validators.js.map +1 -0
  271. package/dist/client/admin/versions.d.ts +106 -0
  272. package/dist/client/admin/versions.d.ts.map +1 -0
  273. package/dist/client/admin/versions.js +57 -0
  274. package/dist/client/admin/versions.js.map +1 -0
  275. package/dist/client/adminApiTypes.d.ts +27 -0
  276. package/dist/client/adminApiTypes.d.ts.map +1 -0
  277. package/dist/client/adminApiTypes.js +12 -0
  278. package/dist/client/adminApiTypes.js.map +1 -0
  279. package/dist/client/{admin-config.d.ts → adminConfig.d.ts} +2 -2
  280. package/dist/client/adminConfig.d.ts.map +1 -0
  281. package/dist/client/{admin-config.js → adminConfig.js} +1 -1
  282. package/dist/client/adminConfig.js.map +1 -0
  283. package/dist/client/agentTools.d.ts +4 -4
  284. package/dist/client/index.d.ts +2 -2
  285. package/dist/client/index.d.ts.map +1 -1
  286. package/dist/client/index.js +15 -2
  287. package/dist/client/index.js.map +1 -1
  288. package/dist/component/contentEntries.d.ts +4 -4
  289. package/dist/component/contentEntryMutations.d.ts +46 -0
  290. package/dist/component/contentEntryMutations.d.ts.map +1 -1
  291. package/dist/component/contentEntryMutations.js +1 -1
  292. package/dist/component/contentEntryMutations.js.map +1 -1
  293. package/dist/component/contentTypeMigration.d.ts +1 -1
  294. package/dist/component/contentTypeMutations.d.ts +22 -0
  295. package/dist/component/contentTypeMutations.d.ts.map +1 -1
  296. package/dist/component/contentTypeMutations.js +1 -1
  297. package/dist/component/contentTypeMutations.js.map +1 -1
  298. package/dist/component/mediaAssetMutations.d.ts +47 -0
  299. package/dist/component/mediaAssetMutations.d.ts.map +1 -1
  300. package/dist/component/mediaAssetMutations.js +1 -1
  301. package/dist/component/mediaAssetMutations.js.map +1 -1
  302. package/dist/component/schema.d.ts +9 -0
  303. package/dist/component/schema.d.ts.map +1 -1
  304. package/dist/component/schema.js +1 -1
  305. package/dist/component/schema.js.map +1 -1
  306. package/package.json +85 -3
  307. package/admin-dist/public/assets/ErrorState-C4nJ-ml4.js +0 -1
  308. package/admin-dist/public/assets/TaxonomyFilter-BgE_SR_O.js +0 -1
  309. package/admin-dist/public/assets/_contentTypeId-DtZectcC.js +0 -1
  310. package/admin-dist/public/assets/content-OEBGlxg1.js +0 -1
  311. package/admin-dist/public/assets/content-types-CjQliqVV.js +0 -2
  312. package/admin-dist/public/assets/globals-hAmgC66w.css +0 -1
  313. package/admin-dist/public/assets/index-BH_ECMhv.js +0 -1
  314. package/admin-dist/public/assets/media-DTJ3-ViE.js +0 -1
  315. package/admin-dist/public/assets/taxonomies-CgG46fIF.js +0 -1
  316. package/admin-dist/public/assets/trash-B3daldm5.js +0 -1
  317. package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +0 -89
  318. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-Dd7AmelK.mjs +0 -4
  319. package/dist/client/admin-config.d.ts.map +0 -1
  320. package/dist/client/admin-config.js.map +0 -1
  321. package/dist/client/adminApi.d.ts.map +0 -1
  322. package/dist/client/adminApi.js +0 -736
  323. package/dist/client/adminApi.js.map +0 -1
@@ -0,0 +1,1095 @@
1
+ import { createFileRoute, useNavigate } from '@tanstack/react-router'
2
+ import { useQuery, useMutation } from 'convex/react'
3
+ import { api } from '../../convex/_generated/api'
4
+ import { useState, useMemo, useCallback, useEffect } from 'react'
5
+ import { useSettingsConfig } from '~/contexts'
6
+ // IDs are strings when crossing component boundaries
7
+ import { UploadDropzone, type UploadedFile } from '../components/UploadDropzone'
8
+ import { CmsPageHeader } from '~/components/cmsds/CmsPageHeader'
9
+ import { CmsToolbar } from '~/components/cmsds/CmsToolbar'
10
+ import { CmsEmptyState } from '~/components/cmsds/CmsEmptyState'
11
+ import { CmsSurface as _CmsSurface } from '~/components/cmsds/CmsSurface'
12
+ import { CmsButton } from '~/components/cmsds/CmsButton'
13
+ import { TaxonomyFilter } from '~/components/filters/TaxonomyFilter'
14
+ import {
15
+ Dialog,
16
+ DialogContent,
17
+ DialogHeader,
18
+ DialogTitle,
19
+ DialogFooter,
20
+ } from '~/components/ui/dialog'
21
+ import { Input } from '~/components/ui/input'
22
+ import { Label } from '~/components/ui/label'
23
+ import {
24
+ Select,
25
+ SelectContent,
26
+ SelectItem,
27
+ SelectTrigger,
28
+ SelectValue,
29
+ } from '~/components/ui/select'
30
+ import { Checkbox } from '~/components/ui/checkbox'
31
+ import { cn } from '~/lib/cn'
32
+ import {
33
+ Image,
34
+ Video,
35
+ Music,
36
+ FileText,
37
+ File,
38
+ Folder,
39
+ Home,
40
+ ChevronLeft,
41
+ FolderPlus,
42
+ Upload,
43
+ Search,
44
+ X,
45
+ Trash2,
46
+ RotateCcw,
47
+ } from 'lucide-react'
48
+ import {
49
+ MediaPreviewModal,
50
+ type MediaAsset,
51
+ } from '~/components/media/MediaPreviewModal'
52
+ import {
53
+ MediaAssetEditDialog,
54
+ type MediaAssetForEdit,
55
+ } from '~/components/media/MediaAssetEditDialog'
56
+ import {
57
+ MediaFolderEditDialog,
58
+ type MediaFolderForEdit,
59
+ } from '~/components/media/MediaFolderEditDialog'
60
+ import { MediaAssetActions } from '~/components/media/MediaAssetActions'
61
+ import { MediaFolderActions } from '~/components/media/MediaFolderActions'
62
+ import { MediaBulkActionBar } from '~/components/media/MediaBulkActionBar'
63
+ import { MediaTrashBulkActionBar } from '~/components/media/MediaTrashBulkActionBar'
64
+ import { MediaMoveModal } from '~/components/media/MediaMoveModal'
65
+ import { CmsConfirmDialog } from '~/components/cmsds/CmsDialog'
66
+ import { Tabs, TabsList, TabsTrigger } from '~/components/ui/tabs'
67
+ import { Badge } from '~/components/ui/badge'
68
+
69
+ type MediaView = 'library' | 'trash'
70
+
71
+ export const Route = createFileRoute('/media')({
72
+ component: MediaPage,
73
+ })
74
+
75
+ function formatFileSize(bytes: number): string {
76
+ if (bytes === 0) return '0 B'
77
+ const k = 1024
78
+ const sizes = ['B', 'KB', 'MB', 'GB']
79
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
80
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
81
+ }
82
+
83
+ function formatDate(timestamp: number): string {
84
+ return new Date(timestamp).toLocaleDateString('en-US', {
85
+ month: 'short',
86
+ day: 'numeric',
87
+ year: 'numeric',
88
+ })
89
+ }
90
+
91
+ function getMediaTypeIcon(type: string, className = 'size-6') {
92
+ const iconProps = { className }
93
+ switch (type) {
94
+ case 'image':
95
+ return <Image {...iconProps} />
96
+ case 'video':
97
+ return <Video {...iconProps} />
98
+ case 'audio':
99
+ return <Music {...iconProps} />
100
+ case 'document':
101
+ return <FileText {...iconProps} />
102
+ default:
103
+ return <File {...iconProps} />
104
+ }
105
+ }
106
+
107
+ type MediaType = 'image' | 'video' | 'audio' | 'document' | 'other'
108
+
109
+ function getMediaTypeFromMimeType(mimeType?: string): MediaType {
110
+ if (!mimeType) return 'other'
111
+ if (mimeType.startsWith('image/')) return 'image'
112
+ if (mimeType.startsWith('video/')) return 'video'
113
+ if (mimeType.startsWith('audio/')) return 'audio'
114
+ if (
115
+ mimeType === 'application/pdf' ||
116
+ mimeType.includes('document') ||
117
+ mimeType.includes('sheet') ||
118
+ mimeType.includes('presentation') ||
119
+ mimeType.startsWith('text/')
120
+ ) {
121
+ return 'document'
122
+ }
123
+ return 'other'
124
+ }
125
+
126
+ function MediaPage() {
127
+ const { settings } = useSettingsConfig()
128
+ const navigate = useNavigate()
129
+
130
+ useEffect(() => {
131
+ if (settings && !settings.features.mediaManagement) {
132
+ navigate({ to: '/' })
133
+ }
134
+ }, [settings, navigate])
135
+
136
+ const [currentFolderId, setCurrentFolderId] = useState<
137
+ string | undefined
138
+ >(undefined)
139
+ const [searchQuery, setSearchQuery] = useState('')
140
+ const [typeFilter, setTypeFilter] = useState<MediaType | ''>('')
141
+ const [selectedTermIds, setSelectedTermIds] = useState<string[]>([])
142
+ const [selectedAssets, setSelectedAssets] = useState<Set<string>>(
143
+ new Set()
144
+ )
145
+ const [isSelectionMode, setIsSelectionMode] = useState(false)
146
+ const [showNewFolderModal, setShowNewFolderModal] = useState(false)
147
+ const [showUploadModal, setShowUploadModal] = useState(false)
148
+ const [newFolderName, setNewFolderName] = useState('')
149
+ const [isCreatingFolder, setIsCreatingFolder] = useState(false)
150
+ const [folderError, setFolderError] = useState('')
151
+ const [previewIndex, setPreviewIndex] = useState<number | null>(null)
152
+ const [editingAsset, setEditingAsset] = useState<MediaAssetForEdit | null>(null)
153
+ const [editingFolder, setEditingFolder] = useState<MediaFolderForEdit | null>(null)
154
+ const [deleteTarget, setDeleteTarget] = useState<{
155
+ type: 'asset' | 'folder'
156
+ id: string
157
+ name: string
158
+ } | null>(null)
159
+ const [isDeleting, setIsDeleting] = useState(false)
160
+ const [showMoveModal, setShowMoveModal] = useState(false)
161
+ const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
162
+ const [isBulkDeleting, setIsBulkDeleting] = useState(false)
163
+ const [activeView, setActiveView] = useState<MediaView>('library')
164
+ const [isRestoring, setIsRestoring] = useState(false)
165
+ const [isPermanentlyDeleting, setIsPermanentlyDeleting] = useState(false)
166
+ const [showPermanentDeleteConfirm, setShowPermanentDeleteConfirm] = useState(false)
167
+ const [permanentDeleteTarget, setPermanentDeleteTarget] = useState<string | 'bulk' | null>(null)
168
+
169
+ const isTrashView = activeView === 'trash'
170
+
171
+ const assetsResult = useQuery(api.media.listAssets, {
172
+ folderId: isTrashView ? undefined : currentFolderId,
173
+ type: typeFilter || undefined,
174
+ search: searchQuery || undefined,
175
+ deletedOnly: isTrashView ? true : undefined,
176
+ paginationOpts: { numItems: 100, cursor: null },
177
+ })
178
+
179
+ const trashCount = useQuery(api.media.getTrashCount, {})
180
+
181
+ const mediaByTermResult0 = useQuery(
182
+ api.taxonomies.getMediaByTerm,
183
+ selectedTermIds[0] ? { termId: selectedTermIds[0] } : 'skip'
184
+ )
185
+ const mediaByTermResult1 = useQuery(
186
+ api.taxonomies.getMediaByTerm,
187
+ selectedTermIds[1] ? { termId: selectedTermIds[1] } : 'skip'
188
+ )
189
+ const mediaByTermResult2 = useQuery(
190
+ api.taxonomies.getMediaByTerm,
191
+ selectedTermIds[2] ? { termId: selectedTermIds[2] } : 'skip'
192
+ )
193
+
194
+ const termFilteredMediaIds = useMemo(() => {
195
+ if (selectedTermIds.length === 0) return null
196
+ const ids = new Set<string>()
197
+ const results = [mediaByTermResult0, mediaByTermResult1, mediaByTermResult2]
198
+ for (let i = 0; i < selectedTermIds.length && i < 3; i++) {
199
+ const result = results[i]
200
+ if (result?.page) {
201
+ for (const mediaId of result.page) {
202
+ ids.add(mediaId)
203
+ }
204
+ }
205
+ }
206
+ return ids
207
+ }, [selectedTermIds, mediaByTermResult0, mediaByTermResult1, mediaByTermResult2])
208
+
209
+ const folders = useQuery(api.media.listFolders, {
210
+ parentId: isTrashView ? undefined : currentFolderId,
211
+ deletedOnly: isTrashView || undefined,
212
+ })
213
+
214
+ const currentFolder = useQuery(
215
+ api.media.getFolder,
216
+ currentFolderId ? { id: currentFolderId } : 'skip'
217
+ )
218
+
219
+ const folderTree = useQuery(api.media.getFolderTree, {})
220
+
221
+ const createFolder = useMutation(api.media.createFolder)
222
+ const deleteAsset = useMutation(api.media.deleteAsset)
223
+ const deleteFolder = useMutation(api.media.deleteFolder)
224
+ const restoreAsset = useMutation(api.media.restoreAsset)
225
+ const restoreFolder = useMutation(api.media.restoreFolder)
226
+ const permanentDeleteAsset = useMutation(api.media.permanentDeleteAsset)
227
+ const bulkPermanentDeleteAssets = useMutation(api.media.bulkPermanentDeleteAssets)
228
+
229
+ const breadcrumbPath = useMemo(() => {
230
+ if (!currentFolderId || !folderTree) return []
231
+
232
+ type FolderItem = (typeof folderTree)[number]
233
+ const path: FolderItem[] = []
234
+ let folder: FolderItem | undefined = folderTree.find(
235
+ (f) => f._id === currentFolderId
236
+ )
237
+
238
+ while (folder) {
239
+ path.unshift(folder)
240
+ const parentId = folder.parentId
241
+ folder = parentId ? folderTree.find((f) => f._id === parentId) : undefined
242
+ }
243
+
244
+ return path
245
+ }, [currentFolderId, folderTree])
246
+
247
+ const handleFolderClick = useCallback((folderId: string) => {
248
+ setCurrentFolderId(folderId)
249
+ setSearchQuery('')
250
+ }, [])
251
+
252
+ const handleNavigateUp = useCallback(() => {
253
+ if (currentFolder?.parentId) {
254
+ setCurrentFolderId(currentFolder.parentId as string)
255
+ } else {
256
+ setCurrentFolderId(undefined)
257
+ }
258
+ }, [currentFolder])
259
+
260
+ const handleNavigateToRoot = useCallback(() => {
261
+ setCurrentFolderId(undefined)
262
+ setSearchQuery('')
263
+ }, [])
264
+
265
+ const handleAssetSelect = useCallback((assetId: string) => {
266
+ setSelectedAssets((prev) => {
267
+ const next = new Set(prev)
268
+ if (next.has(assetId)) {
269
+ next.delete(assetId)
270
+ } else {
271
+ next.add(assetId)
272
+ }
273
+ return next
274
+ })
275
+ }, [])
276
+
277
+ const handleSelectAll = useCallback(() => {
278
+ if (!assetsResult?.page) return
279
+ setSelectedAssets(
280
+ new Set(assetsResult.page.map((a) => a._id as string))
281
+ )
282
+ }, [assetsResult?.page])
283
+
284
+ const handleDeselectAll = useCallback(() => {
285
+ setSelectedAssets(new Set())
286
+ }, [])
287
+
288
+ const handleViewChange = useCallback((view: MediaView) => {
289
+ setActiveView(view)
290
+ setSelectedAssets(new Set())
291
+ setIsSelectionMode(false)
292
+ }, [])
293
+
294
+ const handleAssetClick = useCallback(
295
+ (assetId: string) => {
296
+ if (isSelectionMode) {
297
+ handleAssetSelect(assetId)
298
+ } else {
299
+ const index = assetsResult?.page?.findIndex((a) => a._id === assetId) ?? -1
300
+ if (index !== -1) {
301
+ setPreviewIndex(index)
302
+ }
303
+ }
304
+ },
305
+ [isSelectionMode, handleAssetSelect, assetsResult?.page]
306
+ )
307
+
308
+ const handlePreviewNavigate = useCallback((index: number) => {
309
+ setPreviewIndex(index)
310
+ }, [])
311
+
312
+ const handleDelete = useCallback(async () => {
313
+ if (!deleteTarget) return
314
+
315
+ setIsDeleting(true)
316
+ try {
317
+ if (deleteTarget.type === 'asset') {
318
+ await deleteAsset({ id: deleteTarget.id })
319
+ } else {
320
+ await deleteFolder({ id: deleteTarget.id })
321
+ }
322
+ setDeleteTarget(null)
323
+ } catch (err) {
324
+ console.error('Delete failed:', err)
325
+ } finally {
326
+ setIsDeleting(false)
327
+ }
328
+ }, [deleteTarget, deleteAsset, deleteFolder])
329
+
330
+ const handleBulkDelete = useCallback(async () => {
331
+ if (selectedAssets.size === 0) return
332
+
333
+ setIsBulkDeleting(true)
334
+ try {
335
+ const deletePromises = Array.from(selectedAssets).map((id) =>
336
+ deleteAsset({ id })
337
+ )
338
+ await Promise.all(deletePromises)
339
+ setSelectedAssets(new Set())
340
+ setIsSelectionMode(false)
341
+ setShowBulkDeleteConfirm(false)
342
+ } catch (err) {
343
+ console.error('Bulk delete failed:', err)
344
+ } finally {
345
+ setIsBulkDeleting(false)
346
+ }
347
+ }, [selectedAssets, deleteAsset])
348
+
349
+ const handleBulkMoveComplete = useCallback(() => {
350
+ setSelectedAssets(new Set())
351
+ setIsSelectionMode(false)
352
+ }, [])
353
+
354
+ const handleRestore = useCallback(
355
+ async (assetId: string) => {
356
+ setIsRestoring(true)
357
+ try {
358
+ await restoreAsset({ id: assetId })
359
+ } catch (err) {
360
+ console.error('Restore failed:', err)
361
+ } finally {
362
+ setIsRestoring(false)
363
+ }
364
+ },
365
+ [restoreAsset]
366
+ )
367
+
368
+ const handleBulkRestore = useCallback(async () => {
369
+ if (selectedAssets.size === 0) return
370
+
371
+ setIsRestoring(true)
372
+ try {
373
+ const restorePromises = Array.from(selectedAssets).map((id) =>
374
+ restoreAsset({ id })
375
+ )
376
+ await Promise.all(restorePromises)
377
+ setSelectedAssets(new Set())
378
+ setIsSelectionMode(false)
379
+ } catch (err) {
380
+ console.error('Bulk restore failed:', err)
381
+ } finally {
382
+ setIsRestoring(false)
383
+ }
384
+ }, [selectedAssets, restoreAsset])
385
+
386
+ const handleRestoreFolder = useCallback(
387
+ async (folderId: string) => {
388
+ setIsRestoring(true)
389
+ try {
390
+ await restoreFolder({ id: folderId })
391
+ } catch (err) {
392
+ console.error('Restore folder failed:', err)
393
+ } finally {
394
+ setIsRestoring(false)
395
+ }
396
+ },
397
+ [restoreFolder]
398
+ )
399
+
400
+ const handlePermanentDelete = useCallback(async () => {
401
+ if (!permanentDeleteTarget) return
402
+
403
+ setIsPermanentlyDeleting(true)
404
+ try {
405
+ if (permanentDeleteTarget === 'bulk') {
406
+ await bulkPermanentDeleteAssets({ ids: Array.from(selectedAssets) })
407
+ setSelectedAssets(new Set())
408
+ setIsSelectionMode(false)
409
+ } else {
410
+ await permanentDeleteAsset({ id: permanentDeleteTarget })
411
+ }
412
+ setShowPermanentDeleteConfirm(false)
413
+ setPermanentDeleteTarget(null)
414
+ } catch (err) {
415
+ console.error('Permanent delete failed:', err)
416
+ } finally {
417
+ setIsPermanentlyDeleting(false)
418
+ }
419
+ }, [permanentDeleteTarget, permanentDeleteAsset, bulkPermanentDeleteAssets, selectedAssets])
420
+
421
+ const handleCreateFolder = useCallback(async () => {
422
+ if (!newFolderName.trim()) {
423
+ setFolderError('Folder name is required')
424
+ return
425
+ }
426
+
427
+ setIsCreatingFolder(true)
428
+ setFolderError('')
429
+
430
+ try {
431
+ await createFolder({
432
+ name: newFolderName.trim(),
433
+ parentId: currentFolderId,
434
+ })
435
+ setShowNewFolderModal(false)
436
+ setNewFolderName('')
437
+ } catch (error) {
438
+ setFolderError(
439
+ error instanceof Error ? error.message : 'Failed to create folder'
440
+ )
441
+ } finally {
442
+ setIsCreatingFolder(false)
443
+ }
444
+ }, [newFolderName, currentFolderId, createFolder])
445
+
446
+ const handleUploadComplete = useCallback((_results: UploadedFile[]) => {
447
+ setShowUploadModal(false)
448
+ }, [])
449
+
450
+ const isLoading = assetsResult === undefined || folders === undefined
451
+
452
+ const displayedAssets = useMemo(() => {
453
+ const assets = assetsResult?.page ?? []
454
+ if (termFilteredMediaIds === null) {
455
+ return assets
456
+ }
457
+ return assets.filter((asset) => termFilteredMediaIds.has(asset._id))
458
+ }, [assetsResult?.page, termFilteredMediaIds])
459
+
460
+ return (
461
+ <div className="space-y-6 p-6">
462
+ <CmsPageHeader
463
+ title="Media Library"
464
+ description="Upload, organize, and manage media assets for your content."
465
+ />
466
+
467
+ <Tabs value={activeView} onValueChange={(v) => handleViewChange(v as MediaView)}>
468
+ <TabsList>
469
+ <TabsTrigger value="library">
470
+ <Image className="mr-1.5 size-4" />
471
+ Library
472
+ </TabsTrigger>
473
+ <TabsTrigger value="trash">
474
+ <Trash2 className="mr-1.5 size-4" />
475
+ Trash
476
+ {trashCount && trashCount.total > 0 && (
477
+ <Badge variant="secondary" className="ml-1.5">
478
+ {trashCount.total}
479
+ </Badge>
480
+ )}
481
+ </TabsTrigger>
482
+ </TabsList>
483
+ </Tabs>
484
+
485
+ {!isTrashView && (
486
+ <nav className="flex items-center gap-1" aria-label="Folder navigation">
487
+ <button
488
+ className={cn(
489
+ 'flex items-center gap-1.5 rounded-md px-2 py-1 text-sm transition-colors',
490
+ !currentFolderId
491
+ ? 'bg-primary/10 font-medium text-primary'
492
+ : 'text-muted-foreground hover:bg-muted hover:text-foreground'
493
+ )}
494
+ onClick={handleNavigateToRoot}
495
+ >
496
+ <Home className="size-4" />
497
+ <span>All Files</span>
498
+ </button>
499
+ {breadcrumbPath.map((folder, index) => (
500
+ <span key={folder._id} className="flex items-center">
501
+ <span className="mx-1 text-muted-foreground">/</span>
502
+ <button
503
+ className={cn(
504
+ 'rounded-md px-2 py-1 text-sm transition-colors',
505
+ index === breadcrumbPath.length - 1
506
+ ? 'bg-primary/10 font-medium text-primary'
507
+ : 'text-muted-foreground hover:bg-muted hover:text-foreground'
508
+ )}
509
+ onClick={() => handleFolderClick(folder._id as string)}
510
+ >
511
+ {folder.name}
512
+ </button>
513
+ </span>
514
+ ))}
515
+ </nav>
516
+ )}
517
+
518
+ <CmsToolbar
519
+ left={
520
+ <div className="flex items-center gap-3">
521
+ <div className="relative">
522
+ <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
523
+ <Input
524
+ type="search"
525
+ placeholder="Search files..."
526
+ value={searchQuery}
527
+ onChange={(e) => setSearchQuery(e.target.value)}
528
+ className="w-64 pl-9"
529
+ />
530
+ {searchQuery && (
531
+ <button
532
+ className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 hover:bg-muted"
533
+ onClick={() => setSearchQuery('')}
534
+ aria-label="Clear search"
535
+ >
536
+ <X className="size-4 text-muted-foreground" />
537
+ </button>
538
+ )}
539
+ </div>
540
+
541
+ <Select
542
+ value={typeFilter || 'all'}
543
+ onValueChange={(v) => setTypeFilter(v === 'all' ? '' : (v as MediaType))}
544
+ >
545
+ <SelectTrigger className="w-36">
546
+ <SelectValue placeholder="All Types" />
547
+ </SelectTrigger>
548
+ <SelectContent>
549
+ <SelectItem value="all">All Types</SelectItem>
550
+ <SelectItem value="image">Images</SelectItem>
551
+ <SelectItem value="video">Videos</SelectItem>
552
+ <SelectItem value="audio">Audio</SelectItem>
553
+ <SelectItem value="document">Documents</SelectItem>
554
+ <SelectItem value="other">Other</SelectItem>
555
+ </SelectContent>
556
+ </Select>
557
+
558
+ <TaxonomyFilter
559
+ selectedTermIds={selectedTermIds}
560
+ onChange={setSelectedTermIds}
561
+ placeholder="Tags"
562
+ />
563
+
564
+ {assetsResult?.page && assetsResult.page.length > 0 && (
565
+ <label className="flex cursor-pointer items-center gap-2 text-sm">
566
+ <Checkbox
567
+ checked={isSelectionMode}
568
+ onCheckedChange={(checked) => {
569
+ setIsSelectionMode(checked as boolean)
570
+ if (!checked) {
571
+ setSelectedAssets(new Set())
572
+ }
573
+ }}
574
+ />
575
+ Selection Mode
576
+ </label>
577
+ )}
578
+ </div>
579
+ }
580
+ right={
581
+ <div className="flex items-center gap-2">
582
+ {isSelectionMode && selectedAssets.size > 0 && (
583
+ <span className="text-sm text-muted-foreground">
584
+ {selectedAssets.size} selected
585
+ </span>
586
+ )}
587
+
588
+ {isSelectionMode && (
589
+ <>
590
+ <CmsButton variant="secondary" size="sm" onClick={handleSelectAll}>
591
+ Select All
592
+ </CmsButton>
593
+ <CmsButton variant="secondary" size="sm" onClick={handleDeselectAll}>
594
+ Clear
595
+ </CmsButton>
596
+ </>
597
+ )}
598
+
599
+ {currentFolderId && !isTrashView && (
600
+ <CmsButton variant="secondary" onClick={handleNavigateUp}>
601
+ <ChevronLeft className="size-4" />
602
+ Up
603
+ </CmsButton>
604
+ )}
605
+
606
+ {!isTrashView && (
607
+ <>
608
+ <CmsButton
609
+ variant="secondary"
610
+ onClick={() => setShowNewFolderModal(true)}
611
+ >
612
+ <FolderPlus className="size-4" />
613
+ New Folder
614
+ </CmsButton>
615
+
616
+ <CmsButton onClick={() => setShowUploadModal(true)}>
617
+ <Upload className="size-4" />
618
+ Upload Files
619
+ </CmsButton>
620
+ </>
621
+ )}
622
+ </div>
623
+ }
624
+ />
625
+
626
+ {isLoading ? (
627
+ <div className="flex flex-col items-center justify-center py-12">
628
+ <div className="size-8 animate-spin rounded-full border-2 border-muted border-t-primary" />
629
+ <p className="mt-4 text-sm text-muted-foreground">
630
+ Loading media library...
631
+ </p>
632
+ </div>
633
+ ) : (
634
+ <>
635
+ {!isTrashView && folders && folders.length > 0 && !searchQuery && (
636
+ <section>
637
+ <h3 className="mb-3 text-sm font-medium text-muted-foreground">
638
+ Folders
639
+ </h3>
640
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-4 md:grid-cols-6">
641
+ {folders.map((folder) => (
642
+ <div
643
+ key={folder._id}
644
+ className="group relative flex flex-col items-center gap-2 rounded-lg border bg-card p-4 text-center transition-colors hover:border-primary/50 hover:bg-muted/50 cursor-pointer"
645
+ onClick={() =>
646
+ handleFolderClick(folder._id as string)
647
+ }
648
+ >
649
+ <div className="absolute right-2 top-2">
650
+ <MediaFolderActions
651
+ folder={{ _id: folder._id, name: folder.name }}
652
+ onEdit={() =>
653
+ setEditingFolder({
654
+ _id: folder._id,
655
+ name: folder.name,
656
+ description: folder.description,
657
+ })
658
+ }
659
+ onDelete={() =>
660
+ setDeleteTarget({
661
+ type: 'folder',
662
+ id: folder._id,
663
+ name: folder.name,
664
+ })
665
+ }
666
+ />
667
+ </div>
668
+ <Folder className="size-10 text-amber-500" />
669
+ <span className="truncate text-sm font-medium">
670
+ {folder.name}
671
+ </span>
672
+ </div>
673
+ ))}
674
+ </div>
675
+ </section>
676
+ )}
677
+
678
+ {isTrashView && folders && folders.length > 0 && (
679
+ <section>
680
+ <h3 className="mb-3 text-sm font-medium text-muted-foreground">
681
+ Deleted Folders ({folders.length})
682
+ </h3>
683
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-4 md:grid-cols-6">
684
+ {folders.map((folder) => (
685
+ <div
686
+ key={folder._id}
687
+ className="group relative flex flex-col items-center gap-2 rounded-lg border border-destructive/20 bg-card p-4 text-center opacity-60"
688
+ >
689
+ <Folder className="size-10 text-amber-500/50" />
690
+ <span className="truncate text-sm font-medium">
691
+ {folder.name}
692
+ </span>
693
+ <CmsButton
694
+ variant="secondary"
695
+ size="sm"
696
+ onClick={() => handleRestoreFolder(folder._id)}
697
+ disabled={isRestoring}
698
+ >
699
+ <RotateCcw className="mr-1 size-3" />
700
+ Restore
701
+ </CmsButton>
702
+ </div>
703
+ ))}
704
+ </div>
705
+ </section>
706
+ )}
707
+
708
+ {displayedAssets.length > 0 ? (
709
+ <section>
710
+ {!isTrashView && folders && folders.length > 0 && !searchQuery && (
711
+ <h3 className="mb-3 text-sm font-medium text-muted-foreground">
712
+ Files
713
+ </h3>
714
+ )}
715
+ {isTrashView && (
716
+ <h3 className="mb-3 text-sm font-medium text-muted-foreground">
717
+ Deleted Files ({displayedAssets.length})
718
+ </h3>
719
+ )}
720
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-4 md:grid-cols-6">
721
+ {displayedAssets.map((asset) => {
722
+ const assetId = asset._id as string
723
+ const isSelected = selectedAssets.has(assetId)
724
+ const mediaType = getMediaTypeFromMimeType(asset.mimeType)
725
+
726
+ return (
727
+ <div
728
+ key={asset._id}
729
+ className={cn(
730
+ 'group relative flex flex-col overflow-hidden rounded-lg border bg-card transition-all cursor-pointer hover:border-primary/50',
731
+ isSelected && 'border-primary ring-2 ring-primary/20'
732
+ )}
733
+ onClick={() => handleAssetClick(assetId)}
734
+ >
735
+ {isSelectionMode && (
736
+ <div className="absolute left-2 top-2 z-10">
737
+ <Checkbox
738
+ checked={isSelected}
739
+ onCheckedChange={() => handleAssetSelect(assetId)}
740
+ onClick={(e) => e.stopPropagation()}
741
+ className="bg-white/80"
742
+ />
743
+ </div>
744
+ )}
745
+
746
+ {!isSelectionMode && !isTrashView && (
747
+ <div className="absolute right-2 top-2 z-10">
748
+ <MediaAssetActions
749
+ asset={{
750
+ _id: asset._id,
751
+ name: asset.name,
752
+ url: asset.url,
753
+ }}
754
+ onView={() => {
755
+ const index = displayedAssets.findIndex(
756
+ (a) => a._id === asset._id
757
+ )
758
+ if (index !== -1) setPreviewIndex(index)
759
+ }}
760
+ onEdit={() =>
761
+ setEditingAsset({
762
+ _id: asset._id,
763
+ name: asset.name,
764
+ title: asset.title,
765
+ description: asset.description,
766
+ altText: asset.altText,
767
+ tags: asset.tags,
768
+ })
769
+ }
770
+ onDelete={() =>
771
+ setDeleteTarget({
772
+ type: 'asset',
773
+ id: asset._id,
774
+ name: asset.name,
775
+ })
776
+ }
777
+ />
778
+ </div>
779
+ )}
780
+
781
+ {!isSelectionMode && isTrashView && (
782
+ <div className="absolute right-2 top-2 z-10 flex gap-1 opacity-0 group-hover:opacity-100">
783
+ <CmsButton
784
+ variant="secondary"
785
+ size="icon-sm"
786
+ onClick={(e) => {
787
+ e.stopPropagation()
788
+ handleRestore(asset._id)
789
+ }}
790
+ title="Restore"
791
+ >
792
+ <RotateCcw className="size-4" />
793
+ </CmsButton>
794
+ <CmsButton
795
+ variant="danger"
796
+ size="icon-sm"
797
+ onClick={(e) => {
798
+ e.stopPropagation()
799
+ setPermanentDeleteTarget(asset._id)
800
+ setShowPermanentDeleteConfirm(true)
801
+ }}
802
+ title="Delete Forever"
803
+ >
804
+ <Trash2 className="size-4" />
805
+ </CmsButton>
806
+ </div>
807
+ )}
808
+
809
+ <div className="aspect-square overflow-hidden bg-muted">
810
+ {mediaType === 'image' && asset.url ? (
811
+ <img
812
+ src={asset.url}
813
+ alt={asset.title || asset.name}
814
+ className="h-full w-full object-cover"
815
+ />
816
+ ) : (
817
+ <div className="flex size-full items-center justify-center text-muted-foreground">
818
+ {getMediaTypeIcon(mediaType, 'size-10')}
819
+ </div>
820
+ )}
821
+ </div>
822
+
823
+ <div className="p-2">
824
+ <p
825
+ className="truncate text-sm font-medium"
826
+ title={asset.name}
827
+ >
828
+ {asset.name}
829
+ </p>
830
+ <div className="mt-0.5 flex items-center gap-1 text-xs text-muted-foreground">
831
+ <span className="capitalize">{mediaType}</span>
832
+ <span>•</span>
833
+ <span>{formatFileSize(asset.size ?? 0)}</span>
834
+ </div>
835
+ <p className="mt-0.5 text-xs text-muted-foreground">
836
+ {formatDate(asset._creationTime)}
837
+ </p>
838
+ </div>
839
+ </div>
840
+ )
841
+ })}
842
+ </div>
843
+
844
+ {!assetsResult.isDone && (
845
+ <p className="mt-4 text-center text-sm text-muted-foreground">
846
+ Showing {assetsResult.page.length} files. More files available.
847
+ </p>
848
+ )}
849
+ </section>
850
+ ) : isTrashView ? (
851
+ <CmsEmptyState
852
+ icon={<Trash2 className="size-8" />}
853
+ title="Trash is empty"
854
+ description="Deleted files will appear here. You can restore them or permanently delete them."
855
+ />
856
+ ) : (
857
+ !folders?.length && (
858
+ <CmsEmptyState
859
+ icon={<Image className="size-8" />}
860
+ title="No media assets yet"
861
+ description="Upload images, videos, documents, and other files to use in your content."
862
+ action={{
863
+ label: 'Upload Files',
864
+ onClick: () => setShowUploadModal(true),
865
+ }}
866
+ />
867
+ )
868
+ )}
869
+
870
+ {searchQuery && displayedAssets.length === 0 && !isTrashView && (
871
+ <CmsEmptyState
872
+ icon={<Search className="size-8" />}
873
+ title="No results found"
874
+ description={`No files match "${searchQuery}". Try a different search term.`}
875
+ action={{
876
+ label: 'Clear Search',
877
+ onClick: () => setSearchQuery(''),
878
+ variant: 'secondary',
879
+ }}
880
+ />
881
+ )}
882
+ </>
883
+ )}
884
+
885
+ <Dialog open={showNewFolderModal} onOpenChange={setShowNewFolderModal}>
886
+ <DialogContent>
887
+ <DialogHeader>
888
+ <DialogTitle>Create New Folder</DialogTitle>
889
+ </DialogHeader>
890
+ <div className="space-y-4 py-4">
891
+ <div className="space-y-2">
892
+ <Label htmlFor="folder-name">Folder Name</Label>
893
+ <Input
894
+ id="folder-name"
895
+ value={newFolderName}
896
+ onChange={(e) => {
897
+ setNewFolderName(e.target.value)
898
+ setFolderError('')
899
+ }}
900
+ placeholder="Enter folder name"
901
+ autoFocus
902
+ onKeyDown={(e) => {
903
+ if (e.key === 'Enter' && !isCreatingFolder) {
904
+ handleCreateFolder()
905
+ }
906
+ }}
907
+ />
908
+ {folderError && (
909
+ <p className="text-sm text-destructive">{folderError}</p>
910
+ )}
911
+ </div>
912
+ </div>
913
+ <DialogFooter>
914
+ <CmsButton
915
+ variant="secondary"
916
+ onClick={() => setShowNewFolderModal(false)}
917
+ disabled={isCreatingFolder}
918
+ >
919
+ Cancel
920
+ </CmsButton>
921
+ <CmsButton
922
+ onClick={handleCreateFolder}
923
+ disabled={isCreatingFolder || !newFolderName.trim()}
924
+ loading={isCreatingFolder}
925
+ >
926
+ Create Folder
927
+ </CmsButton>
928
+ </DialogFooter>
929
+ </DialogContent>
930
+ </Dialog>
931
+
932
+ <Dialog open={showUploadModal} onOpenChange={setShowUploadModal}>
933
+ <DialogContent className="max-w-lg">
934
+ <DialogHeader>
935
+ <DialogTitle>Upload Files</DialogTitle>
936
+ </DialogHeader>
937
+ <div className="py-4">
938
+ <UploadDropzone
939
+ currentFolderId={currentFolderId}
940
+ generateUploadUrl={api.media.generateUploadUrl}
941
+ createAsset={api.media.createAsset}
942
+ onUploadComplete={handleUploadComplete}
943
+ maxFileSize={50 * 1024 * 1024}
944
+ maxConcurrentUploads={3}
945
+ />
946
+ </div>
947
+ <DialogFooter>
948
+ <CmsButton variant="secondary" onClick={() => setShowUploadModal(false)}>
949
+ Close
950
+ </CmsButton>
951
+ </DialogFooter>
952
+ </DialogContent>
953
+ </Dialog>
954
+
955
+ {/* Preview Modal */}
956
+ <MediaPreviewModal
957
+ asset={
958
+ previewIndex !== null && displayedAssets[previewIndex]
959
+ ? (displayedAssets[previewIndex] as MediaAsset)
960
+ : null
961
+ }
962
+ assets={displayedAssets as MediaAsset[]}
963
+ currentIndex={previewIndex ?? 0}
964
+ open={previewIndex !== null}
965
+ onOpenChange={(open) => {
966
+ if (!open) setPreviewIndex(null)
967
+ }}
968
+ onNavigate={handlePreviewNavigate}
969
+ onEdit={
970
+ isTrashView
971
+ ? undefined
972
+ : (asset) =>
973
+ setEditingAsset({
974
+ _id: asset._id,
975
+ name: asset.name,
976
+ title: asset.title,
977
+ description: asset.description,
978
+ altText: asset.altText,
979
+ tags: asset.tags,
980
+ })
981
+ }
982
+ onDelete={
983
+ isTrashView
984
+ ? undefined
985
+ : (asset) =>
986
+ setDeleteTarget({
987
+ type: 'asset',
988
+ id: asset._id,
989
+ name: asset.name,
990
+ })
991
+ }
992
+ />
993
+
994
+ {/* Edit Asset Dialog */}
995
+ <MediaAssetEditDialog
996
+ asset={editingAsset}
997
+ open={editingAsset !== null}
998
+ onOpenChange={(open) => {
999
+ if (!open) setEditingAsset(null)
1000
+ }}
1001
+ />
1002
+
1003
+ {/* Edit Folder Dialog */}
1004
+ <MediaFolderEditDialog
1005
+ folder={editingFolder}
1006
+ open={editingFolder !== null}
1007
+ onOpenChange={(open) => {
1008
+ if (!open) setEditingFolder(null)
1009
+ }}
1010
+ />
1011
+
1012
+ {/* Delete Confirmation Dialog */}
1013
+ <CmsConfirmDialog
1014
+ open={deleteTarget !== null}
1015
+ onOpenChange={(open) => {
1016
+ if (!open) setDeleteTarget(null)
1017
+ }}
1018
+ title={`Delete ${deleteTarget?.type === 'folder' ? 'Folder' : 'File'}?`}
1019
+ description={`Are you sure you want to delete "${deleteTarget?.name}"? ${deleteTarget?.type === 'folder' ? 'This will also delete all files inside the folder.' : 'This action can be undone from the trash.'}`}
1020
+ confirmLabel="Delete"
1021
+ onConfirm={handleDelete}
1022
+ variant="danger"
1023
+ loading={isDeleting}
1024
+ />
1025
+
1026
+ {/* Bulk Delete Confirmation Dialog */}
1027
+ <CmsConfirmDialog
1028
+ open={showBulkDeleteConfirm}
1029
+ onOpenChange={setShowBulkDeleteConfirm}
1030
+ title="Delete Selected Files?"
1031
+ description={`Are you sure you want to delete ${selectedAssets.size} ${selectedAssets.size === 1 ? 'file' : 'files'}? This action can be undone from the trash.`}
1032
+ confirmLabel="Delete All"
1033
+ onConfirm={handleBulkDelete}
1034
+ variant="danger"
1035
+ loading={isBulkDeleting}
1036
+ />
1037
+
1038
+ {/* Permanent Delete Confirmation Dialog */}
1039
+ <CmsConfirmDialog
1040
+ open={showPermanentDeleteConfirm}
1041
+ onOpenChange={(open) => {
1042
+ setShowPermanentDeleteConfirm(open)
1043
+ if (!open) setPermanentDeleteTarget(null)
1044
+ }}
1045
+ title={permanentDeleteTarget === 'bulk'
1046
+ ? `Delete ${selectedAssets.size} ${selectedAssets.size === 1 ? 'File' : 'Files'} Forever?`
1047
+ : 'Delete Forever?'
1048
+ }
1049
+ description={permanentDeleteTarget === 'bulk'
1050
+ ? `This will permanently delete ${selectedAssets.size} ${selectedAssets.size === 1 ? 'file' : 'files'}. This action cannot be undone.`
1051
+ : 'This will permanently delete this file. This action cannot be undone.'
1052
+ }
1053
+ confirmLabel="Delete Forever"
1054
+ onConfirm={handlePermanentDelete}
1055
+ variant="danger"
1056
+ loading={isPermanentlyDeleting}
1057
+ />
1058
+
1059
+ {/* Move Modal */}
1060
+ <MediaMoveModal
1061
+ open={showMoveModal}
1062
+ onOpenChange={setShowMoveModal}
1063
+ assetIds={Array.from(selectedAssets)}
1064
+ currentFolderId={currentFolderId}
1065
+ onMoved={handleBulkMoveComplete}
1066
+ />
1067
+
1068
+ {/* Bulk Action Bar - Library View */}
1069
+ {isSelectionMode && !isTrashView && (
1070
+ <MediaBulkActionBar
1071
+ selectedCount={selectedAssets.size}
1072
+ onClear={handleDeselectAll}
1073
+ onMove={() => setShowMoveModal(true)}
1074
+ onDelete={() => setShowBulkDeleteConfirm(true)}
1075
+ isDeleting={isBulkDeleting}
1076
+ />
1077
+ )}
1078
+
1079
+ {/* Bulk Action Bar - Trash View */}
1080
+ {isSelectionMode && isTrashView && (
1081
+ <MediaTrashBulkActionBar
1082
+ selectedCount={selectedAssets.size}
1083
+ onClear={handleDeselectAll}
1084
+ onRestore={handleBulkRestore}
1085
+ onPermanentDelete={() => {
1086
+ setPermanentDeleteTarget('bulk')
1087
+ setShowPermanentDeleteConfirm(true)
1088
+ }}
1089
+ isRestoring={isRestoring}
1090
+ isDeleting={isPermanentlyDeleting}
1091
+ />
1092
+ )}
1093
+ </div>
1094
+ )
1095
+ }